stac-fastapi-core 6.3.0__py3-none-any.whl → 6.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stac_fastapi/core/base_database_logic.py +18 -4
- stac_fastapi/core/core.py +263 -12
- stac_fastapi/core/extensions/__init__.py +7 -1
- stac_fastapi/core/extensions/collections_search.py +190 -0
- stac_fastapi/core/extensions/filter.py +0 -18
- stac_fastapi/core/models/links.py +1 -1
- stac_fastapi/core/version.py +1 -1
- {stac_fastapi_core-6.3.0.dist-info → stac_fastapi_core-6.5.0.dist-info}/METADATA +66 -7
- {stac_fastapi_core-6.3.0.dist-info → stac_fastapi_core-6.5.0.dist-info}/RECORD +11 -10
- {stac_fastapi_core-6.3.0.dist-info → stac_fastapi_core-6.5.0.dist-info}/WHEEL +0 -0
- {stac_fastapi_core-6.3.0.dist-info → stac_fastapi_core-6.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Base database logic."""
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
-
from typing import Any, Dict, Iterable, List, Optional
|
|
4
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class BaseDatabaseLogic(abc.ABC):
|
|
@@ -14,9 +14,23 @@ class BaseDatabaseLogic(abc.ABC):
|
|
|
14
14
|
|
|
15
15
|
@abc.abstractmethod
|
|
16
16
|
async def get_all_collections(
|
|
17
|
-
self,
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
self,
|
|
18
|
+
token: Optional[str],
|
|
19
|
+
limit: int,
|
|
20
|
+
request: Any = None,
|
|
21
|
+
sort: Optional[List[Dict[str, Any]]] = None,
|
|
22
|
+
) -> Tuple[List[Dict[str, Any]], Optional[str]]:
|
|
23
|
+
"""Retrieve a list of collections from the database, supporting pagination.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
token (Optional[str]): The pagination token.
|
|
27
|
+
limit (int): The number of results to return.
|
|
28
|
+
request (Any, optional): The FastAPI request object. Defaults to None.
|
|
29
|
+
sort (Optional[List[Dict[str, Any]]], optional): Optional sort parameter. Defaults to None.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A tuple of (collections, next pagination token if any).
|
|
33
|
+
"""
|
|
20
34
|
pass
|
|
21
35
|
|
|
22
36
|
@abc.abstractmethod
|
stac_fastapi/core/core.py
CHANGED
|
@@ -136,6 +136,20 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
136
136
|
"href": urljoin(base_url, "search"),
|
|
137
137
|
"method": "POST",
|
|
138
138
|
},
|
|
139
|
+
{
|
|
140
|
+
"rel": "collections-search",
|
|
141
|
+
"type": "application/json",
|
|
142
|
+
"title": "Collections Search",
|
|
143
|
+
"href": urljoin(base_url, "collections-search"),
|
|
144
|
+
"method": "GET",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"rel": "collections-search",
|
|
148
|
+
"type": "application/json",
|
|
149
|
+
"title": "Collections Search",
|
|
150
|
+
"href": urljoin(base_url, "collections-search"),
|
|
151
|
+
"method": "POST",
|
|
152
|
+
},
|
|
139
153
|
],
|
|
140
154
|
stac_extensions=extension_schemas,
|
|
141
155
|
)
|
|
@@ -224,10 +238,29 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
224
238
|
|
|
225
239
|
return landing_page
|
|
226
240
|
|
|
227
|
-
async def all_collections(
|
|
241
|
+
async def all_collections(
|
|
242
|
+
self,
|
|
243
|
+
datetime: Optional[str] = None,
|
|
244
|
+
limit: Optional[int] = None,
|
|
245
|
+
fields: Optional[List[str]] = None,
|
|
246
|
+
sortby: Optional[Union[str, List[str]]] = None,
|
|
247
|
+
filter_expr: Optional[str] = None,
|
|
248
|
+
filter_lang: Optional[str] = None,
|
|
249
|
+
q: Optional[Union[str, List[str]]] = None,
|
|
250
|
+
query: Optional[str] = None,
|
|
251
|
+
**kwargs,
|
|
252
|
+
) -> stac_types.Collections:
|
|
228
253
|
"""Read all collections from the database.
|
|
229
254
|
|
|
230
255
|
Args:
|
|
256
|
+
datetime (Optional[str]): Filter collections by datetime range.
|
|
257
|
+
limit (Optional[int]): Maximum number of collections to return.
|
|
258
|
+
fields (Optional[List[str]]): Fields to include or exclude from the results.
|
|
259
|
+
sortby (Optional[str]): Sorting options for the results.
|
|
260
|
+
filter_expr (Optional[str]): Structured filter expression in CQL2 JSON or CQL2-text format.
|
|
261
|
+
query (Optional[str]): Legacy query parameter (deprecated).
|
|
262
|
+
filter_lang (Optional[str]): Must be 'cql2-json' or 'cql2-text' if specified, other values will result in an error.
|
|
263
|
+
q (Optional[Union[str, List[str]]]): Free text search terms.
|
|
231
264
|
**kwargs: Keyword arguments from the request.
|
|
232
265
|
|
|
233
266
|
Returns:
|
|
@@ -235,13 +268,151 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
235
268
|
"""
|
|
236
269
|
request = kwargs["request"]
|
|
237
270
|
base_url = str(request.base_url)
|
|
238
|
-
|
|
271
|
+
|
|
272
|
+
# Get the global limit from environment variable
|
|
273
|
+
global_limit = None
|
|
274
|
+
env_limit = os.getenv("STAC_ITEM_LIMIT")
|
|
275
|
+
if env_limit:
|
|
276
|
+
try:
|
|
277
|
+
global_limit = int(env_limit)
|
|
278
|
+
except ValueError:
|
|
279
|
+
# Handle invalid integer in environment variable
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
# Apply global limit if it exists
|
|
283
|
+
if global_limit is not None:
|
|
284
|
+
# If a limit was provided, use the smaller of the two
|
|
285
|
+
if limit is not None:
|
|
286
|
+
limit = min(limit, global_limit)
|
|
287
|
+
else:
|
|
288
|
+
limit = global_limit
|
|
289
|
+
else:
|
|
290
|
+
# No global limit, use provided limit or default
|
|
291
|
+
if limit is None:
|
|
292
|
+
query_limit = request.query_params.get("limit")
|
|
293
|
+
if query_limit:
|
|
294
|
+
try:
|
|
295
|
+
limit = int(query_limit)
|
|
296
|
+
except ValueError:
|
|
297
|
+
limit = 10
|
|
298
|
+
else:
|
|
299
|
+
limit = 10
|
|
300
|
+
|
|
239
301
|
token = request.query_params.get("token")
|
|
240
302
|
|
|
241
|
-
|
|
242
|
-
|
|
303
|
+
# Process fields parameter for filtering collection properties
|
|
304
|
+
includes, excludes = set(), set()
|
|
305
|
+
if fields:
|
|
306
|
+
for field in fields:
|
|
307
|
+
if field[0] == "-":
|
|
308
|
+
excludes.add(field[1:])
|
|
309
|
+
else:
|
|
310
|
+
include_field = field[1:] if field[0] in "+ " else field
|
|
311
|
+
includes.add(include_field)
|
|
312
|
+
|
|
313
|
+
sort = None
|
|
314
|
+
if sortby:
|
|
315
|
+
parsed_sort = []
|
|
316
|
+
for raw in sortby:
|
|
317
|
+
if not isinstance(raw, str):
|
|
318
|
+
continue
|
|
319
|
+
s = raw.strip()
|
|
320
|
+
if not s:
|
|
321
|
+
continue
|
|
322
|
+
direction = "desc" if s[0] == "-" else "asc"
|
|
323
|
+
field = s[1:] if s and s[0] in "+-" else s
|
|
324
|
+
parsed_sort.append({"field": field, "direction": direction})
|
|
325
|
+
if parsed_sort:
|
|
326
|
+
sort = parsed_sort
|
|
327
|
+
|
|
328
|
+
# Convert q to a list if it's a string
|
|
329
|
+
q_list = None
|
|
330
|
+
if q is not None:
|
|
331
|
+
q_list = [q] if isinstance(q, str) else q
|
|
332
|
+
|
|
333
|
+
# Parse the query parameter if provided
|
|
334
|
+
parsed_query = None
|
|
335
|
+
if query is not None:
|
|
336
|
+
try:
|
|
337
|
+
parsed_query = orjson.loads(query)
|
|
338
|
+
except Exception as e:
|
|
339
|
+
raise HTTPException(
|
|
340
|
+
status_code=400, detail=f"Invalid query parameter: {e}"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Parse the filter parameter if provided
|
|
344
|
+
parsed_filter = None
|
|
345
|
+
if filter_expr is not None:
|
|
346
|
+
try:
|
|
347
|
+
# Only raise an error for explicitly unsupported filter languages
|
|
348
|
+
if filter_lang is not None and filter_lang not in [
|
|
349
|
+
"cql2-json",
|
|
350
|
+
"cql2-text",
|
|
351
|
+
]:
|
|
352
|
+
# Raise an error for unsupported filter languages
|
|
353
|
+
raise HTTPException(
|
|
354
|
+
status_code=400,
|
|
355
|
+
detail=f"Only 'cql2-json' and 'cql2-text' filter languages are supported for collections. Got '{filter_lang}'.",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Handle different filter formats
|
|
359
|
+
try:
|
|
360
|
+
if filter_lang == "cql2-text" or filter_lang is None:
|
|
361
|
+
# For cql2-text or when no filter_lang is specified, try both formats
|
|
362
|
+
try:
|
|
363
|
+
# First try to parse as JSON
|
|
364
|
+
parsed_filter = orjson.loads(unquote_plus(filter_expr))
|
|
365
|
+
except Exception:
|
|
366
|
+
# If that fails, use pygeofilter to convert CQL2-text to CQL2-JSON
|
|
367
|
+
try:
|
|
368
|
+
# Parse CQL2-text and convert to CQL2-JSON
|
|
369
|
+
text_filter = unquote_plus(filter_expr)
|
|
370
|
+
parsed_ast = parse_cql2_text(text_filter)
|
|
371
|
+
parsed_filter = to_cql2(parsed_ast)
|
|
372
|
+
except Exception as e:
|
|
373
|
+
# If parsing fails, provide a helpful error message
|
|
374
|
+
raise HTTPException(
|
|
375
|
+
status_code=400,
|
|
376
|
+
detail=f"Invalid CQL2-text filter: {e}. Please check your syntax.",
|
|
377
|
+
)
|
|
378
|
+
else:
|
|
379
|
+
# For explicit cql2-json, parse as JSON
|
|
380
|
+
parsed_filter = orjson.loads(unquote_plus(filter_expr))
|
|
381
|
+
except Exception as e:
|
|
382
|
+
# Catch any other parsing errors
|
|
383
|
+
raise HTTPException(
|
|
384
|
+
status_code=400, detail=f"Error parsing filter: {e}"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
except Exception as e:
|
|
388
|
+
raise HTTPException(
|
|
389
|
+
status_code=400, detail=f"Invalid filter parameter: {e}"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
parsed_datetime = None
|
|
393
|
+
if datetime:
|
|
394
|
+
parsed_datetime = format_datetime_range(date_str=datetime)
|
|
395
|
+
|
|
396
|
+
collections, next_token, maybe_count = await self.database.get_all_collections(
|
|
397
|
+
token=token,
|
|
398
|
+
limit=limit,
|
|
399
|
+
request=request,
|
|
400
|
+
sort=sort,
|
|
401
|
+
q=q_list,
|
|
402
|
+
filter=parsed_filter,
|
|
403
|
+
query=parsed_query,
|
|
404
|
+
datetime=parsed_datetime,
|
|
243
405
|
)
|
|
244
406
|
|
|
407
|
+
# Apply field filtering if fields parameter was provided
|
|
408
|
+
if fields:
|
|
409
|
+
filtered_collections = [
|
|
410
|
+
filter_fields(collection, includes, excludes)
|
|
411
|
+
for collection in collections
|
|
412
|
+
]
|
|
413
|
+
else:
|
|
414
|
+
filtered_collections = collections
|
|
415
|
+
|
|
245
416
|
links = [
|
|
246
417
|
{"rel": Relations.root.value, "type": MimeTypes.json, "href": base_url},
|
|
247
418
|
{"rel": Relations.parent.value, "type": MimeTypes.json, "href": base_url},
|
|
@@ -256,7 +427,91 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
256
427
|
next_link = PagingLinks(next=next_token, request=request).link_next()
|
|
257
428
|
links.append(next_link)
|
|
258
429
|
|
|
259
|
-
return stac_types.Collections(
|
|
430
|
+
return stac_types.Collections(
|
|
431
|
+
collections=filtered_collections,
|
|
432
|
+
links=links,
|
|
433
|
+
numberMatched=maybe_count,
|
|
434
|
+
numberReturned=len(filtered_collections),
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
async def post_all_collections(
|
|
438
|
+
self, search_request: BaseSearchPostRequest, request: Request, **kwargs
|
|
439
|
+
) -> stac_types.Collections:
|
|
440
|
+
"""Search collections with POST request.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
search_request (BaseSearchPostRequest): The search request.
|
|
444
|
+
request (Request): The request.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
A Collections object containing all the collections in the database and links to various resources.
|
|
448
|
+
"""
|
|
449
|
+
request.postbody = search_request.model_dump(exclude_unset=True)
|
|
450
|
+
|
|
451
|
+
fields = None
|
|
452
|
+
|
|
453
|
+
# Check for field attribute (ExtendedSearch format)
|
|
454
|
+
if hasattr(search_request, "field") and search_request.field:
|
|
455
|
+
fields = []
|
|
456
|
+
|
|
457
|
+
# Handle include fields
|
|
458
|
+
if (
|
|
459
|
+
hasattr(search_request.field, "includes")
|
|
460
|
+
and search_request.field.includes
|
|
461
|
+
):
|
|
462
|
+
for field in search_request.field.includes:
|
|
463
|
+
fields.append(f"+{field}")
|
|
464
|
+
|
|
465
|
+
# Handle exclude fields
|
|
466
|
+
if (
|
|
467
|
+
hasattr(search_request.field, "excludes")
|
|
468
|
+
and search_request.field.excludes
|
|
469
|
+
):
|
|
470
|
+
for field in search_request.field.excludes:
|
|
471
|
+
fields.append(f"-{field}")
|
|
472
|
+
|
|
473
|
+
# Convert sortby parameter from POST format to all_collections format
|
|
474
|
+
sortby = None
|
|
475
|
+
# Check for sortby attribute
|
|
476
|
+
if hasattr(search_request, "sortby") and search_request.sortby:
|
|
477
|
+
# Create a list of sort strings in the format expected by all_collections
|
|
478
|
+
sortby = []
|
|
479
|
+
for sort_item in search_request.sortby:
|
|
480
|
+
# Handle different types of sort items
|
|
481
|
+
if hasattr(sort_item, "field") and hasattr(sort_item, "direction"):
|
|
482
|
+
# This is a Pydantic model with field and direction attributes
|
|
483
|
+
field = sort_item.field
|
|
484
|
+
direction = sort_item.direction
|
|
485
|
+
elif isinstance(sort_item, dict):
|
|
486
|
+
# This is a dictionary with field and direction keys
|
|
487
|
+
field = sort_item.get("field")
|
|
488
|
+
direction = sort_item.get("direction", "asc")
|
|
489
|
+
else:
|
|
490
|
+
# Skip this item if we can't extract field and direction
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
if field:
|
|
494
|
+
# Create a sort string in the format expected by all_collections
|
|
495
|
+
# e.g., "-id" for descending sort on id field
|
|
496
|
+
prefix = "-" if direction.lower() == "desc" else ""
|
|
497
|
+
sortby.append(f"{prefix}{field}")
|
|
498
|
+
|
|
499
|
+
# Pass all parameters from search_request to all_collections
|
|
500
|
+
return await self.all_collections(
|
|
501
|
+
limit=search_request.limit if hasattr(search_request, "limit") else None,
|
|
502
|
+
fields=fields,
|
|
503
|
+
sortby=sortby,
|
|
504
|
+
filter_expr=search_request.filter
|
|
505
|
+
if hasattr(search_request, "filter")
|
|
506
|
+
else None,
|
|
507
|
+
filter_lang=search_request.filter_lang
|
|
508
|
+
if hasattr(search_request, "filter_lang")
|
|
509
|
+
else None,
|
|
510
|
+
query=search_request.query if hasattr(search_request, "query") else None,
|
|
511
|
+
q=search_request.q if hasattr(search_request, "q") else None,
|
|
512
|
+
request=request,
|
|
513
|
+
**kwargs,
|
|
514
|
+
)
|
|
260
515
|
|
|
261
516
|
async def get_collection(
|
|
262
517
|
self, collection_id: str, **kwargs
|
|
@@ -574,11 +829,7 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
574
829
|
datetime_search=datetime_search,
|
|
575
830
|
)
|
|
576
831
|
|
|
577
|
-
fields = (
|
|
578
|
-
getattr(search_request, "fields", None)
|
|
579
|
-
if self.extension_is_enabled("FieldsExtension")
|
|
580
|
-
else None
|
|
581
|
-
)
|
|
832
|
+
fields = getattr(search_request, "fields", None)
|
|
582
833
|
include: Set[str] = fields.include if fields and fields.include else set()
|
|
583
834
|
exclude: Set[str] = fields.exclude if fields and fields.exclude else set()
|
|
584
835
|
|
|
@@ -596,8 +847,8 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
596
847
|
type="FeatureCollection",
|
|
597
848
|
features=items,
|
|
598
849
|
links=links,
|
|
599
|
-
|
|
600
|
-
|
|
850
|
+
numberReturned=len(items),
|
|
851
|
+
numberMatched=maybe_count,
|
|
601
852
|
)
|
|
602
853
|
|
|
603
854
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"""elasticsearch extensions modifications."""
|
|
2
2
|
|
|
3
|
+
from .collections_search import CollectionsSearchEndpointExtension
|
|
3
4
|
from .query import Operator, QueryableTypes, QueryExtension
|
|
4
5
|
|
|
5
|
-
__all__ = [
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Operator",
|
|
8
|
+
"QueryableTypes",
|
|
9
|
+
"QueryExtension",
|
|
10
|
+
"CollectionsSearchEndpointExtension",
|
|
11
|
+
]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Collections search extension."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Type, Union
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, FastAPI, Request
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from stac_pydantic.api.search import ExtendedSearch
|
|
9
|
+
from starlette.responses import Response
|
|
10
|
+
|
|
11
|
+
from stac_fastapi.api.models import APIRequest
|
|
12
|
+
from stac_fastapi.types.core import BaseCoreClient
|
|
13
|
+
from stac_fastapi.types.extension import ApiExtension
|
|
14
|
+
from stac_fastapi.types.stac import Collections
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CollectionsSearchRequest(ExtendedSearch):
|
|
18
|
+
"""Extended search model for collections with free text search support."""
|
|
19
|
+
|
|
20
|
+
q: Optional[Union[str, List[str]]] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CollectionsSearchEndpointExtension(ApiExtension):
|
|
24
|
+
"""Collections search endpoint extension.
|
|
25
|
+
|
|
26
|
+
This extension adds a dedicated /collections-search endpoint for collection search operations.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
client: Optional[BaseCoreClient] = None,
|
|
32
|
+
settings: dict = None,
|
|
33
|
+
GET: Optional[Type[Union[BaseModel, APIRequest]]] = None,
|
|
34
|
+
POST: Optional[Type[Union[BaseModel, APIRequest]]] = None,
|
|
35
|
+
conformance_classes: Optional[List[str]] = None,
|
|
36
|
+
):
|
|
37
|
+
"""Initialize the extension.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
client: Optional BaseCoreClient instance to use for this extension.
|
|
41
|
+
settings: Dictionary of settings to pass to the extension.
|
|
42
|
+
GET: Optional GET request model.
|
|
43
|
+
POST: Optional POST request model.
|
|
44
|
+
conformance_classes: Optional list of conformance classes to add to the API.
|
|
45
|
+
"""
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.client = client
|
|
48
|
+
self.settings = settings or {}
|
|
49
|
+
self.GET = GET
|
|
50
|
+
self.POST = POST
|
|
51
|
+
self.conformance_classes = conformance_classes or []
|
|
52
|
+
self.router = APIRouter()
|
|
53
|
+
self.create_endpoints()
|
|
54
|
+
|
|
55
|
+
def register(self, app: FastAPI) -> None:
|
|
56
|
+
"""Register the extension with a FastAPI application.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
app: target FastAPI application.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
None
|
|
63
|
+
"""
|
|
64
|
+
app.include_router(self.router)
|
|
65
|
+
|
|
66
|
+
def create_endpoints(self) -> None:
|
|
67
|
+
"""Create endpoints for the extension."""
|
|
68
|
+
if self.GET:
|
|
69
|
+
self.router.add_api_route(
|
|
70
|
+
name="Get Collections Search",
|
|
71
|
+
path="/collections-search",
|
|
72
|
+
response_model=None,
|
|
73
|
+
response_class=JSONResponse,
|
|
74
|
+
methods=["GET"],
|
|
75
|
+
endpoint=self.collections_search_get_endpoint,
|
|
76
|
+
**(self.settings if isinstance(self.settings, dict) else {}),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if self.POST:
|
|
80
|
+
self.router.add_api_route(
|
|
81
|
+
name="Post Collections Search",
|
|
82
|
+
path="/collections-search",
|
|
83
|
+
response_model=None,
|
|
84
|
+
response_class=JSONResponse,
|
|
85
|
+
methods=["POST"],
|
|
86
|
+
endpoint=self.collections_search_post_endpoint,
|
|
87
|
+
**(self.settings if isinstance(self.settings, dict) else {}),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def collections_search_get_endpoint(
|
|
91
|
+
self, request: Request
|
|
92
|
+
) -> Union[Collections, Response]:
|
|
93
|
+
"""GET /collections-search endpoint.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
request: Request object.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Collections: Collections object.
|
|
100
|
+
"""
|
|
101
|
+
# Extract query parameters from the request
|
|
102
|
+
params = dict(request.query_params)
|
|
103
|
+
|
|
104
|
+
# Convert query parameters to appropriate types
|
|
105
|
+
if "limit" in params:
|
|
106
|
+
try:
|
|
107
|
+
params["limit"] = int(params["limit"])
|
|
108
|
+
except ValueError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
# Handle fields parameter
|
|
112
|
+
if "fields" in params:
|
|
113
|
+
fields_str = params.pop("fields")
|
|
114
|
+
fields = fields_str.split(",")
|
|
115
|
+
params["fields"] = fields
|
|
116
|
+
|
|
117
|
+
# Handle sortby parameter
|
|
118
|
+
if "sortby" in params:
|
|
119
|
+
sortby_str = params.pop("sortby")
|
|
120
|
+
sortby = sortby_str.split(",")
|
|
121
|
+
params["sortby"] = sortby
|
|
122
|
+
|
|
123
|
+
collections = await self.client.all_collections(request=request, **params)
|
|
124
|
+
return collections
|
|
125
|
+
|
|
126
|
+
async def collections_search_post_endpoint(
|
|
127
|
+
self, request: Request, body: dict
|
|
128
|
+
) -> Union[Collections, Response]:
|
|
129
|
+
"""POST /collections-search endpoint.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
request: Request object.
|
|
133
|
+
body: Search request body.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Collections: Collections object.
|
|
137
|
+
"""
|
|
138
|
+
# Convert the dict to an ExtendedSearch model
|
|
139
|
+
search_request = CollectionsSearchRequest.model_validate(body)
|
|
140
|
+
|
|
141
|
+
# Check if fields are present in the body
|
|
142
|
+
if "fields" in body:
|
|
143
|
+
# Extract fields from body and add them to search_request
|
|
144
|
+
if hasattr(search_request, "field"):
|
|
145
|
+
from stac_pydantic.api.extensions.fields import FieldsExtension
|
|
146
|
+
|
|
147
|
+
fields_data = body["fields"]
|
|
148
|
+
search_request.field = FieldsExtension(
|
|
149
|
+
includes=fields_data.get("include"),
|
|
150
|
+
excludes=fields_data.get("exclude"),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Set the postbody on the request for pagination links
|
|
154
|
+
request.postbody = body
|
|
155
|
+
|
|
156
|
+
collections = await self.client.post_all_collections(
|
|
157
|
+
search_request=search_request, request=request
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return collections
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def from_extensions(
|
|
164
|
+
cls, extensions: List[ApiExtension]
|
|
165
|
+
) -> "CollectionsSearchEndpointExtension":
|
|
166
|
+
"""Create a CollectionsSearchEndpointExtension from a list of extensions.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
extensions: List of extensions to include in the CollectionsSearchEndpointExtension.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
CollectionsSearchEndpointExtension: A new CollectionsSearchEndpointExtension instance.
|
|
173
|
+
"""
|
|
174
|
+
from stac_fastapi.api.models import (
|
|
175
|
+
create_get_request_model,
|
|
176
|
+
create_post_request_model,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
get_model = create_get_request_model(extensions)
|
|
180
|
+
post_model = create_post_request_model(extensions)
|
|
181
|
+
|
|
182
|
+
return cls(
|
|
183
|
+
GET=get_model,
|
|
184
|
+
POST=post_model,
|
|
185
|
+
conformance_classes=[
|
|
186
|
+
ext.conformance_classes
|
|
187
|
+
for ext in extensions
|
|
188
|
+
if hasattr(ext, "conformance_classes")
|
|
189
|
+
],
|
|
190
|
+
)
|
|
@@ -41,24 +41,6 @@ DEFAULT_QUERYABLES: Dict[str, Dict[str, Any]] = {
|
|
|
41
41
|
"description": "Creation Timestamp",
|
|
42
42
|
"$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/updated",
|
|
43
43
|
},
|
|
44
|
-
"cloud_cover": {
|
|
45
|
-
"description": "Cloud Cover",
|
|
46
|
-
"$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/definitions/fields/properties/eo:cloud_cover",
|
|
47
|
-
},
|
|
48
|
-
"cloud_shadow_percentage": {
|
|
49
|
-
"title": "Cloud Shadow Percentage",
|
|
50
|
-
"description": "Cloud Shadow Percentage",
|
|
51
|
-
"type": "number",
|
|
52
|
-
"minimum": 0,
|
|
53
|
-
"maximum": 100,
|
|
54
|
-
},
|
|
55
|
-
"nodata_pixel_percentage": {
|
|
56
|
-
"title": "No Data Pixel Percentage",
|
|
57
|
-
"description": "No Data Pixel Percentage",
|
|
58
|
-
"type": "number",
|
|
59
|
-
"minimum": 0,
|
|
60
|
-
"maximum": 100,
|
|
61
|
-
},
|
|
62
44
|
}
|
|
63
45
|
"""Queryables that are present in all collections."""
|
|
64
46
|
|
|
@@ -139,7 +139,7 @@ class CollectionLinks(BaseLinks):
|
|
|
139
139
|
if "FilterExtension" in self.extensions:
|
|
140
140
|
return dict(
|
|
141
141
|
rel="queryables",
|
|
142
|
-
type=MimeTypes.
|
|
142
|
+
type=MimeTypes.jsonschema.value,
|
|
143
143
|
href=urljoin(
|
|
144
144
|
self.base_url, f"collections/{self.collection_id}/queryables"
|
|
145
145
|
),
|
stac_fastapi/core/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""library version."""
|
|
2
|
-
__version__ = "6.
|
|
2
|
+
__version__ = "6.5.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stac-fastapi-core
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.5.0
|
|
4
4
|
Summary: Core library for the Elasticsearch and Opensearch stac-fastapi backends.
|
|
5
5
|
Home-page: https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch
|
|
6
6
|
License: MIT
|
|
@@ -67,11 +67,10 @@ SFEOS (stac-fastapi-elasticsearch-opensearch) is a high-performance, scalable AP
|
|
|
67
67
|
- **Scale to millions of geospatial assets** with fast search performance through optimized spatial indexing and query capabilities
|
|
68
68
|
- **Support OGC-compliant filtering** including spatial operations (intersects, contains, etc.) and temporal queries
|
|
69
69
|
- **Perform geospatial aggregations** to analyze data distribution across space and time
|
|
70
|
+
- **Enhanced collection search capabilities** with support for sorting and field selection
|
|
70
71
|
|
|
71
72
|
This implementation builds on the STAC-FastAPI framework, providing a production-ready solution specifically optimized for Elasticsearch and OpenSearch databases. It's ideal for organizations managing large geospatial data catalogs who need efficient discovery and access capabilities through standardized APIs.
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
74
|
## Common Deployment Patterns
|
|
76
75
|
|
|
77
76
|
stac-fastapi-elasticsearch-opensearch can be deployed in several ways depending on your needs:
|
|
@@ -98,12 +97,13 @@ This project is built on the following technologies: STAC, stac-fastapi, FastAPI
|
|
|
98
97
|
## Table of Contents
|
|
99
98
|
|
|
100
99
|
- [stac-fastapi-elasticsearch-opensearch](#stac-fastapi-elasticsearch-opensearch)
|
|
101
|
-
- [Sponsors
|
|
100
|
+
- [Sponsors & Supporters](#sponsors--supporters)
|
|
102
101
|
- [Project Introduction - What is SFEOS?](#project-introduction---what-is-sfeos)
|
|
103
102
|
- [Common Deployment Patterns](#common-deployment-patterns)
|
|
104
103
|
- [Technologies](#technologies)
|
|
105
104
|
- [Table of Contents](#table-of-contents)
|
|
106
|
-
- [
|
|
105
|
+
- [Collection Search Extensions](#collection-search-extensions)
|
|
106
|
+
- [Documentation & Resources](#documentation--resources)
|
|
107
107
|
- [Package Structure](#package-structure)
|
|
108
108
|
- [Examples](#examples)
|
|
109
109
|
- [Performance](#performance)
|
|
@@ -144,6 +144,59 @@ This project is built on the following technologies: STAC, stac-fastapi, FastAPI
|
|
|
144
144
|
- [Gitter Chat](https://app.gitter.im/#/room/#stac-fastapi-elasticsearch_community:gitter.im) - For real-time discussions
|
|
145
145
|
- [GitHub Discussions](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/discussions) - For longer-form questions and answers
|
|
146
146
|
|
|
147
|
+
## Collection Search Extensions
|
|
148
|
+
|
|
149
|
+
SFEOS provides enhanced collection search capabilities through two primary routes:
|
|
150
|
+
- **GET/POST `/collections`**: The standard STAC endpoint with extended query parameters
|
|
151
|
+
- **GET/POST `/collections-search`**: A custom endpoint that supports the same parameters, created to avoid conflicts with the STAC Transactions extension if enabled (which uses POST `/collections` for collection creation)
|
|
152
|
+
|
|
153
|
+
These endpoints support advanced collection discovery features including:
|
|
154
|
+
|
|
155
|
+
- **Sorting**: Sort collections by sortable fields using the `sortby` parameter
|
|
156
|
+
- Example: `/collections?sortby=+id` (ascending sort by ID)
|
|
157
|
+
- Example: `/collections?sortby=-id` (descending sort by ID)
|
|
158
|
+
- Example: `/collections?sortby=-temporal` (descending sort by temporal extent)
|
|
159
|
+
|
|
160
|
+
- **Field Selection**: Request only specific fields to be returned using the `fields` parameter
|
|
161
|
+
- Example: `/collections?fields=id,title,description`
|
|
162
|
+
- This helps reduce payload size when only certain fields are needed
|
|
163
|
+
|
|
164
|
+
- **Free Text Search**: Search across collection text fields using the `q` parameter
|
|
165
|
+
- Example: `/collections?q=landsat`
|
|
166
|
+
- Searches across multiple text fields including title, description, and keywords
|
|
167
|
+
- Supports partial word matching and relevance-based sorting
|
|
168
|
+
|
|
169
|
+
- **Structured Filtering**: Filter collections using CQL2 expressions
|
|
170
|
+
- JSON format: `/collections?filter={"op":"=","args":[{"property":"id"},"sentinel-2"]}&filter-lang=cql2-json`
|
|
171
|
+
- Text format: `/collections?filter=id='sentinel-2'&filter-lang=cql2-text` (note: string values must be quoted)
|
|
172
|
+
- Advanced text format: `/collections?filter=id LIKE '%sentinel%'&filter-lang=cql2-text` (supports LIKE, BETWEEN, etc.)
|
|
173
|
+
- Supports both CQL2 JSON and CQL2 text formats with various operators
|
|
174
|
+
- Enables precise filtering on any collection property
|
|
175
|
+
|
|
176
|
+
- **Datetime Filtering**: Filter collections by their temporal extent using the `datetime` parameter
|
|
177
|
+
- Example: `/collections?datetime=2020-01-01T00:00:00Z/2020-12-31T23:59:59Z` (finds collections with temporal extents that overlap this range)
|
|
178
|
+
- Example: `/collections?datetime=2020-06-15T12:00:00Z` (finds collections whose temporal extent includes this specific time)
|
|
179
|
+
- Example: `/collections?datetime=2020-01-01T00:00:00Z/..` (finds collections with temporal extents that extend to or beyond January 1, 2020)
|
|
180
|
+
- Example: `/collections?datetime=../2020-12-31T23:59:59Z` (finds collections with temporal extents that begin on or before December 31, 2020)
|
|
181
|
+
- Collections are matched if their temporal extent overlaps with the provided datetime parameter
|
|
182
|
+
- This allows for efficient discovery of collections based on time periods
|
|
183
|
+
|
|
184
|
+
These extensions make it easier to build user interfaces that display and navigate through collections efficiently.
|
|
185
|
+
|
|
186
|
+
> **Configuration**: Collection search extensions (sorting, field selection, free text search, structured filtering, and datetime filtering) for the `/collections` endpoint can be disabled by setting the `ENABLE_COLLECTIONS_SEARCH` environment variable to `false`. By default, these extensions are enabled.
|
|
187
|
+
>
|
|
188
|
+
> **Configuration**: The custom `/collections-search` endpoint can be enabled by setting the `ENABLE_COLLECTIONS_SEARCH_ROUTE` environment variable to `true`. By default, this endpoint is **disabled**.
|
|
189
|
+
|
|
190
|
+
> **Note**: Sorting is only available on fields that are indexed for sorting in Elasticsearch/OpenSearch. With the default mappings, you can sort on:
|
|
191
|
+
> - `id` (keyword field)
|
|
192
|
+
> - `extent.temporal.interval` (date field)
|
|
193
|
+
> - `temporal` (alias to extent.temporal.interval)
|
|
194
|
+
>
|
|
195
|
+
> Text fields like `title` and `description` are not sortable by default as they use text analysis for better search capabilities. Attempting to sort on these fields will result in a user-friendly error message explaining which fields are sortable and how to make additional fields sortable by updating the mappings.
|
|
196
|
+
>
|
|
197
|
+
> **Important**: Adding keyword fields to make text fields sortable can significantly increase the index size, especially for large text fields. Consider the storage implications when deciding which fields to make sortable.
|
|
198
|
+
|
|
199
|
+
|
|
147
200
|
## Package Structure
|
|
148
201
|
|
|
149
202
|
This project is organized into several packages, each with a specific purpose:
|
|
@@ -156,7 +209,7 @@ This project is organized into several packages, each with a specific purpose:
|
|
|
156
209
|
- Shared logic and utilities that improve code reuse between backends
|
|
157
210
|
|
|
158
211
|
- **stac_fastapi_elasticsearch**: Complete implementation of the STAC API using Elasticsearch as the backend database. This package depends on both `stac_fastapi_core` and `sfeos_helpers`.
|
|
159
|
-
|
|
212
|
+
|
|
160
213
|
- **stac_fastapi_opensearch**: Complete implementation of the STAC API using OpenSearch as the backend database. This package depends on both `stac_fastapi_core` and `sfeos_helpers`.
|
|
161
214
|
|
|
162
215
|
## Examples
|
|
@@ -274,10 +327,13 @@ You can customize additional settings in your `.env` file:
|
|
|
274
327
|
| `ENABLE_DIRECT_RESPONSE` | Enable direct response for maximum performance (disables all FastAPI dependencies, including authentication, custom status codes, and validation) | `false` | Optional |
|
|
275
328
|
| `RAISE_ON_BULK_ERROR` | Controls whether bulk insert operations raise exceptions on errors. If set to `true`, the operation will stop and raise an exception when an error occurs. If set to `false`, errors will be logged, and the operation will continue. **Note:** STAC Item and ItemCollection validation errors will always raise, regardless of this flag. | `false` | Optional |
|
|
276
329
|
| `DATABASE_REFRESH` | Controls whether database operations refresh the index immediately after changes. If set to `true`, changes will be immediately searchable. If set to `false`, changes may not be immediately visible but can improve performance for bulk operations. If set to `wait_for`, changes will wait for the next refresh cycle to become visible. | `false` | Optional |
|
|
277
|
-
| `
|
|
330
|
+
| `ENABLE_COLLECTIONS_SEARCH` | Enable collection search extensions (sort, fields, free text search, structured filtering, and datetime filtering) on the core `/collections` endpoint. | `true` | Optional |
|
|
331
|
+
| `ENABLE_COLLECTIONS_SEARCH_ROUTE` | Enable the custom `/collections-search` endpoint (both GET and POST methods). When disabled, the custom endpoint will not be available, but collection search extensions will still be available on the core `/collections` endpoint if `ENABLE_COLLECTIONS_SEARCH` is true. | `false` | Optional |
|
|
332
|
+
| `ENABLE_TRANSACTIONS_EXTENSIONS` | Enables or disables the Transactions and Bulk Transactions API extensions. This is useful for deployments where mutating the catalog via the API should be prevented. If set to `true`, the POST `/collections` route for search will be unavailable in the API. | `true` | Optional |
|
|
278
333
|
| `STAC_ITEM_LIMIT` | Sets the environment variable for result limiting to SFEOS for the number of returned items and STAC collections. | `10` | Optional |
|
|
279
334
|
| `STAC_INDEX_ASSETS` | Controls if Assets are indexed when added to Elasticsearch/Opensearch. This allows asset fields to be included in search queries. | `false` | Optional |
|
|
280
335
|
| `ENV_MAX_LIMIT` | Configures the environment variable in SFEOS to override the default `MAX_LIMIT`, which controls the limit parameter for returned items and STAC collections. | `10,000` | Optional |
|
|
336
|
+
| `USE_DATETIME` | Configures the datetime search behavior in SFEOS. When enabled, searches both datetime field and falls back to start_datetime/end_datetime range for items with null datetime. When disabled, searches only by start_datetime/end_datetime range. | `true` | Optional |
|
|
281
337
|
|
|
282
338
|
> [!NOTE]
|
|
283
339
|
> The variables `ES_HOST`, `ES_PORT`, `ES_USE_SSL`, `ES_VERIFY_CERTS` and `ES_TIMEOUT` apply to both Elasticsearch and OpenSearch backends, so there is no need to rename the key names to `OS_` even if you're using OpenSearch.
|
|
@@ -419,6 +475,9 @@ The system uses a precise naming convention:
|
|
|
419
475
|
- **Root Path Configuration**: The application root path is the base URL by default.
|
|
420
476
|
- For AWS Lambda with Gateway API: Set `STAC_FASTAPI_ROOT_PATH` to match the Gateway API stage name (e.g., `/v1`)
|
|
421
477
|
|
|
478
|
+
- **Feature Configuration**: Control which features are enabled:
|
|
479
|
+
- `ENABLE_COLLECTIONS_SEARCH`: Set to `true` (default) to enable collection search extensions (sort, fields). Set to `false` to disable.
|
|
480
|
+
- `ENABLE_TRANSACTIONS_EXTENSIONS`: Set to `true` (default) to enable transaction extensions. Set to `false` to disable.
|
|
422
481
|
|
|
423
482
|
## Collection Pagination
|
|
424
483
|
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
stac_fastapi/core/__init__.py,sha256=8izV3IWRGdXmDOK1hIPQAanbWs9EI04PJCGgqG1ZGIs,20
|
|
2
|
-
stac_fastapi/core/base_database_logic.py,sha256=
|
|
2
|
+
stac_fastapi/core/base_database_logic.py,sha256=nhj0CZNur_SRs4GtXTER-Zjq8JPub5zINiCKbCjw0Bs,3814
|
|
3
3
|
stac_fastapi/core/base_settings.py,sha256=R3_Sx7n5XpGMs3zAwFJD7y008WvGU_uI2xkaabm82Kg,239
|
|
4
4
|
stac_fastapi/core/basic_auth.py,sha256=RhFv3RVSHF6OaqnaaU2DO4ncJ_S5nB1q8UNpnVJJsrk,2155
|
|
5
|
-
stac_fastapi/core/core.py,sha256=
|
|
5
|
+
stac_fastapi/core/core.py,sha256=8eGoIcEZVh-4JGZ9PSrY-e8ypEqfmIBxk_napP1HRL4,48014
|
|
6
6
|
stac_fastapi/core/datetime_utils.py,sha256=TrTgbU7AKNC-ic4a3HptfE5XAc9tHR7uJasZyhOuwnc,2633
|
|
7
7
|
stac_fastapi/core/rate_limit.py,sha256=Gu8dAaJReGsj1L91U6m2tflU6RahpXDRs2-AYSKoybA,1318
|
|
8
8
|
stac_fastapi/core/route_dependencies.py,sha256=hdtuMkv-zY1vg0YxiCz1aKP0SbBcORqDGEKDGgEazW8,5482
|
|
9
9
|
stac_fastapi/core/serializers.py,sha256=HU7sVSMa6w_F_qs_gdAeIFZ18GW-6t8ZHFmgI4-1uNw,7455
|
|
10
10
|
stac_fastapi/core/session.py,sha256=aXqu4LXfVbAAsChMVXd9gAhczA2bZPne6HqPeklAwMY,474
|
|
11
11
|
stac_fastapi/core/utilities.py,sha256=WbspaJey_Cs-7TrBKasdqq7yjB7vjKiU01KyJM0m8_E,7506
|
|
12
|
-
stac_fastapi/core/version.py,sha256=
|
|
13
|
-
stac_fastapi/core/extensions/__init__.py,sha256=
|
|
12
|
+
stac_fastapi/core/version.py,sha256=KQjuGSR03-CXgF6wsaZ8qsni161S2BjhOn3wTX8JAMw,45
|
|
13
|
+
stac_fastapi/core/extensions/__init__.py,sha256=zSIAqou8jnakWPbkh4Ddcx1-oazZVBOs7U2PAakAdU0,291
|
|
14
14
|
stac_fastapi/core/extensions/aggregation.py,sha256=v1hUHqlYuMqfQ554g3cTp16pUyRYucQxPERbHPAFtf8,1878
|
|
15
|
+
stac_fastapi/core/extensions/collections_search.py,sha256=bneJQuu-OIsW5P0IbfL6Wz-iowSdAmyz6C0X6vzMn9M,6366
|
|
15
16
|
stac_fastapi/core/extensions/fields.py,sha256=NCT5XHvfaf297eDPNaIFsIzvJnbbUTpScqF0otdx0NA,1066
|
|
16
|
-
stac_fastapi/core/extensions/filter.py,sha256
|
|
17
|
+
stac_fastapi/core/extensions/filter.py,sha256=-NQGME7rR_ereuDx-LAa1M5JhEXFaKiTtkH2asraYHE,2998
|
|
17
18
|
stac_fastapi/core/extensions/query.py,sha256=Xmo8pfZEZKPudZEjjozv3R0wLOP0ayjC9E67sBOXqWY,1803
|
|
18
19
|
stac_fastapi/core/models/__init__.py,sha256=g-D1DiGfmC9Bg27DW9JzkN6fAvscv75wyhyiZ6NzvIk,48
|
|
19
|
-
stac_fastapi/core/models/links.py,sha256=
|
|
20
|
+
stac_fastapi/core/models/links.py,sha256=0dWSEMt3aa7NCISlHwo11zLBeIV1LwXG3JGjrXC3dZI,6672
|
|
20
21
|
stac_fastapi/core/models/search.py,sha256=7SgAUyzHGXBXSqB4G6cwq9FMwoAS00momb7jvBkjyow,27
|
|
21
|
-
stac_fastapi_core-6.
|
|
22
|
-
stac_fastapi_core-6.
|
|
23
|
-
stac_fastapi_core-6.
|
|
24
|
-
stac_fastapi_core-6.
|
|
22
|
+
stac_fastapi_core-6.5.0.dist-info/METADATA,sha256=bTSmvE848AqW76bda8EzNHRORPTXp08pcig6HdGw7ZI,41746
|
|
23
|
+
stac_fastapi_core-6.5.0.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
24
|
+
stac_fastapi_core-6.5.0.dist-info/top_level.txt,sha256=vqn-D9-HsRPTTxy0Vk_KkDmTiMES4owwBQ3ydSZYb2s,13
|
|
25
|
+
stac_fastapi_core-6.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|