Skip to content

Commit 1a0cca5

Browse files
Merge pull request #2810 from ASFHyP3/develop
Release v10.8.0
2 parents 41c7d17 + 863d3c5 commit 1a0cca5

16 files changed

+267
-248
lines changed

CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [10.8.0]
8+
9+
### Added
10+
- `cloudformation:DeleteStack` permissions to the [HyP3 deployment policy](cicd-stacks/JPL-deployment-policy-cf.yml) for JPL accounts
11+
12+
### Changed
13+
- `ARIA_S1_GUNW` now takes `reference_date` and `secondary_date` as inputs instead of `reference` and `secondary` granule lists
14+
- `ARIA_S1_GUNW` jobs now enforce minimum frame coverage of `0.9`.
15+
- `OPERA_RTC_S1` processing bounds have been expanded to scenes north of (or intersecting) -60 degrees latitude.
16+
17+
### Removed
18+
- DEM bounds check for OPERA_RTC_S1 job type since it uses a different DEM.
19+
720
## [10.7.0]
821

922
### Added
@@ -82,7 +95,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8295

8396
### Changed
8497
- `render-cf.py` now determines the version number to report in the API from the git history and tags using [`setuptools_scm`](https://pypi.org/project/setuptools-scm/).
85-
98+
8699
> [!WARNING]
87100
> In CI/CD pipelines, to dynamically calculate the version number you must now check out the full history and tags (no shallow clones). In GitHub Actions, this usually looks like specifying `fetch-depth: 0` with `actions/checkout`. For pipelines where you *do not* care about an accurate version number, you can still use a shallow clone by setting the `SETUPTOOLS_SCM_PRETEND_VERSION_FOR_HYP3` environment variable, see: <http://setuptools-scm.readthedocs.io/en/latest/overrides/>.
88101

apps/api/src/hyp3_api/handlers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
)
1414
from hyp3_api import util
1515
from hyp3_api.multi_burst_validation import MultiBurstValidationError
16-
from hyp3_api.validation import BoundsValidationError, GranuleValidationError, validate_jobs
16+
from hyp3_api.validation import ValidationError, validate_jobs
1717

1818

1919
def problem_format(status: int, message: str) -> Response:
@@ -31,7 +31,7 @@ def post_jobs(body: dict, user: str) -> dict:
3131
except requests.HTTPError as e:
3232
print(f'CMR search failed: {e}')
3333
abort(problem_format(503, 'Could not submit jobs due to a CMR error. Please try again later.'))
34-
except (BoundsValidationError, GranuleValidationError, MultiBurstValidationError) as e:
34+
except (ValidationError, MultiBurstValidationError) as e:
3535
abort(problem_format(400, str(e)))
3636

3737
try:

apps/api/src/hyp3_api/validation.py

Lines changed: 53 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import requests
99
import yaml
10-
from shapely.geometry import MultiPolygon, Polygon, shape
10+
from shapely.geometry import MultiPolygon, Polygon, box, shape
1111

1212
from hyp3_api import CMR_URL, multi_burst_validation
1313
from hyp3_api.util import get_granules
@@ -22,11 +22,7 @@ class InternalValidationError(Exception):
2222
pass
2323

2424

25-
class GranuleValidationError(Exception):
26-
pass
27-
28-
29-
class BoundsValidationError(Exception):
25+
class ValidationError(Exception):
3026
pass
3127

3228

