stac-fastapi.types 6.3.0__tar.gz → 6.3.2__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_types-6.3.0 → stac_fastapi_types-6.3.2}/PKG-INFO +1 -1
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/config.py +2 -3
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/conformance.py +3 -3
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/core.py +44 -40
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/extension.py +5 -6
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/links.py +42 -20
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/rfc3339.py +7 -11
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/search.py +21 -24
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/stac.py +20 -21
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/version.py +1 -1
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/.gitignore +0 -0
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/README.md +0 -0
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/pyproject.toml +0 -0
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/__init__.py +0 -0
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/errors.py +0 -0
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/py.typed +0 -0
- {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/requests.py +0 -0
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"""stac_fastapi.types.config module."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Self
|
|
4
4
|
|
|
5
5
|
from pydantic import model_validator
|
|
6
6
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
7
|
-
from typing_extensions import Self
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class ApiSettings(BaseSettings):
|
|
@@ -56,7 +55,7 @@ class ApiSettings(BaseSettings):
|
|
|
56
55
|
class Settings:
|
|
57
56
|
"""Holds the global instance of settings."""
|
|
58
57
|
|
|
59
|
-
_instance:
|
|
58
|
+
_instance: ApiSettings | None = None
|
|
60
59
|
|
|
61
60
|
@classmethod
|
|
62
61
|
def set(cls, base_settings: ApiSettings):
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Conformance Classes."""
|
|
2
2
|
|
|
3
|
-
from enum import
|
|
3
|
+
from enum import StrEnum
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class STACConformanceClasses(
|
|
6
|
+
class STACConformanceClasses(StrEnum):
|
|
7
7
|
"""Conformance classes for the STAC API spec."""
|
|
8
8
|
|
|
9
9
|
CORE = "https://api.stacspec.org/v1.0.0/core"
|
|
@@ -12,7 +12,7 @@ class STACConformanceClasses(str, Enum):
|
|
|
12
12
|
ITEM_SEARCH = "https://api.stacspec.org/v1.0.0/item-search"
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class OAFConformanceClasses(
|
|
15
|
+
class OAFConformanceClasses(StrEnum):
|
|
16
16
|
"""Conformance classes for OGC API - Features."""
|
|
17
17
|
|
|
18
18
|
CORE = "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Base clients."""
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
-
from typing import Any
|
|
5
|
-
from urllib.parse import urljoin
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import urljoin, urlsplit
|
|
6
6
|
|
|
7
7
|
import attr
|
|
8
8
|
from fastapi import Request
|
|
@@ -26,8 +26,8 @@ __all__ = [
|
|
|
26
26
|
"AsyncBaseCoreClient",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
-
NumType =
|
|
30
|
-
StacType =
|
|
29
|
+
NumType = float | int
|
|
30
|
+
StacType = dict[str, Any]
|
|
31
31
|
|
|
32
32
|
api_settings = ApiSettings()
|
|
33
33
|
|
|
@@ -44,9 +44,10 @@ class LandingPageMixin(abc.ABC):
|
|
|
44
44
|
def _landing_page(
|
|
45
45
|
self,
|
|
46
46
|
base_url: str,
|
|
47
|
-
conformance_classes:
|
|
48
|
-
extension_schemas:
|
|
47
|
+
conformance_classes: list[str],
|
|
48
|
+
extension_schemas: list[str],
|
|
49
49
|
) -> stac.LandingPage:
|
|
50
|
+
path = urlsplit(base_url).path.rstrip("/")
|
|
50
51
|
landing_page = stac.LandingPage(
|
|
51
52
|
type="Catalog",
|
|
52
53
|
id=self.landing_page_id,
|
|
@@ -71,26 +72,26 @@ class LandingPageMixin(abc.ABC):
|
|
|
71
72
|
"rel": Relations.data.value,
|
|
72
73
|
"type": MimeTypes.json.value,
|
|
73
74
|
"title": "Collections available for this Catalog",
|
|
74
|
-
"href": urljoin(base_url, "collections"),
|
|
75
|
+
"href": urljoin(base_url, f"{path}/collections"),
|
|
75
76
|
},
|
|
76
77
|
{
|
|
77
78
|
"rel": Relations.conformance.value,
|
|
78
79
|
"type": MimeTypes.json.value,
|
|
79
80
|
"title": "STAC/OGC conformance classes implemented by this server",
|
|
80
|
-
"href": urljoin(base_url, "conformance"),
|
|
81
|
+
"href": urljoin(base_url, f"{path}/conformance"),
|
|
81
82
|
},
|
|
82
83
|
{
|
|
83
84
|
"rel": Relations.search.value,
|
|
84
85
|
"type": MimeTypes.geojson.value,
|
|
85
86
|
"title": "STAC search [GET]",
|
|
86
|
-
"href": urljoin(base_url, "search"),
|
|
87
|
+
"href": urljoin(base_url, f"{path}/search"),
|
|
87
88
|
"method": "GET",
|
|
88
89
|
},
|
|
89
90
|
{
|
|
90
91
|
"rel": Relations.search.value,
|
|
91
92
|
"type": MimeTypes.geojson.value,
|
|
92
93
|
"title": "STAC search [POST]",
|
|
93
|
-
"href": urljoin(base_url, "search"),
|
|
94
|
+
"href": urljoin(base_url, f"{path}/search"),
|
|
94
95
|
"method": "POST",
|
|
95
96
|
},
|
|
96
97
|
],
|
|
@@ -108,12 +109,12 @@ class BaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
108
109
|
extensions: list of registered api extensions.
|
|
109
110
|
"""
|
|
110
111
|
|
|
111
|
-
base_conformance_classes:
|
|
112
|
+
base_conformance_classes: list[str] = attr.ib(
|
|
112
113
|
factory=lambda: BASE_CONFORMANCE_CLASSES
|
|
113
114
|
)
|
|
114
|
-
extensions:
|
|
115
|
+
extensions: list[ApiExtension] = attr.ib(default=attr.Factory(list))
|
|
115
116
|
|
|
116
|
-
def conformance_classes(self) ->
|
|
117
|
+
def conformance_classes(self) -> list[str]:
|
|
117
118
|
"""Generate conformance classes by adding extension conformance to base
|
|
118
119
|
conformance classes."""
|
|
119
120
|
base_conformance_classes = self.base_conformance_classes.copy()
|
|
@@ -155,6 +156,8 @@ class BaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
155
156
|
extension_schemas=[],
|
|
156
157
|
)
|
|
157
158
|
|
|
159
|
+
path = urlsplit(base_url).path.rstrip("/")
|
|
160
|
+
|
|
158
161
|
# Add Queryables link
|
|
159
162
|
if self.extension_is_enabled("FilterExtension") or self.extension_is_enabled(
|
|
160
163
|
"SearchFilterExtension"
|
|
@@ -164,7 +167,7 @@ class BaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
164
167
|
"rel": Relations.queryables.value,
|
|
165
168
|
"type": MimeTypes.jsonschema.value,
|
|
166
169
|
"title": "Queryables available for this Catalog",
|
|
167
|
-
"href": urljoin(base_url, "queryables"),
|
|
170
|
+
"href": urljoin(base_url, f"{path}/queryables"),
|
|
168
171
|
}
|
|
169
172
|
)
|
|
170
173
|
|
|
@@ -176,13 +179,13 @@ class BaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
176
179
|
"rel": "aggregate",
|
|
177
180
|
"type": "application/json",
|
|
178
181
|
"title": "Aggregate",
|
|
179
|
-
"href": urljoin(base_url, "aggregate"),
|
|
182
|
+
"href": urljoin(base_url, f"{path}/aggregate"),
|
|
180
183
|
},
|
|
181
184
|
{
|
|
182
185
|
"rel": "aggregations",
|
|
183
186
|
"type": "application/json",
|
|
184
187
|
"title": "Aggregations",
|
|
185
|
-
"href": urljoin(base_url, "aggregations"),
|
|
188
|
+
"href": urljoin(base_url, f"{path}/aggregations"),
|
|
186
189
|
},
|
|
187
190
|
]
|
|
188
191
|
)
|
|
@@ -238,12 +241,12 @@ class BaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
238
241
|
@abc.abstractmethod
|
|
239
242
|
def get_search(
|
|
240
243
|
self,
|
|
241
|
-
collections:
|
|
242
|
-
ids:
|
|
243
|
-
bbox:
|
|
244
|
-
intersects:
|
|
245
|
-
datetime:
|
|
246
|
-
limit:
|
|
244
|
+
collections: list[str] | None = None,
|
|
245
|
+
ids: list[str] | None = None,
|
|
246
|
+
bbox: BBox | None = None,
|
|
247
|
+
intersects: Geometry | None = None,
|
|
248
|
+
datetime: str | None = None,
|
|
249
|
+
limit: int | None = 10,
|
|
247
250
|
**kwargs,
|
|
248
251
|
) -> stac.ItemCollection:
|
|
249
252
|
"""Cross catalog search (GET).
|
|
@@ -299,10 +302,10 @@ class BaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
299
302
|
def item_collection(
|
|
300
303
|
self,
|
|
301
304
|
collection_id: str,
|
|
302
|
-
bbox:
|
|
303
|
-
datetime:
|
|
305
|
+
bbox: BBox | None = None,
|
|
306
|
+
datetime: str | None = None,
|
|
304
307
|
limit: int = 10,
|
|
305
|
-
token:
|
|
308
|
+
token: str | None = None,
|
|
306
309
|
**kwargs,
|
|
307
310
|
) -> stac.ItemCollection:
|
|
308
311
|
"""Get all items from a specific collection.
|
|
@@ -328,12 +331,12 @@ class AsyncBaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
328
331
|
extensions: list of registered api extensions.
|
|
329
332
|
"""
|
|
330
333
|
|
|
331
|
-
base_conformance_classes:
|
|
334
|
+
base_conformance_classes: list[str] = attr.ib(
|
|
332
335
|
factory=lambda: BASE_CONFORMANCE_CLASSES
|
|
333
336
|
)
|
|
334
|
-
extensions:
|
|
337
|
+
extensions: list[ApiExtension] = attr.ib(default=attr.Factory(list))
|
|
335
338
|
|
|
336
|
-
def conformance_classes(self) ->
|
|
339
|
+
def conformance_classes(self) -> list[str]:
|
|
337
340
|
"""Generate conformance classes by adding extension conformance to base
|
|
338
341
|
conformance classes."""
|
|
339
342
|
conformance_classes = self.base_conformance_classes.copy()
|
|
@@ -364,6 +367,7 @@ class AsyncBaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
364
367
|
conformance_classes=self.conformance_classes(),
|
|
365
368
|
extension_schemas=[],
|
|
366
369
|
)
|
|
370
|
+
path = urlsplit(base_url).path.rstrip("/")
|
|
367
371
|
|
|
368
372
|
# Add Queryables link
|
|
369
373
|
if self.extension_is_enabled("FilterExtension") or self.extension_is_enabled(
|
|
@@ -374,7 +378,7 @@ class AsyncBaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
374
378
|
"rel": Relations.queryables.value,
|
|
375
379
|
"type": MimeTypes.jsonschema.value,
|
|
376
380
|
"title": "Queryables available for this Catalog",
|
|
377
|
-
"href": urljoin(base_url, "queryables"),
|
|
381
|
+
"href": urljoin(base_url, f"{path}/queryables"),
|
|
378
382
|
"method": "GET",
|
|
379
383
|
}
|
|
380
384
|
)
|
|
@@ -387,13 +391,13 @@ class AsyncBaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
387
391
|
"rel": "aggregate",
|
|
388
392
|
"type": "application/json",
|
|
389
393
|
"title": "Aggregate",
|
|
390
|
-
"href": urljoin(base_url, "aggregate"),
|
|
394
|
+
"href": urljoin(base_url, f"{path}/aggregate"),
|
|
391
395
|
},
|
|
392
396
|
{
|
|
393
397
|
"rel": "aggregations",
|
|
394
398
|
"type": "application/json",
|
|
395
399
|
"title": "Aggregations",
|
|
396
|
-
"href": urljoin(base_url, "aggregations"),
|
|
400
|
+
"href": urljoin(base_url, f"{path}/aggregations"),
|
|
397
401
|
},
|
|
398
402
|
]
|
|
399
403
|
)
|
|
@@ -449,12 +453,12 @@ class AsyncBaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
449
453
|
@abc.abstractmethod
|
|
450
454
|
async def get_search(
|
|
451
455
|
self,
|
|
452
|
-
collections:
|
|
453
|
-
ids:
|
|
454
|
-
bbox:
|
|
455
|
-
intersects:
|
|
456
|
-
datetime:
|
|
457
|
-
limit:
|
|
456
|
+
collections: list[str] | None = None,
|
|
457
|
+
ids: list[str] | None = None,
|
|
458
|
+
bbox: BBox | None = None,
|
|
459
|
+
intersects: Geometry | None = None,
|
|
460
|
+
datetime: str | None = None,
|
|
461
|
+
limit: int | None = 10,
|
|
458
462
|
**kwargs,
|
|
459
463
|
) -> stac.ItemCollection:
|
|
460
464
|
"""Cross catalog search (GET).
|
|
@@ -510,10 +514,10 @@ class AsyncBaseCoreClient(LandingPageMixin, abc.ABC):
|
|
|
510
514
|
async def item_collection(
|
|
511
515
|
self,
|
|
512
516
|
collection_id: str,
|
|
513
|
-
bbox:
|
|
514
|
-
datetime:
|
|
517
|
+
bbox: BBox | None = None,
|
|
518
|
+
datetime: str | None = None,
|
|
515
519
|
limit: int = 10,
|
|
516
|
-
token:
|
|
520
|
+
token: str | None = None,
|
|
517
521
|
**kwargs,
|
|
518
522
|
) -> stac.ItemCollection:
|
|
519
523
|
"""Get all items from a specific collection.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Base api extension."""
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
-
from typing import List, Optional, Type, Union
|
|
5
4
|
|
|
6
5
|
import attr
|
|
7
6
|
from fastapi import FastAPI
|
|
@@ -14,20 +13,20 @@ from stac_fastapi.types.search import APIRequest
|
|
|
14
13
|
class ApiExtension(abc.ABC):
|
|
15
14
|
"""Abstract base class for defining API extensions."""
|
|
16
15
|
|
|
17
|
-
GET:
|
|
18
|
-
POST:
|
|
16
|
+
GET: type[APIRequest] | None = None
|
|
17
|
+
POST: type[BaseModel] | None = None
|
|
19
18
|
|
|
20
19
|
def get_request_model(
|
|
21
20
|
self, verb: str = "GET"
|
|
22
|
-
) ->
|
|
21
|
+
) -> type[BaseModel] | type[APIRequest] | None:
|
|
23
22
|
"""Return the request model for the extension.method.
|
|
24
23
|
|
|
25
24
|
The model can differ based on HTTP verb
|
|
26
25
|
"""
|
|
27
26
|
return getattr(self, verb)
|
|
28
27
|
|
|
29
|
-
conformance_classes:
|
|
30
|
-
schema_href:
|
|
28
|
+
conformance_classes: list[str] = attr.ib(factory=list)
|
|
29
|
+
schema_href: str | None = attr.ib(default=None)
|
|
31
30
|
|
|
32
31
|
@abc.abstractmethod
|
|
33
32
|
def register(self, app: FastAPI) -> None:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Link helpers."""
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
4
|
-
from urllib.parse import urljoin
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import urljoin, urlsplit
|
|
5
5
|
|
|
6
6
|
import attr
|
|
7
7
|
from stac_pydantic.links import Relations
|
|
@@ -13,16 +13,33 @@ from stac_pydantic.shared import MimeTypes
|
|
|
13
13
|
INFERRED_LINK_RELS = ["self", "item", "parent", "collection", "root", "items"]
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def filter_links(links:
|
|
16
|
+
def filter_links(links: list[dict]) -> list[dict]:
|
|
17
17
|
"""Remove inferred links."""
|
|
18
18
|
return [link for link in links if link["rel"] not in INFERRED_LINK_RELS]
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def resolve_links(links: list, base_url: str) ->
|
|
22
|
-
"""Convert relative links to absolute links.
|
|
21
|
+
def resolve_links(links: list, base_url: str) -> list[dict]:
|
|
22
|
+
"""Convert relative links to absolute links while preserving existing absolute URLs.
|
|
23
|
+
|
|
24
|
+
This function processes links and applies the base_url and proxy path to relative URLs
|
|
25
|
+
However, it explicitly preserves absolute URLs (starting with http:// or https://) to
|
|
26
|
+
prevent mangling of external links stored in STAC items (e.g., license URLs, external
|
|
27
|
+
documentation links).
|
|
28
|
+
"""
|
|
23
29
|
filtered_links = filter_links(links)
|
|
30
|
+
path = urlsplit(base_url).path.rstrip("/")
|
|
24
31
|
for link in filtered_links:
|
|
25
|
-
link.
|
|
32
|
+
href = str(link.get("href", ""))
|
|
33
|
+
|
|
34
|
+
# Do not mangle URLs that are already absolute
|
|
35
|
+
if href.startswith(("http://", "https://")):
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
# Remove leading slash to prevent urljoin from replacing the entire path
|
|
39
|
+
href = href.lstrip("/")
|
|
40
|
+
# Construct the full path with proxy path preserved
|
|
41
|
+
full_path = f"{path}/{href}" if path else href
|
|
42
|
+
link.update({"href": urljoin(base_url, full_path)})
|
|
26
43
|
return filtered_links
|
|
27
44
|
|
|
28
45
|
|
|
@@ -33,7 +50,7 @@ class BaseLinks:
|
|
|
33
50
|
collection_id: str = attr.ib()
|
|
34
51
|
base_url: str = attr.ib()
|
|
35
52
|
|
|
36
|
-
def root(self) ->
|
|
53
|
+
def root(self) -> dict[str, Any]:
|
|
37
54
|
"""Return the catalog root."""
|
|
38
55
|
return dict(rel=Relations.root, type=MimeTypes.json, href=self.base_url)
|
|
39
56
|
|
|
@@ -42,27 +59,29 @@ class BaseLinks:
|
|
|
42
59
|
class CollectionLinks(BaseLinks):
|
|
43
60
|
"""Create inferred links specific to collections."""
|
|
44
61
|
|
|
45
|
-
def self(self) ->
|
|
62
|
+
def self(self) -> dict[str, Any]:
|
|
46
63
|
"""Create the `self` link."""
|
|
64
|
+
path = urlsplit(self.base_url).path.rstrip("/")
|
|
47
65
|
return dict(
|
|
48
66
|
rel=Relations.self,
|
|
49
67
|
type=MimeTypes.json,
|
|
50
|
-
href=urljoin(self.base_url, f"collections/{self.collection_id}"),
|
|
68
|
+
href=urljoin(self.base_url, f"{path}/collections/{self.collection_id}"),
|
|
51
69
|
)
|
|
52
70
|
|
|
53
|
-
def parent(self) ->
|
|
71
|
+
def parent(self) -> dict[str, Any]:
|
|
54
72
|
"""Create the `parent` link."""
|
|
55
73
|
return dict(rel=Relations.parent, type=MimeTypes.json, href=self.base_url)
|
|
56
74
|
|
|
57
|
-
def items(self) ->
|
|
75
|
+
def items(self) -> dict[str, Any]:
|
|
58
76
|
"""Create the `items` link."""
|
|
77
|
+
path = urlsplit(self.base_url).path.rstrip("/")
|
|
59
78
|
return dict(
|
|
60
79
|
rel="items",
|
|
61
80
|
type=MimeTypes.geojson,
|
|
62
|
-
href=urljoin(self.base_url, f"collections/{self.collection_id}/items"),
|
|
81
|
+
href=urljoin(self.base_url, f"{path}/collections/{self.collection_id}/items"),
|
|
63
82
|
)
|
|
64
83
|
|
|
65
|
-
def create_links(self) ->
|
|
84
|
+
def create_links(self) -> list[dict[str, Any]]:
|
|
66
85
|
"""Return all inferred links."""
|
|
67
86
|
return [self.self(), self.parent(), self.items(), self.root()]
|
|
68
87
|
|
|
@@ -73,34 +92,37 @@ class ItemLinks(BaseLinks):
|
|
|
73
92
|
|
|
74
93
|
item_id: str = attr.ib()
|
|
75
94
|
|
|
76
|
-
def self(self) ->
|
|
95
|
+
def self(self) -> dict[str, Any]:
|
|
77
96
|
"""Create the `self` link."""
|
|
97
|
+
path = urlsplit(self.base_url).path.rstrip("/")
|
|
78
98
|
return dict(
|
|
79
99
|
rel=Relations.self,
|
|
80
100
|
type=MimeTypes.geojson,
|
|
81
101
|
href=urljoin(
|
|
82
102
|
self.base_url,
|
|
83
|
-
f"collections/{self.collection_id}/items/{self.item_id}",
|
|
103
|
+
f"{path}/collections/{self.collection_id}/items/{self.item_id}",
|
|
84
104
|
),
|
|
85
105
|
)
|
|
86
106
|
|
|
87
|
-
def parent(self) ->
|
|
107
|
+
def parent(self) -> dict[str, Any]:
|
|
88
108
|
"""Create the `parent` link."""
|
|
109
|
+
path = urlsplit(self.base_url).path.rstrip("/")
|
|
89
110
|
return dict(
|
|
90
111
|
rel=Relations.parent,
|
|
91
112
|
type=MimeTypes.json,
|
|
92
|
-
href=urljoin(self.base_url, f"collections/{self.collection_id}"),
|
|
113
|
+
href=urljoin(self.base_url, f"{path}/collections/{self.collection_id}"),
|
|
93
114
|
)
|
|
94
115
|
|
|
95
|
-
def collection(self) ->
|
|
116
|
+
def collection(self) -> dict[str, Any]:
|
|
96
117
|
"""Create the `collection` link."""
|
|
118
|
+
path = urlsplit(self.base_url).path.rstrip("/")
|
|
97
119
|
return dict(
|
|
98
120
|
rel=Relations.collection,
|
|
99
121
|
type=MimeTypes.json,
|
|
100
|
-
href=urljoin(self.base_url, f"collections/{self.collection_id}"),
|
|
122
|
+
href=urljoin(self.base_url, f"{path}/collections/{self.collection_id}"),
|
|
101
123
|
)
|
|
102
124
|
|
|
103
|
-
def create_links(self) ->
|
|
125
|
+
def create_links(self) -> list[dict[str, Any]]:
|
|
104
126
|
"""Return all inferred links."""
|
|
105
127
|
links = [
|
|
106
128
|
self.self(),
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""rfc3339."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
from datetime import
|
|
5
|
-
from typing import Optional, Tuple, Union
|
|
4
|
+
from datetime import UTC, datetime
|
|
6
5
|
|
|
7
6
|
import iso8601
|
|
8
7
|
from fastapi import HTTPException
|
|
@@ -12,12 +11,9 @@ RFC33339_PATTERN = (
|
|
|
12
11
|
r"(Z|([-+])(\d\d):(\d\d))$"
|
|
13
12
|
)
|
|
14
13
|
|
|
15
|
-
DateTimeType =
|
|
16
|
-
datetime,
|
|
17
|
-
|
|
18
|
-
Tuple[datetime, None],
|
|
19
|
-
Tuple[None, datetime],
|
|
20
|
-
]
|
|
14
|
+
DateTimeType = (
|
|
15
|
+
datetime | tuple[datetime, datetime] | tuple[datetime, None] | tuple[None, datetime]
|
|
16
|
+
)
|
|
21
17
|
|
|
22
18
|
|
|
23
19
|
# Borrowed from pystac - https://github.com/stac-utils/pystac/blob/f5e4cf4a29b62e9ef675d4a4dac7977b09f53c8f/pystac/utils.py#L370-L394
|
|
@@ -38,7 +34,7 @@ def datetime_to_str(dt: datetime, timespec: str = "auto") -> str:
|
|
|
38
34
|
str: The ISO8601 (RFC 3339) formatted string representing the datetime.
|
|
39
35
|
"""
|
|
40
36
|
if dt.tzinfo is None:
|
|
41
|
-
dt = dt.replace(tzinfo=
|
|
37
|
+
dt = dt.replace(tzinfo=UTC)
|
|
42
38
|
|
|
43
39
|
timestamp = dt.isoformat(timespec=timespec)
|
|
44
40
|
zulu = "+00:00"
|
|
@@ -92,7 +88,7 @@ def parse_single_date(date_str: str) -> datetime:
|
|
|
92
88
|
return rfc3339_str_to_datetime(date_str)
|
|
93
89
|
|
|
94
90
|
|
|
95
|
-
def str_to_interval(interval:
|
|
91
|
+
def str_to_interval(interval: str | None) -> DateTimeType | None:
|
|
96
92
|
"""
|
|
97
93
|
Extract a single datetime object or a tuple of datetime objects from an
|
|
98
94
|
interval string defined by the OGC API. The interval can either be a
|
|
@@ -151,7 +147,7 @@ def str_to_interval(interval: Optional[str]) -> Optional[DateTimeType]:
|
|
|
151
147
|
|
|
152
148
|
def now_in_utc() -> datetime:
|
|
153
149
|
"""Return a datetime value of now with the UTC timezone applied."""
|
|
154
|
-
return datetime.now(
|
|
150
|
+
return datetime.now(UTC)
|
|
155
151
|
|
|
156
152
|
|
|
157
153
|
def now_to_rfc3339_str() -> str:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""stac_fastapi.types.search module."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime as dt
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Annotated, cast
|
|
5
5
|
|
|
6
6
|
import attr
|
|
7
7
|
from fastapi import HTTPException, Query
|
|
@@ -9,7 +9,6 @@ from pydantic import Field, PositiveInt
|
|
|
9
9
|
from pydantic.functional_validators import AfterValidator
|
|
10
10
|
from stac_pydantic.api import Search
|
|
11
11
|
from stac_pydantic.shared import BBox
|
|
12
|
-
from typing_extensions import Annotated
|
|
13
12
|
|
|
14
13
|
from stac_fastapi.types.rfc3339 import DateTimeType, str_to_interval
|
|
15
14
|
|
|
@@ -22,7 +21,7 @@ def crop(v: PositiveInt) -> PositiveInt:
|
|
|
22
21
|
return v
|
|
23
22
|
|
|
24
23
|
|
|
25
|
-
def str2list(x:
|
|
24
|
+
def str2list(x: str | None) -> list[str] | None:
|
|
26
25
|
"""Convert string to list base on , delimiter."""
|
|
27
26
|
if x:
|
|
28
27
|
return x.split(",")
|
|
@@ -30,7 +29,7 @@ def str2list(x: Optional[str]) -> Optional[List[str]]:
|
|
|
30
29
|
return None
|
|
31
30
|
|
|
32
31
|
|
|
33
|
-
def str2bbox(x: str) ->
|
|
32
|
+
def str2bbox(x: str) -> BBox | None:
|
|
34
33
|
"""Convert string to BBox based on , delimiter."""
|
|
35
34
|
if x:
|
|
36
35
|
try:
|
|
@@ -50,7 +49,7 @@ def str2bbox(x: str) -> Optional[BBox]:
|
|
|
50
49
|
|
|
51
50
|
def _collection_converter(
|
|
52
51
|
val: Annotated[
|
|
53
|
-
|
|
52
|
+
str | None,
|
|
54
53
|
Query(
|
|
55
54
|
description="Array of collection Ids to search for items.",
|
|
56
55
|
openapi_examples={
|
|
@@ -60,7 +59,7 @@ def _collection_converter(
|
|
|
60
59
|
},
|
|
61
60
|
),
|
|
62
61
|
] = None,
|
|
63
|
-
) ->
|
|
62
|
+
) -> list[str] | None:
|
|
64
63
|
if val:
|
|
65
64
|
return val.split(",")
|
|
66
65
|
return None
|
|
@@ -68,7 +67,7 @@ def _collection_converter(
|
|
|
68
67
|
|
|
69
68
|
def _ids_converter(
|
|
70
69
|
val: Annotated[
|
|
71
|
-
|
|
70
|
+
str | None,
|
|
72
71
|
Query(
|
|
73
72
|
description="Array of Item ids to return.",
|
|
74
73
|
openapi_examples={
|
|
@@ -78,7 +77,7 @@ def _ids_converter(
|
|
|
78
77
|
},
|
|
79
78
|
),
|
|
80
79
|
] = None,
|
|
81
|
-
) ->
|
|
80
|
+
) -> list[str] | None:
|
|
82
81
|
if val:
|
|
83
82
|
return val.split(",")
|
|
84
83
|
return None
|
|
@@ -86,7 +85,7 @@ def _ids_converter(
|
|
|
86
85
|
|
|
87
86
|
def _bbox_converter(
|
|
88
87
|
val: Annotated[
|
|
89
|
-
|
|
88
|
+
str | None,
|
|
90
89
|
Query(
|
|
91
90
|
description="Only return items intersecting this bounding box. Mutually exclusive with **intersects**.", # noqa: E501
|
|
92
91
|
openapi_examples={
|
|
@@ -95,7 +94,7 @@ def _bbox_converter(
|
|
|
95
94
|
},
|
|
96
95
|
),
|
|
97
96
|
] = None,
|
|
98
|
-
) ->
|
|
97
|
+
) -> BBox | None:
|
|
99
98
|
if val:
|
|
100
99
|
return str2bbox(val)
|
|
101
100
|
return None
|
|
@@ -107,12 +106,12 @@ def _validate_datetime(instance, attribute, value):
|
|
|
107
106
|
|
|
108
107
|
|
|
109
108
|
# Be careful: https://github.com/samuelcolvin/pydantic/issues/1423#issuecomment-642797287
|
|
110
|
-
NumType =
|
|
109
|
+
NumType = float | int
|
|
111
110
|
Limit = Annotated[PositiveInt, AfterValidator(crop)]
|
|
112
111
|
|
|
113
112
|
|
|
114
113
|
DateTimeQueryType = Annotated[
|
|
115
|
-
|
|
114
|
+
str | None,
|
|
116
115
|
Query(
|
|
117
116
|
description="""Only return items that have a temporal property that intersects this value.\n
|
|
118
117
|
Either a date-time or an interval, open or closed. Date and time expressions adhere to RFC 3339. Open intervals are expressed using double-dots.""", # noqa: E501
|
|
@@ -131,7 +130,7 @@ Either a date-time or an interval, open or closed. Date and time expressions adh
|
|
|
131
130
|
class APIRequest:
|
|
132
131
|
"""Generic API Request base class."""
|
|
133
132
|
|
|
134
|
-
def kwargs(self) ->
|
|
133
|
+
def kwargs(self) -> dict:
|
|
135
134
|
"""Transform api request params into format which matches the signature of the
|
|
136
135
|
endpoint."""
|
|
137
136
|
return self.__dict__
|
|
@@ -143,12 +142,12 @@ class DatetimeMixin:
|
|
|
143
142
|
|
|
144
143
|
datetime: DateTimeQueryType = attr.ib(default=None, validator=_validate_datetime)
|
|
145
144
|
|
|
146
|
-
def parse_datetime(self) ->
|
|
145
|
+
def parse_datetime(self) -> DateTimeType | None:
|
|
147
146
|
"""Return Datetime objects."""
|
|
148
147
|
return str_to_interval(self.datetime)
|
|
149
148
|
|
|
150
149
|
@property
|
|
151
|
-
def start_date(self) ->
|
|
150
|
+
def start_date(self) -> dt | None:
|
|
152
151
|
"""Start Date."""
|
|
153
152
|
parsed = self.parse_datetime()
|
|
154
153
|
if parsed is None:
|
|
@@ -157,7 +156,7 @@ class DatetimeMixin:
|
|
|
157
156
|
return parsed[0] if isinstance(parsed, tuple) else parsed
|
|
158
157
|
|
|
159
158
|
@property
|
|
160
|
-
def end_date(self) ->
|
|
159
|
+
def end_date(self) -> dt | None:
|
|
161
160
|
"""End Date."""
|
|
162
161
|
parsed = self.parse_datetime()
|
|
163
162
|
if parsed is None:
|
|
@@ -170,13 +169,11 @@ class DatetimeMixin:
|
|
|
170
169
|
class BaseSearchGetRequest(APIRequest, DatetimeMixin):
|
|
171
170
|
"""Base arguments for GET Request."""
|
|
172
171
|
|
|
173
|
-
collections:
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
ids: Optional[List[str]] = attr.ib(default=None, converter=_ids_converter)
|
|
177
|
-
bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter)
|
|
172
|
+
collections: list[str] | None = attr.ib(default=None, converter=_collection_converter)
|
|
173
|
+
ids: list[str] | None = attr.ib(default=None, converter=_ids_converter)
|
|
174
|
+
bbox: BBox | None = attr.ib(default=None, converter=_bbox_converter)
|
|
178
175
|
intersects: Annotated[
|
|
179
|
-
|
|
176
|
+
str | None,
|
|
180
177
|
Query(
|
|
181
178
|
description="""Only return items intersecting this GeoJSON Geometry. Mutually exclusive with **bbox**. \n
|
|
182
179
|
*Remember to URL encode the GeoJSON geometry when using GET request*.""", # noqa: E501
|
|
@@ -223,7 +220,7 @@ class BaseSearchGetRequest(APIRequest, DatetimeMixin):
|
|
|
223
220
|
] = attr.ib(default=None)
|
|
224
221
|
datetime: DateTimeQueryType = attr.ib(default=None, validator=_validate_datetime)
|
|
225
222
|
limit: Annotated[
|
|
226
|
-
|
|
223
|
+
Limit | None,
|
|
227
224
|
Query(
|
|
228
225
|
description="Limits the number of results that are included in each page of the response (capped to 10_000)." # noqa: E501
|
|
229
226
|
),
|
|
@@ -233,7 +230,7 @@ class BaseSearchGetRequest(APIRequest, DatetimeMixin):
|
|
|
233
230
|
class BaseSearchPostRequest(Search):
|
|
234
231
|
"""Base arguments for POST Request."""
|
|
235
232
|
|
|
236
|
-
limit:
|
|
233
|
+
limit: Limit | None = Field(
|
|
237
234
|
10,
|
|
238
235
|
description="Limits the number of results that are included in each page of the response (capped to 10_000).", # noqa: E501
|
|
239
236
|
)
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"""STAC types."""
|
|
2
2
|
|
|
3
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, Literal, NotRequired, TypedDict
|
|
4
4
|
|
|
5
5
|
from stac_pydantic.shared import BBox
|
|
6
|
-
from typing_extensions import NotRequired, TypedDict
|
|
7
6
|
|
|
8
|
-
NumType =
|
|
7
|
+
NumType = float | int
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class Catalog(TypedDict):
|
|
@@ -13,34 +12,34 @@ class Catalog(TypedDict):
|
|
|
13
12
|
|
|
14
13
|
type: str
|
|
15
14
|
stac_version: str
|
|
16
|
-
stac_extensions: NotRequired[
|
|
15
|
+
stac_extensions: NotRequired[list[str]]
|
|
17
16
|
id: str
|
|
18
17
|
title: NotRequired[str]
|
|
19
18
|
description: str
|
|
20
|
-
links:
|
|
19
|
+
links: list[dict[str, Any]]
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
class LandingPage(Catalog):
|
|
24
23
|
"""STAC Landing Page."""
|
|
25
24
|
|
|
26
|
-
conformsTo:
|
|
25
|
+
conformsTo: list[str]
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
class Conformance(TypedDict):
|
|
30
29
|
"""STAC Conformance Classes."""
|
|
31
30
|
|
|
32
|
-
conformsTo:
|
|
31
|
+
conformsTo: list[str]
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
class Collection(Catalog):
|
|
36
35
|
"""STAC Collection."""
|
|
37
36
|
|
|
38
|
-
keywords:
|
|
37
|
+
keywords: list[str]
|
|
39
38
|
license: str
|
|
40
|
-
providers:
|
|
41
|
-
extent:
|
|
42
|
-
summaries:
|
|
43
|
-
assets:
|
|
39
|
+
providers: list[dict[str, Any]]
|
|
40
|
+
extent: dict[str, Any]
|
|
41
|
+
summaries: dict[str, Any]
|
|
42
|
+
assets: dict[str, Any]
|
|
44
43
|
|
|
45
44
|
|
|
46
45
|
class Item(TypedDict):
|
|
@@ -48,13 +47,13 @@ class Item(TypedDict):
|
|
|
48
47
|
|
|
49
48
|
type: Literal["Feature"]
|
|
50
49
|
stac_version: str
|
|
51
|
-
stac_extensions: NotRequired[
|
|
50
|
+
stac_extensions: NotRequired[list[str]]
|
|
52
51
|
id: str
|
|
53
|
-
geometry:
|
|
52
|
+
geometry: dict[str, Any]
|
|
54
53
|
bbox: BBox
|
|
55
|
-
properties:
|
|
56
|
-
links:
|
|
57
|
-
assets:
|
|
54
|
+
properties: dict[str, Any]
|
|
55
|
+
links: list[dict[str, Any]]
|
|
56
|
+
assets: dict[str, Any]
|
|
58
57
|
collection: str
|
|
59
58
|
|
|
60
59
|
|
|
@@ -62,8 +61,8 @@ class ItemCollection(TypedDict):
|
|
|
62
61
|
"""STAC Item Collection."""
|
|
63
62
|
|
|
64
63
|
type: Literal["FeatureCollection"]
|
|
65
|
-
features:
|
|
66
|
-
links:
|
|
64
|
+
features: list[Item]
|
|
65
|
+
links: list[dict[str, Any]]
|
|
67
66
|
numberMatched: NotRequired[int]
|
|
68
67
|
numberReturned: NotRequired[int]
|
|
69
68
|
|
|
@@ -73,7 +72,7 @@ class Collections(TypedDict):
|
|
|
73
72
|
https://github.com/radiantearth/stac-api-spec/tree/master/collections
|
|
74
73
|
"""
|
|
75
74
|
|
|
76
|
-
collections:
|
|
77
|
-
links:
|
|
75
|
+
collections: list[Collection]
|
|
76
|
+
links: list[dict[str, Any]]
|
|
78
77
|
numberMatched: NotRequired[int]
|
|
79
78
|
numberReturned: NotRequired[int]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|