Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V8.0.0 Release #310

Merged
merged 56 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
01381ef
Moved logging up, so if version check fails, we can still log it
Cameronsplaze Jul 11, 2023
8816940
Moved logging up, to log when setup doesn't work correctly
Cameronsplaze Jul 11, 2023
8ea19ae
update for using asf_search in SearchAPI
Cameronsplaze Aug 9, 2023
c91bb79
Adding in support for 'circle' keyword. Exposed in ASFSearchOptions, …
Cameronsplaze Aug 10, 2023
f0f9172
Removed debug print I forgot about
Cameronsplaze Aug 10, 2023
92e12b3
Exposed get_urls method, to use in SearchAPI. Fixed bug where fileTyp…
Cameronsplaze Aug 17, 2023
5972616
Made it clear that baseline doesn't support classic search options. T…
Cameronsplaze Aug 17, 2023
d19a24f
Updated with where this branch is currently at
Cameronsplaze Aug 18, 2023
b5a8910
Merge branch 'master' into cs.searchapi-v3-edits
SpicyGarlicAlbacoreRoll Aug 22, 2023
9dc3a33
Added circle key to search, useable without opts. Also made it so gen…
Cameronsplaze Aug 31, 2023
b180715
Merge branch 'master' into cs.searchapi-v3-edits
Aug 31, 2023
45460e3
updates download file test case. Adds _has_multiple_files() method to…
Sep 1, 2023
89ba238
Merge branch 'master' into cs.searchapi-v3-edits
Feb 8, 2024
e4f45ed
removes missing import in baseline_searhc.py
Feb 8, 2024
ec570e7
adds some checks for unavailable fields in jsonlite
Feb 13, 2024
272b3ca
updated to searchapi baseline tests
Feb 27, 2024
a3d1221
line string work
Feb 29, 2024
f92316e
Merge branch 'feature-nisar-dataset' into cs.searchapi-v3-edits
Feb 29, 2024
3bb63e0
moves linestring repair before intersectsWith conversion
Feb 29, 2024
2fd3252
Merge branch 'cmr-keyword-search' into cs.searchapi-v3-edits
Feb 29, 2024
495faa9
bugfix: fixes range params, changes old exception text, adds non-poly…
Mar 5, 2024
7ff2dd5
Merge branch 'master' into cs.searchapi-v3-edits
Mar 5, 2024
1a72148
update test case logic, remove searchapi output tests
Mar 5, 2024
47b46f7
remove "ASFSession.ASFSession" in patching test cases
Mar 5, 2024
55b351e
Merge branch 'master' into cs.searchapi-v3-edits
Mar 6, 2024
bfc67df
fixes broken jsonlite outputs when results are empty
Mar 8, 2024
e510a54
Merge branch 'master' into cs.searchapi-v3-edits
Apr 2, 2024
9410d85
fixes changed method name in S1Product
Apr 3, 2024
8538077
got asfframe known bugs tests passing, added comment for first test i…
Apr 9, 2024
460e261
Exposes esa_frame in RADARSAT Product, orbit can be list in csv (for …
Apr 9, 2024
3b81b75
adds some log messages for measuring performance
Apr 17, 2024
11b47d3
Merge branch 'master' into cs.searchapi-v3-edits
Apr 17, 2024
0b7b394
Merge remote-tracking branch 'origin/topic-ASFProduct_date-speedup' i…
Apr 19, 2024
4923c3d
Merge branch 'master' into cs.searchapi-v3-edits
May 1, 2024
967ffc1
reverts additional attribute optimization (for now)
May 6, 2024
151b1f2
use dict.get() in kml
May 7, 2024
7329d34
adds flake8 lint workflow
Jul 13, 2024
b11cc73
flake8 compliance on non-test code, set line limit to 100
Jul 15, 2024
411ecf5
adds push hook to lint workflow
Jul 15, 2024
8fe4989
modifies test workflow triggers
Jul 15, 2024
02ec00b
fixes metalink output
Jul 15, 2024
58390d7
fixes tests, removes filesToWKT.py
Aug 1, 2024
abcbc0d
Fixes customized CMR_TIMEOUT, sets timeout to 60 on intersection test
Aug 2, 2024
fde4224
changes _properties_paths back to _base_properties
Aug 2, 2024
9f9e300
fixes indentation on _base_properties
Aug 2, 2024
7676949
fixes old reference to properties paths
Aug 2, 2024
651f4b4
removes PR trigger for pytest workflow, update example
Aug 2, 2024
8bad298
Merge pull request #219 from asfadmin/cs.searchapi-v3-edits
SpicyGarlicAlbacoreRoll Aug 2, 2024
397003f
Merge branch 'stable' into master
SpicyGarlicAlbacoreRoll Aug 2, 2024
2ec78d7
bumps from minor to major version
Aug 2, 2024
21462d2
updates tests formatting, remainder merge conflicts
Aug 2, 2024
f2c5133
tests dir added to linting workflow, change wording on deprecation wa…
Aug 2, 2024
8c4e8dd
update import, ASFSession uses warn again
Aug 2, 2024
54affe8
changes warn() import
Aug 2, 2024
74325d9
re-adds lat 64-65 test case, removes asf-search v searchAPI export fo…
Aug 8, 2024
b9473fa
Merge pull request #308 from asfadmin/topic-flake8-action
SpicyGarlicAlbacoreRoll Aug 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run-pytest.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: tests

on: [pull_request, push]
on: [push]

jobs:
run-tests:
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ and uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

-->
------
## [v7.2.0](https://github.com/asfadmin/Discovery-asf_search/compare/v7.1.0...v7.2.0)
SpicyGarlicAlbacoreRoll marked this conversation as resolved.
Show resolved Hide resolved
### Added
- Added `asf.ASFSearchOptions(circle=[lat, long, radius])` search param. Takes list of exactly 3 numbers.
- Exposed `asf.validator_map`, which given a ops search param, can be used to look up which method we're going to validate it against.
- Exposed `ASFProduct.get_urls` which returns the URL's for it's products directly. Can control which products with the `fileType` enum.

## [v7.1.4](https://github.com/asfadmin/Discovery-asf_search/compare/v7.1.3...v7.1.4)
### Changed
- replaces `ciso8601` package with `dateutil` for package wheel compatibility. `ciso8601` used when installed via `extra` dependency
Expand Down
68 changes: 33 additions & 35 deletions asf_search/ASFProduct.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,7 @@ def get_classname(cls):
- `path`: the expected path in the CMR UMM json granule response as a list
- `cast`: (optional): the optional type casting method

Defining `_base_properties` in subclasses allows for defining custom properties or overiding existing ones.
See `S1Product.get_property_paths()` on how subclasses are expected to
combine `ASFProduct._base_properties` with their own separately defined `_base_properties`
Defining `_properties_paths` in subclasses allows for defining custom properties or overiding existing ones.
"""

def __init__(self, args: Dict = {}, session: ASFSession = ASFSession()):
Expand Down Expand Up @@ -115,32 +113,40 @@ def download(self, path: str, filename: str = None, session: ASFSession = None,
default_filename = self.properties['fileName']

if filename is not None:
multiple_files = (
(fileType == FileDownloadType.ADDITIONAL_FILES and len(self.properties['additionalUrls']) > 1)
or fileType == FileDownloadType.ALL_FILES
)
if multiple_files:
warnings.warn(f"Attempting to download multiple files for product, ignoring user provided filename argument \"{filename}\", using default.")
# Check if we should support the filename argument:
if self._has_multiple_files() and fileType in [FileDownloadType.ADDITIONAL_FILES, FileDownloadType.ALL_FILES]:
warnings.warn(f"Attempting to download multiple files for product, ignoring user provided filename argument '{filename}', using default.")
else:
default_filename = filename

if session is None:
session = self.session

urls = self.get_urls(fileType=fileType)

for url in urls:
base_filename = '.'.join(default_filename.split('.')[:-1])
extension = url.split('.')[-1]
download_url(
url=url,
path=path,
filename=f"{base_filename}.{extension}",
session=session
)

def get_urls(self, fileType = FileDownloadType.DEFAULT_FILE) -> list:
urls = []

if fileType == FileDownloadType.DEFAULT_FILE:
urls.append((default_filename, self.properties['url']))
urls.append(self.properties['url'])
elif fileType == FileDownloadType.ADDITIONAL_FILES:
urls.extend(self._get_additional_filenames_and_urls(default_filename))
urls.extend(self.properties.get('additionalUrls', []))
elif fileType == FileDownloadType.ALL_FILES:
urls.append((default_filename, self.properties['url']))
urls.extend(self._get_additional_filenames_and_urls(default_filename))
urls.append(self.properties['url'])
urls.extend(self.properties.get('additionalUrls', []))
else:
raise ValueError("Invalid FileDownloadType provided, the valid types are 'DEFAULT_FILE', 'ADDITIONAL_FILES', and 'ALL_FILES'")

for filename, url in urls:
download_url(url=url, path=path, filename=filename, session=session)
return urls

def _get_additional_filenames_and_urls(
self,
Expand All @@ -163,7 +169,7 @@ def stack(

:param opts: An ASFSearchOptions object describing the search parameters to be used. Search parameters specified outside this object will override in event of a conflict.
:param ASFProductSubclass: An ASFProduct subclass constructor.

:return: ASFSearchResults containing the stack, with the addition of baseline values (temporal, perpendicular) attached to each ASFProduct.
"""
from .search.baseline_search import stack_from_product
Expand Down Expand Up @@ -232,6 +238,9 @@ def remotezip(self, session: ASFSession) -> 'RemoteZip':

return remotezip(self.properties['url'], session=session)

def _has_multiple_files(self):
SpicyGarlicAlbacoreRoll marked this conversation as resolved.
Show resolved Hide resolved
return 'additionalUrls' in self.properties and len(self.properties['additionalUrls']) > 0

def _read_umm_property(self, umm: Dict, mapping: Dict) -> Any:
value = self.umm_get(umm, *mapping['path'])
if mapping.get('cast') is None:
Expand All @@ -252,9 +261,11 @@ def translate_product(self, item: Dict) -> Dict:

umm = item.get('umm')

# additionalAttributes = {attr['Name']: attr['Values'] for attr in umm['AdditionalAttributes']}

properties = {
prop: self._read_umm_property(umm, umm_mapping)
for prop, umm_mapping in self.get_property_paths().items()
prop: self._read_umm_property(umm, umm_mapping)
for prop, umm_mapping in self._base_properties.items()
}

if properties.get('url') is not None:
Expand All @@ -271,19 +282,6 @@ def translate_product(self, item: Dict) -> Dict:

return {'geometry': geometry, 'properties': properties, 'type': 'Feature'}

# ASFProduct subclasses define extra/override param key + UMM pathing here
@staticmethod
def get_property_paths() -> Dict:
"""
Returns _base_properties of class, subclasses such as `S1Product` (or user provided subclasses) can override this to
define which properties they want in their subclass's properties dict.

(See `S1Product.get_property_paths()` for example of combining _base_properties of multiple classes)

:returns dictionary, {`PROPERTY_NAME`: {'path': [umm, path, to, value], 'cast (optional)': Callable_to_cast_value}, ...}
"""
return ASFProduct._base_properties

def get_sort_keys(self) -> Tuple[str, str]:
"""
Returns tuple of primary and secondary date values used for sorting final search results
Expand Down Expand Up @@ -385,7 +383,9 @@ def umm_get(item: Dict, *args):
if item is None:
return None
for key in args:
if isinstance(key, int):
if isinstance(key, str):
item = item.get(key)
elif isinstance(key, int):
item = item[key] if key < len(item) else None
elif isinstance(key, tuple):
(a, b) = key
Expand All @@ -408,8 +408,6 @@ def umm_get(item: Dict, *args):
break
if not found:
return None
else:
item = item.get(key)
if item is None:
return None
if item in [None, 'NA', 'N/A', '']:
Expand Down
12 changes: 11 additions & 1 deletion asf_search/ASFSearchOptions/validator_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from .validators import (
parse_string, parse_float, parse_wkt, parse_date,
parse_string_list, parse_int_list, parse_int_or_range_list,
parse_float_or_range_list, parse_cmr_keywords_list,
parse_float_or_range_list, parse_circle, parse_linestring,
parse_cmr_keywords_list, parse_point, parse_coord_string,
parse_session
)

Expand Down Expand Up @@ -32,10 +33,19 @@ def validate(key, value):
'beamMode': parse_string_list,
'beamSwath': parse_string_list,
'campaign': parse_string,
'circle': parse_circle,
'linestring': parse_linestring,
'point': parse_point,
'maxBaselinePerp': parse_float,
'minBaselinePerp': parse_float,
'maxInsarStackSize': parse_float,
'minInsarStackSize': parse_float,
'maxDoppler': parse_float,
'minDoppler': parse_float,
'maxFaradayRotation': parse_float,
'minFaradayRotation': parse_float,
'maxInsarStackSize': parse_int_or_range_list,
'minInsarStackSize': parse_int_or_range_list,
'flightDirection': parse_string,
'flightLine': parse_string,
'frame': parse_int_or_range_list,
Expand Down
35 changes: 32 additions & 3 deletions asf_search/ASFSearchOptions/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def parse_string(value: str) -> str:
except ValueError as exc: # If this happens, printing v's value would fail too...
raise ValueError(f"Invalid string: Can't cast type '{type(value)}' to string.") from exc
if len(value) == 0:
raise ValueError(f'Invalid string: Empty.')
raise ValueError('Invalid string: Empty.')
return value


Expand All @@ -36,7 +36,7 @@ def parse_float(value: float) -> float:
value = float(value)
except ValueError as exc:
raise ValueError(f'Invalid float: {value}') from exc
if math.isinf(value):
if math.isinf(value) or math.isnan(value):
raise ValueError(f'Float values must be finite: got {value}')
return value

Expand Down Expand Up @@ -127,7 +127,7 @@ def parse_cmr_keywords_list(value: Sequence[Union[Dict, Sequence]]):

# Parse and validate an iterable of strings: "foo,bar,baz"
def parse_string_list(value: Sequence[str]) -> List[str]:
return parse_list(value, str)
return parse_list(value, parse_string)


# Parse and validate an iterable of integers: "1,2,3"
Expand Down Expand Up @@ -216,6 +216,35 @@ def parse_wkt(value: str) -> str:
raise ValueError(f'Invalid wkt: {exc}') from exc
return wkt.dumps(value)

# Parse a CMR circle:
# [longitude, latitude, radius(meters)]
def parse_circle(value: List[float]) -> List[float]:
value = parse_float_list(value)
if len(value) != 3:
raise ValueError(f'Invalid circle, must be 3 values (lat, long, radius). Got: {value}')
return value

# Parse a CMR linestring:
# [longitude, latitude, longitude, latitude, ...]
def parse_linestring(value: List[float]) -> List[float]:
value = parse_float_list(value)
if len(value) % 2 != 0:
raise ValueError(f'Invalid linestring, must be values of format (lat, long, lat, long, ...). Got: {value}')
return value

def parse_point(value: List[float]) -> List[float]:
value = parse_float_list(value)
if len(value) != 2:
raise ValueError(f'Invalid point, must be values of format (lat, long). Got: {value}')
return value

# Parse and validate a coordinate string
def parse_coord_string(value: List):
value = parse_float_list(value)
if len(value) % 2 != 0:
raise ValueError(f'Invalid coordinate string, must be values of format (lat, long, lat, long, ...). Got: {value}')
return value

# Take "requests.Session", or anything that subclasses it:
def parse_session(session: Type[requests.Session]):
if issubclass(type(session), requests.Session):
Expand Down
16 changes: 7 additions & 9 deletions asf_search/ASFStackableProduct.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ class ASFStackableProduct(ASFProduct):
ASF ERS-1 Dataset Documentation Page: https://asf.alaska.edu/datasets/daac/ers-1/
ASF ERS-2 Dataset Documentation Page: https://asf.alaska.edu/datasets/daac/ers-2/
"""
_base_properties = {
}

class BaselineCalcType(Enum):
"""
Expand Down Expand Up @@ -53,13 +51,6 @@ def get_stack_opts(self, opts: ASFSearchOptions = None):
stack_opts.insarStackId = self.properties['insarStackId']
return stack_opts

@staticmethod
def get_property_paths() -> Dict:
return {
**ASFProduct.get_property_paths(),
**ASFStackableProduct._base_properties
}

def is_valid_reference(self):
# we don't stack at all if any of stack is missing insarBaseline, unlike stacking S1 products(?)
if 'insarBaseline' not in self.baseline:
Expand All @@ -73,3 +64,10 @@ def get_default_baseline_product_type() -> Union[str, None]:
Returns the product type to search for when building a baseline stack.
"""
return None

def has_baseline(self) -> bool:
baseline = self.get_baseline_calc_properties()

return (
baseline is not None
)
1 change: 1 addition & 0 deletions asf_search/CMR/field_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
'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}'},
Expand Down
6 changes: 3 additions & 3 deletions asf_search/CMR/subquery.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import List, Optional, Tuple
from typing import List, Tuple
import itertools
from copy import copy

