apitally 0.18.2__py3-none-any.whl → 0.19.1__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.
@@ -5,6 +5,7 @@ import tempfile
5
5
  import threading
6
6
  import time
7
7
  from collections import deque
8
+ from contextlib import suppress
8
9
  from dataclasses import dataclass, field
9
10
  from functools import lru_cache
10
11
  from io import BufferedReader
@@ -22,6 +23,12 @@ from apitally.client.server_errors import (
22
23
  )
23
24
 
24
25
 
26
+ try:
27
+ from typing import NotRequired
28
+ except ImportError:
29
+ from typing_extensions import NotRequired
30
+
31
+
25
32
  logger = get_logger(__name__)
26
33
 
27
34
  MAX_BODY_SIZE = 50_000 # 50 KB (uncompressed)
@@ -31,7 +38,12 @@ MAX_FILES_IN_DEQUE = 50
31
38
  BODY_TOO_LARGE = b"<body too large>"
32
39
  BODY_MASKED = b"<masked>"
33
40
  MASKED = "******"
34
- ALLOWED_CONTENT_TYPES = ["application/json", "text/plain"]
41
+ ALLOWED_CONTENT_TYPES = [
42
+ "application/json",
43
+ "application/problem+json",
44
+ "application/vnd.api+json",
45
+ "text/plain",
46
+ ]
35
47
  EXCLUDE_PATH_PATTERNS = [
36
48
  r"/_?healthz?$",
37
49
  r"/_?health[\-_]?checks?$",
@@ -61,6 +73,16 @@ MASK_HEADER_PATTERNS = [
61
73
  r"token",
62
74
  r"cookie",
63
75
  ]
76
+ MASK_BODY_FIELD_PATTERNS = [
77
+ r"password",
78
+ r"pwd",
79
+ r"token",
80
+ r"secret",
81
+ r"auth",
82
+ r"card[\-_ ]number",
83
+ r"ccv",
84
+ r"ssn",
85
+ ]
64
86
 
65
87
 
66
88
  class RequestDict(TypedDict):
@@ -82,6 +104,20 @@ class ResponseDict(TypedDict):
82
104
  body: Optional[bytes]
83
105
 
84
106
 
107
+ class ExceptionDict(TypedDict):
108
+ type: str
109
+ message: str
110
+ traceback: str
111
+ sentry_event_id: NotRequired[str]
112
+
113
+
114
+ class RequestLogItem(TypedDict):
115
+ uuid: str
116
+ request: RequestDict
117
+ response: ResponseDict
118
+ exception: NotRequired[ExceptionDict]
119
+
120
+
85
121
  @dataclass
86
122
  class RequestLoggingConfig:
87
123
  """
@@ -97,6 +133,7 @@ class RequestLoggingConfig:
97
133
  log_exception: Whether to log unhandled exceptions in case of server errors
98
134
  mask_query_params: Query parameter names to mask in logs. Expects regular expressions.
99
135
  mask_headers: Header names to mask in logs. Expects regular expressions.
136
+ mask_body_fields: Body fields to mask in logs. Expects regular expressions.
100
137
  mask_request_body_callback: Callback to mask the request body. Expects `request: RequestDict` as argument and returns the masked body as bytes or None.
101
138
  mask_response_body_callback: Callback to mask the response body. Expects `request: RequestDict` and `response: ResponseDict` as arguments and returns the masked body as bytes or None.
102
139
  exclude_paths: Paths to exclude from logging. Expects regular expressions.
@@ -112,6 +149,7 @@ class RequestLoggingConfig:
112
149
  log_exception: bool = True
113
150
  mask_query_params: List[str] = field(default_factory=list)
114
151
  mask_headers: List[str] = field(default_factory=list)
152
+ mask_body_fields: List[str] = field(default_factory=list)
115
153
  mask_request_body_callback: Optional[Callable[[RequestDict], Optional[bytes]]] = None
116
154
  mask_response_body_callback: Optional[Callable[[RequestDict, ResponseDict], Optional[bytes]]] = None
117
155
  exclude_paths: List[str] = field(default_factory=list)
@@ -161,7 +199,8 @@ class RequestLogger:
161
199
  self.config = config or RequestLoggingConfig()
162
200
  self.enabled = self.config.enabled and _check_writable_fs()
163
201
  self.serialize = _get_json_serializer()
164
- self.write_deque: deque[Dict[str, Any]] = deque([], MAX_REQUESTS_IN_DEQUE)
202
+ self.deserialize = _get_json_deserializer()
203
+ self.write_deque: deque[RequestLogItem] = deque([], MAX_REQUESTS_IN_DEQUE)
165
204
  self.file_deque: deque[TempGzipFile] = deque([])
166
205
  self.file: Optional[TempGzipFile] = None
167
206
  self.lock = threading.Lock()
@@ -172,10 +211,14 @@ class RequestLogger:
172
211
  return self.file.size if self.file is not None else 0
173
212
 
174
213
  def log_request(
175
- self, request: RequestDict, response: ResponseDict, exception: Optional[BaseException] = None
214
+ self,
215
+ request: RequestDict,
216
+ response: ResponseDict,
217
+ exception: Optional[BaseException] = None,
176
218
  ) -> None:
177
219
  if not self.enabled or self.suspend_until is not None:
178
220
  return
221
+
179
222
  parsed_url = urlparse(request["url"])
180
223
  user_agent = self._get_user_agent(request["headers"])
181
224
  if (
@@ -185,55 +228,20 @@ class RequestLogger:
185
228
  ):
186
229
  return
187
230
 
188
- query = self._mask_query_params(parsed_url.query) if self.config.log_query_params else ""
189
- request["url"] = urlunparse(parsed_url._replace(query=query))
190
-
191
231
  if not self.config.log_request_body or not self._has_supported_content_type(request["headers"]):
192
232
  request["body"] = None
193
- elif (
194
- self.config.mask_request_body_callback is not None
195
- and request["body"] is not None
196
- and request["body"] != BODY_TOO_LARGE
197
- ):
198
- try:
199
- request["body"] = self.config.mask_request_body_callback(request)
200
- except Exception: # pragma: no cover
201
- logger.exception("User-provided mask_request_body_callback function raised an exception")
202
- request["body"] = None
203
- if request["body"] is None:
204
- request["body"] = BODY_MASKED
205
- if request["body"] is not None and len(request["body"]) > MAX_BODY_SIZE:
206
- request["body"] = BODY_TOO_LARGE
207
-
208
233
  if not self.config.log_response_body or not self._has_supported_content_type(response["headers"]):
209
234
  response["body"] = None
210
- elif (
211
- self.config.mask_response_body_callback is not None
212
- and response["body"] is not None
213
- and response["body"] != BODY_TOO_LARGE
214
- ):
215
- try:
216
- response["body"] = self.config.mask_response_body_callback(request, response)
217
- except Exception: # pragma: no cover
218
- logger.exception("User-provided mask_response_body_callback function raised an exception")
219
- response["body"] = None
220
- if response["body"] is None:
221
- response["body"] = BODY_MASKED
222
- if response["body"] is not None and len(response["body"]) > MAX_BODY_SIZE:
223
- response["body"] = BODY_TOO_LARGE
224
-
225
- request["headers"] = self._mask_headers(request["headers"]) if self.config.log_request_headers else []
226
- response["headers"] = self._mask_headers(response["headers"]) if self.config.log_response_headers else []
227
235
 
228
236
  if request["size"] is not None and request["size"] < 0:
229
237
  request["size"] = None
230
238
  if response["size"] is not None and response["size"] < 0:
231
239
  response["size"] = None
232
240
 
233
- item: Dict[str, Any] = {
241
+ item: RequestLogItem = {
234
242
  "uuid": str(uuid4()),
235
- "request": _skip_empty_values(request),
236
- "response": _skip_empty_values(response),
243
+ "request": request,
244
+ "response": response,
237
245
  }
238
246
  if exception is not None and self.config.log_exception:
239
247
  item["exception"] = {
@@ -242,6 +250,7 @@ class RequestLogger:
242
250
  "traceback": get_truncated_exception_traceback(exception),
243
251
  }
244
252
  get_sentry_event_id_async(lambda event_id: item["exception"].update({"sentry_event_id": event_id}))
253
+
245
254
  self.write_deque.append(item)
246
255
 
247
256
  def write_to_file(self) -> None:
@@ -253,6 +262,9 @@ class RequestLogger:
253
262
  while True:
254
263
  try:
255
264
  item = self.write_deque.popleft()
265
+ item = self._apply_masking(item)
266
+ item["request"] = _skip_empty_values(item["request"]) # type: ignore[typeddict-item]
267
+ item["response"] = _skip_empty_values(item["response"]) # type: ignore[typeddict-item]
256
268
  self.file.write_line(self.serialize(item))
257
269
  except IndexError:
258
270
  break
@@ -307,6 +319,67 @@ class RequestLogger:
307
319
  def _should_exclude_user_agent(self, user_agent: Optional[str]) -> bool:
308
320
  return self._match_patterns(user_agent, EXCLUDE_USER_AGENT_PATTERNS) if user_agent is not None else False
309
321
 
322
+ def _apply_masking(self, data: RequestLogItem) -> RequestLogItem:
323
+ # Apply user-provided mask_request_body_callback function
324
+ if (
325
+ self.config.mask_request_body_callback is not None
326
+ and data["request"]["body"] is not None
327
+ and data["request"]["body"] != BODY_TOO_LARGE
328
+ ):
329
+ try:
330
+ data["request"]["body"] = self.config.mask_request_body_callback(data["request"])
331
+ except Exception: # pragma: no cover
332
+ logger.exception("User-provided mask_request_body_callback function raised an exception")
333
+ data["request"]["body"] = None
334
+ if data["request"]["body"] is None:
335
+ data["request"]["body"] = BODY_MASKED
336
+
337
+ # Apply user-provided mask_response_body_callback function
338
+ if (
339
+ self.config.mask_response_body_callback is not None
340
+ and data["response"]["body"] is not None
341
+ and data["response"]["body"] != BODY_TOO_LARGE
342
+ ):
343
+ try:
344
+ data["response"]["body"] = self.config.mask_response_body_callback(data["request"], data["response"])
345
+ except Exception: # pragma: no cover
346
+ logger.exception("User-provided mask_response_body_callback function raised an exception")
347
+ data["response"]["body"] = None
348
+ if data["response"]["body"] is None:
349
+ data["response"]["body"] = BODY_MASKED
350
+
351
+ # Check request and response body sizes
352
+ if data["request"]["body"] is not None and len(data["request"]["body"]) > MAX_BODY_SIZE:
353
+ data["request"]["body"] = BODY_TOO_LARGE
354
+ if data["response"]["body"] is not None and len(data["response"]["body"]) > MAX_BODY_SIZE:
355
+ data["response"]["body"] = BODY_TOO_LARGE
356
+
357
+ # Mask request and response body fields
358
+ for key in ("request", "response"):
359
+ if data[key]["body"] is None or data[key]["body"] == BODY_TOO_LARGE or data[key]["body"] == BODY_MASKED:
360
+ continue
361
+ body = data[key]["body"]
362
+ body_is_json = self._has_json_content_type(data[key]["headers"])
363
+ if body is not None and (body_is_json is None or body_is_json):
364
+ with suppress(Exception):
365
+ masked_body = self._mask_body(self.deserialize(body))
366
+ data[key]["body"] = self.serialize(masked_body)
367
+
368
+ # Mask request and response headers
369
+ data["request"]["headers"] = (
370
+ self._mask_headers(data["request"]["headers"]) if self.config.log_request_headers else []
371
+ )
372
+ data["response"]["headers"] = (
373
+ self._mask_headers(data["response"]["headers"]) if self.config.log_response_headers else []
374
+ )
375
+
376
+ # Mask query params
377
+ parsed_url = urlparse(data["request"]["url"])
378
+ query = self._mask_query_params(parsed_url.query) if self.config.log_query_params else ""
379
+ data["request"]["url"] = urlunparse(parsed_url._replace(query=query))
380
+
381
+ return data
382
+
310
383
  def _mask_query_params(self, query: str) -> str:
311
384
  query_params = parse_qsl(query)
312
385
  masked_query_params = [(k, v if not self._should_mask_query_param(k) else MASKED) for k, v in query_params]
@@ -315,6 +388,16 @@ class RequestLogger:
315
388
  def _mask_headers(self, headers: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
316
389
  return [(k, v if not self._should_mask_header(k) else MASKED) for k, v in headers]
317
390
 
391
+ def _mask_body(self, data: Any) -> Any:
392
+ if isinstance(data, dict):
393
+ return {
394
+ k: (MASKED if isinstance(v, str) and self._should_mask_body_field(k) else self._mask_body(v))
395
+ for k, v in data.items()
396
+ }
397
+ if isinstance(data, list):
398
+ return [self._mask_body(item) for item in data]
399
+ return data
400
+
318
401
  @lru_cache(maxsize=100)
319
402
  def _should_mask_query_param(self, query_param_name: str) -> bool:
320
403
  patterns = self.config.mask_query_params + MASK_QUERY_PARAM_PATTERNS
@@ -325,6 +408,11 @@ class RequestLogger:
325
408
  patterns = self.config.mask_headers + MASK_HEADER_PATTERNS
326
409
  return self._match_patterns(header_name, patterns)
327
410
 
411
+ @lru_cache(maxsize=100)
412
+ def _should_mask_body_field(self, field_name: str) -> bool:
413
+ patterns = self.config.mask_body_fields + MASK_BODY_FIELD_PATTERNS
414
+ return self._match_patterns(field_name, patterns)
415
+
328
416
  @staticmethod
329
417
  def _match_patterns(value: str, patterns: List[str]) -> bool:
330
418
  for pattern in patterns:
@@ -338,12 +426,17 @@ class RequestLogger:
338
426
 
339
427
  @staticmethod
340
428
  def _has_supported_content_type(headers: List[Tuple[str, str]]) -> bool:
341
- content_type = next((v for k, v in headers if k.lower() == "content-type"), None)
429
+ content_type = next((v.lower() for k, v in headers if k.lower() == "content-type"), None)
342
430
  return RequestLogger.is_supported_content_type(content_type)
343
431
 
432
+ @staticmethod
433
+ def _has_json_content_type(headers: List[Tuple[str, str]]) -> Optional[bool]:
434
+ content_type = next((v.lower() for k, v in headers if k.lower() == "content-type"), None)
435
+ return None if content_type is None else (re.search(r"\bjson\b", content_type) is not None)
436
+
344
437
  @staticmethod
345
438
  def is_supported_content_type(content_type: Optional[str]) -> bool:
346
- return content_type is not None and any(content_type.startswith(t) for t in ALLOWED_CONTENT_TYPES)
439
+ return content_type is not None and any(content_type.lower().startswith(t) for t in ALLOWED_CONTENT_TYPES)
347
440
 
348
441
 
349
442
  def _check_writable_fs() -> bool:
@@ -377,6 +470,17 @@ def _get_json_serializer() -> Callable[[Any], bytes]:
377
470
  return json_dumps
378
471
 
379
472
 
473
+ def _get_json_deserializer() -> Callable[[bytes], Any]:
474
+ try:
475
+ import orjson # type: ignore
476
+
477
+ return orjson.loads
478
+ except ImportError:
479
+ import json
480
+
481
+ return json.loads
482
+
483
+
380
484
  def _skip_empty_values(data: Mapping) -> Dict:
381
485
  return {
382
486
  k: v for k, v in data.items() if v is not None and not (isinstance(v, (list, dict, bytes, str)) and len(v) == 0)
apitally/client/sentry.py CHANGED
@@ -6,17 +6,22 @@ from typing import Callable, Set
6
6
  _tasks: Set[asyncio.Task] = set()
7
7
 
8
8
 
9
- def get_sentry_event_id_async(cb: Callable[[str], None]) -> None:
9
+ def get_sentry_event_id_async(cb: Callable[[str], None], raise_on_error: bool = False) -> None:
10
10
  try:
11
- from sentry_sdk.hub import Hub
11
+ import sentry_sdk
12
12
  from sentry_sdk.scope import Scope
13
13
  except ImportError:
14
+ if raise_on_error:
15
+ raise
14
16
  return # pragma: no cover
15
17
  if not hasattr(Scope, "get_isolation_scope") or not hasattr(Scope, "_last_event_id"):
16
- # sentry-sdk < 2.2.0 is not supported
18
+ if raise_on_error:
19
+ raise RuntimeError("sentry-sdk < 2.2.0 is not supported")
17
20
  return # pragma: no cover
18
- if Hub.current.client is None:
19
- return # sentry-sdk not initialized
21
+ if not sentry_sdk.is_initialized():
22
+ if raise_on_error:
23
+ raise RuntimeError("sentry-sdk not initialized")
24
+ return
20
25
 
21
26
  scope = Scope.get_isolation_scope()
22
27
  if event_id := scope._last_event_id:
apitally/fastapi.py CHANGED
@@ -1,4 +1,4 @@
1
- from apitally.starlette import ApitallyConsumer, ApitallyMiddleware, RequestLoggingConfig
1
+ from apitally.starlette import ApitallyConsumer, ApitallyMiddleware, RequestLoggingConfig, set_consumer
2
2
 
3
3
 
4
- __all__ = ["ApitallyMiddleware", "ApitallyConsumer", "RequestLoggingConfig"]
4
+ __all__ = ["ApitallyMiddleware", "ApitallyConsumer", "RequestLoggingConfig", "set_consumer"]
apitally/flask.py CHANGED
@@ -28,7 +28,7 @@ if TYPE_CHECKING:
28
28
  from werkzeug.routing.map import Map
29
29
 
30
30
 
31
- __all__ = ["ApitallyMiddleware", "ApitallyConsumer", "RequestLoggingConfig"]
31
+ __all__ = ["ApitallyMiddleware", "ApitallyConsumer", "RequestLoggingConfig", "set_consumer"]
32
32
 
33
33
 
34
34
  class ApitallyMiddleware:
@@ -216,6 +216,10 @@ class ApitallyMiddleware:
216
216
  return None
217
217
 
218
218
 
219
+ def set_consumer(identifier: str, name: Optional[str] = None, group: Optional[str] = None) -> None:
220
+ g.apitally_consumer = ApitallyConsumer(identifier, name=name, group=group)
221
+
222
+
219
223
  def _get_startup_data(
220
224
  app: Flask, app_version: Optional[str] = None, openapi_url: Optional[str] = None
221
225
  ) -> Dict[str, Any]:
apitally/litestar.py CHANGED
@@ -24,7 +24,7 @@ from apitally.client.request_logging import (
24
24
  from apitally.common import get_versions, parse_int, try_json_loads
25
25
 
26
26
 
27
- __all__ = ["ApitallyPlugin", "ApitallyConsumer", "RequestLoggingConfig"]
27
+ __all__ = ["ApitallyPlugin", "ApitallyConsumer", "RequestLoggingConfig", "set_consumer"]
28
28
 
29
29
 
30
30
  class ApitallyPlugin(InitPluginProtocol):
@@ -287,6 +287,10 @@ class ApitallyPlugin(InitPluginProtocol):
287
287
  return None
288
288
 
289
289
 
290
+ def set_consumer(request: Request, identifier: str, name: Optional[str] = None, group: Optional[str] = None) -> None:
291
+ request.state.apitally_consumer = ApitallyConsumer(identifier, name=name, group=group)
292
+
293
+
290
294
  def _get_openapi(app: Litestar) -> str:
291
295
  schema = app.openapi_schema.to_schema()
292
296
  return json.dumps(schema)
apitally/starlette.py CHANGED
@@ -25,7 +25,7 @@ from apitally.client.request_logging import (
25
25
  from apitally.common import get_versions, parse_int, try_json_loads
26
26
 
27
27
 
28
- __all__ = ["ApitallyMiddleware", "ApitallyConsumer", "RequestLoggingConfig"]
28
+ __all__ = ["ApitallyMiddleware", "ApitallyConsumer", "RequestLoggingConfig", "set_consumer"]
29
29
 
30
30
 
31
31
  class ApitallyMiddleware:
@@ -268,6 +268,10 @@ class ApitallyMiddleware:
268
268
  return None
269
269
 
270
270
 
271
+ def set_consumer(request: Request, identifier: str, name: Optional[str] = None, group: Optional[str] = None) -> None:
272
+ request.state.apitally_consumer = ApitallyConsumer(identifier, name=name, group=group)
273
+
274
+
271
275
  def _get_startup_data(
272
276
  app: ASGIApp, app_version: Optional[str] = None, openapi_url: Optional[str] = None
273
277
  ) -> Dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apitally
3
- Version: 0.18.2
3
+ Version: 0.19.1
4
4
  Summary: Simple API monitoring & analytics for REST APIs built with FastAPI, Flask, Django, Starlette, Litestar and BlackSheep.
5
5
  Project-URL: Homepage, https://apitally.io
6
6
  Project-URL: Documentation, https://docs.apitally.io
@@ -32,6 +32,7 @@ Classifier: Topic :: System :: Monitoring
32
32
  Classifier: Typing :: Typed
33
33
  Requires-Python: <4.0,>=3.9
34
34
  Requires-Dist: backoff>=2.0.0
35
+ Requires-Dist: typing-extensions>=4.0.0; python_version < '3.11'
35
36
  Provides-Extra: blacksheep
36
37
  Requires-Dist: blacksheep>=2; extra == 'blacksheep'
37
38
  Requires-Dist: httpx>=0.22.0; extra == 'blacksheep'
@@ -73,8 +74,8 @@ Description-Content-Type: text/markdown
73
74
  </picture>
74
75
  </a>
75
76
  </p>
76
- <p align="center"><b>Simple, privacy-focused API monitoring & analytics</b></p>
77
- <p align="center" style="color: #ccc;">Apitally gives you the visibility you need to build better APIs with just a few lines of code.</p>
77
+ <p align="center"><b>API monitoring & analytics made simple</b></p>
78
+ <p align="center" style="color: #ccc;">Real-time metrics, request logs, and alerts for your APIs with just a few lines of code.</p>
78
79
  <br>
79
80
  <img alt="Apitally screenshots" src="https://assets.apitally.io/screenshots/overview.png">
80
81
  <br>
@@ -4,23 +4,23 @@ apitally/common.py,sha256=azDxepViH0QW0MuufTHxeSQyLGzCkocAX_KPziWTx8A,1605
4
4
  apitally/django.py,sha256=7eSh1tzuu0wRl9PxJgXMBSz9DfyohCDIqd4ecQTOJ1M,18499
5
5
  apitally/django_ninja.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
6
6
  apitally/django_rest_framework.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
7
- apitally/fastapi.py,sha256=IfKfgsmIY8_AtnuMTW2sW4qnkya61CAE2vBoIpcc9tk,169
8
- apitally/flask.py,sha256=OoCEnjtnD51GUGq-adK80ebuiLj-5HXubxffCv5XTCM,9622
9
- apitally/litestar.py,sha256=mHoMqBO_gyoopeHljY8e8GTcV29UDf3uhQMxY3GeNpA,13451
7
+ apitally/fastapi.py,sha256=813dGFl2eOSSYOYwuubFQYvpRikV3shu7bLR2TlwUC8,199
8
+ apitally/flask.py,sha256=sqcsVmbfqpVSN409xDF8DeGOSNkWHI4SoyTFxNfWJ4w,9819
9
+ apitally/litestar.py,sha256=fmcWNmvGvrjGG6J_q5lqB_qJpTk6PXFK9jam6bTWU8U,13678
10
10
  apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- apitally/starlette.py,sha256=Ep7n_yAqoleFpuLk43kMONiFJVRsFsu-G2_TIjfiCHQ,13878
11
+ apitally/starlette.py,sha256=PnEwplG7i0qJpfEWKdJxGCIioLFp4kQfa-5Y6NWGSSg,14105
12
12
  apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  apitally/client/client_asyncio.py,sha256=rTsH5wlLHK3RmyIuEiT6vzjquU-l2OPC34JnC2U6uYw,6658
14
14
  apitally/client/client_base.py,sha256=DvivGeHd3dyOASRvkIo44Zh8RzdBMfH8_rROa2lFbgw,3799
15
15
  apitally/client/client_threading.py,sha256=sxMRcxRgk1SxJjSq-qpIcDVmD3Q7Kv4CVT5zEUVt0KM,7257
16
16
  apitally/client/consumers.py,sha256=w_AFQhVgdtJVt7pVySBvSZwQg-2JVqmD2JQtVBoMkus,2626
17
17
  apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
18
- apitally/client/request_logging.py,sha256=FIJfbc4fYZnBIKebpJtrk19DcAxJylltghY7RFitWsE,14173
18
+ apitally/client/request_logging.py,sha256=yCF_CH8isp3gpQRVgakk6DNV60mtPZWG6z45pUNACOM,17644
19
19
  apitally/client/requests.py,sha256=SDptGOg9XvaEKFj2o3oxJz-JAuZzUrqpHnbOQixf99o,3794
20
- apitally/client/sentry.py,sha256=qMjHdI0V7c50ruo1WjmjWc8g6oGDv724vSCvcuZ8G9k,1188
20
+ apitally/client/sentry.py,sha256=dXW2zf-wexYSp4CJBsFFRKz9jWkPBy0ftqIk0o5Hkq8,1364
21
21
  apitally/client/server_errors.py,sha256=4B2BKDFoIpoWc55UVH6AIdYSgzj6zxCdMNUW77JjhZw,3423
22
22
  apitally/client/validation_errors.py,sha256=6G8WYWFgJs9VH9swvkPXJGuOJgymj5ooWA9OwjUTbuM,1964
23
- apitally-0.18.2.dist-info/METADATA,sha256=J2Mr1Y-82i7_Batr5bwlTCEhMzcnirozN83VNZgIJ4U,9269
24
- apitally-0.18.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- apitally-0.18.2.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
26
- apitally-0.18.2.dist-info/RECORD,,
23
+ apitally-0.19.1.dist-info/METADATA,sha256=QE1kA2cHRaEPmxm3feYkfbeowkZMGnuAu5plkvfXDz0,9316
24
+ apitally-0.19.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ apitally-0.19.1.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
26
+ apitally-0.19.1.dist-info/RECORD,,