diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 1116782..febc8cd 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -14,4 +14,4 @@ on: jobs: call-changelog-check-workflow: # Docs: https://github.com/ASFHyP3/actions - uses: ASFHyP3/actions/.github/workflows/reusable-changelog-check.yml@v0.11.2 + uses: ASFHyP3/actions/.github/workflows/reusable-changelog-check.yml@v0.12.0 diff --git a/.github/workflows/labeled-pr.yml b/.github/workflows/labeled-pr.yml index 50e66b7..dde2ec5 100644 --- a/.github/workflows/labeled-pr.yml +++ b/.github/workflows/labeled-pr.yml @@ -13,4 +13,4 @@ on: jobs: call-labeled-pr-check-workflow: # Docs: https://github.com/ASFHyP3/actions - uses: ASFHyP3/actions/.github/workflows/reusable-labeled-pr-check.yml@v0.11.2 + uses: ASFHyP3/actions/.github/workflows/reusable-labeled-pr-check.yml@v0.12.0 diff --git a/.github/workflows/release-checklist-comment.yml b/.github/workflows/release-checklist-comment.yml index 26d10fd..eab5714 100644 --- a/.github/workflows/release-checklist-comment.yml +++ b/.github/workflows/release-checklist-comment.yml @@ -10,7 +10,7 @@ on: jobs: call-release-workflow: # Docs: https://github.com/ASFHyP3/actions - uses: ASFHyP3/actions/.github/workflows/reusable-release-checklist-comment.yml@v0.11.2 + uses: ASFHyP3/actions/.github/workflows/reusable-release-checklist-comment.yml@v0.12.0 permissions: pull-requests: write secrets: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0c8b44..71c2f1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ on: jobs: call-release-workflow: # Docs: https://github.com/ASFHyP3/actions - uses: ASFHyP3/actions/.github/workflows/reusable-release.yml@v0.11.2 + uses: ASFHyP3/actions/.github/workflows/reusable-release.yml@v0.12.0 with: release_prefix: HyP3 SRG release_branch: main diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 9531ac2..1d08910 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -5,10 +5,8 @@ on: push jobs: call-secrets-analysis-workflow: # Docs: https://github.com/ASFHyP3/actions - uses: ASFHyP3/actions/.github/workflows/reusable-secrets-analysis.yml@v0.11.2 + uses: ASFHyP3/actions/.github/workflows/reusable-secrets-analysis.yml@v0.12.0 - call-flake8-workflow: + call-ruff-workflow: # Docs: https://github.com/ASFHyP3/actions - uses: ASFHyP3/actions/.github/workflows/reusable-flake8.yml@v0.11.2 - with: - local_package_names: hyp3_srg + uses: ASFHyP3/actions/.github/workflows/reusable-ruff.yml@v0.12.0 diff --git a/.github/workflows/tag-version.yml b/.github/workflows/tag-version.yml index da2a91c..4c4ddea 100644 --- a/.github/workflows/tag-version.yml +++ b/.github/workflows/tag-version.yml @@ -8,7 +8,7 @@ on: jobs: call-bump-version-workflow: # Docs: https://github.com/ASFHyP3/actions - uses: ASFHyP3/actions/.github/workflows/reusable-bump-version.yml@v0.11.2 + uses: ASFHyP3/actions/.github/workflows/reusable-bump-version.yml@v0.12.0 with: user: tools-bot email: UAF-asf-apd@alaska.edu diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index 85d82b9..dc85394 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -13,7 +13,7 @@ on: jobs: call-pytest-workflow: # Docs: https://github.com/ASFHyP3/actions - uses: ASFHyP3/actions/.github/workflows/reusable-pytest.yml@v0.11.2 + uses: ASFHyP3/actions/.github/workflows/reusable-pytest.yml@v0.12.0 with: local_package_name: hyp3_srg python_versions: >- @@ -21,7 +21,7 @@ jobs: call-version-info-workflow: # Docs: https://github.com/ASFHyP3/actions - uses: ASFHyP3/actions/.github/workflows/reusable-version-info.yml@v0.11.2 + uses: ASFHyP3/actions/.github/workflows/reusable-version-info.yml@v0.12.0 with: python_version: "3.10" diff --git a/CHANGELOG.md b/CHANGELOG.md index 85cacd4..13b4af5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [PEP 440](https://www.python.org/dev/peps/pep-0440/) and uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.1] + +### Changed +* The [`static-analysis`](.github/workflows/static-analysis.yml) Github Actions workflow now uses `ruff` rather than `flake8` for linting. + ## [0.9.0] ### Added diff --git a/environment.yml b/environment.yml index 24403dc..f7f7259 100644 --- a/environment.yml +++ b/environment.yml @@ -6,10 +6,7 @@ dependencies: - python>=3.10 - pip # For packaging, and testing - - flake8 - - flake8-import-order - - flake8-blind-except - - flake8-builtins + - ruff - setuptools - setuptools_scm - wheel diff --git a/pyproject.toml b/pyproject.toml index 5b222ff..292728b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,10 +30,7 @@ dynamic = ["version", "readme"] [project.optional-dependencies] develop = [ - "flake8", - "flake8-import-order", - "flake8-blind-except", - "flake8-builtins", + "ruff", "pytest", "pytest-cov", "pytest-console-scripts", @@ -62,3 +59,29 @@ readme = {file = ["README.md"], content-type = "text/markdown"} where = ["src"] [tool.setuptools_scm] + +[tool.ruff] +line-length = 120 +src = ["src", "tests"] + +[tool.ruff.format] +indent-style = "space" +quote-style = "single" + +[tool.ruff.lint] +extend-select = [ + "I", # isort: https://docs.astral.sh/ruff/rules/#isort-i + + # TODO: uncomment the following extensions and address their warnings: + #"UP", # pyupgrade: https://docs.astral.sh/ruff/rules/#pyupgrade-up + #"D", # pydocstyle: https://docs.astral.sh/ruff/rules/#pydocstyle-d + #"ANN", # annotations: https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + #"PTH", # use-pathlib-pth: https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth +] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.isort] +case-sensitive = true +lines-after-imports = 2 diff --git a/src/hyp3_srg/__init__.py b/src/hyp3_srg/__init__.py index d42f103..6a0756a 100644 --- a/src/hyp3_srg/__init__.py +++ b/src/hyp3_srg/__init__.py @@ -2,6 +2,7 @@ from importlib.metadata import version + __version__ = version(__name__) __all__ = [ diff --git a/src/hyp3_srg/__main__.py b/src/hyp3_srg/__main__.py index 6d863a4..c54a0f2 100644 --- a/src/hyp3_srg/__main__.py +++ b/src/hyp3_srg/__main__.py @@ -1,6 +1,5 @@ -""" -HyP3 plugin for Stanford Radar Group (SRG) SAR Processor -""" +"""HyP3 plugin for Stanford Radar Group (SRG) SAR Processor""" + import argparse import sys from importlib.metadata import entry_points diff --git a/src/hyp3_srg/back_projection.py b/src/hyp3_srg/back_projection.py index 8fd5c70..e838167 100644 --- a/src/hyp3_srg/back_projection.py +++ b/src/hyp3_srg/back_projection.py @@ -1,13 +1,11 @@ -""" -GSLC back-projection processing -""" +"""GSLC back-projection processing""" import argparse import logging import os import zipfile +from collections.abc import Iterable from pathlib import Path -from typing import Iterable, Optional from hyp3lib.aws import upload_file_to_s3 from shapely import unary_union @@ -93,7 +91,7 @@ def back_project( bucket: str = None, bucket_prefix: str = '', use_gslc_prefix: bool = False, - work_dir: Optional[Path] = None, + work_dir: Path | None = None, gpu: bool = False, ): """Back-project a set of Sentinel-1 level-0 granules. @@ -162,17 +160,27 @@ def main(): help=( 'Upload GSLC granules to a subprefix located within the bucket and prefix given by the' ' --bucket and --bucket-prefix options' - ) + ), + ) + parser.add_argument( + '--gpu', + default=False, + action='store_true', + help='Use the GPU-based version of the workflow.', ) - parser.add_argument('--gpu', default=False, action='store_true', help='Use the GPU-based version of the workflow.') parser.add_argument( '--bounds', default=None, type=str.split, nargs='+', - help='DEM extent bbox in EPSG:4326: [min_lon, min_lat, max_lon, max_lat].' + help='DEM extent bbox in EPSG:4326: [min_lon, min_lat, max_lon, max_lat].', + ) + parser.add_argument( + 'granules', + type=str.split, + nargs='+', + help='Level-0 S1 granule(s) to back-project.', ) - parser.add_argument('granules', type=str.split, nargs='+', help='Level-0 S1 granule(s) to back-project.') args = parser.parse_args() args.granules = [item for sublist in args.granules for item in sublist] diff --git a/src/hyp3_srg/dem.py b/src/hyp3_srg/dem.py index 2a7223b..3f3596c 100644 --- a/src/hyp3_srg/dem.py +++ b/src/hyp3_srg/dem.py @@ -1,7 +1,7 @@ """Prepare a Copernicus GLO-30 DEM virtual raster (VRT) covering a given geometry""" + import logging from pathlib import Path -from typing import Optional import requests @@ -27,7 +27,7 @@ def ensure_egm_model_available(): f.write(chunk) -def download_dem_for_srg(bounds: list[float], work_dir: Optional[Path]): +def download_dem_for_srg(bounds: list[float], work_dir: Path | None): """Download the DEM for the given bounds - [min_lon, min_lat, max_lon, max_lat]. Args: @@ -37,9 +37,9 @@ def download_dem_for_srg(bounds: list[float], work_dir: Optional[Path]): Returns: The path to the downloaded DEM """ - if (bounds[0] >= bounds[2] or bounds[1] >= bounds[3]): + if bounds[0] >= bounds[2] or bounds[1] >= bounds[3]: raise ValueError( - "Improper bounding box formatting, should be [max latitude, min latitude, min longitude, max longitude]." + 'Improper bounding box formatting, should be [max latitude, min latitude, min longitude, max longitude].' ) dem_path = work_dir / 'elevation.dem' diff --git a/src/hyp3_srg/time_series.py b/src/hyp3_srg/time_series.py index 44c99cb..871d586 100644 --- a/src/hyp3_srg/time_series.py +++ b/src/hyp3_srg/time_series.py @@ -1,15 +1,13 @@ -""" -Sentinel-1 GSLC time series processing -""" +"""Sentinel-1 GSLC time series processing""" import argparse import logging import shutil +from collections.abc import Iterable from os import mkdir from pathlib import Path from secrets import token_hex from shutil import copyfile -from typing import Iterable, Optional from hyp3lib.aws import upload_file_to_s3 from hyp3lib.fetch import download_file as download_from_http @@ -87,7 +85,10 @@ def get_size_from_dem(dem_path: str) -> tuple[int, int]: def generate_wrapped_interferograms( - looks: tuple[int, int], baselines: tuple[int, int], dem_shape: tuple[int, int], work_dir: Path + looks: tuple[int, int], + baselines: tuple[int, int], + dem_shape: tuple[int, int], + work_dir: Path, ) -> None: """Generates wrapped interferograms from GSLCs @@ -101,9 +102,22 @@ def generate_wrapped_interferograms( looks_down, looks_across = looks time_baseline, spatial_baseline = baselines - utils.call_stanford_module('sentinel/sbas_list.py', args=[time_baseline, spatial_baseline], work_dir=work_dir) + utils.call_stanford_module( + 'sentinel/sbas_list.py', + args=[time_baseline, spatial_baseline], + work_dir=work_dir, + ) - sbas_args = ['sbas_list', '../elevation.dem.rsc', 1, 1, dem_width, dem_length, looks_down, looks_across] + sbas_args = [ + 'sbas_list', + '../elevation.dem.rsc', + 1, + 1, + dem_width, + dem_length, + looks_down, + looks_across, + ] utils.call_stanford_module('sentinel/ps_sbas_igrams.py', args=sbas_args, work_dir=work_dir) @@ -118,13 +132,22 @@ def unwrap_interferograms(dem_shape: tuple[int, int], unw_shape: tuple[int, int] dem_width, dem_length = dem_shape unw_width, unw_length = unw_shape - reduce_dem_args = ['../elevation.dem', 'dem', dem_width, dem_width // unw_width, dem_length // unw_length] + reduce_dem_args = [ + '../elevation.dem', + 'dem', + dem_width, + dem_width // unw_width, + dem_length // unw_length, + ] utils.call_stanford_module('util/nbymi2', args=reduce_dem_args, work_dir=work_dir) utils.call_stanford_module('util/unwrap_parallel.py', args=[unw_width], work_dir=work_dir) def compute_sbas_velocity_solution( - threshold: float, do_tropo_correction: bool, unw_shape: tuple[int, int], work_dir: Path + threshold: float, + do_tropo_correction: bool, + unw_shape: tuple[int, int], + work_dir: Path, ) -> None: """Computes the sbas velocity solution from the unwrapped interferograms @@ -147,10 +170,10 @@ def compute_sbas_velocity_solution( tropo_correct_args = ['unwlist', unw_width, unw_length] utils.call_stanford_module('int/tropocorrect.py', args=tropo_correct_args, work_dir=work_dir) - with open(work_dir / 'unwlist', 'r') as unw_list: + with open(work_dir / 'unwlist') as unw_list: num_unw_files = len(unw_list.readlines()) - with open(work_dir / 'geolist', 'r') as slc_list: + with open(work_dir / 'geolist') as slc_list: num_slcs = len(slc_list.readlines()) sbas_velocity_args = ['unwlist', num_unw_files, num_slcs, unw_width, 'ref_locs'] @@ -180,7 +203,10 @@ def create_time_series( unwrap_interferograms(dem_shape=dem_shape, unw_shape=unw_shape, work_dir=work_dir) compute_sbas_velocity_solution( - threshold=threshold, do_tropo_correction=do_tropo_correction, unw_shape=unw_shape, work_dir=work_dir + threshold=threshold, + do_tropo_correction=do_tropo_correction, + unw_shape=unw_shape, + work_dir=work_dir, ) @@ -197,11 +223,11 @@ def create_time_series_product_name( Returns: the product name as a string. """ - prefix = "S1_SRG_SBAS" - split_names = [granule.split("_") for granule in granule_names] + prefix = 'S1_SRG_SBAS' + split_names = [granule.split('_') for granule in granule_names] absolute_orbit = split_names[0][7] - if split_names[0][0] == "S1A": + if split_names[0][0] == 'S1A': relative_orbit = str(((int(absolute_orbit) - 73) % 175) + 1) else: relative_orbit = str(((int(absolute_orbit) - 27) % 175) + 1) @@ -216,24 +242,22 @@ def lat_string(lat): def lon_string(lon): return ('E' if lon >= 0 else 'W') + f"{('%.1f' % abs(lon)).zfill(5)}".replace('.', '_') - return '_'.join([ - prefix, - relative_orbit, - lon_string(bounds[0]), - lat_string(bounds[1]), - lon_string(bounds[2]), - lat_string(bounds[3]), - earliest_granule, - latest_granule, - token_hex(2).upper() - ]) - - -def package_time_series( - granules: list[str], - bounds: list[float], - work_dir: Optional[Path] = None -) -> Path: + return '_'.join( + [ + prefix, + relative_orbit, + lon_string(bounds[0]), + lat_string(bounds[1]), + lon_string(bounds[2]), + lat_string(bounds[3]), + earliest_granule, + latest_granule, + token_hex(2).upper(), + ] + ) + + +def package_time_series(granules: list[str], bounds: list[float], work_dir: Path | None = None) -> Path: """Package the time series into a product zip file. Args: @@ -278,7 +302,7 @@ def time_series( use_gslc_prefix: bool, bucket: str = None, bucket_prefix: str = '', - work_dir: Optional[Path] = None, + work_dir: Path | None = None, ) -> None: """Create and package a time series stack from a set of Sentinel-1 GSLCs. @@ -337,7 +361,7 @@ def main(): default=None, type=str.split, nargs='+', - help='DEM extent bbox in EPSG:4326: [min_lon, min_lat, max_lon, max_lat].' + help='DEM extent bbox in EPSG:4326: [min_lon, min_lat, max_lon, max_lat].', ) parser.add_argument( '--use-gslc-prefix', @@ -345,7 +369,7 @@ def main(): help=( 'Download GSLC input granules from a subprefix located within the bucket and prefix given by the' ' --bucket and --bucket-prefix options' - ) + ), ) parser.add_argument('granules', type=str.split, nargs='*', default='', help='GSLC granules.') args = parser.parse_args() diff --git a/src/hyp3_srg/utils.py b/src/hyp3_srg/utils.py index eb976f9..1641008 100644 --- a/src/hyp3_srg/utils.py +++ b/src/hyp3_srg/utils.py @@ -4,7 +4,6 @@ import subprocess from pathlib import Path from platform import system -from typing import List, Optional, Tuple from zipfile import ZipFile import asf_search @@ -58,7 +57,7 @@ def set_creds(service, username, password) -> None: os.environ[f'{service.upper()}_PASSWORD'] = password -def find_creds_in_env(username_name, password_name) -> Tuple[str, str]: +def find_creds_in_env(username_name, password_name) -> tuple[str, str]: """Find credentials for a service in the environment. Args: @@ -76,7 +75,7 @@ def find_creds_in_env(username_name, password_name) -> Tuple[str, str]: return None, None -def find_creds_in_netrc(service) -> Tuple[str, str]: +def find_creds_in_netrc(service) -> tuple[str, str]: """Find credentials for a service in the netrc file. Args: @@ -96,7 +95,7 @@ def find_creds_in_netrc(service) -> Tuple[str, str]: return None, None -def get_earthdata_credentials() -> Tuple[str, str]: +def get_earthdata_credentials() -> tuple[str, str]: """Get NASA EarthData credentials from the environment or netrc file. Returns: @@ -116,7 +115,7 @@ def get_earthdata_credentials() -> Tuple[str, str]: ) -def download_raw_granule(granule_name: str, output_dir: Path, unzip: bool = False) -> Tuple[Path, Polygon]: +def download_raw_granule(granule_name: str, output_dir: Path, unzip: bool = False) -> tuple[Path, Polygon]: """Download a S1 granule using asf_search. Return its path and buffered extent. @@ -156,7 +155,7 @@ def download_raw_granule(granule_name: str, output_dir: Path, unzip: bool = Fals return out_path, bbox -def get_bbox(granule_name: str) -> Tuple[Path, Polygon]: +def get_bbox(granule_name: str) -> tuple[Path, Polygon]: """Get the buffered extent from asf_search. Args: @@ -204,7 +203,7 @@ def create_param_file(dem_path: Path, dem_rsc_path: Path, output_dir: Path): f.write('\n'.join(lines)) -def call_stanford_module(local_name, args: List = [], work_dir: Optional[Path] = None) -> None: +def call_stanford_module(local_name, args: list = [], work_dir: Path | None = None) -> None: """Call a Stanford Processor modules (via subprocess) with the given arguments. Args: @@ -231,7 +230,7 @@ def how_many_gpus(): return ngpus -def get_s3_args(uri: str, dest_dir: Optional[Path] = None) -> None: +def get_s3_args(uri: str, dest_dir: Path | None = None) -> None: """Retrieve the arguments for downloading from an S3 bucket Args: @@ -268,7 +267,7 @@ def s3_list_objects(bucket: str, prefix: str = '') -> dict: return res -def download_from_s3(uri: str, dest_dir: Optional[Path] = None) -> None: +def download_from_s3(uri: str, dest_dir: Path | None = None) -> None: """Download a file from an S3 bucket Args: diff --git a/tests/test_dem.py b/tests/test_dem.py index b34be8e..e488c1d 100644 --- a/tests/test_dem.py +++ b/tests/test_dem.py @@ -17,13 +17,20 @@ def test_download_dem_for_srg(monkeypatch): mock_ensure_egm_model_available.assert_called_once() mock_call_stanford_module.assert_called_once_with( 'DEM/createDEMcop.py', - [str(Path.cwd() / 'elevation.dem'), str(Path.cwd() / 'elevation.dem.rsc'), 3, 1, 0, 2], + [ + str(Path.cwd() / 'elevation.dem'), + str(Path.cwd() / 'elevation.dem.rsc'), + 3, + 1, + 0, + 2, + ], work_dir=Path.cwd(), ) bad_bboxs = [ [0.0, 1.0, -1.0, 1.0], [1.0, 1.0, -1.0, 2.0], - [1.0, 0.0, 2.0, -1.0] + [1.0, 0.0, 2.0, -1.0], ] for bbox in bad_bboxs: with pytest.raises(ValueError, match=r'Improper bounding box formatting*'): diff --git a/tests/test_time_series.py b/tests/test_time_series.py index bd0e519..a243a8e 100644 --- a/tests/test_time_series.py +++ b/tests/test_time_series.py @@ -5,7 +5,7 @@ def test_create_time_series_product_name(): granule_names = [ 'S1A_IW_RAW__0SDV_001_003_054532_06A2F8_8276.zip', 'S1A_IW_RAW__0SDV_004_005_054882_06AF26_2CE5.zip', - 'S1A_IW_RAW__0SDV_010_020_055057_06B527_1346.zip' + 'S1A_IW_RAW__0SDV_010_020_055057_06B527_1346.zip', ] bounds = [-100, 45, -90, 50] name = time_series.create_time_series_product_name(granule_names, bounds) diff --git a/tests/test_utils.py b/tests/test_utils.py index 1f51aed..a9348e3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -58,7 +58,10 @@ def test_find_creds_in_env(monkeypatch): with monkeypatch.context() as m: m.setenv('TEST_USERNAME', 'foo') m.setenv('TEST_PASSWORD', 'bar') - assert utils.find_creds_in_env('TEST_USERNAME', 'TEST_PASSWORD') == ('foo', 'bar') + assert utils.find_creds_in_env('TEST_USERNAME', 'TEST_PASSWORD') == ( + 'foo', + 'bar', + ) with monkeypatch.context() as m: m.delenv('TEST_USERNAME', raising=False) @@ -91,5 +94,9 @@ def test_get_s3_args(): s3_uri_1 = 's3://foo/bar.zip' s3_uri_2 = 's3://foo/bing/bong/bar.zip' dest_dir = Path('output') - assert utils.get_s3_args(s3_uri_1) == ('foo', 'bar.zip', Path.cwd() / "bar.zip") - assert utils.get_s3_args(s3_uri_2, dest_dir) == ('foo', 'bing/bong/bar.zip', dest_dir / 'bar.zip') + assert utils.get_s3_args(s3_uri_1) == ('foo', 'bar.zip', Path.cwd() / 'bar.zip') + assert utils.get_s3_args(s3_uri_2, dest_dir) == ( + 'foo', + 'bing/bong/bar.zip', + dest_dir / 'bar.zip', + )