from asf_search.ASFSearchOptions import ASFSearchOptions
from asf_search.constants import CMR_PAGE_SIZE

from asf_search.CMR.field_map import field_map
from asf_search.CMR.datasets import collections_by_processing_level, collections_per_platform, dataset_collections, get_concept_id_alias, get_dataset_concept_ids
from numpy import intersect1d, union1d

Expand All @@ -22,7 +22,7 @@ def build_subqueries(opts: ASFSearchOptions) -> List[ASFSearchOptions]:
if params.get(chunked_key) is not None:
params[chunked_key] = chunk_list(params[chunked_key], CMR_PAGE_SIZE)

list_param_names = ['platform', 'season', 'collections', 'dataset', 'cmr_keywords', 'shortName'] # these parameters will dodge the subquery system
list_param_names = ['platform', 'season', 'collections', 'cmr_keywords', 'shortName', 'circle', 'linestring', 'point', 'dataset'] # these parameters will dodge the subquery system
skip_param_names = ['maxResults']# these params exist in opts, but shouldn't be passed on to subqueries at ALL

collections, aliased_keywords = get_keyword_concept_ids(params, opts.collectionAlias)
Expand Down
22 changes: 21 additions & 1 deletion asf_search/CMR/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ def translate_opts(opts: ASFSearchOptions) -> List:
if escape_commas in dict_opts:
dict_opts[escape_commas] = dict_opts[escape_commas].replace(",", "\\,")

