From d646df5fef94e712882f5a60a55ceb1dc6c0c9f0 Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Mon, 26 Oct 2026 15:03:24 -0800 Subject: [PATCH 01/11] README update, cdk dependency bump, /timeseries description --- README.md | 86 ++++++++++++++++++++++++++++++++++++++++--- cdk/requirements.txt | 2 +- oidc/requirements.txt | 2 +- src/application.py | 2 +- 4 files changed, 83 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7697c27..c565998 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,87 @@ -# Discovery-HDFReaderAPI -- A HDF5 based file reader implemented with FastAPI, made for sampling timeseries from remote HDF5/NetCDF files in s3 containers. +# Discovery-DisplacementAPI +- A FastAPI based Lambda API for querying point based timeseries on OPERA `DISP-S1` data products, querying remote gzipped zarr json stores in S3. + +# Overview +## Endpoints + +### GET `/` +Returns a basic endpoint status message + +### POST `/timeseries` +Returns a json timeseries, mapping file names to a sampled point's `short_wavelength_displacement` value with x & y coordinates, source s3 netcdf4 uri, frame, and dates +```json +{ + "OPERA_L3_DISP-S1_IW_F16942_VV_20160707T015033Z_20160731T015034Z_v1.0_20250617T175432Z.nc": + { + "netcdf_uri": "s3:\/\/asf-cumulus-prod-opera-products\/OPERA_L3_DISP-S1_V1\/OPERA_L3_DISP-S1_IW_F16942_VV_20160707T015033Z_20160731T015034Z_v1.0_20250617T175432Z\/OPERA_L3_DISP-S1_IW_F16942_VV_20160707T015033Z_20160731T015034Z_v1.0_20250617T175432Z.nc", + "bytes": 390070272.0, + "reference_datetime": "2016-07-07T01:50:33.224", + "secondary_datetime": "2016-07-31T01:50:34.507", + "temporal_baseline": 24.0, + "short_wavelength_displacement": 0.003633976, + "x": 437565.0, + "y": 3966615.0, + "frame": "F16942", + "is_masked": false, + }, + ..., + "mean": + { + "netcdf_uri": null, + "bytes": 412385371.2757660151, + "reference_datetime": null, + "secondary_datetPime": null, + "temporal_baseline": 1879.8384401114, + "short_wavelength_displacement": 0.3834199607, + "x": 437565.0, + "y": 3966615.0, + "frame": null, + "is_masked": null, + }, + } +} +``` +params: + wkt: str + - A WKT POINT string + flightDirection: str + - `ASCENDING` or `DESCENDING` pass + bucket: str + - The bucket to query with the zarr stores, ie `asf-cumulus-prod` + polarization: Polarizations = Polarizations.VV + frameId: str | None = Field(None, pattern=r'^\d{5}$') + - Overrides which frame to search + +### POST `/get_frame_id` +params: + wkt: str + - A WKT POINT string + flightDirection: str + - `ASCENDING` or `DESCENDING` pass # Install and Run Locally Run pip install from the project's root folder -``` - pip install -e . +``` bash + pip install -r src/requirements.txt ``` -``` python +``` bash uvicorn src.application:app --port 8080 --reload -``` \ No newline at end of file +``` + +# Install and Run Test Dependencies Locally + +[!IMPORTANT] +To run all tests locally valid AWS credentials are needed for accessing zarr reference stores in EDC + +To install test dependencies: +``` bash + pip install -r tests/requirements.txt +``` + +To run +``` bash +pytest --ignore=tests/yml_tests/test_authorized +``` + diff --git a/cdk/requirements.txt b/cdk/requirements.txt index 6b74f25..c21d595 100644 --- a/cdk/requirements.txt +++ b/cdk/requirements.txt @@ -1,2 +1,2 @@ -aws-cdk-lib==2.182.0 +aws-cdk-lib==2.253 # constructs>=10.0.0,<11.0.0 diff --git a/oidc/requirements.txt b/oidc/requirements.txt index 0406081..23819bd 100644 --- a/oidc/requirements.txt +++ b/oidc/requirements.txt @@ -1,2 +1,2 @@ -aws-cdk-lib==2.166.0 +aws-cdk-lib==2.253 constructs>=10.0.0,<11.0.0 diff --git a/src/application.py b/src/application.py index 6e1d091..a3e3827 100644 --- a/src/application.py +++ b/src/application.py @@ -47,7 +47,7 @@ class ReadLayer(BaseModel): @app.post( '/timeseries', - description='Returns a json response with values read at "unwrapped_phase", "interferometric_correlation", and "temporal_coherence" layers in provided files at given point', + description='Returns a json dictionary mapping netcdf4 file names to their sampled point value based off the given POINT wkt', ) def get_timeseries(series: TimeSeriesModel): # # TODO: REMOVE BEFORE ANOTHER PRODUCTION RELEASE From 70098c1bfa9206e860fe57f08b6d648db9a74996 Mon Sep 17 00:00:00 2001 From: Kim <33294735+SpicyGarlicAlbacoreRoll@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:23:10 -0800 Subject: [PATCH 02/11] Create LICENSE --- LICENSE | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d263aaf --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Alaska Satellite Facility + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 1f9cdb5bf6c18846b0288affbdee5a884bc58670 Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Mon, 26 Oct 2026 16:34:29 -0800 Subject: [PATCH 03/11] chore: add PR label and changelog check --- .github/workflows/changelog.yml | 19 +++++++++++++++++++ .github/workflows/pr-labeled.yml | 17 +++++++++++++++++ CHANGELOG.md | 5 +++++ 3 files changed, 41 insertions(+) create mode 100644 .github/workflows/changelog.yml create mode 100644 .github/workflows/pr-labeled.yml create mode 100644 CHANGELOG.md diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..5cdfbbd --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,19 @@ +name: Changelog updated? + +on: + pull_request: + types: + - opened + - labeled + - unlabeled + - synchronize + branches: + - test + - prod + +jobs: + call-changelog-check-workflow: + # Docs: https://github.com/ASFHyP3/actions + uses: ASFHyP3/actions/.github/workflows/reusable-changelog-check.yml@v0.22.0 + permissions: + contents: read diff --git a/.github/workflows/pr-labeled.yml b/.github/workflows/pr-labeled.yml new file mode 100644 index 0000000..0638211 --- /dev/null +++ b/.github/workflows/pr-labeled.yml @@ -0,0 +1,17 @@ +name: Is PR labeled? + +on: + pull_request: + types: + - opened + - labeled + - unlabeled + - synchronize + branches: + - prod + +jobs: + call-labeled-pr-check-workflow: + uses: ASFHyP3/actions/.github/workflows/reusable-labeled-pr-check.yml@v0.22.0 + permissions: + contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a0fee1a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +## [1.0.0] + +### Added + +- Initial Release From a474f602f8ef1de4247154defd1a0a5ee1723301 Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Wed, 28 Oct 2026 15:49:37 -0800 Subject: [PATCH 04/11] chore: remove commented code --- src/HDF/data_reader.py | 1 - src/application.py | 35 ------------------ src/timeseries/timeseries.py | 72 +++--------------------------------- 3 files changed, 6 insertions(+), 102 deletions(-) diff --git a/src/HDF/data_reader.py b/src/HDF/data_reader.py index a81b67a..854c4f2 100644 --- a/src/HDF/data_reader.py +++ b/src/HDF/data_reader.py @@ -1,7 +1,6 @@ import rasterio import xarray as xr -# from fsspec.implementations.local import S3FileSystem from s3fs.core import S3FileSystem from src import api_logger diff --git a/src/application.py b/src/application.py index a3e3827..22cfc81 100644 --- a/src/application.py +++ b/src/application.py @@ -50,11 +50,6 @@ class ReadLayer(BaseModel): description='Returns a json dictionary mapping netcdf4 file names to their sampled point value based off the given POINT wkt', ) def get_timeseries(series: TimeSeriesModel): - # # TODO: REMOVE BEFORE ANOTHER PRODUCTION RELEASE - # return HTTPException( - # status_code=400, - # detail='The test displacement API is temporarily disabled due to unintended usage. For programmatic queries please access the zarr stack store `displacement` layer directly. For more info or questions please contact uso@asf.alaska.edu', - # ) start = perf_counter() try: geometry, wrapped_geometry = _wrap_wkt(series.wkt) @@ -113,36 +108,6 @@ def get_frame_ids(wkt_event: GetFramesModel): ) -# @app.post( -# '/frame_intersection', -# description='Returns the frame ids for a given aoi, splitting any cross frames', -# ) -# def frame_intersection(wkt_event: GetFramesModel): -# wkt = wkt_event.wkt -# flight_direction = wkt_event.flightDirection -# overlapping_frames = _get_frames_for_wkt(wkt, flight_direction) -# frame_path = Path(__file__).parent.parent / 'data' / 'frames.geojson' -# geometry = shapely_wkt.loads(wkt_event.wkt) -# gdf = gpd.read_file(frame_path, engine='pyogrio') -# frames: gpd.GeoDataFrame = gdf[gdf['frame_id'].isin([int(p) for p in overlapping_frames])] -# intersection = frames.intersection(geometry) -# return JSONResponse( -# { -# _pad_frame_str(frame_id): _simplify_multipolygons(geom).wkt -# for frame_id, geom in zip(gdf.iloc[intersection.index]['frame_id'], intersection) -# }, -# headers=DEFAULT_HEADERS, -# ) - - -# def _simplify_multipolygons(geometry: BaseGeometry) -> BaseGeometry: -# """In case polygon intersects different sections of frame at once and get multi-polygon (swath corners for example), perform convex hull instead""" -# if geometry.geom_type == 'MultiPolygon': -# geometry = convex_hull(geometry) - -# return geometry - - def _get_file_store_for_wkt(bucket: str, frame_id: str, polarization: str): return f's3://{bucket}/OPERA_L3_DISP-S1_STACK_V1/OPERA_L3_DISP-S1_IW_F{frame_id}_{polarization}_V1/OPERA_L3_DISP-S1_IW_F{frame_id}_{polarization}_short_wavelength_displacement.zarr.json.gz' diff --git a/src/timeseries/timeseries.py b/src/timeseries/timeseries.py index c114d80..0969308 100644 --- a/src/timeseries/timeseries.py +++ b/src/timeseries/timeseries.py @@ -1,4 +1,3 @@ -# import rasterio import re from datetime import datetime, timedelta from time import perf_counter @@ -47,21 +46,6 @@ def get_timeseries_over_aoi( **fsspec_params, }, ) - # fs = SimpleCacheFileSystem( - # target_protocol='reference', - # cache_storage=f'/mnt/filesystem/{file_store.split("/")[-1]}', - # target_options={ - # 'target_options': dict(compression='gzip'), - # 'fo': f'{file_store}', - # 'remote_options': { - # 'anon': False, - # 'default_fill_cache': True, - # 'skip_instance_cache': False, - # **fsspec_params, - # }, - # }, - # asynchronous=True, - # ) except Exception as exc: api_logger.error(f'Failed to open file. Original Error: {exc}') @@ -94,8 +78,6 @@ def get_timeseries_over_aoi( start = perf_counter() if isinstance(wrapped_geometry, Point): values = _read_layers_at_pixel(values, lon=wrapped_geometry.x, lat=wrapped_geometry.y) - # elif isinstance(geometry, Polygon): - # values = _read_layers_with_aoi(values, geometry) api_logger.info(f'AOI Read Speed: {perf_counter() - start} seconds') return _pandas_output(values, wkt=geometry.wkt) @@ -109,7 +91,7 @@ def _latest_product_versions(values: xr.Dataset) -> xr.Dataset: def _minimum_version(values: xr.Dataset) -> xr.Dataset: """Only keep products of minimum version `0.9` or above""" - mask = values.coords['source_file_name'].str.match(r'.*((v1\..)|(v0\.9)).*\.nc?') + mask = values.coords['source_file_name'].str.match(r'.*(v1\..).*\.nc?') return values.isel(source_file_name=mask) @@ -125,52 +107,6 @@ def _read_layers_at_pixel(dataset: xr.Dataset, lat: float, lon: float) -> xr.Dat return dataset[_data_variables].isel(x=xx_index, y=yy_index) -# Saving this functionality for polygons in future -# def _read_layers_with_aoi(dataset: xr.Dataset, geometry: BaseGeometry): -# """ -# Read a spatial subset from the stack with a reprojection of the provided geometry -# Returns the average values for each aoi at each timestep -# """ -# crs = get_crs(dataset) -# epsg = crs.to_epsg() - -# projected = _transform_geometry(geometry=geometry, target_proj=epsg) - -# dataset.rio.set_spatial_dims(x_dim='x', y_dim='y', inplace=True) -# dataset.rio.write_crs(str(crs), inplace=True) - -# d = {'col1': ['aoi'], 'geometry': [projected]} -# gdf = gpd.GeoDataFrame(d, crs=str(epsg)) # type: ignore - -# # clip_box limits the clipped geometry to the aoi bounds, -# # otherwise rioxarray will load THE ENTIRE DATASET FOOTPRINT into memory. -# # Not a perfect solution, as any shape that isn't perfectly aligned with -# # bounding dimensions will lead to more remote reads than necessary -# # ex: the trianges between a bounding box and a rotated square will be -# # loaded by `clip()` - -# # TODO: Explore opening only one frame, clipping it, then use those -# # indexes to read the remaining timesteps without loading the extra data -# # (assuming all data in uniformly alligned over a frame id, which should be the case) -# clipped_box = dataset.rio.clip_box(*projected.bounds) -# ds = clipped_box.rio.clip(gdf.geometry.values, gdf.crs, drop=False) -# for v in list(ds.coords.keys()): -# if ds.coords[v].dtype == object: -# ds.coords[v] = ds.coords[v].astype('unicode') - -# for v in list(ds.variables.keys()): -# if ds[v].dtype == object: -# ds[v] = ds[v].astype('unicode') -# # ds.to_zarr('tests/zarr_test_store/', mode='w', consolidated=False) - -# return ds.mean(['x', 'y']) - - -# def _transform_geometry(geometry: BaseGeometry, source_proj='4326', target_proj='32610'): -# project = Transformer.from_crs(source_proj, target_proj, always_xy=True).transform -# return shapely_transform(project, geometry) - - def _to_crs(lat: float, lon: float, source_proj='4326', target_proj='32610'): source_crs = CRS.from_epsg(source_proj) target_crs = CRS.from_epsg(target_proj) @@ -226,7 +162,11 @@ def _fix_rolling_reference(data: pandas.DataFrame) -> pandas.DataFrame: def _pandas_output(data: xr.Dataset, wkt: str): start = perf_counter() data = data.sortby('secondary_datetime') - pandas_values = data.to_dataframe() + try: + pandas_values = data.to_pandas() + except Exception as e: + print(e) + raise nans = pandas_values['short_wavelength_displacement'].isna() if len(pandas_values[~nans]) == 0: From 732998a2c430c319ac971495c72848d11ce1e7ae Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Wed, 28 Oct 2026 16:53:08 -0800 Subject: [PATCH 05/11] chore: ruff format, update readme, issue and pr templates --- .github/ISSUE_TEMPLATE/bug_report.md | 23 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 12 ++++++++++++ .github/workflows/changelog.yml | 1 - README.md | 5 +++++ oidc/cdk_oidc_role_stack.py | 15 +++------------ tests/pytest-managers.py | 18 +++++++++--------- 8 files changed, 73 insertions(+), 22 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..4382613 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[Bug]" +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3ba13e0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..f9acdb3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature]" +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c05f572 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +## Summary +Describe clearly what this PR changes and why. + +## Related Issues +Fixes # or Closes # + +## Tests & Build +- [ ] `ruff format` passes +- [ ] `pytest --ignore=tests/yml_tests/test_authorized` passes (or N/A) + +## Notes +Anything unusual reviewers should know. diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 5cdfbbd..28f69c9 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -8,7 +8,6 @@ on: - unlabeled - synchronize branches: - - test - prod jobs: diff --git a/README.md b/README.md index c565998..9713256 100644 --- a/README.md +++ b/README.md @@ -85,3 +85,8 @@ To run pytest --ignore=tests/yml_tests/test_authorized ``` +# Release Workflow + +The intended flow from pull request to release is: +`feat/bug-branch`->`test`->`prod` + diff --git a/oidc/cdk_oidc_role_stack.py b/oidc/cdk_oidc_role_stack.py index f286c4b..ff78184 100644 --- a/oidc/cdk_oidc_role_stack.py +++ b/oidc/cdk_oidc_role_stack.py @@ -1,16 +1,9 @@ -from aws_cdk import ( - Stack, - cloudformation_include as cfn_inc -) +from aws_cdk import Stack, cloudformation_include as cfn_inc from constructs import Construct class CdkOIDCRoleStack(Stack): - - def __init__( - self, scope: Construct, construct_id: str, **kwargs - ) -> None: - + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # Only needs to be done once per account @@ -21,7 +14,5 @@ def __init__( # ) self.oidc_role_template = cfn_inc.CfnInclude( - self, - "OIDCRoleTemplate", - template_file="github-actions-oidc.yml" + self, 'OIDCRoleTemplate', template_file='github-actions-oidc.yml' ) diff --git a/tests/pytest-managers.py b/tests/pytest-managers.py index d589fd6..33937f7 100644 --- a/tests/pytest-managers.py +++ b/tests/pytest-managers.py @@ -38,12 +38,12 @@ def test_wkt_wrapping(**args) -> None: actual, actual_wrapped = _wrap_wkt(wkt) - assert actual.wkt == wkt, ( - f'Original geometry modified when unwrapping wkt. Expected wkt: {wkt}. Actual: {actual.wkt}' - ) - assert actual_wrapped.wkt == wrapped, ( - f'Failed to wrap wkt. Expected wkt: {wrapped}. Actual: {actual_wrapped}' - ) + assert ( + actual.wkt == wkt + ), f'Original geometry modified when unwrapping wkt. Expected wkt: {wkt}. Actual: {actual.wkt}' + assert ( + actual_wrapped.wkt == wrapped + ), f'Failed to wrap wkt. Expected wkt: {wrapped}. Actual: {actual_wrapped}' def test_endpoints(**args) -> None: @@ -60,9 +60,9 @@ def test_endpoints(**args) -> None: if status_code == 200: response.raise_for_status() else: - assert response.status_code == status_code, ( - f'Expected error code {status_code}, got response code {response.status_code}, "{response.text}"' - ) + assert ( + response.status_code == status_code + ), f'Expected error code {status_code}, got response code {response.status_code}, "{response.text}"' expected_output = expected_response.get('expected_output') From 5a415f24e374e883a70ab44d21ed32d1d3b0432f Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Fri, 30 Oct 2026 13:29:11 -0800 Subject: [PATCH 06/11] chore: github actions use 2 space tabs. Remove deploy test/prod workflows --- .github/workflows/changelog.yml | 26 +++++----- .github/workflows/deploy-prod.yml | 23 --------- .github/workflows/deploy-test.yml | 25 --------- .github/workflows/lint.yml | 20 ++++---- .github/workflows/pr-labeled.yml | 24 ++++----- .github/workflows/run-pytest-against-uat.yml | 36 ++++++------- .github/workflows/run-pytest.yml | 54 ++++++++++---------- 7 files changed, 80 insertions(+), 128 deletions(-) delete mode 100644 .github/workflows/deploy-prod.yml delete mode 100644 .github/workflows/deploy-test.yml diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 28f69c9..3305f42 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -1,18 +1,18 @@ name: Changelog updated? on: - pull_request: - types: - - opened - - labeled - - unlabeled - - synchronize - branches: - - prod + pull_request: + types: + - opened + - labeled + - unlabeled + - synchronize + branches: + - prod jobs: - call-changelog-check-workflow: - # Docs: https://github.com/ASFHyP3/actions - uses: ASFHyP3/actions/.github/workflows/reusable-changelog-check.yml@v0.22.0 - permissions: - contents: read + call-changelog-check-workflow: + # Docs: https://github.com/ASFHyP3/actions + uses: ASFHyP3/actions/.github/workflows/reusable-changelog-check.yml@v0.22.0 + permissions: + contents: read diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml deleted file mode 100644 index e34a0f7..0000000 --- a/.github/workflows/deploy-prod.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: HDFReaderAPI deploy action (prod) - -on: - push: - branches: - - prod - -jobs: - deploy: - runs-on: ubuntu-latest - environment: prod - permissions: - id-token: write - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: build - uses: ./.github/workflows/hdf-reader-api-composite - with: - aws-account-id: ${{ secrets.AWS_ACCOUNT_ID }} diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml deleted file mode 100644 index 28ed2e4..0000000 --- a/.github/workflows/deploy-test.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: HDFReaderAPI deploy action (test) - -on: - push: - branches: - - edc-deploy - - test - -jobs: - deploy: - runs-on: ubuntu-latest - environment: test - permissions: - id-token: write - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: build - uses: ./.github/workflows/hdf-reader-api-composite - with: - aws-account-id: ${{ secrets.AWS_ACCOUNT_ID }} - stack-id: CdkOperaDispStack diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index eb21636..1e3f92e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,16 +1,16 @@ name: Lint on: - pull_request: - types: [opened, edited, reopened] - push: + pull_request: + types: [opened, edited, reopened] + push: jobs: - lint: - runs-on: ubuntu-latest + lint: + runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/ruff-action@v4.1.0 - - run: ruff check --fix - - run: ruff format + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v4.1.0 + - run: ruff check --fix + - run: ruff format diff --git a/.github/workflows/pr-labeled.yml b/.github/workflows/pr-labeled.yml index 0638211..157540b 100644 --- a/.github/workflows/pr-labeled.yml +++ b/.github/workflows/pr-labeled.yml @@ -1,17 +1,17 @@ name: Is PR labeled? on: - pull_request: - types: - - opened - - labeled - - unlabeled - - synchronize - branches: - - prod + pull_request: + types: + - opened + - labeled + - unlabeled + - synchronize + branches: + - prod jobs: - call-labeled-pr-check-workflow: - uses: ASFHyP3/actions/.github/workflows/reusable-labeled-pr-check.yml@v0.22.0 - permissions: - contents: read + call-labeled-pr-check-workflow: + uses: ASFHyP3/actions/.github/workflows/reusable-labeled-pr-check.yml@v0.22.0 + permissions: + contents: read diff --git a/.github/workflows/run-pytest-against-uat.yml b/.github/workflows/run-pytest-against-uat.yml index 5d46568..84eb573 100644 --- a/.github/workflows/run-pytest-against-uat.yml +++ b/.github/workflows/run-pytest-against-uat.yml @@ -1,24 +1,24 @@ name: tests against uat on: - pull_request: - branches: prod - paths: - - "**.py" + pull_request: + branches: prod + paths: + - "**.py" jobs: - run-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v6 - with: - python-version: "3.13" - - name: Install Dependencies - run: | - python3 -m pip install --upgrade pip - python3 -m pip install -r src/requirements.txt - python3 -m pip install -r tests/requirements.txt + run-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install Dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install -r src/requirements.txt + python3 -m pip install -r tests/requirements.txt - - name: Run Tests - run: python3 -m pytest --use-uat tests/yml_tests/test_authorized.yml + - name: Run Tests + run: python3 -m pytest --use-uat tests/yml_tests/test_authorized.yml diff --git a/.github/workflows/run-pytest.yml b/.github/workflows/run-pytest.yml index 061a6f3..d49babd 100644 --- a/.github/workflows/run-pytest.yml +++ b/.github/workflows/run-pytest.yml @@ -1,33 +1,33 @@ name: tests on: - pull_request: - branches: test - paths: - - "**.py" + pull_request: + branches: test + paths: + - "**.py" jobs: - run-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v6 - with: - python-version: "3.13" - - name: Install Dependencies - run: | - python3 -m pip install --upgrade pip - python3 -m pip install -r src/requirements.txt - python3 -m pip install -r tests/requirements.txt + run-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install Dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install -r src/requirements.txt + python3 -m pip install -r tests/requirements.txt - - name: Run Tests - run: python3 -m pytest --df=test_authorized + - name: Run Tests + run: python3 -m pytest --df=test_authorized - # - name: Upload coverage to Codecov - # uses: codecov/codecov-action@v5 - # with: - # token: ${{ secrets.CODECOV_TOKEN }} - # fail_ci_if_error: false - # files: ./coverage.xml - # flags: unittests - # name: asf_admin pytest - # verbose: true + # - name: Upload coverage to Codecov + # uses: codecov/codecov-action@v5 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} + # fail_ci_if_error: false + # files: ./coverage.xml + # flags: unittests + # name: asf_admin pytest + # verbose: true From e9e14ecea5049a48dfaae52cb23e6793cea04e21 Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Fri, 30 Oct 2026 14:13:25 -0800 Subject: [PATCH 07/11] chore: switch to reusable-ruff workflow --- .github/workflows/lint.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1e3f92e..06233ba 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,11 +6,8 @@ on: push: jobs: - lint: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: astral-sh/ruff-action@v4.1.0 - - run: ruff check --fix - - run: ruff format + call-ruff-workflow: + # Docs: https://github.com/ASFHyP3/actions + uses: ASFHyP3/actions/.github/workflows/reusable-ruff.yml@v0.22.0 + permissions: + contents: read From 3c302e5f0f0e603ee30e912535dbd32fc8829d2d Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Fri, 30 Oct 2026 14:15:45 -0800 Subject: [PATCH 08/11] bug: README markdown formatting --- README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9713256..995173c 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ Returns a json timeseries, mapping file names to a sampled point's `short_wavele }, } } +``` + ``` params: wkt: str @@ -51,18 +53,21 @@ params: polarization: Polarizations = Polarizations.VV frameId: str | None = Field(None, pattern=r'^\d{5}$') - Overrides which frame to search +``` ### POST `/get_frame_id` +``` params: wkt: str - A WKT POINT string flightDirection: str - `ASCENDING` or `DESCENDING` pass +``` # Install and Run Locally Run pip install from the project's root folder ``` bash - pip install -r src/requirements.txt +pip install -r src/requirements.txt ``` @@ -77,7 +82,7 @@ To run all tests locally valid AWS credentials are needed for accessing zarr ref To install test dependencies: ``` bash - pip install -r tests/requirements.txt +pip install -r tests/requirements.txt ``` To run From 65f15e6472d6dec55e457a818fdb5a56dcb085c7 Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Fri, 30 Oct 2026 15:00:53 -0800 Subject: [PATCH 09/11] bug: revert ruff format --- .github/workflows/lint.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 06233ba..1e3f92e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,8 +6,11 @@ on: push: jobs: - call-ruff-workflow: - # Docs: https://github.com/ASFHyP3/actions - uses: ASFHyP3/actions/.github/workflows/reusable-ruff.yml@v0.22.0 - permissions: - contents: read + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v4.1.0 + - run: ruff check --fix + - run: ruff format From 2ffc1f0ddd3a63c4cf2353a211b8f246a0dd5849 Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Tue, 3 Nov 2026 11:13:13 -0900 Subject: [PATCH 10/11] chore: update configure-aws-credentials action --- .github/workflows/hdf-reader-api-composite/action.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/hdf-reader-api-composite/action.yml b/.github/workflows/hdf-reader-api-composite/action.yml index 422f359..f388a8f 100644 --- a/.github/workflows/hdf-reader-api-composite/action.yml +++ b/.github/workflows/hdf-reader-api-composite/action.yml @@ -17,7 +17,6 @@ inputs: required: false type: string - runs: using: "composite" steps: @@ -25,7 +24,7 @@ runs: uses: actions/checkout@v4 - name: Configure AWS OIDC credentials - uses: aws-actions/configure-aws-credentials@v3 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::${{ inputs.aws-account-id }}:role/HDFReaderActionsOIDCRole aws-region: us-west-2 From 56a56120e2162a256ddeef81903be89cb0cb35a5 Mon Sep 17 00:00:00 2001 From: SpicyGarlicAlbacoreRoll Date: Tue, 3 Nov 2026 11:55:25 -0900 Subject: [PATCH 11/11] test: update uat test endpoint to DNS endpoint --- tests/pytest-managers.py | 64 ++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/tests/pytest-managers.py b/tests/pytest-managers.py index 33937f7..c5e9d08 100644 --- a/tests/pytest-managers.py +++ b/tests/pytest-managers.py @@ -8,77 +8,77 @@ from src.application import _wrap_wkt, app -timeseries_endpoint = '/timeseries' +timeseries_endpoint = "/timeseries" client = TestClient(app) -uat_endpoint = 'https://d8itg4twhevb5.cloudfront.net' +uat_endpoint = "https://displacement-api-test.asf.alaska.edu" def _query_api(endpoint: str, params: dict, method: str, use_uat: bool): match method: - case 'GET': + case "GET": p = urllib.parse.urlencode(params) return ( - requests.get(f'{uat_endpoint}{endpoint}?{p}', timeout=30) + requests.get(f"{uat_endpoint}{endpoint}?{p}", timeout=30) if use_uat - else client.get(url=f'{endpoint}?{p}') + else client.get(url=f"{endpoint}?{p}") ) - case 'POST': + case "POST": return ( - requests.post(f'{uat_endpoint}{endpoint}', json=params, timeout=30) + requests.post(f"{uat_endpoint}{endpoint}", json=params, timeout=30) if use_uat - else client.post(url=f'{endpoint}', json=params) + else client.post(url=f"{endpoint}", json=params) ) case _: - raise NotImplementedError(f'Method {method} not implemented') + raise NotImplementedError(f"Method {method} not implemented") def test_wkt_wrapping(**args) -> None: - test_info = args['test_info'] - wkt, wrapped = test_info['wkt'], test_info['wrapped'] + test_info = args["test_info"] + wkt, wrapped = test_info["wkt"], test_info["wrapped"] actual, actual_wrapped = _wrap_wkt(wkt) - assert ( - actual.wkt == wkt - ), f'Original geometry modified when unwrapping wkt. Expected wkt: {wkt}. Actual: {actual.wkt}' - assert ( - actual_wrapped.wkt == wrapped - ), f'Failed to wrap wkt. Expected wkt: {wrapped}. Actual: {actual_wrapped}' + assert actual.wkt == wkt, ( + f"Original geometry modified when unwrapping wkt. Expected wkt: {wkt}. Actual: {actual.wkt}" + ) + assert actual_wrapped.wkt == wrapped, ( + f"Failed to wrap wkt. Expected wkt: {wrapped}. Actual: {actual_wrapped}" + ) def test_endpoints(**args) -> None: - test_info = args['test_info'] - params = test_info['params'] - expected_response = test_info['response'] - endpoint = test_info['endpoint'] - method = test_info['method'] - use_uat = args['config'].getoption('use_uat') + test_info = args["test_info"] + params = test_info["params"] + expected_response = test_info["response"] + endpoint = test_info["endpoint"] + method = test_info["method"] + use_uat = args["config"].getoption("use_uat") - status_code = expected_response['status_code'] + status_code = expected_response["status_code"] response = _query_api(endpoint, params, method, use_uat) if status_code == 200: response.raise_for_status() else: - assert ( - response.status_code == status_code - ), f'Expected error code {status_code}, got response code {response.status_code}, "{response.text}"' + assert response.status_code == status_code, ( + f'Expected error code {status_code}, got response code {response.status_code}, "{response.text}"' + ) - expected_output = expected_response.get('expected_output') + expected_output = expected_response.get("expected_output") if expected_output is not None: expected_output = _get_resource(expected_output) body = response.json() - assert sorted(body) == sorted(expected_output), f'Expected {expected_output}, got {body}' + assert sorted(body) == sorted(expected_output), f"Expected {expected_output}, got {body}" # Finds and loads file from yml_tests/Resouces/ if loaded field ends with .yml/yaml extension def _get_resource(yml_file): if isinstance(yml_file, str): - if yml_file.endswith(('.yml', '.yaml')): + if yml_file.endswith((".yml", ".yaml")): base_path = pathlib.Path(__file__).parent.resolve() - with open(os.path.join(base_path, 'yml_tests', 'resources', yml_file), 'r') as f: + with open(os.path.join(base_path, "yml_tests", "resources", yml_file), "r") as f: try: return yaml.safe_load(f) except yaml.YAMLError as exc: @@ -86,7 +86,7 @@ def _get_resource(yml_file): elif isinstance(yml_file, list): # noqa: SIM102 # check if it's a list of yml files if len(yml_file) > 0: # noqa: SIM102 if isinstance(yml_file[0], str): # noqa: SIM102 - if yml_file[0].endswith(('.yml', '.yaml')): + if yml_file[0].endswith((".yml", ".yaml")): return [_get_resource(file) for file in yml_file] return yml_file