apitally 0.11.2__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/client/asyncio.py +9 -5
- apitally/client/threading.py +9 -5
- apitally/django.py +1 -1
- apitally/flask.py +1 -1
- apitally/starlette.py +64 -67
- {apitally-0.11.2.dist-info → apitally-0.12.0.dist-info}/METADATA +1 -1
- {apitally-0.11.2.dist-info → apitally-0.12.0.dist-info}/RECORD +10 -10
- {apitally-0.11.2.dist-info → apitally-0.12.0.dist-info}/LICENSE +0 -0
- {apitally-0.11.2.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/client/asyncio.py
CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
import logging
|
5
|
+
import random
|
5
6
|
import time
|
6
7
|
from functools import partial
|
7
8
|
from typing import Any, Dict, Optional, Tuple
|
@@ -82,18 +83,21 @@ class ApitallyClient(ApitallyClientBase):
|
|
82
83
|
data = self.get_sync_data()
|
83
84
|
self._sync_data_queue.put_nowait((time.time(), data))
|
84
85
|
|
85
|
-
|
86
|
+
i = 0
|
86
87
|
while not self._sync_data_queue.empty():
|
87
88
|
timestamp, data = self._sync_data_queue.get_nowait()
|
88
89
|
try:
|
89
90
|
if (time_offset := time.time() - timestamp) <= MAX_QUEUE_TIME:
|
91
|
+
if i > 0:
|
92
|
+
await asyncio.sleep(random.uniform(0.1, 0.3))
|
90
93
|
data["time_offset"] = time_offset
|
91
94
|
await self._send_sync_data(client, data)
|
92
|
-
|
95
|
+
i += 1
|
93
96
|
except httpx.HTTPError:
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
+
self._sync_data_queue.put_nowait((timestamp, data))
|
98
|
+
break
|
99
|
+
finally:
|
100
|
+
self._sync_data_queue.task_done()
|
97
101
|
|
98
102
|
@retry(raise_on_giveup=False)
|
99
103
|
async def _send_startup_data(self, client: httpx.AsyncClient, data: Dict[str, Any]) -> None:
|
apitally/client/threading.py
CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import logging
|
4
4
|
import queue
|
5
|
+
import random
|
5
6
|
import time
|
6
7
|
from functools import partial
|
7
8
|
from threading import Event, Thread
|
@@ -95,18 +96,21 @@ class ApitallyClient(ApitallyClientBase):
|
|
95
96
|
data = self.get_sync_data()
|
96
97
|
self._sync_data_queue.put_nowait((time.time(), data))
|
97
98
|
|
98
|
-
|
99
|
+
i = 0
|
99
100
|
while not self._sync_data_queue.empty():
|
100
101
|
timestamp, data = self._sync_data_queue.get_nowait()
|
101
102
|
try:
|
102
103
|
if (time_offset := time.time() - timestamp) <= MAX_QUEUE_TIME:
|
104
|
+
if i > 0:
|
105
|
+
time.sleep(random.uniform(0.1, 0.3))
|
103
106
|
data["time_offset"] = time_offset
|
104
107
|
self._send_sync_data(session, data)
|
105
|
-
|
108
|
+
i += 1
|
106
109
|
except requests.RequestException:
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
+
self._sync_data_queue.put_nowait((timestamp, data))
|
111
|
+
break
|
112
|
+
finally:
|
113
|
+
self._sync_data_queue.task_done()
|
110
114
|
|
111
115
|
@retry(raise_on_giveup=False)
|
112
116
|
def _send_startup_data(self, session: requests.Session, data: Dict[str, Any]) -> None:
|
apitally/django.py
CHANGED
@@ -94,7 +94,7 @@ class ApitallyMiddleware:
|
|
94
94
|
response = self.get_response(request)
|
95
95
|
response_time = time.perf_counter() - start_time
|
96
96
|
path = self.get_path(request)
|
97
|
-
if request.method is not None and path is not None:
|
97
|
+
if request.method is not None and request.method != "OPTIONS" and path is not None:
|
98
98
|
try:
|
99
99
|
consumer = self.get_consumer(request)
|
100
100
|
consumer_identifier = consumer.identifier if consumer else None
|
apitally/flask.py
CHANGED
@@ -93,7 +93,7 @@ class ApitallyMiddleware:
|
|
93
93
|
response_headers: Headers,
|
94
94
|
) -> None:
|
95
95
|
rule, is_handled_path = self.get_rule(environ)
|
96
|
-
if is_handled_path or not self.filter_unhandled_paths:
|
96
|
+
if (is_handled_path or not self.filter_unhandled_paths) and environ["REQUEST_METHOD"] != "OPTIONS":
|
97
97
|
consumer = self.get_consumer()
|
98
98
|
consumer_identifier = consumer.identifier if consumer else None
|
99
99
|
self.client.consumer_registry.add_or_update_consumer(consumer)
|
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,34 +55,55 @@ 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)
|
@@ -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,19 +1,19 @@
|
|
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
|
-
apitally/client/asyncio.py,sha256=
|
3
|
+
apitally/client/asyncio.py,sha256=Y5sbRLRnJCIJx9VQ2DGgQsYNKGvURV2U1y3VxHuPhgQ,4874
|
4
4
|
apitally/client/base.py,sha256=v4LSOYNIOoeL3KTVyBlXBY5LCXc79Lvu9yK5Y_KILSQ,15442
|
5
5
|
apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
|
6
|
-
apitally/client/threading.py,sha256=
|
6
|
+
apitally/client/threading.py,sha256=cASa0C9nyRp5gf5IzCDj6TE-v8t8SW4zJ38W6NdJ3Q8,5204
|
7
7
|
apitally/common.py,sha256=GbVmnXxhRvV30d7CfCQ9r0AeXj14Mv9Jm_Yd1bRWP28,1088
|
8
|
-
apitally/django.py,sha256=
|
8
|
+
apitally/django.py,sha256=Zw8a971UwGKaEMPUtmlBbjufAYwMkSjRSQlss8FDY-E,13795
|
9
9
|
apitally/django_ninja.py,sha256=dqQtnz2s8YWYHCwvkK5BjokjvpZJpPNhP0vng4kFtrQ,120
|
10
10
|
apitally/django_rest_framework.py,sha256=dqQtnz2s8YWYHCwvkK5BjokjvpZJpPNhP0vng4kFtrQ,120
|
11
11
|
apitally/fastapi.py,sha256=hEyYZsvIaA3OXZSSFdey5iqeEjfBPHgfNbyX8pLm7GI,123
|
12
|
-
apitally/flask.py,sha256=
|
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
|