stac-fastapi.api 6.1.3__tar.gz → 6.1.5__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.1.3
3
+ Version: 6.1.5
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>
@@ -8,6 +8,7 @@ import attr
8
8
  from brotli_asgi import BrotliMiddleware
9
9
  from fastapi import APIRouter, FastAPI
10
10
  from fastapi.params import Depends
11
+ from pydantic import BaseModel
11
12
  from stac_pydantic import api
12
13
  from stac_pydantic.shared import MimeTypes
13
14
  from stac_pydantic.version import STAC_VERSION
@@ -114,12 +115,8 @@ class StacApi:
114
115
  converter=update_openapi, # type: ignore
115
116
  )
116
117
  router: APIRouter = attr.ib(default=attr.Factory(APIRouter))
117
- search_get_request_model: Type[BaseSearchGetRequest] = attr.ib(
118
- default=BaseSearchGetRequest
119
- )
120
- search_post_request_model: Type[BaseSearchPostRequest] = attr.ib(
121
- default=BaseSearchPostRequest
122
- )
118
+ search_get_request_model: Type[APIRequest] = attr.ib(default=BaseSearchGetRequest)
119
+ search_post_request_model: Type[BaseModel] = attr.ib(default=BaseSearchPostRequest)
123
120
  collections_get_request_model: Type[APIRequest] = attr.ib(default=EmptyRequest)
124
121
  collection_get_request_model: Type[APIRequest] = attr.ib(default=CollectionUri)
125
122
  items_get_request_model: Type[APIRequest] = attr.ib(default=ItemCollectionUri)
