stac-fastapi.core 3.0.0a3__tar.gz → 3.2.0__tar.gz
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-3.0.0a3 → stac-fastapi.core-3.2.0}/PKG-INFO +5 -14
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/README.md +5 -1
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/setup.py +4 -3
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/core.py +17 -11
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/extensions/aggregation.py +39 -15
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/extensions/filter.py +25 -15
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/models/links.py +8 -0
- stac-fastapi.core-3.2.0/stac_fastapi/core/rate_limit.py +44 -0
- stac-fastapi.core-3.2.0/stac_fastapi/core/version.py +2 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi.core.egg-info/PKG-INFO +5 -14
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi.core.egg-info/SOURCES.txt +1 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi.core.egg-info/requires.txt +4 -3
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi.core.egg-info/top_level.txt +0 -1
- stac_fastapi_core-3.0.0a3/stac_fastapi/core/version.py +0 -2
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/setup.cfg +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/__init__.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/base_database_logic.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/base_settings.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/basic_auth.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/datetime_utils.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/extensions/__init__.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/extensions/fields.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/extensions/query.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/models/__init__.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/models/search.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/route_dependencies.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/serializers.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/session.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/utilities.py +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi.core.egg-info/dependency_links.txt +0 -0
- {stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi.core.egg-info/not-zip-safe +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stac-fastapi.core
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.2.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
|
|
@@ -15,19 +15,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Requires-Python: >=3.8
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: fastapi-slim
|
|
19
|
-
Requires-Dist: attrs>=23.2.0
|
|
20
|
-
Requires-Dist: pydantic[dotenv]
|
|
21
|
-
Requires-Dist: stac_pydantic>=3
|
|
22
|
-
Requires-Dist: stac-fastapi.types==3.0.0a4
|
|
23
|
-
Requires-Dist: stac-fastapi.api==3.0.0a4
|
|
24
|
-
Requires-Dist: stac-fastapi.extensions==3.0.0a4
|
|
25
|
-
Requires-Dist: orjson
|
|
26
|
-
Requires-Dist: overrides
|
|
27
|
-
Requires-Dist: geojson-pydantic
|
|
28
|
-
Requires-Dist: pygeofilter==0.2.1
|
|
29
|
-
Requires-Dist: typing_extensions==4.8.0
|
|
30
|
-
Requires-Dist: jsonschema
|
|
31
18
|
|
|
32
19
|
# stac-fastapi-elasticsearch-opensearch (sfeos)
|
|
33
20
|
|
|
@@ -415,3 +402,7 @@ Available aggregations are:
|
|
|
415
402
|
- geometry_geotile_grid_frequency ([geotile grid](https://opensearch.org/docs/latest/aggregations/bucket/geotile-grid/) on Item.geometry)
|
|
416
403
|
|
|
417
404
|
Support for additional fields and new aggregations can be added in the associated `database_logic.py` file.
|
|
405
|
+
|
|
406
|
+
## Rate Limiting
|
|
407
|
+
|
|
408
|
+
Rate limiting is an optional security feature that controls API request frequency on a remote address basis. It's enabled by setting the `STAC_FASTAPI_RATE_LIMIT` environment variable, e.g., `500/minute`. This limits each client to 500 requests per minute, helping prevent abuse and maintain API stability. Implementation examples are available in the [examples/rate_limit](examples/rate_limit) directory.
|
|
@@ -383,4 +383,8 @@ Available aggregations are:
|
|
|
383
383
|
- geometry_geohash_grid_frequency ([geohash grid](https://opensearch.org/docs/latest/aggregations/bucket/geohash-grid/) on Item.geometry)
|
|
384
384
|
- geometry_geotile_grid_frequency ([geotile grid](https://opensearch.org/docs/latest/aggregations/bucket/geotile-grid/) on Item.geometry)
|
|
385
385
|
|
|
386
|
-
Support for additional fields and new aggregations can be added in the associated `database_logic.py` file.
|
|
386
|
+
Support for additional fields and new aggregations can be added in the associated `database_logic.py` file.
|
|
387
|
+
|
|
388
|
+
## Rate Limiting
|
|
389
|
+
|
|
390
|
+
Rate limiting is an optional security feature that controls API request frequency on a remote address basis. It's enabled by setting the `STAC_FASTAPI_RATE_LIMIT` environment variable, e.g., `500/minute`. This limits each client to 500 requests per minute, helping prevent abuse and maintain API stability. Implementation examples are available in the [examples/rate_limit](examples/rate_limit) directory.
|
|
@@ -10,15 +10,16 @@ install_requires = [
|
|
|
10
10
|
"attrs>=23.2.0",
|
|
11
11
|
"pydantic[dotenv]",
|
|
12
12
|
"stac_pydantic>=3",
|
|
13
|
-
"stac-fastapi.types==3.0.
|
|
14
|
-
"stac-fastapi.api==3.0.
|
|
15
|
-
"stac-fastapi.extensions==3.0.
|
|
13
|
+
"stac-fastapi.types==3.0.0",
|
|
14
|
+
"stac-fastapi.api==3.0.0",
|
|
15
|
+
"stac-fastapi.extensions==3.0.0",
|
|
16
16
|
"orjson",
|
|
17
17
|
"overrides",
|
|
18
18
|
"geojson-pydantic",
|
|
19
19
|
"pygeofilter==0.2.1",
|
|
20
20
|
"typing_extensions==4.8.0",
|
|
21
21
|
"jsonschema",
|
|
22
|
+
"slowapi==0.1.9",
|
|
22
23
|
]
|
|
23
24
|
|
|
24
25
|
setup(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Core client."""
|
|
2
|
+
|
|
2
3
|
import logging
|
|
3
|
-
import re
|
|
4
4
|
from datetime import datetime as datetime_type
|
|
5
5
|
from datetime import timezone
|
|
6
6
|
from enum import Enum
|
|
@@ -279,8 +279,8 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
279
279
|
collection_id: str,
|
|
280
280
|
bbox: Optional[BBox] = None,
|
|
281
281
|
datetime: Optional[DateTimeType] = None,
|
|
282
|
-
limit: int = 10,
|
|
283
|
-
token: str = None,
|
|
282
|
+
limit: Optional[int] = 10,
|
|
283
|
+
token: Optional[str] = None,
|
|
284
284
|
**kwargs,
|
|
285
285
|
) -> stac_types.ItemCollection:
|
|
286
286
|
"""Read items from a specific collection in the database.
|
|
@@ -302,6 +302,8 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
302
302
|
Exception: If any error occurs while reading the items from the database.
|
|
303
303
|
"""
|
|
304
304
|
request: Request = kwargs["request"]
|
|
305
|
+
token = request.query_params.get("token")
|
|
306
|
+
|
|
305
307
|
base_url = str(request.base_url)
|
|
306
308
|
|
|
307
309
|
collection = await self.get_collection(
|
|
@@ -455,6 +457,7 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
455
457
|
token: Optional[str] = None,
|
|
456
458
|
fields: Optional[List[str]] = None,
|
|
457
459
|
sortby: Optional[str] = None,
|
|
460
|
+
q: Optional[List[str]] = None,
|
|
458
461
|
intersects: Optional[str] = None,
|
|
459
462
|
filter: Optional[str] = None,
|
|
460
463
|
filter_lang: Optional[str] = None,
|
|
@@ -472,6 +475,7 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
472
475
|
token (Optional[str]): Access token to use when searching the catalog.
|
|
473
476
|
fields (Optional[List[str]]): Fields to include or exclude from the results.
|
|
474
477
|
sortby (Optional[str]): Sorting options for the results.
|
|
478
|
+
q (Optional[List[str]]): Free text query to filter the results.
|
|
475
479
|
intersects (Optional[str]): GeoJSON geometry to search in.
|
|
476
480
|
kwargs: Additional parameters to be passed to the API.
|
|
477
481
|
|
|
@@ -488,16 +492,9 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
488
492
|
"limit": limit,
|
|
489
493
|
"token": token,
|
|
490
494
|
"query": orjson.loads(query) if query else query,
|
|
495
|
+
"q": q,
|
|
491
496
|
}
|
|
492
497
|
|
|
493
|
-
# this is borrowed from stac-fastapi-pgstac
|
|
494
|
-
# Kludgy fix because using factory does not allow alias for filter-lan
|
|
495
|
-
query_params = str(request.query_params)
|
|
496
|
-
if filter_lang is None:
|
|
497
|
-
match = re.search(r"filter-lang=([a-z0-9-]+)", query_params, re.IGNORECASE)
|
|
498
|
-
if match:
|
|
499
|
-
filter_lang = match.group(1)
|
|
500
|
-
|
|
501
498
|
if datetime:
|
|
502
499
|
base_args["datetime"] = self._format_datetime_range(datetime)
|
|
503
500
|
|
|
@@ -606,6 +603,15 @@ class CoreClient(AsyncBaseCoreClient):
|
|
|
606
603
|
status_code=400, detail=f"Error with cql2_json filter: {e}"
|
|
607
604
|
)
|
|
608
605
|
|
|
606
|
+
if hasattr(search_request, "q"):
|
|
607
|
+
free_text_queries = getattr(search_request, "q", None)
|
|
608
|
+
try:
|
|
609
|
+
search = self.database.apply_free_text_filter(search, free_text_queries)
|
|
610
|
+
except Exception as e:
|
|
611
|
+
raise HTTPException(
|
|
612
|
+
status_code=400, detail=f"Error with free text query: {e}"
|
|
613
|
+
)
|
|
614
|
+
|
|
609
615
|
sort = None
|
|
610
616
|
if search_request.sortby:
|
|
611
617
|
sort = self.database.populate_sort(search_request.sortby)
|
{stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/extensions/aggregation.py
RENAMED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"""Request model for the Aggregation extension."""
|
|
2
2
|
|
|
3
|
-
import re
|
|
4
3
|
from datetime import datetime
|
|
5
4
|
from datetime import datetime as datetime_type
|
|
6
5
|
from typing import Dict, List, Literal, Optional, Union
|
|
@@ -51,6 +50,7 @@ class EsAggregationExtensionGetRequest(
|
|
|
51
50
|
centroid_geotile_grid_frequency_precision: Optional[int] = attr.ib(default=None)
|
|
52
51
|
geometry_geohash_grid_frequency_precision: Optional[int] = attr.ib(default=None)
|
|
53
52
|
geometry_geotile_grid_frequency_precision: Optional[int] = attr.ib(default=None)
|
|
53
|
+
datetime_frequency_interval: Optional[str] = attr.ib(default=None)
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
class EsAggregationExtensionPostRequest(
|
|
@@ -63,6 +63,7 @@ class EsAggregationExtensionPostRequest(
|
|
|
63
63
|
centroid_geotile_grid_frequency_precision: Optional[int] = None
|
|
64
64
|
geometry_geohash_grid_frequency_precision: Optional[int] = None
|
|
65
65
|
geometry_geotile_grid_frequency_precision: Optional[int] = None
|
|
66
|
+
datetime_frequency_interval: Optional[str] = None
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
@attr.s
|
|
@@ -125,6 +126,8 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
|
|
|
125
126
|
MAX_GEOHASH_PRECISION = 12
|
|
126
127
|
MAX_GEOHEX_PRECISION = 15
|
|
127
128
|
MAX_GEOTILE_PRECISION = 29
|
|
129
|
+
SUPPORTED_DATETIME_INTERVAL = {"day", "month", "year"}
|
|
130
|
+
DEFAULT_DATETIME_INTERVAL = "month"
|
|
128
131
|
|
|
129
132
|
async def get_aggregations(self, collection_id: Optional[str] = None, **kwargs):
|
|
130
133
|
"""Get the available aggregations for a catalog or collection defined in the STAC JSON. If no aggregations, default aggregations are used."""
|
|
@@ -144,7 +147,7 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
|
|
|
144
147
|
{
|
|
145
148
|
"rel": "self",
|
|
146
149
|
"type": "application/json",
|
|
147
|
-
"href": urljoin(collection_endpoint, "aggregations"),
|
|
150
|
+
"href": urljoin(collection_endpoint + "/", "aggregations"),
|
|
148
151
|
},
|
|
149
152
|
]
|
|
150
153
|
)
|
|
@@ -183,6 +186,30 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
|
|
|
183
186
|
else:
|
|
184
187
|
return min_value
|
|
185
188
|
|
|
189
|
+
def extract_date_histogram_interval(self, value: Optional[str]) -> str:
|
|
190
|
+
"""
|
|
191
|
+
Ensure that the interval for the date histogram is valid. If no value is provided, the default will be returned.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
value: value entered by the user
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
string containing the date histogram interval to use.
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
HTTPException: if the supplied value is not in the supported intervals
|
|
201
|
+
"""
|
|
202
|
+
if value is not None:
|
|
203
|
+
if value not in self.SUPPORTED_DATETIME_INTERVAL:
|
|
204
|
+
raise HTTPException(
|
|
205
|
+
status_code=400,
|
|
206
|
+
detail=f"Invalid datetime interval. Must be one of {self.SUPPORTED_DATETIME_INTERVAL}",
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
return value
|
|
210
|
+
else:
|
|
211
|
+
return self.DEFAULT_DATETIME_INTERVAL
|
|
212
|
+
|
|
186
213
|
@staticmethod
|
|
187
214
|
def _return_date(
|
|
188
215
|
interval: Optional[Union[DateTimeType, str]]
|
|
@@ -271,7 +298,7 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
|
|
|
271
298
|
def get_filter(self, filter, filter_lang):
|
|
272
299
|
"""Format the filter parameter in cql2-json or cql2-text."""
|
|
273
300
|
if filter_lang == "cql2-text":
|
|
274
|
-
return orjson.loads(
|
|
301
|
+
return orjson.loads(to_cql2(parse_cql2_text(filter)))
|
|
275
302
|
elif filter_lang == "cql2-json":
|
|
276
303
|
if isinstance(filter, str):
|
|
277
304
|
return orjson.loads(unquote_plus(filter))
|
|
@@ -320,6 +347,7 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
|
|
|
320
347
|
centroid_geotile_grid_frequency_precision: Optional[int] = None,
|
|
321
348
|
geometry_geohash_grid_frequency_precision: Optional[int] = None,
|
|
322
349
|
geometry_geotile_grid_frequency_precision: Optional[int] = None,
|
|
350
|
+
datetime_frequency_interval: Optional[str] = None,
|
|
323
351
|
**kwargs,
|
|
324
352
|
) -> Union[Dict, Exception]:
|
|
325
353
|
"""Get aggregations from the database."""
|
|
@@ -340,6 +368,7 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
|
|
|
340
368
|
"centroid_geotile_grid_frequency_precision": centroid_geotile_grid_frequency_precision,
|
|
341
369
|
"geometry_geohash_grid_frequency_precision": geometry_geohash_grid_frequency_precision,
|
|
342
370
|
"geometry_geotile_grid_frequency_precision": geometry_geotile_grid_frequency_precision,
|
|
371
|
+
"datetime_frequency_interval": datetime_frequency_interval,
|
|
343
372
|
}
|
|
344
373
|
|
|
345
374
|
if collection_id:
|
|
@@ -351,18 +380,6 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
|
|
|
351
380
|
if datetime:
|
|
352
381
|
base_args["datetime"] = self._format_datetime_range(datetime)
|
|
353
382
|
|
|
354
|
-
# this is borrowed from stac-fastapi-pgstac
|
|
355
|
-
# Kludgy fix because using factory does not allow alias for filter-lang
|
|
356
|
-
# If the value is the default, check if the request is different.
|
|
357
|
-
query_params = str(request.query_params)
|
|
358
|
-
if filter_lang is None:
|
|
359
|
-
match = re.search(
|
|
360
|
-
r"filter-lang=([a-z0-9-]+)", query_params, re.IGNORECASE
|
|
361
|
-
)
|
|
362
|
-
if match:
|
|
363
|
-
filter_lang = match.group(1)
|
|
364
|
-
else:
|
|
365
|
-
filter_lang = "cql2-text"
|
|
366
383
|
if filter:
|
|
367
384
|
base_args["filter"] = self.get_filter(filter, filter_lang)
|
|
368
385
|
aggregate_request = EsAggregationExtensionPostRequest(**base_args)
|
|
@@ -447,6 +464,8 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
|
|
|
447
464
|
status_code=400,
|
|
448
465
|
detail=f"Aggregation {agg_name} not supported at catalog level",
|
|
449
466
|
)
|
|
467
|
+
|
|
468
|
+
if aggregate_request.filter:
|
|
450
469
|
try:
|
|
451
470
|
search = self.database.apply_cql2_filter(
|
|
452
471
|
search, aggregate_request.filter
|
|
@@ -486,6 +505,10 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
|
|
|
486
505
|
self.MAX_GEOTILE_PRECISION,
|
|
487
506
|
)
|
|
488
507
|
|
|
508
|
+
datetime_frequency_interval = self.extract_date_histogram_interval(
|
|
509
|
+
aggregate_request.datetime_frequency_interval,
|
|
510
|
+
)
|
|
511
|
+
|
|
489
512
|
try:
|
|
490
513
|
db_response = await self.database.aggregate(
|
|
491
514
|
collections,
|
|
@@ -496,6 +519,7 @@ class EsAsyncAggregationClient(AsyncBaseAggregationClient):
|
|
|
496
519
|
centroid_geotile_grid_precision,
|
|
497
520
|
geometry_geohash_grid_precision,
|
|
498
521
|
geometry_geotile_grid_precision,
|
|
522
|
+
datetime_frequency_interval,
|
|
499
523
|
)
|
|
500
524
|
except Exception as error:
|
|
501
525
|
if not isinstance(error, IndexError):
|
{stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/extensions/filter.py
RENAMED
|
@@ -17,31 +17,41 @@ import re
|
|
|
17
17
|
from enum import Enum
|
|
18
18
|
from typing import Any, Dict
|
|
19
19
|
|
|
20
|
+
_cql2_like_patterns = re.compile(r"\\.|[%_]|\\$")
|
|
21
|
+
_valid_like_substitutions = {
|
|
22
|
+
"\\\\": "\\",
|
|
23
|
+
"\\%": "%",
|
|
24
|
+
"\\_": "_",
|
|
25
|
+
"%": "*",
|
|
26
|
+
"_": "?",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _replace_like_patterns(match: re.Match) -> str:
|
|
31
|
+
pattern = match.group()
|
|
32
|
+
try:
|
|
33
|
+
return _valid_like_substitutions[pattern]
|
|
34
|
+
except KeyError:
|
|
35
|
+
raise ValueError(f"'{pattern}' is not a valid escape sequence")
|
|
36
|
+
|
|
20
37
|
|
|
21
38
|
def cql2_like_to_es(string: str) -> str:
|
|
22
39
|
"""
|
|
23
|
-
Convert CQL2
|
|
40
|
+
Convert CQL2 "LIKE" characters to Elasticsearch "wildcard" characters.
|
|
24
41
|
|
|
25
42
|
Args:
|
|
26
43
|
string (str): The string containing CQL2 wildcard characters.
|
|
27
44
|
|
|
28
45
|
Returns:
|
|
29
46
|
str: The converted string with Elasticsearch compatible wildcards.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If an invalid escape sequence is encountered.
|
|
30
50
|
"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
escape_pattern = r"\\(?=[_%])"
|
|
36
|
-
|
|
37
|
-
# Replace '%' with '*' for broad wildcard matching
|
|
38
|
-
string = re.sub(percent_pattern, "*", string)
|
|
39
|
-
# Replace '_' with '?' for single character wildcard matching
|
|
40
|
-
string = re.sub(underscore_pattern, "?", string)
|
|
41
|
-
# Remove the escape character used in the CQL2 format
|
|
42
|
-
string = re.sub(escape_pattern, "", string)
|
|
43
|
-
|
|
44
|
-
return string
|
|
51
|
+
return _cql2_like_patterns.sub(
|
|
52
|
+
repl=_replace_like_patterns,
|
|
53
|
+
string=string,
|
|
54
|
+
)
|
|
45
55
|
|
|
46
56
|
|
|
47
57
|
class LogicalOp(str, Enum):
|
|
@@ -114,6 +114,14 @@ class CollectionLinks(BaseLinks):
|
|
|
114
114
|
collection_id: str = attr.ib()
|
|
115
115
|
extensions: List[str] = attr.ib(default=attr.Factory(list))
|
|
116
116
|
|
|
117
|
+
def link_self(self) -> Dict:
|
|
118
|
+
"""Return the self link."""
|
|
119
|
+
return dict(
|
|
120
|
+
rel=Relations.self.value,
|
|
121
|
+
type=MimeTypes.json.value,
|
|
122
|
+
href=urljoin(self.base_url, f"collections/{self.collection_id}"),
|
|
123
|
+
)
|
|
124
|
+
|
|
117
125
|
def link_parent(self) -> Dict[str, Any]:
|
|
118
126
|
"""Create the `parent` link."""
|
|
119
127
|
return dict(rel=Relations.parent, type=MimeTypes.json.value, href=self.base_url)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Rate limiting middleware."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI, Request
|
|
8
|
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
9
|
+
from slowapi.errors import RateLimitExceeded
|
|
10
|
+
from slowapi.middleware import SlowAPIMiddleware
|
|
11
|
+
from slowapi.util import get_remote_address
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_limiter(key_func=get_remote_address):
|
|
17
|
+
"""Create and return a Limiter instance for rate limiting."""
|
|
18
|
+
return Limiter(key_func=key_func)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def setup_rate_limit(
|
|
22
|
+
app: FastAPI, rate_limit: Optional[str] = None, key_func=get_remote_address
|
|
23
|
+
):
|
|
24
|
+
"""Set up rate limiting middleware."""
|
|
25
|
+
RATE_LIMIT = rate_limit or os.getenv("STAC_FASTAPI_RATE_LIMIT")
|
|
26
|
+
|
|
27
|
+
if not RATE_LIMIT:
|
|
28
|
+
logger.info("Rate limiting is disabled")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
logger.info(f"Setting up rate limit with RATE_LIMIT={RATE_LIMIT}")
|
|
32
|
+
|
|
33
|
+
limiter = get_limiter(key_func)
|
|
34
|
+
app.state.limiter = limiter
|
|
35
|
+
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
36
|
+
app.add_middleware(SlowAPIMiddleware)
|
|
37
|
+
|
|
38
|
+
@app.middleware("http")
|
|
39
|
+
@limiter.limit(RATE_LIMIT)
|
|
40
|
+
async def rate_limit_middleware(request: Request, call_next):
|
|
41
|
+
response = await call_next(request)
|
|
42
|
+
return response
|
|
43
|
+
|
|
44
|
+
logger.info("Rate limit setup complete")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: stac-fastapi.core
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.2.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
|
|
@@ -15,19 +15,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
16
16
|
Requires-Python: >=3.8
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: fastapi-slim
|
|
19
|
-
Requires-Dist: attrs>=23.2.0
|
|
20
|
-
Requires-Dist: pydantic[dotenv]
|
|
21
|
-
Requires-Dist: stac_pydantic>=3
|
|
22
|
-
Requires-Dist: stac-fastapi.types==3.0.0a4
|
|
23
|
-
Requires-Dist: stac-fastapi.api==3.0.0a4
|
|
24
|
-
Requires-Dist: stac-fastapi.extensions==3.0.0a4
|
|
25
|
-
Requires-Dist: orjson
|
|
26
|
-
Requires-Dist: overrides
|
|
27
|
-
Requires-Dist: geojson-pydantic
|
|
28
|
-
Requires-Dist: pygeofilter==0.2.1
|
|
29
|
-
Requires-Dist: typing_extensions==4.8.0
|
|
30
|
-
Requires-Dist: jsonschema
|
|
31
18
|
|
|
32
19
|
# stac-fastapi-elasticsearch-opensearch (sfeos)
|
|
33
20
|
|
|
@@ -415,3 +402,7 @@ Available aggregations are:
|
|
|
415
402
|
- geometry_geotile_grid_frequency ([geotile grid](https://opensearch.org/docs/latest/aggregations/bucket/geotile-grid/) on Item.geometry)
|
|
416
403
|
|
|
417
404
|
Support for additional fields and new aggregations can be added in the associated `database_logic.py` file.
|
|
405
|
+
|
|
406
|
+
## Rate Limiting
|
|
407
|
+
|
|
408
|
+
Rate limiting is an optional security feature that controls API request frequency on a remote address basis. It's enabled by setting the `STAC_FASTAPI_RATE_LIMIT` environment variable, e.g., `500/minute`. This limits each client to 500 requests per minute, helping prevent abuse and maintain API stability. Implementation examples are available in the [examples/rate_limit](examples/rate_limit) directory.
|
{stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi.core.egg-info/SOURCES.txt
RENAMED
|
@@ -13,6 +13,7 @@ stac_fastapi/core/base_settings.py
|
|
|
13
13
|
stac_fastapi/core/basic_auth.py
|
|
14
14
|
stac_fastapi/core/core.py
|
|
15
15
|
stac_fastapi/core/datetime_utils.py
|
|
16
|
+
stac_fastapi/core/rate_limit.py
|
|
16
17
|
stac_fastapi/core/route_dependencies.py
|
|
17
18
|
stac_fastapi/core/serializers.py
|
|
18
19
|
stac_fastapi/core/session.py
|
{stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi.core.egg-info/requires.txt
RENAMED
|
@@ -2,12 +2,13 @@ fastapi-slim
|
|
|
2
2
|
attrs>=23.2.0
|
|
3
3
|
pydantic[dotenv]
|
|
4
4
|
stac_pydantic>=3
|
|
5
|
-
stac-fastapi.types==3.0.
|
|
6
|
-
stac-fastapi.api==3.0.
|
|
7
|
-
stac-fastapi.extensions==3.0.
|
|
5
|
+
stac-fastapi.types==3.0.0
|
|
6
|
+
stac-fastapi.api==3.0.0
|
|
7
|
+
stac-fastapi.extensions==3.0.0
|
|
8
8
|
orjson
|
|
9
9
|
overrides
|
|
10
10
|
geojson-pydantic
|
|
11
11
|
pygeofilter==0.2.1
|
|
12
12
|
typing_extensions==4.8.0
|
|
13
13
|
jsonschema
|
|
14
|
+
slowapi==0.1.9
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/base_database_logic.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/extensions/__init__.py
RENAMED
|
File without changes
|
{stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/extensions/fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi/core/route_dependencies.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{stac_fastapi_core-3.0.0a3 → stac-fastapi.core-3.2.0}/stac_fastapi.core.egg-info/not-zip-safe
RENAMED
|
File without changes
|