stac-fastapi.api 6.2.1__tar.gz → 6.3.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stac-fastapi-api
3
- Version: 6.2.1
3
+ Version: 6.3.1
4
4
  Summary: An implementation of STAC API based on the FastAPI framework.
5
5
  Project-URL: Homepage, https://github.com/stac-utils/stac-fastapi
6
6
  Author-email: ArturoAI <engineering@arturo.ai>
@@ -1,8 +1,7 @@
1
1
  """Fastapi app creation."""
2
2
 
3
-
4
3
  import inspect
5
- from typing import Awaitable, Callable, Dict, List, Optional, Tuple, Type, Union
4
+ from collections.abc import Awaitable, Callable
6
5
 
7
6
  import attr
8
7
  from brotli_asgi import BrotliMiddleware
@@ -79,9 +78,9 @@ class StacApi:
79
78
  """
80
79
 
81
80
  settings: ApiSettings = attr.ib()
82
- client: Union[AsyncBaseCoreClient, BaseCoreClient] = attr.ib()
83
- extensions: List[ApiExtension] = attr.ib(default=attr.Factory(list))
84
- exceptions: Dict[Type[Exception], int] = attr.ib(
81
+ client: AsyncBaseCoreClient | BaseCoreClient = attr.ib()
82
+ extensions: list[ApiExtension] = attr.ib(default=attr.Factory(list))
83
+ exceptions: dict[type[Exception], int] = attr.ib(
85
84
  default=attr.Factory(lambda: DEFAULT_STATUS_CODES)
86
85
  )
87
86
  title: str = attr.ib(
@@ -116,14 +115,14 @@ class StacApi:
116
115
  converter=update_openapi, # type: ignore
117
116
  )
118
117
  router: APIRouter = attr.ib(default=attr.Factory(APIRouter))
119
- search_get_request_model: Type[APIRequest] = attr.ib(default=BaseSearchGetRequest)
120
- search_post_request_model: Type[BaseModel] = attr.ib(default=BaseSearchPostRequest)
121
- collections_get_request_model: Type[APIRequest] = attr.ib(default=EmptyRequest)
122
- collection_get_request_model: Type[APIRequest] = attr.ib(default=CollectionUri)
123
- items_get_request_model: Type[APIRequest] = attr.ib(default=ItemCollectionUri)
124
- item_get_request_model: Type[APIRequest] = attr.ib(default=ItemUri)
125
- response_class: Type[Response] = attr.ib(default=JSONResponse)
126
- middlewares: List[Middleware] = attr.ib(
118
+ search_get_request_model: type[APIRequest] = attr.ib(default=BaseSearchGetRequest)
119
+ search_post_request_model: type[BaseModel] = attr.ib(default=BaseSearchPostRequest)
120
+ collections_get_request_model: type[APIRequest] = attr.ib(default=EmptyRequest)
121
+ collection_get_request_model: type[APIRequest] = attr.ib(default=CollectionUri)
122
+ items_get_request_model: type[APIRequest] = attr.ib(default=ItemCollectionUri)
123
+ item_get_request_model: type[APIRequest] = attr.ib(default=ItemUri)
124
+ response_class: type[Response] = attr.ib(default=JSONResponse)
125
+ middlewares: list[Middleware] = attr.ib(
127
126
  default=attr.Factory(
128
127
  lambda: [
129
128
  Middleware(BrotliMiddleware),
@@ -139,12 +138,12 @@ class StacApi:
139
138
  ]
140
139
  )
141
140
  )
142
- route_dependencies: List[Tuple[List[Scope], List[Depends]]] = attr.ib(default=[])
143
- health_check: Union[Callable[[], Dict], Callable[[], Awaitable[Dict]]] = attr.ib(
141
+ route_dependencies: list[tuple[list[Scope], list[Depends]]] = attr.ib(default=[])
142
+ health_check: Callable[[], dict] | Callable[[], Awaitable[dict]] = attr.ib(
144
143
  default=lambda: {"status": "UP"}
145
144
  )
146
145
 
147
- def get_extension(self, extension: Type[ApiExtension]) -> Optional[ApiExtension]:
146
+ def get_extension(self, extension: type[ApiExtension]) -> ApiExtension | None:
148
147
  """Get an extension.