dict_opts = fix_cmr_shapes(dict_opts)

# Special case to unravel WKT field a little for compatibility
if "intersectsWith" in dict_opts:
shape = wkt.loads(dict_opts.pop('intersectsWith', None))
Expand All @@ -49,10 +51,13 @@ def translate_opts(opts: ASFSearchOptions) -> List:
(shapeType, shape) = wkt_to_cmr_shape(shape).split(':')
dict_opts[shapeType] = shape


# If you need to use the temporal key:
if any(key in dict_opts for key in ['start', 'end', 'season']):
dict_opts = fix_date(dict_opts)

dict_opts = fix_range_params(dict_opts)

# convert the above parameters to a list of key/value tuples
cmr_opts = []

Expand Down Expand Up @@ -97,6 +102,14 @@ def translate_opts(opts: ASFSearchOptions) -> List:

return cmr_opts

def fix_cmr_shapes(fixed_params: Dict[str, Any]) -> Dict[str, Any]:
"""Fixes raw CMR lon lat coord shapes"""
for param in ['point', 'linestring', 'circle']:
if param in fixed_params:
fixed_params[param] = ','.join(map(str, fixed_params[param]))

return fixed_params

def should_use_asf_frame(cmr_opts):
asf_frame_platforms = ['SENTINEL-1A', 'SENTINEL-1B', 'ALOS']

