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.
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/PKG-INFO +1 -1
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/stac_fastapi/api/app.py +17 -18
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/stac_fastapi/api/errors.py +3 -2
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/stac_fastapi/api/middleware.py +6 -7
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/stac_fastapi/api/models.py +18 -19
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/stac_fastapi/api/routes.py +59 -48
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/stac_fastapi/api/version.py +1 -1
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/.gitignore +0 -0
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/README.md +0 -0
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/pyproject.toml +0 -0
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/stac_fastapi/api/__init__.py +0 -0
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/stac_fastapi/api/config.py +0 -0
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/stac_fastapi/api/openapi.py +0 -0
- {stac_fastapi_api-6.2.1 → stac_fastapi_api-6.3.1}/stac_fastapi/api/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stac-fastapi-api
|
|
3
|
-
Version: 6.
|
|
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
|
|
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:
|
|
83
|
-
extensions:
|
|
84
|
-
exceptions:
|
|
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:
|
|
120
|
-
search_post_request_model:
|
|
121
|
-
collections_get_request_model:
|
|
122
|
-
collection_get_request_model:
|
|
123
|
-
items_get_request_model:
|
|
124
|
-
item_get_request_model:
|
|
125
|
-
response_class:
|
|
126
|
-
middlewares:
|
|
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:
|
|
143
|
-
health_check:
|
|
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:
|
|
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=
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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) ->
|
|
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:
|
|
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:
|
|
148
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
|
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:
|
|
33
|
-
extensions:
|
|
34
|
-
mixins:
|
|
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
|
-
) ->
|
|
35
|
+
) -> type[BaseModel] | type[APIRequest]:
|
|
37
36
|
"""Create a pydantic model for validating request bodies."""
|
|
38
37
|
fields = {}
|
|
39
|
-
extension_models:
|
|
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(
|
|
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(
|
|
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:
|
|
69
|
-
base_model:
|
|
70
|
-
) ->
|
|
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(
|
|
77
|
+
return cast(type[APIRequest], model)
|
|
79
78
|
|
|
80
79
|
|
|
81
80
|
def create_post_request_model(
|
|
82
|
-
extensions:
|
|
83
|
-
base_model:
|
|
84
|
-
) ->
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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:
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|