@@ -96,5 +96,5 @@ def add_exception_handlers(
96
96
  # TODO: Argument 2 to "add_exception_handler" of "Starlette" has incompatible type
97
97
  app.add_exception_handler(
98
98
  RequestValidationError,
99
- request_validation_exception_handler, # type: ignore
99
+ request_validation_exception_handler, # type: ignore [arg-type]
100
100
  )
@@ -82,20 +82,33 @@ class ProxyHeaderMiddleware:
82
82
 
83
83
  await self.app(scope, receive, send)
84
84
 
85
- def _get_forwarded_url_parts(self, scope: Scope) -> Tuple[str, str, str]:
85
+ def _get_forwarded_url_parts(self, scope: Scope) -> Tuple[str, str, int]:
86
86
  proto = scope.get("scheme", "http")
87
- header_host = self._get_header_value_by_name(scope, "host")
88
- if header_host is None:
89
- domain, port = scope["server"]
90
- else:
87
+ # Assume default port based on protocol, can be overridden later
88
+ port = 443 if proto == "https" else 80
89
+
90
+ if header_host := self._get_header_value_by_name(scope, "host"):
91
91
  header_host_parts = header_host.split(":")
92
+ domain = header_host_parts[0]
92
93
  if len(header_host_parts) == 2:
93
- domain, port = header_host_parts
94
- else:
95
- domain = header_host_parts[0]
96
- port = None
97
-
98
- port_str = None # make sure it is defined in all paths since we access it later
94
+ with contextlib.suppress(ValueError):
95
+ port = int(header_host_parts[1])
96
+ else:
97
+ # Not sure when we would not have a host header, but fallback to server info
98
+ domain, port = scope["server"]
99
+ port = int(port)
100
+
101
+ forwarded_port: Optional[str] = None
102
+ forwarding_occurred = any(
103
+ key
104
+ in [
105
+ b"forwarded",
106
+ b"x-forwarded-proto",
107
+ b"x-forwarded-host",
108
+ b"x-forwarded-port",
109
+ ]
110
+ for key, _ in scope["headers"]
111
+ )
99
112
 
100
113
  if forwarded := self._get_header_value_by_name(scope, "forwarded"):
101
114
  for proxy in forwarded.split(","):
@@ -103,15 +116,21 @@ class ProxyHeaderMiddleware:
103
116
  proto = proto_expr.group("proto")
104
117
  if host_expr := _HOST_HEADER_REGEX.search(proxy):
105
118
  domain = host_expr.group("host")
106
- port_str = host_expr.group("port") # None if not present in the match
119
+ forwarded_port = host_expr.group("port") # None if not present
107
120
 
108
121
  else:
109
- domain = self._get_header_value_by_name(scope, "x-forwarded-host", domain)
110
- proto = self._get_header_value_by_name(scope, "x-forwarded-proto", proto)
111
- port_str = self._get_header_value_by_name(scope, "x-forwarded-port", port)
112
-
113
- with contextlib.suppress(ValueError): # ignore ports that are not valid integers
114
- port = int(port_str) if port_str is not None else port
122
+ domain = self._get_header_value_by_name(scope, "x-forwarded-host") or domain
123
+ proto = self._get_header_value_by_name(scope, "x-forwarded-proto") or proto
124
+ forwarded_port = self._get_header_value_by_name(scope, "x-forwarded-port")
125
+
126
+ if forwarding_occurred and not forwarded_port:
127
+ # If forwarding occurred but no port was specified, use protocol default
128
+ forwarded_port = "443" if proto == "https" else "80"
129
+
130
+ if forwarded_port:
131
+ # ignore ports that are not valid integers
132
+ with contextlib.suppress(ValueError):
133
+ port = int(forwarded_port)
115
134
 
116
135
  return (proto, domain, port)
117
136
 
@@ -1,6 +1,6 @@
1
1
  """Api request/response models."""
2
2
 
3
- from typing import List, Literal, Optional, Type, Union
3
+ from typing import List, Literal, Optional, Type, Union, cast
4
4
 
5
5
  import attr
6
6
  from fastapi import Path, Query
@@ -24,7 +24,7 @@ try:
24
24
  import orjson # noqa
25
25
  from fastapi.responses import ORJSONResponse as JSONResponse
26
26
  except ImportError: # pragma: nocover
27
- from starlette.responses import JSONResponse # type: ignore
27
+ from starlette.responses import JSONResponse # type: ignore [assignment]
28
28
 
29
29
 
30
30
  def create_request_model(
@@ -49,7 +49,8 @@ def create_request_model(
49
49
 
50
50
  # Handle GET requests
51
51
  if all([issubclass(m, APIRequest) for m in models]):
52
- return attr.make_class(model_name, attrs={}, bases=tuple(models))
52
+ get_model = attr.make_class(model_name, attrs={}, bases=tuple(models))
53
+ return cast(Type[APIRequest], get_model)
53
54
 
54
55
  # Handle POST requests
55
56
  elif all([issubclass(m, BaseModel) for m in models]):
@@ -57,35 +58,38 @@ def create_request_model(
57
58
  for k, field_info in model.model_fields.items(): # type: ignore
58
59
  fields[k] = (field_info.annotation, field_info)
59
60
 
60
- return create_model(model_name, **fields, __base__=base_model) # type: ignore
61
+ post_model = create_model(model_name, **fields, __base__=base_model) # type: ignore
62
+ return cast(Type[BaseModel], post_model)
61
63
 
62
64
  raise TypeError("Mixed Request Model types. Check extension request types.")
63
65
 
64
66
 
65
67
  def create_get_request_model(
66
68
  extensions: Optional[List[ApiExtension]],
67
- base_model: Type[BaseSearchGetRequest] = BaseSearchGetRequest,
69
+ base_model: Type[APIRequest] = BaseSearchGetRequest,
68
70
  ) -> Type[APIRequest]:
69
71
  """Wrap create_request_model to create the GET request model."""
70
- return create_request_model( # type: ignore
72
+ model = create_request_model(
71
73
  "SearchGetRequest",
72
74
  base_model=base_model,
73
75
  extensions=extensions,
74
76
  request_type="GET",
75
77
  )
78
+ return cast(Type[APIRequest], model)
76
79
 
77
80
 
78
81
  def create_post_request_model(
79
82
  extensions: Optional[List[ApiExtension]],
80
- base_model: Type[BaseSearchPostRequest] = BaseSearchPostRequest,
83
+ base_model: Type[BaseModel] = BaseSearchPostRequest,
81
84
  ) -> Type[BaseModel]:
82
85
  """Wrap create_request_model to create the POST request model."""
83
- return create_request_model( # type: ignore
86
+ model = create_request_model(
84
87
  "SearchPostRequest",
85
88
  base_model=base_model,
86
89
  extensions=extensions,
87
90
  request_type="POST",
88
91
  )
92
+ return cast(Type[BaseModel], model)
89
93
 
90
94
 
91
95
  @attr.s
@@ -121,7 +125,7 @@ class ItemCollectionUri(APIRequest, DatetimeMixin):
121
125
  description="Limits the number of results that are included in each page of the response (capped to 10_000)." # noqa: E501
122
126
  ),
123
127
  ] = attr.ib(default=10)
124
- bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter) # type: ignore
128
+ bbox: Optional[BBox] = attr.ib(default=None, converter=_bbox_converter) # type: ignore [misc]
125
129
  datetime: DateTimeQueryType = attr.ib(default=None, validator=_validate_datetime)
126
130
 
127
131
 
@@ -1,3 +1,3 @@
1
1
  """Library version."""
2
2
 
3
- __version__ = "6.1.3"
3
+ __version__ = "6.1.5"