Expand Down Expand Up @@ -175,7 +188,7 @@ def try_parse_date(value: str) -> Optional[str]:

return date.strftime('%Y-%m-%dT%H:%M:%SZ')

def fix_date(fixed_params: Dict[str, Any]):
def fix_date(fixed_params: Dict[str, Any]) -> Dict[str, Any]:
if 'start' in fixed_params or 'end' in fixed_params or 'season' in fixed_params:
fixed_params["start"] = fixed_params["start"] if "start" in fixed_params else "1978-01-01T00:00:00Z"
fixed_params["end"] = fixed_params["end"] if "end" in fixed_params else datetime.utcnow().isoformat()
Expand All @@ -190,6 +203,13 @@ def fix_date(fixed_params: Dict[str, Any]):

return fixed_params

def fix_range_params(fixed_params: Dict[str, Any]) -> Dict[str, Any]:
"""Converts ranges to comma separated strings"""
for param in ['offNadirAngle', 'relativeOrbit', 'absoluteOrbit', 'frame', 'asfFrame']:
if param in fixed_params.keys() and isinstance(fixed_params[param], list):
fixed_params[param] = ','.join([str(val) for val in fixed_params[param]])

return fixed_params

def should_use_bbox(shape: BaseGeometry):
"""
Expand Down
8 changes: 1 addition & 7 deletions asf_search/Products/AIRSARProduct.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class AIRSARProduct(ASFProduct):
ASF Dataset Overview Page: https://asf.alaska.edu/data-sets/sar-data-sets/airsar/
"""
_base_properties = {
**ASFProduct._base_properties,
'frameNumber': {'path': ['AdditionalAttributes', ('Name', 'CENTER_ESA_FRAME'), 'Values', 0], 'cast': try_parse_int},
'groupID': {'path': [ 'AdditionalAttributes', ('Name', 'GROUP_ID'), 'Values', 0]},
'insarStackId': {'path': [ 'AdditionalAttributes', ('Name', 'INSAR_STACK_ID'), 'Values', 0]},
Expand All @@ -16,10 +17,3 @@ class AIRSARProduct(ASFProduct):

def __init__(self, args: Dict = {}, session: ASFSession = ASFSession()):
super().__init__(args, session)

@staticmethod
def get_property_paths() -> Dict:
return {
**ASFProduct.get_property_paths(),
**AIRSARProduct._base_properties
}
Loading
Loading