apitally 0.11.3__py3-none-any.whl → 0.12.0__py3-none-any.whl
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.
- apitally/__init__.py +1 -1
- apitally/starlette.py +65 -68
- {apitally-0.11.3.dist-info → apitally-0.12.0.dist-info}/METADATA +1 -1
- {apitally-0.11.3.dist-info → apitally-0.12.0.dist-info}/RECORD +6 -6
- {apitally-0.11.3.dist-info → apitally-0.12.0.dist-info}/LICENSE +0 -0
- {apitally-0.11.3.dist-info → apitally-0.12.0.dist-info}/WHEEL +0 -0
apitally/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.
|
1
|
+
__version__ = "0.12.0"
|
apitally/starlette.py
CHANGED
@@ -1,35 +1,29 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
+
import contextlib
|
4
5
|
import json
|
5
6
|
import time
|
6
|
-
from typing import
|
7
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
7
8
|
from warnings import warn
|
8
9
|
|
9
10
|
from httpx import HTTPStatusError
|
10
|
-
from starlette.
|
11
|
-
from starlette.
|
11
|
+
from starlette.datastructures import Headers
|
12
|
+
from starlette.requests import Request
|
12
13
|
from starlette.routing import BaseRoute, Match, Router
|
13
14
|
from starlette.schemas import EndpointInfo, SchemaGenerator
|
14
|
-
from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR
|
15
15
|
from starlette.testclient import TestClient
|
16
|
-
from starlette.types import ASGIApp
|
16
|
+
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
17
17
|
|
18
18
|
from apitally.client.asyncio import ApitallyClient
|
19
19
|
from apitally.client.base import Consumer as ApitallyConsumer
|
20
20
|
from apitally.common import get_versions
|
21
21
|
|
22
22
|
|
23
|
-
if TYPE_CHECKING:
|
24
|
-
from starlette.middleware.base import RequestResponseEndpoint
|
25
|
-
from starlette.requests import Request
|
26
|
-
from starlette.responses import Response
|
27
|
-
|
28
|
-
|
29
23
|
__all__ = ["ApitallyMiddleware", "ApitallyConsumer"]
|
30
24
|
|
31
25
|
|
32
|
-
class ApitallyMiddleware
|
26
|
+
class ApitallyMiddleware:
|
33
27
|
def __init__(
|
34
28
|
self,
|
35
29
|
app: ASGIApp,
|
@@ -40,6 +34,7 @@ class ApitallyMiddleware(BaseHTTPMiddleware):
|
|
40
34
|
filter_unhandled_paths: bool = True,
|
41
35
|
identify_consumer_callback: Optional[Callable[[Request], Union[str, ApitallyConsumer, None]]] = None,
|
42
36
|
) -> None:
|
37
|
+
self.app = app
|
43
38
|
self.filter_unhandled_paths = filter_unhandled_paths
|
44
39
|
self.identify_consumer_callback = identify_consumer_callback
|
45
40
|
self.client = ApitallyClient(client_id=client_id, env=env)
|
@@ -47,7 +42,6 @@ class ApitallyMiddleware(BaseHTTPMiddleware):
|
|
47
42
|
self._delayed_set_startup_data_task: Optional[asyncio.Task] = None
|
48
43
|
self.delayed_set_startup_data(app_version, openapi_url)
|
49
44
|
_register_shutdown_handler(app, self.client.handle_shutdown)
|
50
|
-
super().__init__(app)
|
51
45
|
|
52
46
|
def delayed_set_startup_data(self, app_version: Optional[str] = None, openapi_url: Optional[str] = None) -> None:
|
53
47
|
self._delayed_set_startup_data_task = asyncio.create_task(
|
@@ -61,38 +55,59 @@ class ApitallyMiddleware(BaseHTTPMiddleware):
|
|
61
55
|
data = _get_startup_data(self.app, app_version, openapi_url)
|
62
56
|
self.client.set_startup_data(data)
|
63
57
|
|
64
|
-
async def
|
65
|
-
|
58
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
59
|
+
if scope["type"] == "http" and scope["method"] != "OPTIONS":
|
60
|
+
request = Request(scope)
|
61
|
+
response_status = 0
|
62
|
+
response_time = 0.0
|
63
|
+
response_headers = Headers()
|
64
|
+
response_body = b""
|
66
65
|
start_time = time.perf_counter()
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
66
|
+
|
67
|
+
async def send_wrapper(message: Message) -> None:
|
68
|
+
nonlocal response_time, response_status, response_headers, response_body
|
69
|
+
if message["type"] == "http.response.start":
|
70
|
+
response_time = time.perf_counter() - start_time
|
71
|
+
response_status = message["status"]
|
72
|
+
response_headers = Headers(scope=message)
|
73
|
+
elif message["type"] == "http.response.body" and response_status == 422:
|
74
|
+
response_body += message["body"]
|
75
|
+
await send(message)
|
76
|
+
|
77
|
+
try:
|
78
|
+
await self.app(scope, receive, send_wrapper)
|
79
|
+
except BaseException as e:
|
80
|
+
self.add_request(
|
81
|
+
request=request,
|
82
|
+
response_status=500,
|
83
|
+
response_time=time.perf_counter() - start_time,
|
84
|
+
response_headers=response_headers,
|
85
|
+
response_body=response_body,
|
86
|
+
exception=e,
|
87
|
+
)
|
88
|
+
raise e from None
|
89
|
+
else:
|
90
|
+
self.add_request(
|
91
|
+
request=request,
|
92
|
+
response_status=response_status,
|
93
|
+
response_time=response_time,
|
94
|
+
response_headers=response_headers,
|
95
|
+
response_body=response_body,
|
96
|
+
)
|
77
97
|
else:
|
78
|
-
await self.
|
79
|
-
request=request,
|
80
|
-
response=response,
|
81
|
-
status_code=response.status_code,
|
82
|
-
response_time=time.perf_counter() - start_time,
|
83
|
-
)
|
84
|
-
return response
|
98
|
+
await self.app(scope, receive, send) # pragma: no cover
|
85
99
|
|
86
|
-
|
100
|
+
def add_request(
|
87
101
|
self,
|
88
102
|
request: Request,
|
89
|
-
|
90
|
-
status_code: int,
|
103
|
+
response_status: int,
|
91
104
|
response_time: float,
|
105
|
+
response_headers: Headers,
|
106
|
+
response_body: bytes,
|
92
107
|
exception: Optional[BaseException] = None,
|
93
108
|
) -> None:
|
94
109
|
path_template, is_handled_path = self.get_path_template(request)
|
95
|
-
if
|
110
|
+
if is_handled_path or not self.filter_unhandled_paths:
|
96
111
|
consumer = self.get_consumer(request)
|
97
112
|
consumer_identifier = consumer.identifier if consumer else None
|
98
113
|
self.client.consumer_registry.add_or_update_consumer(consumer)
|
@@ -100,26 +115,23 @@ class ApitallyMiddleware(BaseHTTPMiddleware):
|
|
100
115
|
consumer=consumer_identifier,
|
101
116
|
method=request.method,
|
102
117
|
path=path_template,
|
103
|
-
status_code=
|
118
|
+
status_code=response_status,
|
104
119
|
response_time=response_time,
|
105
120
|
request_size=request.headers.get("Content-Length"),
|
106
|
-
response_size=
|
121
|
+
response_size=response_headers.get("Content-Length"),
|
107
122
|
)
|
108
|
-
if (
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
detail=body["detail"],
|
121
|
-
)
|
122
|
-
if status_code == 500 and exception is not None:
|
123
|
+
if response_status == 422 and response_body and response_headers.get("Content-Type") == "application/json":
|
124
|
+
with contextlib.suppress(json.JSONDecodeError):
|
125
|
+
body = json.loads(response_body)
|
126
|
+
if isinstance(body, dict) and "detail" in body and isinstance(body["detail"], list):
|
127
|
+
# Log FastAPI / Pydantic validation errors
|
128
|
+
self.client.validation_error_counter.add_validation_errors(
|
129
|
+
consumer=consumer_identifier,
|
130
|
+
method=request.method,
|
131
|
+
path=path_template,
|
132
|
+
detail=body["detail"],
|
133
|
+
)
|
134
|
+
if response_status == 500 and exception is not None:
|
123
135
|
self.client.server_error_counter.add_server_error(
|
124
136
|
consumer=consumer_identifier,
|
125
137
|
method=request.method,
|
@@ -127,21 +139,6 @@ class ApitallyMiddleware(BaseHTTPMiddleware):
|
|
127
139
|
exception=exception,
|
128
140
|
)
|
129
141
|
|
130
|
-
@staticmethod
|
131
|
-
async def get_response_json(response: Response) -> Any:
|
132
|
-
if hasattr(response, "body"):
|
133
|
-
try:
|
134
|
-
return json.loads(response.body)
|
135
|
-
except json.JSONDecodeError: # pragma: no cover
|
136
|
-
return None
|
137
|
-
elif hasattr(response, "body_iterator"):
|
138
|
-
try:
|
139
|
-
response_body = [section async for section in response.body_iterator]
|
140
|
-
response.body_iterator = iterate_in_threadpool(iter(response_body))
|
141
|
-
return json.loads(b"".join(response_body))
|
142
|
-
except json.JSONDecodeError: # pragma: no cover
|
143
|
-
return None
|
144
|
-
|
145
142
|
@staticmethod
|
146
143
|
def get_path_template(request: Request) -> Tuple[str, bool]:
|
147
144
|
for route in request.app.routes:
|
@@ -1,4 +1,4 @@
|
|
1
|
-
apitally/__init__.py,sha256=
|
1
|
+
apitally/__init__.py,sha256=eHjt9DPsMbptabS2yGx9Yhbyzq5hFSUHXb7zc8Q_8-o,23
|
2
2
|
apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
apitally/client/asyncio.py,sha256=Y5sbRLRnJCIJx9VQ2DGgQsYNKGvURV2U1y3VxHuPhgQ,4874
|
4
4
|
apitally/client/base.py,sha256=v4LSOYNIOoeL3KTVyBlXBY5LCXc79Lvu9yK5Y_KILSQ,15442
|
@@ -12,8 +12,8 @@ apitally/fastapi.py,sha256=hEyYZsvIaA3OXZSSFdey5iqeEjfBPHgfNbyX8pLm7GI,123
|
|
12
12
|
apitally/flask.py,sha256=7TJIoAT91-bR_7gZkL0clDk-Whl-V21hbo4nASaDmB4,6447
|
13
13
|
apitally/litestar.py,sha256=sQcrHw-JV9AlpnXlrczmaDe0k6tD9PYQsc8nyQul8Ko,8802
|
14
14
|
apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
-
apitally/starlette.py,sha256=
|
16
|
-
apitally-0.
|
17
|
-
apitally-0.
|
18
|
-
apitally-0.
|
19
|
-
apitally-0.
|
15
|
+
apitally/starlette.py,sha256=PA_0BTy9aVtrY2_QrWU7Js5vjW1uxZ476rmg95fXq2g,8881
|
16
|
+
apitally-0.12.0.dist-info/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
|
17
|
+
apitally-0.12.0.dist-info/METADATA,sha256=gGryZ9u9sLV8mYrcoRhvYK4lQ4bFf9vhbvrCENA6T-I,6994
|
18
|
+
apitally-0.12.0.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
|
19
|
+
apitally-0.12.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|