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.
Files changed (17) hide show
  1. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/PKG-INFO +1 -1
  2. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/config.py +2 -3
  3. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/conformance.py +3 -3
  4. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/core.py +44 -40
  5. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/extension.py +5 -6
  6. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/links.py +42 -20
  7. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/rfc3339.py +7 -11
  8. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/search.py +21 -24
  9. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/stac.py +20 -21
  10. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/version.py +1 -1
  11. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/.gitignore +0 -0
  12. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/README.md +0 -0
  13. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/pyproject.toml +0 -0
  14. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/__init__.py +0 -0
  15. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/errors.py +0 -0
  16. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/py.typed +0 -0
  17. {stac_fastapi_types-6.3.0 → stac_fastapi_types-6.3.2}/stac_fastapi/types/requests.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stac-fastapi-types
3
- Version: 6.3.0
3
+ Version: 6.3.2
4
4
  Summary: Core API for stac-fastapi modules.
5
5
  Project-URL: Homepage, https://github.com/stac-utils/stac-fastapi
6
6
  Author-email: ArturoAI <engineering@arturo.ai>
@@ -1,10 +1,9 @@
1
1
  """stac_fastapi.types.config module."""
2
2
 
3
- from typing import Optional
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: Optional[ApiSettings] = None
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 Enum
3
+ from enum import StrEnum
4
4
 
5
5
 
6
- class STACConformanceClasses(str, Enum):
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(str, Enum):
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, Dict, List, Optional, Union
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 = Union[float, int]
30
- StacType = Dict[str, Any]
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: List[str],
48
- extension_schemas: List[str],
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: List[str] = attr.ib(
112
+ base_conformance_classes: list[str] = attr.ib(
112
113
  factory=lambda: BASE_CONFORMANCE_CLASSES
113
114
  )
114
- extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list))
115
+ extensions: list[ApiExtension] = attr.ib(default=attr.Factory(list))
115
116
 
116
- def conformance_classes(self) -> List[str]:
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: Optional[List[str]] = None,
242
- ids: Optional[List[str]] = None,
243
- bbox: Optional[BBox] = None,
244
- intersects: Optional[Geometry] = None,
245
- datetime: Optional[str] = None,
246
- limit: Optional[int] = 10,
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: Optional[BBox] = None,
303
- datetime: Optional[str] = None,
305
+ bbox: BBox | None = None,
306
+ datetime: str | None = None,
304
307
  limit: int = 10,
305
- token: Optional[str] = None,
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: List[str] = attr.ib(
334
+ base_conformance_classes: list[str] = attr.ib(
332
335
  factory=lambda: BASE_CONFORMANCE_CLASSES
333
336
  )
334
- extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list))
337
+ extensions: list[ApiExtension] = attr.ib(default=attr.Factory(list))
335
338
 
336
- def conformance_classes(self) -> List[str]:
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: Optional[List[str]] = None,
453
- ids: Optional[List[str]] = None,
454
- bbox: Optional[BBox] = None,
455
- intersects: Optional[Geometry] = None,
456
- datetime: Optional[str] = None,
457
- limit: Optional[int] = 10,
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: Optional[BBox] = None,
514
- datetime: Optional[str] = None,
517
+ bbox: BBox | None = None,
518
+ datetime: str | None = None,
515
519
  limit: int = 10,
516
- token: Optional[str] = None,
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: Optional[Type[APIRequest]] = None
18
- POST: Optional[Type[BaseModel]] = None
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
- ) -> Optional[Union[Type[BaseModel], Type[APIRequest]]]:
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: List[str] = attr.ib(factory=list)
30
- schema_href: Optional[str] = attr.ib(default=None)
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, Dict, List
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: List[Dict]) -> List[Dict]:
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) -> List[Dict]:
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.update({"href": urljoin(base_url, link["href"])})
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) -> Dict[str, Any]:
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) -> Dict[str, Any]:
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) -> Dict[str, Any]:
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) -> Dict[str, Any]:
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) -> List[Dict[str, Any]]:
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) -> Dict[str, Any]:
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) -> Dict[str, Any]:
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) -> Dict[str, Any]:
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) -> List[Dict[str, Any]]:
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 datetime, timezone
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 = Union[
16
- datetime,
17
- Tuple[datetime, datetime],
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=timezone.utc)
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: Optional[str]) -> Optional[DateTimeType]:
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(timezone.utc)
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 Dict, List, Optional, Union, cast
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: Optional[str]) -> Optional[List[str]]:
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) -> Optional[BBox]:
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
- Optional[str],
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
- ) -> Optional[List[str]]:
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
- Optional[str],
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
- ) -> Optional[List[str]]:
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
- Optional[str],
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
- ) -> Optional[BBox]:
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 = Union[float, int]
109
+ NumType = float | int
111
110
  Limit = Annotated[PositiveInt, AfterValidator(crop)]
112
111
 
113
112
 
114
113
  DateTimeQueryType = Annotated[
115
- Optional[str],
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) -> Dict:
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) -> Optional[DateTimeType]:
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) -> Optional[dt]:
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) -> Optional[dt]:
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: Optional[List[str]] = attr.ib(
174
- default=None, converter=_collection_converter
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
- Optional[str],
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
- Optional[Limit],
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: Optional[Limit] = Field(
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, Dict, List, Literal, Union
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 = Union[float, int]
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[List[str]]
15
+ stac_extensions: NotRequired[list[str]]
17
16
  id: str
18
17
  title: NotRequired[str]
19
18
  description: str
20
- links: List[Dict[str, Any]]
19
+ links: list[dict[str, Any]]
21
20
 
22
21
 
23
22
  class LandingPage(Catalog):
24
23
  """STAC Landing Page."""
25
24
 
26
- conformsTo: List[str]
25
+ conformsTo: list[str]
27
26
 
28
27
 
29
28
  class Conformance(TypedDict):
30
29
  """STAC Conformance Classes."""
31
30
 
32
- conformsTo: List[str]
31
+ conformsTo: list[str]
33
32
 
34
33
 
35
34
  class Collection(Catalog):
36
35
  """STAC Collection."""
37
36
 
38
- keywords: List[str]
37
+ keywords: list[str]
39
38
  license: str
40
- providers: List[Dict[str, Any]]
41
- extent: Dict[str, Any]
42
- summaries: Dict[str, Any]
43
- assets: Dict[str, Any]
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[List[str]]
50
+ stac_extensions: NotRequired[list[str]]
52
51
  id: str
53
- geometry: Dict[str, Any]
52
+ geometry: dict[str, Any]
54
53
  bbox: BBox
55
- properties: Dict[str, Any]
56
- links: List[Dict[str, Any]]
57
- assets: Dict[str, Any]
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: List[Item]
66
- links: List[Dict[str, Any]]
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: List[Collection]
77
- links: List[Dict[str, Any]]
75
+ collections: list[Collection]
76
+ links: list[dict[str, Any]]
78
77
  numberMatched: NotRequired[int]
79
78
  numberReturned: NotRequired[int]
@@ -1,3 +1,3 @@
1
1
  """Library version."""
2
2
 
3
- __version__ = "6.3.0"
3
+ __version__ = "6.3.2"