stac-fastapi-core 4.0.0a1__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/__init__.py +1 -0
- stac_fastapi/core/base_database_logic.py +54 -0
- stac_fastapi/core/base_settings.py +12 -0
- stac_fastapi/core/basic_auth.py +61 -0
- stac_fastapi/core/core.py +1068 -0
- stac_fastapi/core/database_logic.py +226 -0
- stac_fastapi/core/datetime_utils.py +39 -0
- stac_fastapi/core/extensions/__init__.py +5 -0
- stac_fastapi/core/extensions/aggregation.py +577 -0
- stac_fastapi/core/extensions/fields.py +41 -0
- stac_fastapi/core/extensions/filter.py +202 -0
- stac_fastapi/core/extensions/query.py +79 -0
- stac_fastapi/core/models/__init__.py +1 -0
- stac_fastapi/core/models/links.py +205 -0
- stac_fastapi/core/models/search.py +1 -0
- stac_fastapi/core/rate_limit.py +44 -0
- stac_fastapi/core/route_dependencies.py +176 -0
- stac_fastapi/core/serializers.py +177 -0
- stac_fastapi/core/session.py +25 -0
- stac_fastapi/core/utilities.py +135 -0
- stac_fastapi/core/version.py +2 -0
- stac_fastapi_core-4.0.0a1.dist-info/METADATA +361 -0
- stac_fastapi_core-4.0.0a1.dist-info/RECORD +25 -0
- stac_fastapi_core-4.0.0a1.dist-info/WHEEL +5 -0
- stac_fastapi_core-4.0.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Database logic core."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Dict, List, Optional, Protocol
|
|
6
|
+
|
|
7
|
+
from stac_fastapi.types.stac import Item
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# stac_pydantic classes extend _GeometryBase, which doesn't have a type field,
|
|
11
|
+
# So create our own Protocol for typing
|
|
12
|
+
# Union[ Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection]
|
|
13
|
+
class Geometry(Protocol): # noqa
|
|
14
|
+
type: str
|
|
15
|
+
coordinates: Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
COLLECTIONS_INDEX = os.getenv("STAC_COLLECTIONS_INDEX", "collections")
|
|
19
|
+
ITEMS_INDEX_PREFIX = os.getenv("STAC_ITEMS_INDEX_PREFIX", "items_")
|
|
20
|
+
|
|
21
|
+
ES_INDEX_NAME_UNSUPPORTED_CHARS = {
|
|
22
|
+
"\\",
|
|
23
|
+
"/",
|
|
24
|
+
"*",
|
|
25
|
+
"?",
|
|
26
|
+
'"',
|
|
27
|
+
"<",
|
|
28
|
+
">",
|
|
29
|
+
"|",
|
|
30
|
+
" ",
|
|
31
|
+
",",
|
|
32
|
+
"#",
|
|
33
|
+
":",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_ES_INDEX_NAME_UNSUPPORTED_CHARS_TABLE = str.maketrans(
|
|
37
|
+
"", "", "".join(ES_INDEX_NAME_UNSUPPORTED_CHARS)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
ITEM_INDICES = f"{ITEMS_INDEX_PREFIX}*,-*kibana*,-{COLLECTIONS_INDEX}*"
|
|
41
|
+
|
|
42
|
+
DEFAULT_SORT = {
|
|
43
|
+
"properties.datetime": {"order": "desc"},
|
|
44
|
+
"id": {"order": "desc"},
|
|
45
|
+
"collection": {"order": "desc"},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ES_ITEMS_SETTINGS = {
|
|
49
|
+
"index": {
|
|
50
|
+
"sort.field": list(DEFAULT_SORT.keys()),
|
|
51
|
+
"sort.order": [v["order"] for v in DEFAULT_SORT.values()],
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ES_MAPPINGS_DYNAMIC_TEMPLATES = [
|
|
56
|
+
# Common https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md
|
|
57
|
+
{
|
|
58
|
+
"descriptions": {
|
|
59
|
+
"match_mapping_type": "string",
|
|
60
|
+
"match": "description",
|
|
61
|
+
"mapping": {"type": "text"},
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"titles": {
|
|
66
|
+
"match_mapping_type": "string",
|
|
67
|
+
"match": "title",
|
|
68
|
+
"mapping": {"type": "text"},
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
# Projection Extension https://github.com/stac-extensions/projection
|
|
72
|
+
{"proj_epsg": {"match": "proj:epsg", "mapping": {"type": "integer"}}},
|
|
73
|
+
{
|
|
74
|
+
"proj_projjson": {
|
|
75
|
+
"match": "proj:projjson",
|
|
76
|
+
"mapping": {"type": "object", "enabled": False},
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"proj_centroid": {
|
|
81
|
+
"match": "proj:centroid",
|
|
82
|
+
"mapping": {"type": "geo_point"},
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"proj_geometry": {
|
|
87
|
+
"match": "proj:geometry",
|
|
88
|
+
"mapping": {"type": "object", "enabled": False},
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"no_index_href": {
|
|
93
|
+
"match": "href",
|
|
94
|
+
"mapping": {"type": "text", "index": False},
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
# Default all other strings not otherwise specified to keyword
|
|
98
|
+
{"strings": {"match_mapping_type": "string", "mapping": {"type": "keyword"}}},
|
|
99
|
+
{"numerics": {"match_mapping_type": "long", "mapping": {"type": "float"}}},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
ES_ITEMS_MAPPINGS = {
|
|
103
|
+
"numeric_detection": False,
|
|
104
|
+
"dynamic_templates": ES_MAPPINGS_DYNAMIC_TEMPLATES,
|
|
105
|
+
"properties": {
|
|
106
|
+
"id": {"type": "keyword"},
|
|
107
|
+
"collection": {"type": "keyword"},
|
|
108
|
+
"geometry": {"type": "geo_shape"},
|
|
109
|
+
"assets": {"type": "object", "enabled": False},
|
|
110
|
+
"links": {"type": "object", "enabled": False},
|
|
111
|
+
"properties": {
|
|
112
|
+
"type": "object",
|
|
113
|
+
"properties": {
|
|
114
|
+
# Common https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md
|
|
115
|
+
"datetime": {"type": "date"},
|
|
116
|
+
"start_datetime": {"type": "date"},
|
|
117
|
+
"end_datetime": {"type": "date"},
|
|
118
|
+
"created": {"type": "date"},
|
|
119
|
+
"updated": {"type": "date"},
|
|
120
|
+
# Satellite Extension https://github.com/stac-extensions/sat
|
|
121
|
+
"sat:absolute_orbit": {"type": "integer"},
|
|
122
|
+
"sat:relative_orbit": {"type": "integer"},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
ES_COLLECTIONS_MAPPINGS = {
|
|
129
|
+
"numeric_detection": False,
|
|
130
|
+
"dynamic_templates": ES_MAPPINGS_DYNAMIC_TEMPLATES,
|
|
131
|
+
"properties": {
|
|
132
|
+
"id": {"type": "keyword"},
|
|
133
|
+
"extent.spatial.bbox": {"type": "long"},
|
|
134
|
+
"extent.temporal.interval": {"type": "date"},
|
|
135
|
+
"providers": {"type": "object", "enabled": False},
|
|
136
|
+
"links": {"type": "object", "enabled": False},
|
|
137
|
+
"item_assets": {"type": "object", "enabled": False},
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@lru_cache(256)
|
|
143
|
+
def index_by_collection_id(collection_id: str) -> str:
|
|
144
|
+
"""
|
|
145
|
+
Translate a collection id into an Elasticsearch index name.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
collection_id (str): The collection id to translate into an index name.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
str: The index name derived from the collection id.
|
|
152
|
+
"""
|
|
153
|
+
cleaned = collection_id.translate(_ES_INDEX_NAME_UNSUPPORTED_CHARS_TABLE)
|
|
154
|
+
return (
|
|
155
|
+
f"{ITEMS_INDEX_PREFIX}{cleaned.lower()}_{collection_id.encode('utf-8').hex()}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@lru_cache(256)
|
|
160
|
+
def index_alias_by_collection_id(collection_id: str) -> str:
|
|
161
|
+
"""
|
|
162
|
+
Translate a collection id into an Elasticsearch index alias.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
collection_id (str): The collection id to translate into an index alias.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
str: The index alias derived from the collection id.
|
|
169
|
+
"""
|
|
170
|
+
cleaned = collection_id.translate(_ES_INDEX_NAME_UNSUPPORTED_CHARS_TABLE)
|
|
171
|
+
return f"{ITEMS_INDEX_PREFIX}{cleaned}"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def indices(collection_ids: Optional[List[str]]) -> str:
|
|
175
|
+
"""
|
|
176
|
+
Get a comma-separated string of index names for a given list of collection ids.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
collection_ids: A list of collection ids.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A string of comma-separated index names. If `collection_ids` is empty, returns the default indices.
|
|
183
|
+
"""
|
|
184
|
+
return (
|
|
185
|
+
",".join(map(index_alias_by_collection_id, collection_ids))
|
|
186
|
+
if collection_ids
|
|
187
|
+
else ITEM_INDICES
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def mk_item_id(item_id: str, collection_id: str) -> str:
|
|
192
|
+
"""Create the document id for an Item in Elasticsearch.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
item_id (str): The id of the Item.
|
|
196
|
+
collection_id (str): The id of the Collection that the Item belongs to.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
str: The document id for the Item, combining the Item id and the Collection id, separated by a `|` character.
|
|
200
|
+
"""
|
|
201
|
+
return f"{item_id}|{collection_id}"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def mk_actions(collection_id: str, processed_items: List[Item]) -> List[Dict[str, Any]]:
|
|
205
|
+
"""Create Elasticsearch bulk actions for a list of processed items.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
collection_id (str): The identifier for the collection the items belong to.
|
|
209
|
+
processed_items (List[Item]): The list of processed items to be bulk indexed.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List[Dict[str, Union[str, Dict]]]: The list of bulk actions to be executed,
|
|
213
|
+
each action being a dictionary with the following keys:
|
|
214
|
+
- `_index`: the index to store the document in.
|
|
215
|
+
- `_id`: the document's identifier.
|
|
216
|
+
- `_source`: the source of the document.
|
|
217
|
+
"""
|
|
218
|
+
index_alias = index_alias_by_collection_id(collection_id)
|
|
219
|
+
return [
|
|
220
|
+
{
|
|
221
|
+
"_index": index_alias,
|
|
222
|
+
"_id": mk_item_id(item["id"], item["collection"]),
|
|
223
|
+
"_source": item,
|
|
224
|
+
}
|
|
225
|
+
for item in processed_items
|
|
226
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""A few datetime methods."""
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# Borrowed from pystac - https://github.com/stac-utils/pystac/blob/f5e4cf4a29b62e9ef675d4a4dac7977b09f53c8f/pystac/utils.py#L370-L394
|
|
6
|
+
def datetime_to_str(dt: datetime, timespec: str = "auto") -> str:
|
|
7
|
+
"""Convert a :class:`datetime.datetime` instance to an ISO8601 string in the `RFC 3339, section 5.6.
|
|
8
|
+
|
|
9
|
+
<https://datatracker.ietf.org/doc/html/rfc3339#section-5.6>`__ format required by
|
|
10
|
+
the :stac-spec:`STAC Spec <master/item-spec/common-metadata.md#date-and-time>`.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
dt : The datetime to convert.
|
|
14
|
+
timespec: An optional argument that specifies the number of additional
|
|
15
|
+
terms of the time to include. Valid options are 'auto', 'hours',
|
|
16
|
+
'minutes', 'seconds', 'milliseconds' and 'microseconds'. The default value
|
|
17
|
+
is 'auto'.
|
|
18
|
+
Returns:
|
|
19
|
+
str: The ISO8601 (RFC 3339) formatted string representing the datetime.
|
|
20
|
+
"""
|
|
21
|
+
if dt.tzinfo is None:
|
|
22
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
23
|
+
|
|
24
|
+
timestamp = dt.isoformat(timespec=timespec)
|
|
25
|
+
zulu = "+00:00"
|
|
26
|
+
if timestamp.endswith(zulu):
|
|
27
|
+
timestamp = f"{timestamp[: -len(zulu)]}Z"
|
|
28
|
+
|
|
29
|
+
return timestamp
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def now_in_utc() -> datetime:
|
|
33
|
+
"""Return a datetime value of now with the UTC timezone applied."""
|
|
34
|
+
return datetime.now(timezone.utc)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def now_to_rfc3339_str() -> str:
|
|
38
|
+
"""Return an RFC 3339 string representing now."""
|
|
39
|
+
return datetime_to_str(now_in_utc())
|