apitally 0.14.0rc1__py3-none-any.whl → 0.14.2__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/client/client_asyncio.py +14 -3
- apitally/client/client_threading.py +26 -4
- apitally/client/request_logging.py +24 -9
- apitally/django.py +4 -0
- apitally/flask.py +7 -1
- apitally/litestar.py +8 -1
- apitally/starlette.py +8 -2
- {apitally-0.14.0rc1.dist-info → apitally-0.14.2.dist-info}/METADATA +1 -1
- {apitally-0.14.0rc1.dist-info → apitally-0.14.2.dist-info}/RECORD +11 -11
- {apitally-0.14.0rc1.dist-info → apitally-0.14.2.dist-info}/WHEEL +0 -0
- {apitally-0.14.0rc1.dist-info → apitally-0.14.2.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ import random
|
|
6
6
|
import time
|
7
7
|
from contextlib import suppress
|
8
8
|
from functools import partial
|
9
|
-
from typing import Any, AsyncIterator, Dict, Optional, Tuple
|
9
|
+
from typing import Any, AsyncIterator, Dict, Optional, Tuple, Union
|
10
10
|
from uuid import UUID
|
11
11
|
|
12
12
|
import backoff
|
@@ -29,15 +29,26 @@ retry = partial(
|
|
29
29
|
|
30
30
|
|
31
31
|
class ApitallyClient(ApitallyClientBase):
|
32
|
-
def __init__(
|
32
|
+
def __init__(
|
33
|
+
self,
|
34
|
+
client_id: str,
|
35
|
+
env: str,
|
36
|
+
request_logging_config: Optional[RequestLoggingConfig] = None,
|
37
|
+
proxy: Optional[Union[str, httpx.Proxy]] = None,
|
38
|
+
) -> None:
|
33
39
|
super().__init__(client_id=client_id, env=env, request_logging_config=request_logging_config)
|
40
|
+
self.proxy = proxy
|
34
41
|
self._stop_sync_loop = False
|
35
42
|
self._sync_loop_task: Optional[asyncio.Task] = None
|
36
43
|
self._sync_data_queue: asyncio.Queue[Tuple[float, Dict[str, Any]]] = asyncio.Queue()
|
37
44
|
self._set_startup_data_task: Optional[asyncio.Task] = None
|
38
45
|
|
39
46
|
def get_http_client(self) -> httpx.AsyncClient:
|
40
|
-
|
47
|
+
if httpx.__version__ >= "0.26.0":
|
48
|
+
# `proxy` parameter was added in version 0.26.0
|
49
|
+
return httpx.AsyncClient(base_url=self.hub_url, timeout=REQUEST_TIMEOUT, proxy=self.proxy)
|
50
|
+
else:
|
51
|
+
return httpx.AsyncClient(base_url=self.hub_url, timeout=REQUEST_TIMEOUT, proxies=self.proxy)
|
41
52
|
|
42
53
|
def start_sync_loop(self) -> None:
|
43
54
|
self._stop_sync_loop = False
|
@@ -47,8 +47,15 @@ except NameError:
|
|
47
47
|
|
48
48
|
|
49
49
|
class ApitallyClient(ApitallyClientBase):
|
50
|
-
def __init__(
|
50
|
+
def __init__(
|
51
|
+
self,
|
52
|
+
client_id: str,
|
53
|
+
env: str,
|
54
|
+
request_logging_config: Optional[RequestLoggingConfig] = None,
|
55
|
+
proxy: Optional[str] = None,
|
56
|
+
) -> None:
|
51
57
|
super().__init__(client_id=client_id, env=env, request_logging_config=request_logging_config)
|
58
|
+
self.proxies = {"https": proxy} if proxy else None
|
52
59
|
self._thread: Optional[Thread] = None
|
53
60
|
self._stop_sync_loop = Event()
|
54
61
|
self._sync_data_queue: Queue[Tuple[float, Dict[str, Any]]] = Queue()
|
@@ -145,7 +152,12 @@ class ApitallyClient(ApitallyClientBase):
|
|
145
152
|
@retry(raise_on_giveup=False)
|
146
153
|
def _send_startup_data(self, session: requests.Session, data: Dict[str, Any]) -> None:
|
147
154
|
logger.debug("Sending startup data to Apitally hub")
|
148
|
-
response = session.post(
|
155
|
+
response = session.post(
|
156
|
+
url=f"{self.hub_url}/startup",
|
157
|
+
json=data,
|
158
|
+
timeout=REQUEST_TIMEOUT,
|
159
|
+
proxies=self.proxies,
|
160
|
+
)
|
149
161
|
self._handle_hub_response(response)
|
150
162
|
self._startup_data_sent = True
|
151
163
|
self._startup_data = None
|
@@ -153,12 +165,22 @@ class ApitallyClient(ApitallyClientBase):
|
|
153
165
|
@retry()
|
154
166
|
def _send_sync_data(self, session: requests.Session, data: Dict[str, Any]) -> None:
|
155
167
|
logger.debug("Synchronizing data with Apitally hub")
|
156
|
-
response = session.post(
|
168
|
+
response = session.post(
|
169
|
+
url=f"{self.hub_url}/sync",
|
170
|
+
json=data,
|
171
|
+
timeout=REQUEST_TIMEOUT,
|
172
|
+
proxies=self.proxies,
|
173
|
+
)
|
157
174
|
self._handle_hub_response(response)
|
158
175
|
|
159
176
|
def _send_log_data(self, session: requests.Session, uuid: UUID, fp: BufferedReader) -> None:
|
160
177
|
logger.debug("Streaming request log data to Apitally hub")
|
161
|
-
response = session.post(
|
178
|
+
response = session.post(
|
179
|
+
url=f"{self.hub_url}/log?uuid={uuid}",
|
180
|
+
data=fp,
|
181
|
+
timeout=REQUEST_TIMEOUT,
|
182
|
+
proxies=self.proxies,
|
183
|
+
)
|
162
184
|
if response.status_code == 402 and "Retry-After" in response.headers:
|
163
185
|
with suppress(ValueError):
|
164
186
|
retry_after = int(response.headers["Retry-After"])
|
@@ -35,6 +35,12 @@ EXCLUDE_PATH_PATTERNS = [
|
|
35
35
|
r"/ready$",
|
36
36
|
r"/live$",
|
37
37
|
]
|
38
|
+
EXCLUDE_USER_AGENT_PATTERNS = [
|
39
|
+
r"health[_- ]?check",
|
40
|
+
r"microsoft-azure-application-lb",
|
41
|
+
r"googlehc",
|
42
|
+
r"kube-probe",
|
43
|
+
]
|
38
44
|
MASK_QUERY_PARAM_PATTERNS = [
|
39
45
|
r"auth",
|
40
46
|
r"api-?key",
|
@@ -99,8 +105,8 @@ class RequestLoggingConfig:
|
|
99
105
|
log_response_body: bool = False
|
100
106
|
mask_query_params: List[str] = field(default_factory=list)
|
101
107
|
mask_headers: List[str] = field(default_factory=list)
|
102
|
-
mask_request_body_callback: Optional[Callable[[
|
103
|
-
mask_response_body_callback: Optional[Callable[[
|
108
|
+
mask_request_body_callback: Optional[Callable[[RequestDict], Optional[bytes]]] = None
|
109
|
+
mask_response_body_callback: Optional[Callable[[RequestDict, ResponseDict], Optional[bytes]]] = None
|
104
110
|
exclude_paths: List[str] = field(default_factory=list)
|
105
111
|
exclude_callback: Optional[Callable[[RequestDict, ResponseDict], bool]] = None
|
106
112
|
|
@@ -162,7 +168,12 @@ class RequestLogger:
|
|
162
168
|
if not self.enabled or self.suspend_until is not None:
|
163
169
|
return
|
164
170
|
parsed_url = urlparse(request["url"])
|
165
|
-
|
171
|
+
user_agent = self._get_user_agent(request["headers"])
|
172
|
+
if (
|
173
|
+
self._should_exclude_path(request["path"] or parsed_url.path)
|
174
|
+
or self._should_exclude_user_agent(user_agent)
|
175
|
+
or self._should_exclude(request, response)
|
176
|
+
):
|
166
177
|
return
|
167
178
|
|
168
179
|
query = self._mask_query_params(parsed_url.query) if self.config.log_query_params else ""
|
@@ -178,9 +189,7 @@ class RequestLogger:
|
|
178
189
|
and request["body"] != BODY_TOO_LARGE
|
179
190
|
):
|
180
191
|
try:
|
181
|
-
request["body"] = self.config.mask_request_body_callback(
|
182
|
-
request["method"], request["path"] or parsed_url.path, request["body"]
|
183
|
-
)
|
192
|
+
request["body"] = self.config.mask_request_body_callback(request)
|
184
193
|
except Exception: # pragma: no cover
|
185
194
|
logger.exception("User-provided mask_request_body_callback function raised an exception")
|
186
195
|
request["body"] = None
|
@@ -197,9 +206,7 @@ class RequestLogger:
|
|
197
206
|
and response["body"] != BODY_TOO_LARGE
|
198
207
|
):
|
199
208
|
try:
|
200
|
-
response["body"] = self.config.mask_response_body_callback(
|
201
|
-
request["method"], request["path"] or parsed_url.path, response["body"]
|
202
|
-
)
|
209
|
+
response["body"] = self.config.mask_response_body_callback(request, response)
|
203
210
|
except Exception: # pragma: no cover
|
204
211
|
logger.exception("User-provided mask_response_body_callback function raised an exception")
|
205
212
|
response["body"] = None
|
@@ -275,6 +282,10 @@ class RequestLogger:
|
|
275
282
|
patterns = self.config.exclude_paths + EXCLUDE_PATH_PATTERNS
|
276
283
|
return self._match_patterns(url_path, patterns)
|
277
284
|
|
285
|
+
@lru_cache(maxsize=1000)
|
286
|
+
def _should_exclude_user_agent(self, user_agent: Optional[str]) -> bool:
|
287
|
+
return self._match_patterns(user_agent, EXCLUDE_USER_AGENT_PATTERNS) if user_agent is not None else False
|
288
|
+
|
278
289
|
def _mask_query_params(self, query: str) -> str:
|
279
290
|
query_params = parse_qsl(query)
|
280
291
|
masked_query_params = [(k, v if not self._should_mask_query_param(k) else MASKED) for k, v in query_params]
|
@@ -306,6 +317,10 @@ class RequestLogger:
|
|
306
317
|
content_type = next((v for k, v in headers if k.lower() == "content-type"), None)
|
307
318
|
return content_type is not None and any(content_type.startswith(t) for t in ALLOWED_CONTENT_TYPES)
|
308
319
|
|
320
|
+
@staticmethod
|
321
|
+
def _get_user_agent(headers: List[Tuple[str, str]]) -> Optional[str]:
|
322
|
+
return next((v for k, v in headers if k.lower() == "user-agent"), None)
|
323
|
+
|
309
324
|
|
310
325
|
def _check_writable_fs() -> bool:
|
311
326
|
try:
|
apitally/django.py
CHANGED
@@ -41,6 +41,7 @@ class ApitallyMiddlewareConfig:
|
|
41
41
|
app_version: Optional[str]
|
42
42
|
identify_consumer_callback: Optional[Callable[[HttpRequest], Union[str, ApitallyConsumer, None]]]
|
43
43
|
urlconfs: List[Optional[str]]
|
44
|
+
proxy: Optional[str]
|
44
45
|
|
45
46
|
|
46
47
|
class ApitallyMiddleware:
|
@@ -71,6 +72,7 @@ class ApitallyMiddleware:
|
|
71
72
|
client_id=self.config.client_id,
|
72
73
|
env=self.config.env,
|
73
74
|
request_logging_config=self.config.request_logging_config,
|
75
|
+
proxy=self.config.proxy,
|
74
76
|
)
|
75
77
|
self.client.start_sync_loop()
|
76
78
|
self.client.set_startup_data(
|
@@ -96,6 +98,7 @@ class ApitallyMiddleware:
|
|
96
98
|
app_version: Optional[str] = None,
|
97
99
|
identify_consumer_callback: Optional[str] = None,
|
98
100
|
urlconf: Optional[Union[List[Optional[str]], str]] = None,
|
101
|
+
proxy: Optional[str] = None,
|
99
102
|
) -> None:
|
100
103
|
cls.config = ApitallyMiddlewareConfig(
|
101
104
|
client_id=client_id,
|
@@ -106,6 +109,7 @@ class ApitallyMiddleware:
|
|
106
109
|
if identify_consumer_callback
|
107
110
|
else None,
|
108
111
|
urlconfs=[urlconf] if urlconf is None or isinstance(urlconf, str) else urlconf,
|
112
|
+
proxy=proxy,
|
109
113
|
)
|
110
114
|
|
111
115
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
apitally/flask.py
CHANGED
@@ -39,11 +39,17 @@ class ApitallyMiddleware:
|
|
39
39
|
request_logging_config: Optional[RequestLoggingConfig] = None,
|
40
40
|
app_version: Optional[str] = None,
|
41
41
|
openapi_url: Optional[str] = None,
|
42
|
+
proxy: Optional[str] = None,
|
42
43
|
) -> None:
|
43
44
|
self.app = app
|
44
45
|
self.wsgi_app = app.wsgi_app
|
45
46
|
self.patch_handle_exception()
|
46
|
-
self.client = ApitallyClient(
|
47
|
+
self.client = ApitallyClient(
|
48
|
+
client_id=client_id,
|
49
|
+
env=env,
|
50
|
+
request_logging_config=request_logging_config,
|
51
|
+
proxy=proxy,
|
52
|
+
)
|
47
53
|
self.client.start_sync_loop()
|
48
54
|
self.delayed_set_startup_data(app_version, openapi_url)
|
49
55
|
|
apitally/litestar.py
CHANGED
@@ -4,6 +4,7 @@ import time
|
|
4
4
|
from typing import Callable, Dict, List, Optional, Union
|
5
5
|
from warnings import warn
|
6
6
|
|
7
|
+
from httpx import Proxy
|
7
8
|
from litestar.app import DEFAULT_OPENAPI_CONFIG, Litestar
|
8
9
|
from litestar.config.app import AppConfig
|
9
10
|
from litestar.connection import Request
|
@@ -35,8 +36,14 @@ class ApitallyPlugin(InitPluginProtocol):
|
|
35
36
|
app_version: Optional[str] = None,
|
36
37
|
filter_openapi_paths: bool = True,
|
37
38
|
identify_consumer_callback: Optional[Callable[[Request], Union[str, ApitallyConsumer, None]]] = None,
|
39
|
+
proxy: Optional[Union[str, Proxy]] = None,
|
38
40
|
) -> None:
|
39
|
-
self.client = ApitallyClient(
|
41
|
+
self.client = ApitallyClient(
|
42
|
+
client_id=client_id,
|
43
|
+
env=env,
|
44
|
+
request_logging_config=request_logging_config,
|
45
|
+
proxy=proxy,
|
46
|
+
)
|
40
47
|
self.app_version = app_version
|
41
48
|
self.filter_openapi_paths = filter_openapi_paths
|
42
49
|
self.identify_consumer_callback = identify_consumer_callback
|
apitally/starlette.py
CHANGED
@@ -7,7 +7,7 @@ import time
|
|
7
7
|
from typing import Any, Callable, Dict, List, Optional, Union
|
8
8
|
from warnings import warn
|
9
9
|
|
10
|
-
from httpx import HTTPStatusError
|
10
|
+
from httpx import HTTPStatusError, Proxy
|
11
11
|
from starlette.datastructures import Headers
|
12
12
|
from starlette.requests import Request
|
13
13
|
from starlette.routing import BaseRoute, Match, Router
|
@@ -38,10 +38,16 @@ class ApitallyMiddleware:
|
|
38
38
|
app_version: Optional[str] = None,
|
39
39
|
openapi_url: Optional[str] = "/openapi.json",
|
40
40
|
identify_consumer_callback: Optional[Callable[[Request], Union[str, ApitallyConsumer, None]]] = None,
|
41
|
+
proxy: Optional[Union[str, Proxy]] = None,
|
41
42
|
) -> None:
|
42
43
|
self.app = app
|
43
44
|
self.identify_consumer_callback = identify_consumer_callback
|
44
|
-
self.client = ApitallyClient(
|
45
|
+
self.client = ApitallyClient(
|
46
|
+
client_id=client_id,
|
47
|
+
env=env,
|
48
|
+
request_logging_config=request_logging_config,
|
49
|
+
proxy=proxy,
|
50
|
+
)
|
45
51
|
self.client.start_sync_loop()
|
46
52
|
self._delayed_set_startup_data_task: Optional[asyncio.Task] = None
|
47
53
|
self.delayed_set_startup_data(app_version, openapi_url)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: apitally
|
3
|
-
Version: 0.14.
|
3
|
+
Version: 0.14.2
|
4
4
|
Summary: Simple API monitoring & analytics for REST APIs built with FastAPI, Flask, Django, Starlette and Litestar.
|
5
5
|
Project-URL: Homepage, https://apitally.io
|
6
6
|
Project-URL: Documentation, https://docs.apitally.io
|
@@ -1,24 +1,24 @@
|
|
1
1
|
apitally/__init__.py,sha256=ShXQBVjyiSOHxoQJS2BvNG395W4KZfqMxZWBAR0MZrE,22
|
2
2
|
apitally/common.py,sha256=Y8MRuTUHFUeQkcDrCLUxnqIPRpYIiW8S43T0QUab-_A,1267
|
3
|
-
apitally/django.py,sha256=
|
3
|
+
apitally/django.py,sha256=xM8zyH8LYr4BxAlhvfpUyau4OF5i_TMcISs3eJ7xvpY,16621
|
4
4
|
apitally/django_ninja.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
|
5
5
|
apitally/django_rest_framework.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
|
6
6
|
apitally/fastapi.py,sha256=IfKfgsmIY8_AtnuMTW2sW4qnkya61CAE2vBoIpcc9tk,169
|
7
|
-
apitally/flask.py,sha256=
|
8
|
-
apitally/litestar.py,sha256=
|
7
|
+
apitally/flask.py,sha256=Th5LsMsTKkWERPrKfSWPhzrp99tg0pDtKXgtlVLx3eo,9279
|
8
|
+
apitally/litestar.py,sha256=hAH2-OVVXBDVY8LopfIGv30yYwi-71tSEsKd6648CYc,13098
|
9
9
|
apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
apitally/starlette.py,sha256=
|
10
|
+
apitally/starlette.py,sha256=8WAWQPePJAYy-B5TrxawxAeUqBiSXD15Ay17i2B22jc,12500
|
11
11
|
apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
-
apitally/client/client_asyncio.py,sha256=
|
12
|
+
apitally/client/client_asyncio.py,sha256=lRvVKFU6eiapJnB9WrRq6Nuadx-n8NCKwQg-da1vPSM,7064
|
13
13
|
apitally/client/client_base.py,sha256=w5AXAbg3hw5Qds5rovCZFtePB9bHNcJsr9l7kDgbroc,3733
|
14
|
-
apitally/client/client_threading.py,sha256=
|
14
|
+
apitally/client/client_threading.py,sha256=MbytG8EopF84nmr5ShAZaq-VviSXYnBfBl7cRFRe1Kg,7479
|
15
15
|
apitally/client/consumers.py,sha256=w_AFQhVgdtJVt7pVySBvSZwQg-2JVqmD2JQtVBoMkus,2626
|
16
16
|
apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
|
17
|
-
apitally/client/request_logging.py,sha256=
|
17
|
+
apitally/client/request_logging.py,sha256=5i7Gv4yP7iq4tBgw-ppkhlZd_OwMc719ZvWEm16TCvg,13047
|
18
18
|
apitally/client/requests.py,sha256=RdJyvIqQGVHvS-wjpAPUwcO7byOJ6jO8dYqNTU2Furg,3685
|
19
19
|
apitally/client/server_errors.py,sha256=axEhOxqV5SWjk0QCZTLVv2UMIaTfqPc81Typ4DXt66A,4646
|
20
20
|
apitally/client/validation_errors.py,sha256=6G8WYWFgJs9VH9swvkPXJGuOJgymj5ooWA9OwjUTbuM,1964
|
21
|
-
apitally-0.14.
|
22
|
-
apitally-0.14.
|
23
|
-
apitally-0.14.
|
24
|
-
apitally-0.14.
|
21
|
+
apitally-0.14.2.dist-info/METADATA,sha256=AUai6BWmcRZKC2mnN3om1SLIxPkyzPPMRAY-qdu1kus,7577
|
22
|
+
apitally-0.14.2.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
23
|
+
apitally-0.14.2.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
|
24
|
+
apitally-0.14.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|