149
148
 
150
149
  Args:
@@ -388,7 +387,7 @@ class StacApi:
388
387
  mgmt_router.add_api_route(
389
388
  name="Ping",
390
389
  path="/_mgmt/ping",
391
- response_model=Dict,
390
+ response_model=dict,
392
391
  responses={
393
392
  200: {
394
393
  "content": {
@@ -424,7 +423,7 @@ class StacApi:
424
423
  self.app.include_router(mgmt_router, tags=["Liveliness/Readiness"])
425
424
 
426
425
  def add_route_dependencies(
427
- self, scopes: List[Scope], dependencies: List[Depends]
426
+ self, scopes: list[Scope], dependencies: list[Depends]
428
427
  ) -> None:
429
428
  """Add custom dependencies to routes.
430
429
 
@@ -1,7 +1,8 @@
1
1
  """Error handling."""
2
2
 
3
3
  import logging
4
- from typing import Callable, Dict, Type, TypedDict
4
+ from collections.abc import Callable
5
+ from typing import TypedDict
5
6
 
6
7
  from fastapi import FastAPI
7
8
  from fastapi.encoders import jsonable_encoder
@@ -69,7 +70,7 @@ def exception_handler_factory(status_code: int) -> Callable:
69
70
 
70
71
 
71
72
  def add_exception_handlers(
72
- app: FastAPI, status_codes: Dict[Type[Exception], int]
73
+ app: FastAPI, status_codes: dict[type[Exception], int]
73
74
  ) -> None:
74
75
  """Add exception handlers to the FastAPI application.
75
76
 
@@ -5,7 +5,6 @@ import re
5
5
  import typing
6
6
  import warnings
7
7
  from http.client import HTTP_PORT, HTTPS_PORT
8
- from typing import List, Optional, Tuple
9
8
 
10
9
  from starlette.middleware.cors import CORSMiddleware as _CORSMiddleware
11
10
  from starlette.types import ASGIApp, Receive, Scope, Send
@@ -29,7 +28,7 @@ class CORSMiddleware(_CORSMiddleware):
29
28
  ),
30
29
  allow_headers: typing.Sequence[str] = ("Content-Type",),
31
30
  allow_credentials: bool = False,
32
- allow_origin_regex: typing.Optional[str] = None,
31
+ allow_origin_regex: str | None = None,
33
32
  expose_headers: typing.Sequence[str] = (),
34
33
  max_age: int = 600,
35
34
  **kwargs: typing.Any,
@@ -91,7 +90,7 @@ class ProxyHeaderMiddleware:
91
90
 
92
91
  await self.app(scope, receive, send)
93
92
 
94
- def _get_forwarded_url_parts(self, scope: Scope) -> Tuple[str, str, int]:
93
+ def _get_forwarded_url_parts(self, scope: Scope) -> tuple[str, str, int]:
95
94
  proto = scope.get("scheme", "http")
96
95
  # Assume default port based on protocol, can be overridden later
97
96
  port = 443 if proto == "https" else 80
@@ -107,7 +106,7 @@ class ProxyHeaderMiddleware:
107
106
  domain, port = scope["server"]
108
107
  port = int(port)
109
108
 
110
- forwarded_port: Optional[str] = None
109
+ forwarded_port: str | None = None
111
110
  forwarding_occurred = any(
112
111
  key
113
112
  in [
@@ -144,8 +143,8 @@ class ProxyHeaderMiddleware:
144
143
  return (proto, domain, port)
145
144
 
146
145
  def _get_header_value_by_name(
147
- self, scope: Scope, header_name: str, default_value: Optional[str] = None
148
- ) -> Optional[str]:
146
+ self, scope: Scope, header_name: str, default_value: str | None = None
147
+ ) -> str | None:
149
148
  headers = scope["headers"]
150
149
  candidates = [
151
150
  value.decode() for key, value in headers if key.decode() == header_name
@@ -155,7 +154,7 @@ class ProxyHeaderMiddleware:
155
154
  @staticmethod
156
155
  def _replace_header_value_by_name(
157
156
  scope: Scope, header_name: str, new_value: str
158
- ) -> List[Tuple[str, str]]:
157
+ ) -> list[tuple[str, str]]:
159
158
  return [
160
159
  (name, value)
161
160
  for name, value in scope["headers"]
@@ -1,12 +1,11 @@
1
1
  """Api request/response models."""
2
2
 
3
- from typing import List, Literal, Optional, Type, Union, cast
3
+ from typing import Annotated, Literal, cast
4
4
 
5
5
  import attr
6
6
  from fastapi import Path, Query
7
7
  from pydantic import BaseModel, create_model
8
8
  from stac_pydantic.shared import BBox
9
- from typing_extensions import Annotated
10
9
 
11
10
  from stac_fastapi.types.extension import ApiExtension
12
11
  from stac_fastapi.types.search import (
@@ -29,14 +28,14 @@ except ImportError: # pragma: nocover
29
28
 
30
29
  def create_request_model(
31
30
  model_name="SearchGetRequest",
32
- base_model: Union[Type[BaseModel], Type[APIRequest]] = BaseSearchGetRequest,
33
- extensions: Optional[List[ApiExtension]] = None,
34
- mixins: Optional[Union[List[Type[BaseModel]], List[Type[APIRequest]]]] = None,
31
+ base_model: type[BaseModel] | type[APIRequest] = BaseSearchGetRequest,
32
+ extensions: list[ApiExtension] | None = None,
33
+ mixins: list[type[BaseModel]] | list[type[APIRequest]] | None = None,
35
34
  request_type: str = "GET",
36
- ) -> Union[Type[BaseModel], Type[APIRequest]]:
35
+ ) -> type[BaseModel] | type[APIRequest]:
37
36
  """Create a pydantic model for validating request bodies."""
38
37
  fields = {}
39
- extension_models: List[Union[Type[BaseModel], Type[APIRequest]]] = []
38
+ extension_models: list[type[BaseModel] | type[APIRequest]] = []
40
39
 
41
40
  # Check extensions for additional parameters to search
42
41
  for extension in extensions or []:
@@ -50,7 +49,7 @@ def create_request_model(
50
49
  # Handle GET requests
51
50
  if all([issubclass(m, APIRequest) for m in models]):
52
51
  get_model = attr.make_class(model_name, attrs={}, bases=tuple(models))
53
- return cast(Type[APIRequest], get_model)
52
+ return cast(type[APIRequest], get_model)
54
53
 
55
54
  # Handle POST requests
56
55
  elif all([issubclass(m, BaseModel) for m in models]):
@@ -59,15 +58,15 @@ def create_request_model(
59
58
  fields[k] = (field_info.annotation, field_info)
60
59
 
61
60
  post_model = create_model(model_name, **fields, __base__=base_model) # type: ignore
62
- return cast(Type[BaseModel], post_model)
61
+ return cast(type[BaseModel], post_model)
63
62
 
64
63
  raise TypeError("Mixed Request Model types. Check extension request types.")
65
64
 
66
65
 
67
66
  def create_get_request_model(
68
- extensions: Optional[List[ApiExtension]],
69
- base_model: Type[APIRequest] = BaseSearchGetRequest,
70
- ) -> Type[APIRequest]:
67
+ extensions: list[ApiExtension] | None,
68
+ base_model: type[APIRequest] = BaseSearchGetRequest,
69
+ ) -> type[APIRequest]:
71
70
  """Wrap create_request_model to create the GET request model."""
72
71
  model = create_request_model(
73
72
  "SearchGetRequest",
@@ -75,13 +74,13 @@ def create_get_request_model(
75
74
  extensions=extensions,
76
75
  request_type="GET",
77
76
  )
78
- return cast(Type[APIRequest], model)
77
+ return cast(type[APIRequest], model)
79
78
 
80
79
 
81
80
  def create_post_request_model(
82
- extensions: Optional[List[ApiExtension]],
83
- base_model: Type[BaseModel] = BaseSearchPostRequest,
84
- ) -> Type[BaseModel]:
81
+ extensions: list[ApiExtension] | None,
82
+ base_model: type[BaseModel] = BaseSearchPostRequest,
83
+ ) -> type[BaseModel]:
85
84
  """Wrap create_request_model to create the POST request model."""
86
85
  model = create_request_model(
87
86
  "SearchPostRequest",
@@ -89,7 +88,7 @@ def create_post_request_model(
89
88
  extensions=extensions,
90
89
  request_type="POST",
91
90
  )
92
- return cast(Type[BaseModel], model)
91
+ return cast(type[BaseModel], model)
93
92
 
94
93
 
95
94
  @attr.s
@@ -120,12 +119,12 @@ class ItemCollectionUri(APIRequest, DatetimeMixin):
120
119
 
121
120
  collection_id: Annotated[str, Path(description="Collection ID")] = attr.ib()
122
121
  limit: Annotated[
123
- Optional[Limit],
122
+ Limit | None,
124
123
  Query(
125
124
  description="Limits the number of results that are included in each page of the response (capped to 10_000)." # noqa: E501
126
125
  ),
127
126
  ] = attr.ib(default=10)
128
- bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter) # type: ignore [misc]
127
+ bbox: BBox | None = attr.ib(default=None, converter=_bbox_converter) # type: ignore [misc]
129
128
  datetime: DateTimeQueryType = attr.ib(default=None, validator=_validate_datetime)
130
129
 
131
130
 
@@ -3,7 +3,8 @@
3
3
  import copy
4
4
  import functools
5
5
  import inspect
6
- from typing import Any, Awaitable, Callable, Dict, List, Optional, Type, TypedDict, Union
6
+ from collections.abc import Awaitable, Callable
7
+ from typing import Any, TypedDict
7
8
 
8
9
  from fastapi import Depends, FastAPI, params
9
10
  from fastapi.datastructures import DefaultPlaceholder
@@ -38,7 +39,7 @@ def sync_to_async(func):
38
39
 
39
40
  def create_async_endpoint(
40
41
  func: Callable,
41
- request_model: Union[Type[APIRequest], Type[BaseModel], Dict],
42
+ request_model: type[APIRequest] | type[BaseModel] | dict,
42
43
  ) -> Callable[[Any, Any], Awaitable[Any]]:
43
44
  """Wrap a function in a coroutine which may be used to create a FastAPI endpoint.
44
45
 
@@ -52,7 +53,7 @@ def create_async_endpoint(
52
53
 
53
54
  if isinstance(request_model, dict):
54
55
 
55
- async def _endpoint(request: Request, request_data: Dict[str, Any]):
56
+ async def _endpoint(request: Request, request_data: dict[str, Any]):
56
57
  """Endpoint."""
57
58
  return _wrap_response(await func(request_data, request=request))
58
59
 
@@ -80,11 +81,50 @@ class Scope(TypedDict, total=False):
80
81
  # https://github.com/encode/starlette/blob/6af5c515e0a896cbf3f86ee043b88f6c24200bcf/starlette/types.py#L3
81
82
  path: str
82
83
  method: str
83
- type: Optional[str]
84
+ type: str | None
85
+
86
+
87
+ def _is_endpoint_route(route: BaseRoute) -> bool:
88
+ """Check if route is a standard endpoint (not a mount or sub-router)."""
89
+ return (
90
+ hasattr(route, "path") and hasattr(route, "methods") and route.methods is not None
91
+ )
92
+
93
+
94
+ def _apply_dependencies_to_route(
95
+ route: BaseRoute, scopes: list[Scope], dependencies: list[params.Depends]
96
+ ) -> None:
97
+ """Apply dependencies to a single route matching the given scopes."""
98
+ for scope in scopes:
99
+ _scope = copy.deepcopy(scope)
100
+
101
+ if scope["path"] == "*":
102
+ _scope["path"] = route.path # type: ignore
103
+
104
+ if scope["method"] == "*":
105
+ _scope["method"] = list(route.methods)[0] # type: ignore
106
+
107
+ match, _ = route.matches({"type": "http", **_scope})
108
+ if match != Match.FULL:
109
+ continue
110
+
111
+ if not hasattr(route, "dependant"):
112
+ continue
113
+
114
+ for depends in dependencies[::-1]:
115
+ route.dependant.dependencies.insert(
116
+ 0,
117
+ get_parameterless_sub_dependant(
118
+ depends=depends,
119
+ path=route.path_format, # type: ignore
120
+ ),
121
+ )
122
+
123
+ route.dependencies.extend(dependencies) # type: ignore
84
124
 
85
125
 
86
126
  def add_route_dependencies(
87
- routes: List[BaseRoute], scopes: List[Scope], dependencies: List[params.Depends]
127
+ routes: list[BaseRoute], scopes: list[Scope], dependencies: list[params.Depends]
88
128
  ) -> None:
89
129
  """Add dependencies to routes.
90
130
 
@@ -96,48 +136,19 @@ def add_route_dependencies(
96
136
  Returns:
97
137
  None
98
138
  """
99
- for scope in scopes:
100
- _scope = copy.deepcopy(scope)
101
- for route in routes:
102
- if scope["path"] == "*":
103
- # NOTE: ignore type, because BaseRoute has no "path" attribute
104
- # but APIRoute does.
105
- _scope["path"] = route.path # type: ignore
106
-
107
- # NOTE: ignore type, because BaseRoute has no "method" attribute
108
- # but APIRoute does.
109
- if scope["method"] == "*":
110
- _scope["method"] = list(route.methods)[0] # type: ignore
111
-
112
- match, _ = route.matches({"type": "http", **_scope})
113
- if match != Match.FULL:
114
- continue
115
-
116
- # Ignore paths without dependants, e.g. /api, /api.html, /docs/oauth2-redirect
117
- if not hasattr(route, "dependant"):
118
- continue
119
-
120
- # Mimicking how APIRoute handles dependencies:
121
- # https://github.com/tiangolo/fastapi/blob/1760da0efa55585c19835d81afa8ca386036c325/fastapi/routing.py#L408-L412
122
- for depends in dependencies[::-1]:
123
- route.dependant.dependencies.insert(
124
- 0,
125
- get_parameterless_sub_dependant(
126
- # NOTE: ignore type, because BaseRoute has no "path_format"
127
- # attribute but APIRoute does.
128
- depends=depends,
129
- path=route.path_format, # type: ignore
130
- ),
131
- )
132
-
133
- # Register dependencies directly on route so that they aren't ignored if
134
- # the routes are later associated with an app (e.g.
135
- # app.include_router(router))
136
- # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/applications.py#L337-L360
137
- # https://github.com/tiangolo/fastapi/blob/58ab733f19846b4875c5b79bfb1f4d1cb7f4823f/fastapi/routing.py#L677-L678
138
- # NOTE: ignore type, because BaseRoute has no "dependencies" attribute
139
- # but APIRoute does.
140
- route.dependencies.extend(dependencies) # type: ignore
139
+ for route in routes:
140
+ if hasattr(route, "original_router"):
141
+ add_route_dependencies(route.original_router.routes, scopes, dependencies)
142
+ continue
143
+
144
+ if hasattr(route, "routes") and route.routes:
145
+ add_route_dependencies(route.routes, scopes, dependencies)
146
+ continue
147
+
148
+ if not _is_endpoint_route(route):
149
+ continue
150
+
151
+ _apply_dependencies_to_route(route, scopes, dependencies)
141
152
 
142
153
 
143
154
  def add_direct_response(app: FastAPI) -> None:
@@ -148,7 +159,7 @@ def add_direct_response(app: FastAPI) -> None:
148
159
  ref: https://gist.github.com/Zaczero/00f3a2679ebc0a25eb938ed82bc63553
149
160
  """
150
161
 
151
- def wrap_endpoint(endpoint: Callable, cls: Type[Response]):
162
+ def wrap_endpoint(endpoint: Callable, cls: type[Response]):
152
163
  @functools.wraps(endpoint)
153
164
  async def wrapper(*args, **kwargs):
154
165
  content = await endpoint(*args, **kwargs)
@@ -1,3 +1,3 @@
1
1
  """Library version."""
2
2
 
3
- __version__ = "6.2.1"
3
+ __version__ = "6.3.1"