@@ -84,13 +80,13 @@ def _make_sure_granules_exist(granules: Iterable[str], granule_metadata: list[di
8480
not_found_granules = set(granules) - set(found_granules)
8581
not_found_granules = {granule for granule in not_found_granules if not _is_third_party_granule(granule)}
8682
if not_found_granules:
87-
raise GranuleValidationError(f'Some requested scenes could not be found: {", ".join(not_found_granules)}')
83+
raise ValidationError(f'Some requested scenes could not be found: {", ".join(not_found_granules)}')
8884

8985

9086
def check_dem_coverage(_, granule_metadata: list[dict]) -> None:
9187
bad_granules = [g['name'] for g in granule_metadata if not _has_sufficient_coverage(g['polygon'])]
9288
if bad_granules:
93-
raise GranuleValidationError(f'Some requested scenes do not have DEM coverage: {", ".join(bad_granules)}')
89+
raise ValidationError(f'Some requested scenes do not have DEM coverage: {", ".join(bad_granules)}')
9490

9591

9692
def check_multi_burst_pairs(job: dict, _) -> None:
@@ -114,17 +110,17 @@ def check_single_burst_pair(job: dict, _) -> None:
114110
granule2_id = '_'.join(granule2.split('_')[1:3])
115111

116112
if granule1_id != granule2_id:
117-
raise GranuleValidationError(f'Burst IDs do not match for {granule1} and {granule2}.')
113+
raise ValidationError(f'Burst IDs do not match for {granule1} and {granule2}.')
118114

119115
granule1_pol = granule1.split('_')[4]
120116
granule2_pol = granule2.split('_')[4]
121117

122118
if granule1_pol != granule2_pol:
123-
raise GranuleValidationError(
119+
raise ValidationError(
124120
f'The requested scenes need to have the same polarization, got: {", ".join([granule1_pol, granule2_pol])}'
125121
)
126122
if granule1_pol not in ['VV', 'HH']:
127-
raise GranuleValidationError(f'Only VV and HH polarizations are currently supported, got: {granule1_pol}')
123+
raise ValidationError(f'Only VV and HH polarizations are currently supported, got: {granule1_pol}')
128124

129125

130126
def check_not_antimeridian(_, granule_metadata: list[dict]) -> None:
@@ -135,7 +131,7 @@ def check_not_antimeridian(_, granule_metadata: list[dict]) -> None:
135131
f'Granule {granule["name"]} crosses the antimeridian.'
136132
' Processing across the antimeridian is not currently supported.'
137133
)
138-
raise GranuleValidationError(msg)
134+
raise ValidationError(msg)
139135

140136

141137
def _format_points(point_string: str) -> list:
@@ -155,10 +151,10 @@ def _get_multipolygon_from_geojson(input_file: str) -> MultiPolygon:
155151
def check_bounds_formatting(job: dict, _) -> None:
156152
bounds = job['job_parameters']['bounds']
157153
if bounds == [0.0, 0.0, 0.0, 0.0]:
158-
raise BoundsValidationError('Invalid bounds. Bounds cannot be [0, 0, 0, 0].')
154+
raise ValidationError('Invalid bounds. Bounds cannot be [0, 0, 0, 0].')
159155

160156
if bounds[0] >= bounds[2] or bounds[1] >= bounds[3]:
161-
raise BoundsValidationError(
157+
raise ValidationError(
162158
'Invalid order for bounds. Bounds should be ordered [min lon, min lat, max lon, max lat].'
163159
)
164160

@@ -169,15 +165,15 @@ def bad_lon(lon: float) -> bool:
169165
return lon > 180 or lon < -180
170166

171167
if any([bad_lon(bounds[0]), bad_lon(bounds[2]), bad_lat(bounds[1]), bad_lat(bounds[3])]):
172-
raise BoundsValidationError(
168+
raise ValidationError(
173169
'Invalid lon/lat value(s) in bounds. Bounds should be ordered [min lon, min lat, max lon, max lat].'
174170
)
175171

176172

177173
def check_granules_intersecting_bounds(job: dict, granule_metadata: list[dict]) -> None:
178174
bounds = job['job_parameters']['bounds']
179175
if bounds == [0.0, 0.0, 0.0, 0.0]:
180-
raise BoundsValidationError('Invalid bounds. Bounds cannot be [0, 0, 0, 0].')
176+
raise ValidationError('Invalid bounds. Bounds cannot be [0, 0, 0, 0].')
181177

182178
bounds = Polygon.from_bounds(*bounds)
183179
bad_granules = []
@@ -186,7 +182,7 @@ def check_granules_intersecting_bounds(job: dict, granule_metadata: list[dict])
186182
if not bbox.intersection(bounds):
187183
bad_granules.append(granule['name'])
188184
if bad_granules:
189-
raise GranuleValidationError(f'The following granules do not intersect the provided bounds: {bad_granules}.')
185+
raise ValidationError(f'The following granules do not intersect the provided bounds: {bad_granules}.')
190186

191187

192188
def check_same_relative_orbits(_, granule_metadata: list[dict]) -> None:
@@ -200,7 +196,7 @@ def check_same_relative_orbits(_, granule_metadata: list[dict]) -> None:
200196
if not previous_relative_orbit:
201197
previous_relative_orbit = relative_orbit
202198
if relative_orbit != previous_relative_orbit:
203-
raise GranuleValidationError(
199+
raise ValidationError(
204200
f'Relative orbit number for {granule["name"]} does not match that of the previous granules: '
205201
f'{relative_orbit} is not {previous_relative_orbit}.'
206202
)
@@ -212,32 +208,43 @@ def check_bounding_box_size(job: dict, _, max_bounds_area: float = 4.5) -> None:
212208
bounds_area = (bounds[3] - bounds[1]) * (bounds[2] - bounds[0])
213209

214210
if bounds_area > max_bounds_area:
215-
raise BoundsValidationError(
211+
raise ValidationError(
216212
f'Bounds must be smaller than {max_bounds_area} degrees squared. Box provided was {bounds_area:.2f}'
217213
)
218214

219215

220-
def _has_opera_rtc_s1_static_coverage(granule_name: str) -> bool:
221-
burst_number, swath = granule_name.split('_')[1:3]
222-
params = {
223-
'short_name': 'OPERA_L2_RTC-S1-STATIC_V1',
224-
'granule_ur': f'OPERA_L2_RTC-S1-STATIC_T*-{burst_number}-{swath}_*',
225-
'options[granule_ur][pattern]': 'true',
226-
}
227-
response = requests.get(CMR_URL, params=params)
228-
response.raise_for_status()
229-
return bool(response.json()['feed']['entry'])
216+
def check_opera_rtc_s1_bounds(_, granule_metadata: list[dict]) -> None:
217+
opera_rtc_s1_bounds = box(-180, -60, 180, 90)
218+
for granule in granule_metadata:
219+
if not granule['polygon'].intersects(opera_rtc_s1_bounds):
220+
raise ValidationError(
221+
f'Granule {granule["name"]} is south of -60 degrees latitude and outside the valid processing extent '
222+
f'for OPERA RTC-S1 products.'
223+
)
230224

231225

232-
def check_opera_rtc_s1_static_coverage(job: dict, _) -> None:
233-
granules = job['job_parameters']['granules']
234-
if len(granules) != 1:
235-
raise InternalValidationError(f'Expected 1 granule, got {granules}')
226+
def check_aria_s1_gunw_dates(job: dict, _) -> None:
227+
def format_date(key: str) -> date:
228+
return date.fromisoformat(job['job_parameters'][key])
236229

237-
granule = granules[0]
238-
if not _has_opera_rtc_s1_static_coverage(granule):
239-
raise GranuleValidationError(
240-
f'Granule {granule} is outside the valid processing extent for OPERA RTC-S1 products.'
230+
reference, secondary = format_date('reference_date'), format_date('secondary_date')
231+
_validate_date_during_s1('reference_date', reference)
232+
_validate_date_during_s1('secondary_date', secondary)
233+
234+
if secondary >= reference:
235+
raise ValidationError('secondary date must be earlier than reference date.')
236+
237+
238+
def _validate_date_during_s1(date_name: str, date_value: date) -> None:
239+
s1_start_date = date(2014, 6, 15)
240+
todays_date = date.today()
241+
242+
if date_value > todays_date:
243+
raise ValidationError(f'"{date_name}" is {date_value} which is a date in the future.')
244+
245+
if date_value < s1_start_date:
246+
raise ValidationError(
247+
f'"{date_name}" is {date_value} which is before the start of the sentinel 1 mission ({s1_start_date}).'
241248
)
242249

243250

@@ -252,7 +259,7 @@ def check_opera_rtc_s1_date(job: dict, _) -> None:
252259
# Disallow IPF version < 002.70 according to the dates given at https://sar-mpc.eu/processor/ipf/
253260
# Also see https://github.com/ASFHyP3/hyp3/issues/2739
254261
if granule_date < date(2016, 4, 14):
255-
raise GranuleValidationError(
262+
raise ValidationError(
256263
f'Granule {granule} was acquired before 2016-04-14 '
257264
'and is not available for On-Demand OPERA RTC-S1 processing.'
258265
)
@@ -263,7 +270,7 @@ def check_opera_rtc_s1_date(job: dict, _) -> None:
263270

264271
end_date = datetime.strptime(end_date_str, '%Y-%m-%d').date()
265272
if granule_date >= end_date:
266-
raise GranuleValidationError(
273+
raise ValidationError(
267274
f'Granule {granule} was acquired on or after {end_date_str} '
268275
'and is not available for On-Demand OPERA RTC-S1 processing. '
269276
'You can download the product from the ASF DAAC archive.'
@@ -272,8 +279,13 @@ def check_opera_rtc_s1_date(job: dict, _) -> None:
272279

273280
def validate_jobs(jobs: list[dict]) -> None:
274281
granules = get_granules(jobs)
275-
granule_metadata = _get_cmr_metadata(granules)
276-
_make_sure_granules_exist(granules, granule_metadata)
282+
283+
if granules:
284+
granule_metadata = _get_cmr_metadata(granules)
285+
_make_sure_granules_exist(granules, granule_metadata)
286+
else:
287+
granule_metadata = []
288+
277289
for job in jobs:
278290
for validator_name in JOB_VALIDATION_MAP[job['job_type']]:
279291
job_granule_metadata = [granule for granule in granule_metadata if granule['name'] in get_granules([job])]

cicd-stacks/JPL-deployment-policy-cf.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Resources:
5151
- cloudformation:SetStackPolicy
5252
- cloudformation:CreateStack
5353
- cloudformation:UpdateStack
54+
- cloudformation:DeleteStack
5455
- cloudformation:CreateChangeSet
5556
- cloudformation:DescribeChangeSet
5657
- cloudformation:ExecuteChangeSet

job_spec/ARIA_S1_GUNW.yml

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,21 @@
11
ARIA_S1_GUNW:
22
required_parameters:
3-
- reference
4-
- secondary
3+
- reference_date
4+
- secondary_date
55
- frame_id
66
parameters:
7-
reference:
7+
reference_date:
88
api_schema:
9-
type: array
10-
minItems: 1
11-
maxItems: 3
12-
example:
13-
- S1A_IW_SLC__1SDV_20191231T135206_20191231T135234_030593_03813A_D336
14-
- S1A_IW_SLC__1SDV_20191231T135141_20191231T135208_030593_03813A_837F
15-
items:
16-
description: The names of the Sentinel-1 SLC granules to use as reference scenes for InSAR processing
17-
type: string
18-
pattern: "^S1[AB]_IW_SLC__1S[SD]V"
19-
minLength: 67
20-
maxLength: 67
21-
example: S1A_IW_SLC__1SDV_20191231T135206_20191231T135234_030593_03813A_D336
22-
secondary:
9+
description: The date to find Sentinel-1 SLC granules to use as reference scenes for InSAR processing
10+
type: string
11+
format: date
12+
example: "2019-12-31"
13+
secondary_date:
2314
api_schema:
24-
type: array
25-
minItems: 1
26-
maxItems: 3
27-
example:
28-
- S1A_IW_SLC__1SDV_20181212T135155_20181212T135222_024993_02C174_2111
29-
- S1A_IW_SLC__1SDV_20181212T135130_20181212T135157_024993_02C174_57AC
30-
items:
31-
description: The names of the Sentinel-1 SLC granules to use as secondary scenes for InSAR processing
32-
type: string
33-
pattern: "^S1[AB]_IW_SLC__1S[SD]V"
34-
minLength: 67
35-
maxLength: 67
36-
example: S1A_IW_SLC__1SDV_20181212T135155_20181212T135222_024993_02C174_2111
15+
description: The date to find Sentinel-1 SLC granules to use as secondary scenes for InSAR processing
16+
type: string
17+
format: date
18+
example: "2018-12-12"
3719
frame_id:
3820
api_schema:
3921
description: Subset GUNW products to this frame.
@@ -47,7 +29,7 @@ ARIA_S1_GUNW:
4729
DEFAULT:
4830
cost: 1.0
4931
validators:
50-
- check_dem_coverage
32+
- check_aria_s1_gunw_dates
5133
steps:
5234
- name: ''
5335
image: ghcr.io/access-cloud-based-insar/dockerizedtopsapp
@@ -58,14 +40,14 @@ ARIA_S1_GUNW:
5840
- '!Ref Bucket'
5941
- --bucket-prefix
6042
- Ref::job_id
61-
- --reference-scenes
62-
- Ref::reference
63-
- --secondary-scenes
64-
- Ref::secondary
43+
- --reference-date
44+
- Ref::reference_date
45+
- --secondary-date
46+
- Ref::secondary_date
6547
- --frame-id
6648
- Ref::frame_id
6749
- --min-frame-coverage
68-
- '0.01'
50+
- '0.9'
6951
timeout: 21600 # 6 hr
7052
compute_environment: AriaS1Gunw
7153
vcpu: 1

job_spec/OPERA_RTC_S1.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ OPERA_RTC_S1:
2424
cost: 1.0
2525
validators:
2626
- check_opera_rtc_s1_date
27-
- check_dem_coverage
28-
# Static coverage check is the slowest because it involves a CMR query, so should come last
29-
- check_opera_rtc_s1_static_coverage
27+
- check_opera_rtc_s1_bounds
3028
steps:
3129
- name: ''
3230
image: ghcr.io/asfhyp3/hyp3-opera-rtc

requirements-all.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
-r requirements-apps-start-execution.txt
55
-r requirements-apps-disable-private-dns.txt
66
-r requirements-apps-update-db.txt
7-
boto3==1.38.32
7+
boto3==1.38.37
88
jinja2==3.1.6
9-
moto[dynamodb]==5.1.5
9+
moto[dynamodb]==5.1.6
1010
pytest==8.4.0
1111
PyYAML==6.0.2
1212
responses==0.25.7
1313
ruff==0.11.13
14-
mypy==1.16.0
14+
mypy==1.16.1
1515
setuptools==80.9.0
1616
setuptools_scm==8.3.1
1717
openapi-spec-validator==0.7.2
18-
cfn-lint==1.35.4
18+
cfn-lint==1.36.0

requirements-apps-api-binary.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
cryptography==45.0.3
1+
cryptography==45.0.4

requirements-apps-api.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
flask==3.1.1
2-
Flask-Cors==6.0.0
2+
Flask-Cors==6.0.1
33
jsonschema==4.24.0
44
openapi-core==0.19.5
55
prance==25.4.8.0
66
PyJWT==2.10.1
77
requests==2.32.4
8-
serverless_wsgi==3.0.5
8+
serverless_wsgi==3.1.0
99
shapely==2.1.1
1010
strict-rfc3339==0.7
1111
./lib/dynamo/

0 commit comments

Comments
 (0)