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.
@@ -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__(self, client_id: str, env: str, request_logging_config: Optional[RequestLoggingConfig] = None) -> None:
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
- return httpx.AsyncClient(base_url=self.hub_url, timeout=REQUEST_TIMEOUT)
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__(self, client_id: str, env: str, request_logging_config: Optional[RequestLoggingConfig] = None) -> None:
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(url=f"{self.hub_url}/startup", json=data, timeout=REQUEST_TIMEOUT)
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(url=f"{self.hub_url}/sync", json=data, timeout=REQUEST_TIMEOUT)
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(url=f"{self.hub_url}/log?uuid={uuid}", data=fp, timeout=REQUEST_TIMEOUT)
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[[str, str, bytes], Optional[bytes]]] = None
103
- mask_response_body_callback: Optional[Callable[[str, str, bytes], Optional[bytes]]] = None
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
- if self._should_exclude_path(request["path"] or parsed_url.path) or self._should_exclude(request, response):
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(client_id=client_id, env=env, request_logging_config=request_logging_config)
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(client_id=client_id, env=env, request_logging_config=request_logging_config)
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(client_id=client_id, env=env, request_logging_config=request_logging_config)
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.0rc1
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=2Wg89-NpHGm1Yc25DjtDRVdvW8H3ugSslhxPOQKiXXY,16497
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=Q-3_nrCPkinZ8QERVoa_jiZaEmZoY43oIdj5UGx2tk4,9170
8
- apitally/litestar.py,sha256=tEaoqRJJNtbijjyrI3ue82ePhZBUk6A2yuXIjlN5oMQ,12951
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=ooXr9StS8gU3y8mfyrscUdMiPIBfYMxX5Y6LfIMYQgA,12370
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=cCCtA8Lf-FOfXCstVZEAT3_0bnwH_dU1xe0YzhfLUYU,6691
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=lLtr89LXrD2roDeUORJkx7QeQOPQqt162Vo8bSExPh4,7101
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=Ar7bqeJbQ8gMbIt3j-wWRBH0E3OhWF0hYwiLyrBJdU0,12548
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.0rc1.dist-info/METADATA,sha256=9LAy2ffwdz6zicCpyqYKct_zso_lCtZokjY0USYMc-E,7580
22
- apitally-0.14.0rc1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
23
- apitally-0.14.0rc1.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
24
- apitally-0.14.0rc1.dist-info/RECORD,,
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,,