[Git][debian-gis-team/asf-search][upstream] New upstream version 8.1.2
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Sat Apr 19 06:47:30 BST 2025
Antonio Valentino pushed to branch upstream at Debian GIS Project / asf-search
Commits:
de27f2f9 by Antonio Valentino at 2025-04-19T05:41:42+00:00
New upstream version 8.1.2
- - - - -
16 changed files:
- CHANGELOG.md
- asf_search/ASFSearchOptions/validator_map.py
- asf_search/CMR/field_map.py
- asf_search/CMR/translate.py
- asf_search/Products/NISARProduct.py
- asf_search/__init__.py
- asf_search/constants/POLARIZATION.py
- + asf_search/constants/RANGE_BANDWIDTH.py
- asf_search/constants/__init__.py
- asf_search/export/jsonlite.py
- asf_search/export/jsonlite2.py
- asf_search/search/geo_search.py
- asf_search/search/search.py
- asf_search/search/search_count.py
- asf_search/search/search_generator.py
- tests/yml_tests/test_ASFSearchOptions.yml
Changes:
=====================================
CHANGELOG.md
=====================================
@@ -26,6 +26,12 @@ and uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-->
------
+## [v8.1.2](https://github.com/asfadmin/Discovery-asf_search/compare/v8.1.1...v8.1.2)
+### Added
+- Added NISAR search parameters `frameCoverage`, `jointObservation`, `mainBandPolarization`, `sideBandPolarization`, `rangeBandwidth`.
+- Updated `NISARProduct` to include these new searchable fields in `properties` dictionary
+- Include new NISAR fields in jsonlite & jsonlite2 output
+------
## [v8.1.1](https://github.com/asfadmin/Discovery-asf_search/compare/v8.1.0...v8.1.1)
### Fixed
- SLC Burst product urls are now searchable with `find_urls()`
=====================================
asf_search/ASFSearchOptions/validator_map.py
=====================================
@@ -70,13 +70,22 @@ validator_map = {
'instrument': parse_string,
'collections': parse_string_list,
'shortName': parse_string_list,
+ 'dataset': parse_string_list,
+ 'cmr_keywords': parse_cmr_keywords_list,
+ # S1 Inteferrogram Filters
'temporalBaselineDays': parse_string_list,
+ # Opera Burst Filters
'operaBurstID': parse_string_list,
+ # SLC Burst Filters
'absoluteBurstID': parse_int_list,
'relativeBurstID': parse_int_list,
'fullBurstID': parse_string_list,
- 'dataset': parse_string_list,
- 'cmr_keywords': parse_cmr_keywords_list,
+ # nisar paramaters
+ 'frameCoverage': parse_string,
+ 'jointObservation': bool,
+ 'mainBandPolarization': parse_string_list,
+ 'sideBandPolarization': parse_string_list,
+ 'rangeBandwidth': parse_string_list,
# Config parameters Parser
'session': parse_session,
'host': parse_string,
=====================================
asf_search/CMR/field_map.py
=====================================
@@ -1,50 +1,53 @@
field_map = {
- # API parameter CMR keyword CMR format strings
- 'absoluteOrbit': {'key': 'orbit_number', 'fmt': '{0}'},
- 'asfFrame': {'key': 'attribute[]', 'fmt': 'int,FRAME_NUMBER,{0}'},
- 'maxBaselinePerp': {'key': 'attribute[]', 'fmt': 'float,INSAR_BASELINE,,{0}'},
- 'minBaselinePerp': {'key': 'attribute[]', 'fmt': 'float,INSAR_BASELINE,{0},'},
- 'bbox': {'key': 'bounding_box', 'fmt': '{0}'},
- 'beamMode': {'key': 'attribute[]', 'fmt': 'string,BEAM_MODE,{0}'},
- 'beamSwath': {'key': 'attribute[]', 'fmt': 'string,BEAM_MODE_TYPE,{0}'},
- 'campaign': {'key': 'attribute[]', 'fmt': 'string,MISSION_NAME,{0}'},
- 'circle': {'key': 'circle', 'fmt': '{0}'},
- 'maxDoppler': {'key': 'attribute[]', 'fmt': 'float,DOPPLER,,{0}'},
- 'minDoppler': {'key': 'attribute[]', 'fmt': 'float,DOPPLER,{0},'},
- 'maxFaradayRotation': {'key': 'attribute[]', 'fmt': 'float,FARADAY_ROTATION,,{0}'}, # noqa F401
- 'minFaradayRotation': {'key': 'attribute[]', 'fmt': 'float,FARADAY_ROTATION,{0},'}, # noqa F401
- 'flightDirection': {'key': 'attribute[]', 'fmt': 'string,ASCENDING_DESCENDING,{0}'}, # noqa F401
- 'flightLine': {'key': 'attribute[]', 'fmt': 'string,FLIGHT_LINE,{0}'},
- 'frame': {'key': 'attribute[]', 'fmt': 'int,CENTER_ESA_FRAME,{0}'},
- 'granule_list': {'key': 'readable_granule_name[]', 'fmt': '{0}'},
- 'groupID': {'key': 'attribute[]', 'fmt': 'string,GROUP_ID,{0}'},
- 'insarStackId': {'key': 'attribute[]', 'fmt': 'int,INSAR_STACK_ID,{0}'},
- 'linestring': {'key': 'line', 'fmt': '{0}'},
- 'lookDirection': {'key': 'attribute[]', 'fmt': 'string,LOOK_DIRECTION,{0}'},
- 'maxInsarStackSize': {'key': 'attribute[]', 'fmt': 'int,INSAR_STACK_SIZE,,{0}'},
- 'minInsarStackSize': {'key': 'attribute[]', 'fmt': 'int,INSAR_STACK_SIZE,{0},'},
- 'instrument': {'key': 'instrument[]', 'fmt': '{0}'},
- 'offNadirAngle': {'key': 'attribute[]', 'fmt': 'float,OFF_NADIR_ANGLE,{0}'},
- 'platform': {'key': 'platform[]', 'fmt': '{0}'},
- 'polarization': {'key': 'attribute[]', 'fmt': 'string,POLARIZATION,{0}'},
- 'point': {'key': 'point', 'fmt': '{0}'},
- 'polygon': {'key': 'polygon', 'fmt': '{0}'},
- 'processingDate': {'key': 'updated_since', 'fmt': '{0}'},
- 'processingLevel': {'key': 'attribute[]', 'fmt': 'string,PROCESSING_TYPE,{0}'},
- 'product_list': {'key': 'granule_ur[]', 'fmt': '{0}'},
- 'provider': {'key': 'provider', 'fmt': '{0}'},
- 'relativeOrbit': {'key': 'attribute[]', 'fmt': 'int,PATH_NUMBER,{0}'},
- 'temporal': {'key': 'temporal', 'fmt': '{0}'},
- 'collections': {'key': 'echo_collection_id[]', 'fmt': '{0}'},
- 'shortName': {'key': 'shortName', 'fmt': '{0}'},
- 'temporalBaselineDays': {
- 'key': 'attribute[]',
- 'fmt': 'int,TEMPORAL_BASELINE_DAYS,{0}',
- }, # noqa F401
+ # API parameter CMR keyword CMR format strings
+ 'absoluteOrbit': {'key': 'orbit_number', 'fmt': '{0}'},
+ 'asfFrame': {'key': 'attribute[]', 'fmt': 'int,FRAME_NUMBER,{0}'},
+ 'maxBaselinePerp': {'key': 'attribute[]', 'fmt': 'float,INSAR_BASELINE,,{0}'},
+ 'minBaselinePerp': {'key': 'attribute[]', 'fmt': 'float,INSAR_BASELINE,{0},'},
+ 'bbox': {'key': 'bounding_box', 'fmt': '{0}'},
+ 'beamMode': {'key': 'attribute[]', 'fmt': 'string,BEAM_MODE,{0}'},
+ 'beamSwath': {'key': 'attribute[]', 'fmt': 'string,BEAM_MODE_TYPE,{0}'},
+ 'campaign': {'key': 'attribute[]', 'fmt': 'string,MISSION_NAME,{0}'},
+ 'circle': {'key': 'circle', 'fmt': '{0}'},
+ 'maxDoppler': {'key': 'attribute[]', 'fmt': 'float,DOPPLER,,{0}'},
+ 'minDoppler': {'key': 'attribute[]', 'fmt': 'float,DOPPLER,{0},'},
+ 'maxFaradayRotation': {'key': 'attribute[]', 'fmt': 'float,FARADAY_ROTATION,,{0}'}, # noqa F401
+ 'minFaradayRotation': {'key': 'attribute[]', 'fmt': 'float,FARADAY_ROTATION,{0},'}, # noqa F401
+ 'flightDirection': {'key': 'attribute[]', 'fmt': 'string,ASCENDING_DESCENDING,{0}'}, # noqa F401
+ 'flightLine': {'key': 'attribute[]', 'fmt': 'string,FLIGHT_LINE,{0}'},
+ 'frame': {'key': 'attribute[]', 'fmt': 'int,CENTER_ESA_FRAME,{0}'},
+ 'granule_list': {'key': 'readable_granule_name[]', 'fmt': '{0}'},
+ 'groupID': {'key': 'attribute[]', 'fmt': 'string,GROUP_ID,{0}'},
+ 'insarStackId': {'key': 'attribute[]', 'fmt': 'int,INSAR_STACK_ID,{0}'},
+ 'linestring': {'key': 'line', 'fmt': '{0}'},
+ 'lookDirection': {'key': 'attribute[]', 'fmt': 'string,LOOK_DIRECTION,{0}'},
+ 'maxInsarStackSize': {'key': 'attribute[]', 'fmt': 'int,INSAR_STACK_SIZE,,{0}'},
+ 'minInsarStackSize': {'key': 'attribute[]', 'fmt': 'int,INSAR_STACK_SIZE,{0},'},
+ 'instrument': {'key': 'instrument[]', 'fmt': '{0}'},
+ 'offNadirAngle': {'key': 'attribute[]', 'fmt': 'float,OFF_NADIR_ANGLE,{0}'},
+ 'platform': {'key': 'platform[]', 'fmt': '{0}'},
+ 'polarization': {'key': 'attribute[]', 'fmt': 'string,POLARIZATION,{0}'},
+ 'point': {'key': 'point', 'fmt': '{0}'},
+ 'polygon': {'key': 'polygon', 'fmt': '{0}'},
+ 'processingDate': {'key': 'updated_since', 'fmt': '{0}'},
+ 'processingLevel': {'key': 'attribute[]', 'fmt': 'string,PROCESSING_TYPE,{0}'},
+ 'product_list': {'key': 'granule_ur[]', 'fmt': '{0}'},
+ 'provider': {'key': 'provider', 'fmt': '{0}'},
+ 'relativeOrbit': {'key': 'attribute[]', 'fmt': 'int,PATH_NUMBER,{0}'},
+ 'temporal': {'key': 'temporal', 'fmt': '{0}'},
+ 'collections': {'key': 'echo_collection_id[]', 'fmt': '{0}'},
+ 'shortName': {'key': 'shortName', 'fmt': '{0}'},
+ 'temporalBaselineDays': {'key': 'attribute[]', 'fmt': 'int,TEMPORAL_BASELINE_DAYS,{0}'}, # noqa F401
# SLC BURST fields
- 'absoluteBurstID': {'key': 'attribute[]', 'fmt': 'int,BURST_ID_ABSOLUTE,{0}'},
- 'relativeBurstID': {'key': 'attribute[]', 'fmt': 'int,BURST_ID_RELATIVE,{0}'},
- 'fullBurstID': {'key': 'attribute[]', 'fmt': 'string,BURST_ID_FULL,{0}'},
+ 'absoluteBurstID': {'key': 'attribute[]', 'fmt': 'int,BURST_ID_ABSOLUTE,{0}'},
+ 'relativeBurstID': {'key': 'attribute[]', 'fmt': 'int,BURST_ID_RELATIVE,{0}'},
+ 'fullBurstID': {'key': 'attribute[]', 'fmt': 'string,BURST_ID_FULL,{0}'},
# OPERA-S1 field
- 'operaBurstID': {'key': 'attribute[]', 'fmt': 'string,OPERA_BURST_ID,{0}'},
+ 'operaBurstID': {'key': 'attribute[]', 'fmt': 'string,OPERA_BURST_ID,{0}'},
+ # NISAR fields
+ 'mainBandPolarization': {'key': 'attribute[]', 'fmt': 'string,FREQUENCY_A_POLARIZATION_CONCAT,{0}'},
+ 'sideBandPolarization': {'key': 'attribute[]', 'fmt': 'string,FREQUENCY_B_POLARIZATION_CONCAT,{0}'},
+ 'frameCoverage': {'key': 'attribute[]', 'fmt': 'string,FULL_FRAME,{0}'},
+ 'jointObservation': {'key': 'attribute[]', 'fmt': 'string,JOINT_OBSERVATION,{0}'},
+ 'rangeBandwidth': {'key': 'attribute[]', 'fmt': 'string,RANGE_BANDWIDTH_CONCAT,{0}'},
}
=====================================
asf_search/CMR/translate.py
=====================================
@@ -30,6 +30,16 @@ def translate_opts(opts: ASFSearchOptions) -> List:
dict_opts = fix_cmr_shapes(dict_opts)
+ # Additional Attribute FULL_FRAME stored as a TRUE/FALSE string
+ if 'frameCoverage' in dict_opts:
+ dict_opts['frameCoverage'] = {
+ 'F': 'TRUE',
+ 'P': 'FALSE',
+ }[dict_opts['frameCoverage'][0].upper()]
+
+ if 'jointObservation' in dict_opts:
+ dict_opts['jointObservation'] = str(dict_opts['jointObservation']).upper()
+
# Special case to unravel WKT field a little for compatibility
if 'intersectsWith' in dict_opts:
shape = wkt.loads(dict_opts.pop('intersectsWith', None))
@@ -176,6 +186,22 @@ def try_parse_float(value: str) -> Optional[float]:
return float(value)
+def try_parse_bool(val: str) -> Optional[bool]:
+ """Boolean values are stored as strings in umm json"""
+ if val is None:
+ return None
+
+ return val.lower() == 'true'
+
+def try_parse_frame_coverage(val: str) -> Optional[str]:
+ """Frame Coverage is stored as a string boolean in FULL_FRAME, convert it to Partial/Full"""
+ if val is not None:
+ if val.lower() == 'true':
+ val = 'Full'
+ else:
+ val = 'Partial'
+
+ return val
def try_parse_date(value: str) -> Optional[str]:
if value is None:
=====================================
asf_search/Products/NISARProduct.py
=====================================
@@ -1,6 +1,6 @@
from typing import Dict, Tuple, Union
from asf_search import ASFSearchOptions, ASFSession, ASFStackableProduct
-
+from asf_search.CMR.translate import try_parse_frame_coverage, try_parse_bool
class NISARProduct(ASFStackableProduct):
"""
@@ -8,12 +8,15 @@ class NISARProduct(ASFStackableProduct):
ASF Dataset Documentation Page: https://asf.alaska.edu/nisar/
"""
-
_base_properties = {
**ASFStackableProduct._base_properties,
'pgeVersion': {'path': ['PGEVersionClass', 'PGEVersion']},
+ 'mainBandPolarization': {'path': ['AdditionalAttributes', ('Name', 'FREQUENCY_A_POLARIZATION'), 'Values']},
+ 'sideBandPolarization': {'path': ['AdditionalAttributes', ('Name', 'FREQUENCY_B_POLARIZATION'), 'Values']},
+ 'frameCoverage': {'path': ['AdditionalAttributes', ('Name', 'FULL_FRAME'), 'Values', 0], 'cast': try_parse_frame_coverage},
+ 'jointObservation': {'path': ['AdditionalAttributes', ('Name', 'JOINT_OBSERVATION'), 'Values', 0], 'cast': try_parse_bool},
+ 'rangeBandwidth': {'path': ['AdditionalAttributes', ('Name', 'RANGE_BANDWIDTH_CONCAT'), 'Values']},
}
-
def __init__(self, args: Dict = {}, session: ASFSession = ASFSession()):
super().__init__(args, session)
=====================================
asf_search/__init__.py
=====================================
@@ -45,6 +45,7 @@ from .constants import ( # noqa: F401 E402
PRODUCT_TYPE, # noqa: F401 E402
INTERNAL, # noqa: F401 E402
DATASET, # noqa: F401 E402
+ RANGE_BANDWIDTH, # noqa: F401 E402
)
from .health import * # noqa: F403 F401 E402
from .search import * # noqa: F403 F401 E402
=====================================
asf_search/constants/POLARIZATION.py
=====================================
@@ -14,3 +14,7 @@ HH_VV = 'HH+VV'
HH_HV_VH_VV = 'HH+HV+VH+VV'
FULL = 'full'
UNKNOWN = 'UNKNOWN'
+# NISAR
+LH_LV="LH+LV"
+RH_RV="RH+RV"
+HH_HV_VV_VH="HH+HV+VV+VH"
=====================================
asf_search/constants/RANGE_BANDWIDTH.py
=====================================
@@ -0,0 +1,13 @@
+# Nisar Sensor Bandwidths
+## L-SAR
+BW_20_5 = "20+5"
+BW_40_5 = "40+5"
+BW_77 = "77"
+BW_5 = "5"
+BW_5_5 = "5+5"
+
+## S-SAR
+BW_10 = "10"
+BW_25 = "25"
+BW_37 = "37"
+BW_75 = "75"
=====================================
asf_search/constants/__init__.py
=====================================
@@ -9,3 +9,4 @@ from .POLARIZATION import * # noqa: F403 F401
from .PRODUCT_TYPE import * # noqa: F403 F401
from .INTERNAL import * # noqa: F403 F401
from .DATASET import * # noqa: F403 F401
+from .RANGE_BANDWIDTH import * # noqa: F403 F401
=====================================
asf_search/export/jsonlite.py
=====================================
@@ -229,6 +229,16 @@ class JSONLiteStreamArray(list):
if p.get('validityStartDate'):
result['opera']['validityStartDate'] = p.get('validityStartDate')
+ if p.get('platform') == 'NISAR':
+ result['nisar'] = {
+ 'pgeVersion': p.get('pgeVersion'),
+ 'mainBandPolarization': p.get('mainBandPolarization'),
+ 'sideBandPolarization': p.get('sideBandPolarization'),
+ 'frameCoverage': p.get('frameCoverage'),
+ 'jointObservation': p.get('jointObservation'),
+ 'rangeBandwidth': p.get('rangeBandwidth'),
+ }
+
return result
def getOutputType(self) -> str:
=====================================
asf_search/export/jsonlite2.py
=====================================
@@ -76,6 +76,9 @@ class JSONLite2StreamArray(JSONLiteStreamArray):
if p.get('opera') is not None:
result['s1o'] = p['opera']
+ if p.get('nisar') is not None:
+ result['nisar'] = p['nisar']
+
return result
def getOutputType(self) -> str:
=====================================
asf_search/search/geo_search.py
=====================================
@@ -1,4 +1,4 @@
-from typing import Tuple, Union, Sequence
+from typing import Literal, Tuple, Union, Sequence
import datetime
from copy import copy
@@ -47,10 +47,15 @@ def geo_search(
absoluteBurstID: Union[int, Sequence[int]] = None,
relativeBurstID: Union[int, Sequence[int]] = None,
fullBurstID: Union[str, Sequence[str]] = None,
- collections: Union[str, Sequence[str]] = None,
temporalBaselineDays: Union[str, Sequence[str]] = None,
operaBurstID: Union[str, Sequence[str]] = None,
+ frameCoverage: Literal["FULL", "PARTIAL"] = None,
+ mainBandPolarization: Union[str, Sequence[str]] = None,
+ sideBandPolarization: Union[str, Sequence[str]] = None,
+ rangeBandwidth: Union[str, Sequence[str]] = None,
+ jointObservation: bool = None,
dataset: Union[str, Sequence[str]] = None,
+ collections: Union[str, Sequence[str]] = None,
shortName: Union[str, Sequence[str]] = None,
cmr_keywords: Union[Tuple[str, str], Sequence[Tuple[str, str]]] = None,
maxResults: int = None,
=====================================
asf_search/search/search.py
=====================================
@@ -1,5 +1,5 @@
import time
-from typing import Union, Sequence, Tuple
+from typing import Literal, Union, Sequence, Tuple
from copy import copy
import datetime
@@ -48,10 +48,15 @@ def search(
absoluteBurstID: Union[int, Sequence[int]] = None,
relativeBurstID: Union[int, Sequence[int]] = None,
fullBurstID: Union[str, Sequence[str]] = None,
- collections: Union[str, Sequence[str]] = None,
temporalBaselineDays: Union[str, Sequence[str]] = None,
operaBurstID: Union[str, Sequence[str]] = None,
+ frameCoverage: Literal['FULL', 'PARTIAL'] = None,
+ mainBandPolarization: Union[str, Sequence[str]] = None,
+ sideBandPolarization: Union[str, Sequence[str]] = None,
+ rangeBandwidth: Union[str, Sequence[str]] = None,
+ jointObservation: bool = None,
dataset: Union[str, Sequence[str]] = None,
+ collections: Union[str, Sequence[str]] = None,
shortName: Union[str, Sequence[str]] = None,
cmr_keywords: Union[Tuple[str, str], Sequence[Tuple[str, str]]] = None,
maxResults: int = None,
=====================================
asf_search/search/search_count.py
=====================================
@@ -1,5 +1,5 @@
import datetime
-from typing import Sequence, Tuple, Union
+from typing import Literal, Sequence, Tuple, Union
from copy import copy
from asf_search.ASFSearchOptions import ASFSearchOptions
from asf_search.CMR.subquery import build_subqueries
@@ -48,10 +48,15 @@ def search_count(
absoluteBurstID: Union[int, Sequence[int]] = None,
relativeBurstID: Union[int, Sequence[int]] = None,
fullBurstID: Union[str, Sequence[str]] = None,
- collections: Union[str, Sequence[str]] = None,
temporalBaselineDays: Union[str, Sequence[str]] = None,
operaBurstID: Union[str, Sequence[str]] = None,
+ frameCoverage: Literal["FULL", "PARTIAL"] = None,
+ mainBandPolarization: Union[str, Sequence[str]] = None,
+ sideBandPolarization: Union[str, Sequence[str]] = None,
+ rangeBandwidth: Union[str, Sequence[str]] = None,
+ jointObservation: bool = None,
dataset: Union[str, Sequence[str]] = None,
+ collections: Union[str, Sequence[str]] = None,
shortName: Union[str, Sequence[str]] = None,
cmr_keywords: Union[Tuple[str, str], Sequence[Tuple[str, str]]] = None,
maxResults: int = None,
=====================================
asf_search/search/search_generator.py
=====================================
@@ -1,5 +1,5 @@
import time
-from typing import Dict, Generator, Union, Sequence, Tuple, List
+from typing import Dict, Generator, Literal, Union, Sequence, Tuple, List
from copy import copy
from requests.exceptions import HTTPError
from requests import ReadTimeout, Response
@@ -74,10 +74,15 @@ def search_generator(
absoluteBurstID: Union[int, Sequence[int]] = None,
relativeBurstID: Union[int, Sequence[int]] = None,
fullBurstID: Union[str, Sequence[str]] = None,
- collections: Union[str, Sequence[str]] = None,
temporalBaselineDays: Union[str, Sequence[str]] = None,
operaBurstID: Union[str, Sequence[str]] = None,
+ frameCoverage: Literal["FULL", "PARTIAL"] = None,
+ mainBandPolarization: Union[str, Sequence[str]] = None,
+ sideBandPolarization: Union[str, Sequence[str]] = None,
+ rangeBandwidth: Union[str, Sequence[str]] = None,
+ jointObservation: bool = None,
dataset: Union[str, Sequence[str]] = None,
+ collections: Union[str, Sequence[str]] = None,
shortName: Union[str, Sequence[str]] = None,
cmr_keywords: Union[Tuple[str, str], Sequence[Tuple[str, str]]] = None,
maxResults: int = None,
=====================================
tests/yml_tests/test_ASFSearchOptions.yml
=====================================
@@ -99,6 +99,24 @@ tests:
output: null
error: Invalid int list
+- test-validators parse_bbox_list:
+ validator: parse_bbox_list
+ input: [0.0, 0.0, 1.1, 2.5]
+ output: [0.0, 0.0, 1.1, 2.5]
+ error: null
+
+- test-validators parse_bbox_list error strings:
+ validator: parse_bbox_list
+ input: [0.0, 0.0, 1.1, 2.5, 5.5]
+ output: null
+ error: Invalid coordinate list
+
+- test-validators parse_bbox_list error strings:
+ validator: parse_bbox_list
+ input: [0.0, 0.0, 1.1, 2.5, 5.5, 0.0]
+ output: null
+ error: Invalid bbox
+
- test-ASFSearchOptions - create blank object:
exception: Null
# At least once, make sure they all exist but are None:
View it on GitLab: https://salsa.debian.org/debian-gis-team/asf-search/-/commit/de27f2f9407540c05db07426e89c9a0a29e0b103
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/asf-search/-/commit/de27f2f9407540c05db07426e89c9a0a29e0b103
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20250419/d7f82d2c/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list