diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19f5ea5..96aed99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,20 @@ and uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-
-->
+------
+## [1.0.16](https://github.com/asfadmin/Discovery-SearchAPI-v3/compare/v1.0.15...v1.0.16)
+### Changed
+- bump asf-search to v12.1.0 for:
+ - `granule_list` wildcard support
+ - `OPERA_L3_DIST-ALERT-S1_V1` collection update
+ - `NISAR_EA` collections
+
+### Fixed
+- Fixed `getpass` typo in python snippet output
+
+### Style
+- ruff linting on modified files
+
------
## [1.0.15](https://github.com/asfadmin/Discovery-SearchAPI-v3/compare/v1.0.14...v1.0.15)
### Changed
diff --git a/README.md b/README.md
index 026c7bb..d52e8ae 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ SearchAPI-v3 is a wrapper around the [asf-search python module](https://github.c
feat-* |
Always branch off dev, for new features |
-
+
product_list with platform invalid
| Issues |
bugfix-* |
Always branch off dev, for bugfixes |
@@ -75,7 +75,7 @@ SearchAPI-v3 is a wrapper around the [asf-search python module](https://github.c
| production staging and integration testing |
prod-staging |
- Only acepts merges from testing, deploys to prod-staging deployment |
+ Only accepts merges from testing, deploys to prod-staging deployment |
| release |
diff --git a/requirements.txt b/requirements.txt
index 322b923..4527f0f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -22,7 +22,7 @@ ujson==5.7.0
uvicorn==0.21.1
watchfiles==0.19.0
-asf-search[asf-enumeration]==12.0.6
+asf-search[asf-enumeration]==12.1.0
python-json-logger==2.0.7
pyshp==2.1.3
diff --git a/src/SearchAPI/application/application.py b/src/SearchAPI/application/application.py
index 4578365..6ea6084 100644
--- a/src/SearchAPI/application/application.py
+++ b/src/SearchAPI/application/application.py
@@ -25,7 +25,7 @@
from asf_search import ASFSearchResults
from asf_enumeration import aria_s1_gunw
-asf_config['session'] = SearchAPISession()
+asf_config["session"] = SearchAPISession()
asf.REPORT_ERRORS = False
router = APIRouter(route_class=LoggingRoute)
@@ -43,7 +43,7 @@
cfg = load_config_maturity()
-cmr_health = get_cmr_health(cfg['cmr_base'], cfg['cmr_health'])
+cmr_health = get_cmr_health(cfg["cmr_base"], cfg["cmr_health"])
@router.api_route("/services/search/param", methods=["GET", "POST", "HEAD"])
@@ -54,37 +54,37 @@ async def query_params(searchOptions: SearchOptsModel = Depends(process_search_r
output = searchOptions.output
opts = searchOptions.opts
- non_search_param = ['output', 'maxresults', 'pagesize', 'maturity']
+ non_search_param = ["output", "maxresults", "pagesize", "maturity"]
try:
any_searchables = any([key.lower() not in non_search_param for key, _ in opts])
if not any_searchables:
raise ValueError(
- 'No searchable parameters specified, queries must include'
- ' parameters besides output= and maxresults='
+ "No searchable parameters specified, queries must include"
+ " parameters besides output= and maxresults="
)
except ValueError as exc:
raise HTTPException(detail=repr(exc), status_code=400) from exc
- if output.lower() == 'count':
- count=asf.search_count(opts=opts)
+ if output.lower() == "count":
+ count = asf.search_count(opts=opts)
return Response(
content=str(count),
status_code=200,
- media_type='text/html; charset=utf-8',
- headers=constants.DEFAULT_HEADERS
+ media_type="text/html; charset=utf-8",
+ headers=constants.DEFAULT_HEADERS,
)
- if output.lower() == 'python':
+ if output.lower() == "python":
file_name, search_script = get_asf_search_script(opts)
-
+
return Response(
content=search_script,
status_code=200,
- media_type='text/x-python',
- headers= {
- **constants.DEFAULT_HEADERS,
- 'Content-Disposition': f"attachment; filename={file_name}",
- }
+ media_type="text/x-python",
+ headers={
+ **constants.DEFAULT_HEADERS,
+ "Content-Disposition": f"attachment; filename={file_name}",
+ },
)
try:
results = asf.search(opts=opts)
@@ -93,13 +93,14 @@ async def query_params(searchOptions: SearchOptsModel = Depends(process_search_r
except (asf.ASFSearchError, asf.CMRError, ValueError) as exc:
raise HTTPException(
- detail=f"Search failed to find results: {exc}",
- status_code=400
+ detail=f"Search failed to find results: {exc}", status_code=400
) from exc
@router.api_route("/services/search/baseline", methods=["GET", "POST", "HEAD"])
-async def query_baseline(searchOptions: BaselineSearchOptsModel = Depends(process_baseline_request)):
+async def query_baseline(
+ searchOptions: BaselineSearchOptsModel = Depends(process_baseline_request),
+):
opts = searchOptions.opts
opts.maxResults = None
output = searchOptions.output
@@ -109,18 +110,22 @@ async def query_baseline(searchOptions: BaselineSearchOptsModel = Depends(proces
is_frame_based = searchOptions.opts.dataset is not None
# Load the reference scene:
-
- if output.lower() == 'python':
- file_name, search_script = get_asf_search_script(opts, reference=reference, search_endpoint='baseline')
-
+
+ if "*" in reference:
+ raise HTTPException(detail="Reference scene cannot include wildcards", status_code=400)
+ if output.lower() == "python":
+ file_name, search_script = get_asf_search_script(
+ opts, reference=reference, search_endpoint="baseline"
+ )
+
return Response(
content=search_script,
status_code=200,
- media_type='text/x-python',
- headers= {
- **constants.DEFAULT_HEADERS,
- 'Content-Disposition': f"attachment; filename={file_name}",
- }
+ media_type="text/x-python",
+ headers={
+ **constants.DEFAULT_HEADERS,
+ "Content-Disposition": f"attachment; filename={file_name}",
+ },
)
if is_frame_based and opts.dataset[0] == asf.DATASET.ARIA_S1_GUNW:
@@ -129,12 +134,21 @@ async def query_baseline(searchOptions: BaselineSearchOptsModel = Depends(proces
try:
reference_product = asf.granule_search(granule_list=[reference], opts=opts)[0]
except (KeyError, IndexError, ValueError) as exc:
- raise HTTPException(detail=f"Reference scene not found: {reference}", status_code=400) from exc
+ raise HTTPException(
+ detail=f"Reference scene not found: {reference}", status_code=400
+ ) from exc
try:
if reference_product.get_stack_opts() is None:
- reference_product = asf.ASFStackableProduct(args={'umm': reference_product.umm, 'meta': reference_product.meta}, session=reference_product.session)
- if (not reference_product.has_baseline() or not reference_product.is_valid_reference() or not reference_product.has_baseline()) and not is_frame_based:
+ reference_product = asf.ASFStackableProduct(
+ args={"umm": reference_product.umm, "meta": reference_product.meta},
+ session=reference_product.session,
+ )
+ if (
+ not reference_product.has_baseline()
+ or not reference_product.is_valid_reference()
+ or not reference_product.has_baseline()
+ ) and not is_frame_based:
raise asf.exceptions.ASFBaselineError(f"Requested reference scene has no baseline")
except (asf.exceptions.ASFBaselineError, ValueError) as exc:
raise HTTPException(detail=f"Search failed to find results: {exc}", status_code=400)
@@ -142,28 +156,26 @@ async def query_baseline(searchOptions: BaselineSearchOptsModel = Depends(proces
if request_method == "HEAD":
# Need head request separately, so it doesn't do all
# the work to figure out the body
- if output.lower() == 'count':
+ if output.lower() == "count":
return Response(
status_code=200,
- media_type='text/html; charset=utf-8',
- headers=constants.DEFAULT_HEADERS
+ media_type="text/html; charset=utf-8",
+ headers=constants.DEFAULT_HEADERS,
)
metadata = as_output(asf.ASFSearchResults([]), output)
return Response(
- status_code=200,
- headers=metadata["headers"],
- media_type=metadata["media_type"]
+ status_code=200, headers=metadata["headers"], media_type=metadata["media_type"]
)
# Figure out the response params:
- if output.lower() == 'count':
+ if output.lower() == "count":
stack_opts = reference_product.get_stack_opts()
count = asf.search_count(opts=stack_opts)
return Response(
content=str(count),
status_code=200,
- media_type='text/html; charset=utf-8',
- headers=constants.DEFAULT_HEADERS
+ media_type="text/html; charset=utf-8",
+ headers=constants.DEFAULT_HEADERS,
)
# Finally stream everything back:
@@ -173,40 +185,34 @@ async def query_baseline(searchOptions: BaselineSearchOptsModel = Depends(proces
return Response(**response_info)
except (asf.ASFSearchError, asf.CMRError, ValueError) as exc:
- raise HTTPException(detail=f"Search failed to find results: {exc}", status_code=400) from exc
+ raise HTTPException(
+ detail=f"Search failed to find results: {exc}", status_code=400
+ ) from exc
-@router.get('/services/utils/date', response_class=JSONResponse)
+@router.get("/services/utils/date", response_class=JSONResponse)
async def query_date_validation(date: str):
parsed_date = dateparser.parse(date)
if parsed_date is None:
raise HTTPException(detail=f"Could not parse date: {date}", status_code=400)
response = {
- 'date': {
- 'original': date,
- 'parsed': parsed_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
+ "date": {
+ "original": date,
+ "parsed": parsed_date.strftime("%Y-%m-%dT%H:%M:%SZ"),
}
}
- return JSONResponse(
- content=response,
- status_code=200,
- headers=constants.DEFAULT_HEADERS
- )
+ return JSONResponse(content=response, status_code=200, headers=constants.DEFAULT_HEADERS)
-@router.get('/services/utils/mission_list', response_class=JSONResponse)
+@router.get("/services/utils/mission_list", response_class=JSONResponse)
async def query_mission_list(platform: str | None = None):
if platform is not None:
platform = platform.upper()
- response = {'result': asf.campaigns(platform)}
+ response = {"result": asf.campaigns(platform)}
- return JSONResponse(
- content=response,
- status_code=200,
- headers=constants.DEFAULT_HEADERS
- )
+ return JSONResponse(content=response, status_code=200, headers=constants.DEFAULT_HEADERS)
@router.api_route("/services/utils/wkt", methods=["GET", "POST"])
@@ -214,63 +220,64 @@ async def wkt_validation(wkt: str = Depends(process_wkt_request)):
return Response(
content=json.dumps(validate_wkt(wkt)),
status_code=200,
- media_type='application/json; charset=utf-8',
- headers=constants.DEFAULT_HEADERS
+ media_type="application/json; charset=utf-8",
+ headers=constants.DEFAULT_HEADERS,
)
-@router.post('/services/utils/files_to_wkt')
+
+@router.post("/services/utils/files_to_wkt")
async def file_to_wkt(files: list[UploadFile]):
for file in files:
file.file.filename = file.filename
data = FilesToWKT([file.file for file in files]).getWKT()
- return JSONResponse(content={
- ** data,
- ** validate_wkt(data["parsed wkt"])},
+ return JSONResponse(
+ content={**data, **validate_wkt(data["parsed wkt"])},
status_code=200,
- headers=constants.DEFAULT_HEADERS
+ headers=constants.DEFAULT_HEADERS,
)
-@router.get('/services/utils/kml_footprint')
-async def kml_to_footprint(granule: str, cmr_token: Optional[str] = None, maturity: str = 'prod'):
+
+@router.get("/services/utils/kml_footprint")
+async def kml_to_footprint(granule: str, cmr_token: Optional[str] = None, maturity: str = "prod"):
config = load_config_maturity(maturity=maturity)
query_opts = asf.ASFSearchOptions(granule_list=[granule])
if (cmr_token) is not None:
session = SearchAPISession()
- session.headers.update({'Authorization': f'Bearer {cmr_token}'})
+ session.headers.update({"Authorization": f"Bearer {cmr_token}"})
query_opts.session = session
-
- query_opts.host = config['cmr_base']
+ query_opts.host = config["cmr_base"]
results = asf.search(opts=query_opts, dataset=asf.DATASET.NISAR)
- kml_file = results.find_urls(extension='.kml')[0]
-
+ kml_file = results.find_urls(extension=".kml")[0]
+
kml_response = query_opts.session.get(kml_file)
return Response(
content=str(kml_response.text),
status_code=200,
- media_type='text/html; charset=utf-8',
- headers=constants.DEFAULT_HEADERS
+ media_type="text/html; charset=utf-8",
+ headers=constants.DEFAULT_HEADERS,
)
-@router.get('/services/utils/nisar_orbit_ephemera')
+
+@router.get("/services/utils/nisar_orbit_ephemera")
async def get_nisar_orbit_ephemera():
"""Returns the latest nisar orbit ephemera products for POE, MOE, NOE, and FOE in that order. Returns as jsonlite2"""
try:
oe_dict = asf.utils.get_nisar_orbit_ephemeras()
results = ASFSearchResults([product for product in oe_dict.values()])
- response_info = as_output(results, 'jsonlite2')
+ response_info = as_output(results, "jsonlite2")
return Response(**response_info)
except (asf.ASFSearchError, asf.CMRError, ValueError) as exc:
raise HTTPException(
- detail=f"Search failed to find results: {exc}",
- status_code=400
+ detail=f"Search failed to find results: {exc}", status_code=400
) from exc
+
# example: https://api.daac.asf.alaska.edu/services/redirect/NISAR_L2_STATIC/{granule_id}.h5
# @router.get('/services/redirect/{short_name}/{granule_id}')
# async def nisar_static_layer(short_name: str, granule_id: str):
@@ -289,7 +296,7 @@ async def get_nisar_orbit_ephemera():
# )[0]
# except IndexError:
# raise HTTPException(status_code=400, detail=f'Unable to find static layer, provided scene named "{granule_id}" not found in CMR record')
-
+
# static_layer = granule.get_static_layer(opts=asf.ASFSearchOptions(shortName=short_name))
# if static_layer is None:
# raise HTTPException(status_code=500, detail=f'Static layer not found for scene named "{granule_id}"')
@@ -300,61 +307,57 @@ async def get_nisar_orbit_ephemera():
def validate_wkt(wkt: str):
try:
wrapped, unwrapped, reports = asf.validate_wkt(wkt)
- repairs = [{'type': report.report_type, 'report': report.report} for report in reports if report.report_type != "'type': 'WRAP'"]
+ repairs = [
+ {"type": report.report_type, "report": report.report}
+ for report in reports
+ if report.report_type != "'type': 'WRAP'"
+ ]
except Exception as exc:
raise HTTPException(detail=f"Failed to validate wkt {wkt}: {exc}", status_code=400) from exc
- return {
- 'wkt': {
- 'unwrapped': unwrapped.wkt,
- 'wrapped': wrapped.wkt
- },
- 'repairs': repairs
- }
+ return {"wkt": {"unwrapped": unwrapped.wkt, "wrapped": wrapped.wkt}, "repairs": repairs}
+
def _get_aria_baseline_stack(reference: str, opts: asf.ASFSearchOptions, output: str):
- if output.lower() == 'count':
- stack_opts = asf.Products.ARIAS1GUNWProduct.get_stack_opts_for_frame(int(reference), opts=opts)
- count=asf.search_count(opts=stack_opts)
+ if output.lower() == "count":
+ stack_opts = asf.Products.ARIAS1GUNWProduct.get_stack_opts_for_frame(
+ int(reference), opts=opts
+ )
+ count = asf.search_count(opts=stack_opts)
return Response(
content=str(count),
status_code=200,
- media_type='text/html; charset=utf-8',
- headers=constants.DEFAULT_HEADERS
+ media_type="text/html; charset=utf-8",
+ headers=constants.DEFAULT_HEADERS,
)
try:
stack = asf.stack_from_id(reference, opts=opts)
response_info = as_output(stack, output)
return Response(**response_info)
except (KeyError, IndexError, ValueError) as exc:
- raise HTTPException(detail=f"Ran into an issue building stack for frame: {reference}\nException: {str(exc)}", status_code=400) from exc
-
+ raise HTTPException(
+ detail=f"Ran into an issue building stack for frame: {reference}\nException: {str(exc)}",
+ status_code=400,
+ ) from exc
-@router.get('/', response_class=JSONResponse)
-@router.get('/health', response_class=JSONResponse)
+
+@router.get("/", response_class=JSONResponse)
+@router.get("/health", response_class=JSONResponse)
async def health_check():
try:
version_path = os.path.join("SearchAPI", "version.json")
- with open(version_path, 'r', encoding="utf-8") as version_file:
+ with open(version_path, "r", encoding="utf-8") as version_file:
api_version = json.load(version_file)
except Exception as exc:
api_logger.info(exc)
- api_version = {'version': 'unknown'}
+ api_version = {"version": "unknown"}
api_health = {
- 'ASFSearchAPI': {
- 'ok?': True,
- 'version': api_version['version'],
- 'config': cfg
- },
- 'CMRSearchAPI': cmr_health
+ "ASFSearchAPI": {"ok?": True, "version": api_version["version"], "config": cfg},
+ "CMRSearchAPI": cmr_health,
}
- return JSONResponse(
- content=api_health,
- status_code=200,
- headers=constants.DEFAULT_HEADERS
- )
+ return JSONResponse(content=api_health, status_code=200, headers=constants.DEFAULT_HEADERS)
@app.exception_handler(HTTPException)
@@ -366,9 +369,7 @@ async def handle_error(request: Request, error: HTTPException):
}
}
return JSONResponse(
- content=response,
- status_code=error.status_code,
- headers=constants.DEFAULT_HEADERS
+ content=response, status_code=error.status_code, headers=constants.DEFAULT_HEADERS
)
diff --git a/src/SearchAPI/application/asf_opts.py b/src/SearchAPI/application/asf_opts.py
index 8fd1397..a19cc71 100644
--- a/src/SearchAPI/application/asf_opts.py
+++ b/src/SearchAPI/application/asf_opts.py
@@ -13,23 +13,24 @@
from .SearchAPISession import SearchAPISession
from .logger import api_logger
-non_search_param = ['output', 'maxresults', 'pagesize', 'maturity']
+non_search_param = ["output", "maxresults", "pagesize", "maturity"]
+
def string_to_range(v: Union[str, list]) -> tuple:
if isinstance(v, list):
return v
try:
- v = v.replace(' ', '')
- m = re.search(r'^(-?\d+(\.\d*)?)-(-?\d+(\.\d*)?)$', v)
+ v = v.replace(" ", "")
+ m = re.search(r"^(-?\d+(\.\d*)?)-(-?\d+(\.\d*)?)$", v)
if m is None:
- raise ValueError(f'Invalid range: {v}')
+ raise ValueError(f"Invalid range: {v}")
a = (m.group(1), m.group(3))
if float(a[0]) > float(a[1]):
raise ValueError()
if a[0] == a[1]:
a = a[0]
except ValueError as exc:
- raise ValueError(f'Invalid range: {exc}') from exc
+ raise ValueError(f"Invalid range: {exc}") from exc
return a
@@ -41,7 +42,7 @@ def string_to_list(v: Union[str, list[str]]) -> list:
def parse_number_or_range(v: Union[str, list]):
- m = re.search(r'^(-?\d+(\.\d*)?)$', v)
+ m = re.search(r"^(-?\d+(\.\d*)?)$", v)
# If it's a digit:
if m:
return v
@@ -60,21 +61,20 @@ def string_to_num_or_range_list(v: Union[str, list]):
string_to_obj_map = {
# Range only:
- asf.validators.parse_date_range: string_to_range,
- asf.validators.parse_int_range: string_to_range,
- asf.validators.parse_float_range: string_to_range,
+ asf.validators.parse_date_range: string_to_range,
+ asf.validators.parse_int_range: string_to_range,
+ asf.validators.parse_float_range: string_to_range,
# List only:
- asf.validators.parse_string_list: string_to_list,
- asf.validators.parse_int_list: string_to_list,
- asf.validators.parse_float_list: string_to_list,
- asf.validators.parse_circle: string_to_list,
- asf.validators.parse_linestring: string_to_list,
- asf.validators.parse_point: string_to_list,
- asf.validators.parse_bbox: string_to_list,
-
+ asf.validators.parse_string_list: string_to_list,
+ asf.validators.parse_int_list: string_to_list,
+ asf.validators.parse_float_list: string_to_list,
+ asf.validators.parse_circle: string_to_list,
+ asf.validators.parse_linestring: string_to_list,
+ asf.validators.parse_point: string_to_list,
+ asf.validators.parse_bbox: string_to_list,
# Number or Range-list:
- asf.validators.parse_int_or_range_list: string_to_num_or_range_list,
- asf.validators.parse_float_or_range_list: string_to_num_or_range_list,
+ asf.validators.parse_int_or_range_list: string_to_num_or_range_list,
+ asf.validators.parse_float_or_range_list: string_to_num_or_range_list,
}
@@ -87,17 +87,10 @@ class ValidatorMap(collections.UserDict):
>>> v["mAxReSuLtS"] # Pointer to validator method
>>> v.actual_key_case("mAxReSuLtS") # "maxResults", what asf_search expects
"""
- ALIASED_KEYWORDS = {
- 'collectionname': 'campaign'
- }
- FLIGHT_DIRECTIONS = {
- 'A': 'ASCENDING',
- 'D': 'DESCENDING'
- }
- LOOK_DIRECTIONS = {
- 'R': 'R',
- 'L': 'L'
- }
+
+ ALIASED_KEYWORDS = {"collectionname": "campaign"}
+ FLIGHT_DIRECTIONS = {"A": "ASCENDING", "D": "DESCENDING"}
+ LOOK_DIRECTIONS = {"R": "R", "L": "L"}
"""
legacy SearchAPI keys that have new names in asf-search
"""
@@ -122,23 +115,22 @@ def actual_key_case(self, k):
return self.lower_lookup[k.lower()]
def alias_params(self, params: dict) -> dict:
- return {
- self.ALIASED_KEYWORDS.get(k.lower(), k): v
- for k, v in params.items()
- }
+ return {self.ALIASED_KEYWORDS.get(k.lower(), k): v for k, v in params.items()}
async def get_body(request: Request):
"""
Can remove when ASFSearchOptions uses Pydantic Model
"""
- if (content_type := request.headers.get('content-type')) is not None:
+ if (content_type := request.headers.get("content-type")) is not None:
try:
api_logger.debug(f"Request received, content-type header: {content_type})")
- if content_type == 'application/json':
+ if content_type == "application/json":
data = await request.json()
return data
- elif content_type == 'application/x-www-form-urlencoded' or content_type.startswith('multipart/form-data'):
+ elif content_type == "application/x-www-form-urlencoded" or content_type.startswith(
+ "multipart/form-data"
+ ):
data = await request.form()
return dict(data)
except Exception as exc:
@@ -165,42 +157,44 @@ async def process_search_request(request: Request, is_baseline: bool = False) ->
merged_args = {**query_params, **body}
- if (token := merged_args.get('cmr_token')):
+ if token := merged_args.get("cmr_token"):
session = SearchAPISession()
- session.headers.update({'Authorization': 'Bearer {0}'.format(token)})
+ session.headers.update({"Authorization": "Bearer {0}".format(token)})
query_opts.session = session
-
- if (cmr_provider := merged_args.get('cmr_provider')):
- query_opts.provider=cmr_provider
- output = merged_args.get('output', 'metalink')
- maturity = merged_args.get('maturity', 'prod')
+ if cmr_provider := merged_args.get("cmr_provider"):
+ query_opts.provider = cmr_provider
+
+ output = merged_args.get("output", "metalink")
+ maturity = merged_args.get("maturity", "prod")
config = load_config_maturity(maturity=maturity)
- query_opts.host = config['cmr_base']
+ query_opts.host = config["cmr_base"]
try:
# we are no longer allowing unbounded searches
if (
- query_opts.granule_list is None
- and query_opts.product_list is None
- and output not in ['python', 'count']
+ query_opts.granule_list is None
+ and query_opts.product_list is None
+ and output not in ["python", "count"]
and not is_baseline
- ):
+ ):
if query_opts.maxResults is None:
maxResults = asf.search_count(opts=query_opts)
if maxResults > 2000:
raise ValueError(
(
- 'SearchAPI no longer supports unbounded searches with expected results over 2000, '
- 'please use the asf-search python module for long-lived searches or set `maxResults` to 2000 or less. '
- 'To have SearchAPI automatically generate a python script for the equivalent search to your SearchAPI query '
- 'set `output=python`'
+ "SearchAPI no longer supports unbounded searches with expected results over 2000, "
+ "please use the asf-search python module for long-lived searches or set `maxResults` to 2000 or less. "
+ "To have SearchAPI automatically generate a python script for the equivalent search to your SearchAPI query "
+ "set `output=python`"
)
)
elif query_opts.maxResults <= 0:
raise ValueError('Search keyword "maxResults" must be greater than 0')
- searchOpts = SearchOptsModel(opts=query_opts, output=output, merged_args=merged_args, request_method=request.method)
+ searchOpts = SearchOptsModel(
+ opts=query_opts, output=output, merged_args=merged_args, request_method=request.method
+ )
except (ValueError, ValidationError) as exc:
raise HTTPException(detail=repr(exc), status_code=400) from exc
@@ -210,7 +204,7 @@ async def process_search_request(request: Request, is_baseline: bool = False) ->
async def process_baseline_request(request: Request) -> BaselineSearchOptsModel:
"""Processes request to baseline endpoint"""
searchOpts = await process_search_request(request=request, is_baseline=True)
- reference = searchOpts.merged_args.get('reference')
+ reference = searchOpts.merged_args.get("reference")
try:
baselineSearchOpts = BaselineSearchOptsModel(**searchOpts.model_dump(), reference=reference)
except (ValueError, ValidationError) as exc:
@@ -218,32 +212,36 @@ async def process_baseline_request(request: Request) -> BaselineSearchOptsModel:
return baselineSearchOpts
+
async def process_wkt_request(request: Request) -> str:
"""
Extracts the request's query+body params, returns wkt string
"""
query_params = dict(request.query_params)
- wkt = query_params.get('wkt')
+ wkt = query_params.get("wkt")
body = await get_body(request)
- body_wkt = body.get('wkt')
+ body_wkt = body.get("wkt")
if wkt is None:
wkt = body_wkt
if wkt is None:
- raise HTTPException(500, 'Validation Error: `wkt` string required')
+ raise HTTPException(500, "Validation Error: `wkt` string required")
return wkt
+
def get_asf_opts(params: dict) -> asf.ASFSearchOptions:
"""
Parses ASFSearchOptions from a dictionary
"""
try:
for param, value in params.items():
- if value is None or ((isinstance(value, str) or isinstance(value, list)) and len(value) == 0):
+ if value is None or (
+ (isinstance(value, str) or isinstance(value, list)) and len(value) == 0
+ ):
raise ValueError(f'Empty value passed to search keyword "{param}"')
except ValueError as exc:
raise HTTPException(detail=repr(exc), status_code=400) from exc
@@ -253,10 +251,7 @@ def get_asf_opts(params: dict) -> asf.ASFSearchOptions:
params = validatorMap.alias_params(params)
# You have to rebuild the dict, since you can't change keys in place
params = {
- validatorMap.actual_key_case(k)
- if k in validatorMap
- else k: v
- for k, v in params.items()
+ validatorMap.actual_key_case(k) if k in validatorMap else k: v for k, v in params.items()
}
# De-stringify the values if we know how:
@@ -272,24 +267,57 @@ def get_asf_opts(params: dict) -> asf.ASFSearchOptions:
# SearchOpts doesn't know how to handle these keys, but other methods need them
# (We still want to throw on any UNKNOWN keys)
- ignore_keys_lower = ["output", "reference", "maturity", "cmr_keywords", "cmr_token", "cmr_provider"]
+ ignore_keys_lower = [
+ "output",
+ "reference",
+ "maturity",
+ "cmr_keywords",
+ "cmr_token",
+ "cmr_provider",
+ ]
+
+ output = params.get("output", "").lower()
params = {k: params[k] for k in params.keys() if k.lower() not in ignore_keys_lower}
try:
if "granule_list" in params or "product_list" in params:
+ if "granule_list" in params:
+ if (
+ any("*" in granule for granule in params.get("granule_list", []))
+ and "maxResults" not in params
+ and output
+ not in [
+ "python",
+ "count",
+ ]
+ ):
+ raise ValueError(
+ "Unbound wildcard searches not supported with SearchAPI."
+ "Specify `maxresults` or use the asf-search python module directly (try `output=python` to download the equivalent script)"
+ )
if len([param for param in params if param not in ["collections", "maxResults"]]) > 1:
- raise ValueError(f'Cannot use search keywords "granule_list/product_list" with other search params')
+ raise ValueError(
+ 'Cannot use search keywords "granule_list/product_list" with other search params'
+ )
- if (flight_direction := params.get('flightDirection')) is not None:
+ if (flight_direction := params.get("flightDirection")) is not None:
if isinstance(flight_direction, str) and len(flight_direction):
- params['flightDirection'] = ValidatorMap.FLIGHT_DIRECTIONS.get(flight_direction.upper()[0], None)
- if params['flightDirection'] is None:
- raise ValueError(f'Invalid value passed to search keyword "flightDirection": "{flight_direction}". Valid directions are "ASCENDING" or "DESCENDING"')
- if (lookDirection := params.get('lookDirection')) is not None:
+ params["flightDirection"] = ValidatorMap.FLIGHT_DIRECTIONS.get(
+ flight_direction.upper()[0], None
+ )
+ if params["flightDirection"] is None:
+ raise ValueError(
+ f'Invalid value passed to search keyword "flightDirection": "{flight_direction}". Valid directions are "ASCENDING" or "DESCENDING"'
+ )
+ if (lookDirection := params.get("lookDirection")) is not None:
if isinstance(lookDirection, str) and len(lookDirection):
- params['lookDirection'] = ValidatorMap.LOOK_DIRECTIONS.get(lookDirection.upper()[0], None)
- if params['lookDirection'] is None:
- raise ValueError(f'Invalid value passed to search keyword "lookDirection": "{lookDirection}". Valid directions are "R" or "L"')
+ params["lookDirection"] = ValidatorMap.LOOK_DIRECTIONS.get(
+ lookDirection.upper()[0], None
+ )
+ if params["lookDirection"] is None:
+ raise ValueError(
+ f'Invalid value passed to search keyword "lookDirection": "{lookDirection}". Valid directions are "R" or "L"'
+ )
except ValueError as exc:
raise HTTPException(detail=repr(exc), status_code=400) from exc
diff --git a/src/SearchAPI/application/output.py b/src/SearchAPI/application/output.py
index fdd6b3b..6afc33c 100644
--- a/src/SearchAPI/application/output.py
+++ b/src/SearchAPI/application/output.py
@@ -10,7 +10,7 @@
from . import constants
from . import asf_env
-asf_search_script_template = '''## This script requires that the asf-search python module is installed
+asf_search_script_template = """## This script requires that the asf-search python module is installed
## to install, run the following in a terminal
## `pip install asf-search`
## Then from the correct folder in your terminal run:
@@ -34,9 +34,9 @@
results=asf.search(opts=opts)
pprint.pp(results.geojson())
-'''
+"""
-asf_search_baseline_script_template= '''## This script requires that the asf-search python module is installed
+asf_search_baseline_script_template = """## This script requires that the asf-search python module is installed
## to install, run the following in a terminal
## `pip install asf-search`
## Then from the correct folder in your terminal run:
@@ -52,7 +52,7 @@
## if the search requires authentication, uncomment
## the lines below, and enter your EDL credentials when prompted
## (use `session.auth_with_token(getpass('EDL Token'))` instead if a CMR bearer token is required)
-# from get_pass import get_pass
+# from getpass import getpass
# session=asf.ASFSession()
# session.auth_with_creds(input('EDL Username'), getpass('EDL Password'))
# opts.session = session
@@ -62,98 +62,98 @@
pprint.pp(stack.geojson())
-'''
+"""
+
def as_output(results: asf.ASFSearchResults, output: str) -> dict:
output_format = output.lower()
# Use a switch statement, so you only load the type of output you need:
match output_format:
- case 'json':
+ case "json":
return {
- 'content': ''.join(results.json()),
- 'media_type': 'application/json; charset=utf-8',
- 'headers': {
+ "content": "".join(results.json()),
+ "media_type": "application/json; charset=utf-8",
+ "headers": {
**constants.DEFAULT_HEADERS,
- 'Content-Disposition': f"attachment; filename={make_filename('json')}",
- }
+ "Content-Disposition": f"attachment; filename={make_filename('json')}",
+ },
}
- case 'jsonlite':
+ case "jsonlite":
return {
- 'content': ''.join(results.jsonlite()),
- 'media_type': 'application/json; charset=utf-8',
- 'headers': {
+ "content": "".join(results.jsonlite()),
+ "media_type": "application/json; charset=utf-8",
+ "headers": {
**constants.DEFAULT_HEADERS,
- 'Content-Disposition': f"attachment; filename={make_filename('json')}",
- }
+ "Content-Disposition": f"attachment; filename={make_filename('json')}",
+ },
}
- case 'jsonlite2':
+ case "jsonlite2":
return {
- 'content': ''.join(results.jsonlite2()),
- 'media_type': 'application/json; charset=utf-8',
- 'headers': {
+ "content": "".join(results.jsonlite2()),
+ "media_type": "application/json; charset=utf-8",
+ "headers": {
**constants.DEFAULT_HEADERS,
- 'Content-Disposition': f"attachment; filename={make_filename('json')}",
- }
+ "Content-Disposition": f"attachment; filename={make_filename('json')}",
+ },
}
- case 'geojson':
+ case "geojson":
return {
- 'content': json.dumps(results.geojson(), indent=4),
- 'media_type': 'application/geo+json; charset=utf-8',
- 'headers': {
+ "content": json.dumps(results.geojson(), indent=4),
+ "media_type": "application/geo+json; charset=utf-8",
+ "headers": {
**constants.DEFAULT_HEADERS,
- 'Content-Disposition': f"attachment; filename={make_filename('geojson')}",
- }
+ "Content-Disposition": f"attachment; filename={make_filename('geojson')}",
+ },
}
- case 'csv':
+ case "csv":
return {
- 'content': ''.join(results.csv()),
- 'media_type': 'text/csv; charset=utf-8',
- 'headers': {
+ "content": "".join(results.csv()),
+ "media_type": "text/csv; charset=utf-8",
+ "headers": {
**constants.DEFAULT_HEADERS,
- 'Content-Disposition': f"attachment; filename={make_filename('csv')}",
- }
+ "Content-Disposition": f"attachment; filename={make_filename('csv')}",
+ },
}
- case 'kml':
+ case "kml":
return {
- 'content': ''.join(results.kml()),
- 'media_type': 'application/vnd.google-earth.kml+xml; charset=utf-8',
- 'headers': {
+ "content": "".join(results.kml()),
+ "media_type": "application/vnd.google-earth.kml+xml; charset=utf-8",
+ "headers": {
**constants.DEFAULT_HEADERS,
- 'Content-Disposition': f"attachment; filename={make_filename('kml')}",
- }
+ "Content-Disposition": f"attachment; filename={make_filename('kml')}",
+ },
}
- case 'metalink':
+ case "metalink":
return {
- 'content': ''.join(results.metalink()),
- 'media_type': 'application/metalink+xml; charset=utf-8',
- 'headers': {
+ "content": "".join(results.metalink()),
+ "media_type": "application/metalink+xml; charset=utf-8",
+ "headers": {
**constants.DEFAULT_HEADERS,
- 'Content-Disposition': f"attachment; filename={make_filename('metalink')}",
- }
+ "Content-Disposition": f"attachment; filename={make_filename('metalink')}",
+ },
}
- case 'download':
+ case "download":
# Only call this once to guarantee the names always are the same:
- filename = make_filename('py')
+ filename = make_filename("py")
return {
- 'content': get_download(results, filename=filename),
- 'media_type': 'text/x-python',
- 'headers': {
+ "content": get_download(results, filename=filename),
+ "media_type": "text/x-python",
+ "headers": {
**constants.DEFAULT_HEADERS,
- 'Content-Disposition': f"attachment; filename={filename}",
- }
+ "Content-Disposition": f"attachment; filename={filename}",
+ },
}
# The default case. Throw if you get this far:
case _:
raise HTTPException(
- detail=f"Unknown output '{output_format}' was requested.",
- status_code=400
+ detail=f"Unknown output '{output_format}' was requested.", status_code=400
)
def get_download(results: asf.ASFSearchResults, filename=None):
# Load basic consts:
- script_url = asf_env.load_config_maturity()['bulk_download_api']
+ script_url = asf_env.load_config_maturity()["bulk_download_api"]
file_type = asf.FileDownloadType.DEFAULT_FILE
# Build the url list:
url_list = []
@@ -161,29 +161,31 @@ def get_download(results: asf.ASFSearchResults, filename=None):
url_list.extend(product.get_urls(fileType=file_type))
# Setup the data you're posting with. Optional filename so it lines up with our headers:
- script_data = {'products': ','.join(url_list)}
+ script_data = {"products": ",".join(url_list)}
if filename:
- script_data['filename'] = filename
+ script_data["filename"] = filename
# Finally make the request:
script_request = requests.post(script_url, data=script_data, timeout=30)
return script_request.text
+
def get_asf_search_script(
- opts: asf.ASFSearchOptions,
- reference: str = None,
- search_endpoint: Literal['param', 'baseline'] = 'param'
- ) -> tuple[str, str]:
-
+ opts: asf.ASFSearchOptions,
+ reference: str = None,
+ search_endpoint: Literal["param", "baseline"] = "param",
+) -> tuple[str, str]:
+
opts.session = None
# ASFSearchOptions formatting uses json.dumps for serialization. Add proper python capitalization
- opts_str = str(opts).replace('true', 'True', -1).replace('false', 'False')
- if search_endpoint == 'param':
- file_name=make_filename('py', prefix='asf-search-script')
+ opts_str = str(opts).replace("true", "True", -1).replace("false", "False")
+ if search_endpoint == "param":
+ file_name = make_filename("py", prefix="asf-search-script")
output_script = asf_search_script_template.format(file_name, opts_str)
else:
- file_name=make_filename('py', prefix='asf-search-baseline-script')
+ file_name = make_filename("py", prefix="asf-search-baseline-script")
output_script = asf_search_baseline_script_template.format(file_name, reference, opts_str)
return file_name, output_script
-def make_filename(suffix, prefix:str = 'asf-results'):
- return f'{prefix}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}.{suffix}'
+
+def make_filename(suffix, prefix: str = "asf-results"):
+ return f"{prefix}-{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.{suffix}"