apitally 0.8.0__py3-none-any.whl → 0.10.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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.8.0"
1
+ __version__ = "0.10.0"
apitally/client/base.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import asyncio
3
4
  import contextlib
4
5
  import os
5
6
  import re
@@ -243,6 +244,7 @@ class ServerError:
243
244
  class ServerErrorCounter:
244
245
  def __init__(self) -> None:
245
246
  self.error_counts: Counter[ServerError] = Counter()
247
+ self.sentry_event_ids: Dict[ServerError, str] = {}
246
248
  self._lock = threading.Lock()
247
249
 
248
250
  def add_server_error(self, consumer: Optional[str], method: str, path: str, exception: BaseException) -> None:
@@ -259,6 +261,36 @@ class ServerErrorCounter:
259
261
  traceback=self._get_truncated_exception_traceback(exception),
260
262
  )
261
263
  self.error_counts[server_error] += 1
264
+ self.capture_sentry_event_id(server_error)
265
+
266
+ def capture_sentry_event_id(self, server_error: ServerError) -> None:
267
+ try:
268
+ from sentry_sdk.hub import Hub
269
+ from sentry_sdk.scope import Scope
270
+ except ImportError:
271
+ return # pragma: no cover
272
+ if not hasattr(Scope, "get_isolation_scope") or not hasattr(Scope, "last_event_id"):
273
+ # sentry-sdk < 2.2.0 is not supported
274
+ return # pragma: no cover
275
+ if Hub.current.client is None:
276
+ return # sentry-sdk not initialized
277
+
278
+ scope = Scope.get_isolation_scope()
279
+ if event_id := scope.last_event_id():
280
+ self.sentry_event_ids[server_error] = event_id
281
+ return
282
+
283
+ async def _wait_for_sentry_event_id(scope: Scope) -> None:
284
+ i = 0
285
+ while not (event_id := scope.last_event_id()) and i < 100:
286
+ i += 1
287
+ await asyncio.sleep(0.001)
288
+ if event_id:
289
+ self.sentry_event_ids[server_error] = event_id
290
+
291
+ with contextlib.suppress(RuntimeError): # ignore no running loop
292
+ loop = asyncio.get_running_loop()
293
+ loop.create_task(_wait_for_sentry_event_id(scope))
262
294
 
263
295
  def get_and_reset_server_errors(self) -> List[Dict[str, Any]]:
264
296
  data: List[Dict[str, Any]] = []
@@ -272,10 +304,12 @@ class ServerErrorCounter:
272
304
  "type": server_error.type,
273
305
  "msg": server_error.msg,
274
306
  "traceback": server_error.traceback,
307
+ "sentry_event_id": self.sentry_event_ids.get(server_error),
275
308
  "error_count": count,
276
309
  }
277
310
  )
278
311
  self.error_counts.clear()
312
+ self.sentry_event_ids.clear()
279
313
  return data
280
314
 
281
315
  @staticmethod
apitally/django.py CHANGED
@@ -164,12 +164,14 @@ class ApitallyMiddleware:
164
164
 
165
165
  def get_consumer(self, request: HttpRequest) -> Optional[str]:
166
166
  try:
167
- if hasattr(request, "consumer_identifier"):
167
+ if hasattr(request, "apitally_consumer"):
168
+ return str(request.apitally_consumer)
169
+ if hasattr(request, "consumer_identifier"): # Keeping this for legacy support
168
170
  return str(request.consumer_identifier)
169
171
  if self.config is not None and self.config.identify_consumer_callback is not None:
170
- consumer_identifier = self.config.identify_consumer_callback(request)
171
- if consumer_identifier is not None:
172
- return str(consumer_identifier)
172
+ consumer = self.config.identify_consumer_callback(request)
173
+ if consumer is not None:
174
+ return str(consumer)
173
175
  except Exception: # pragma: no cover
174
176
  logger.exception("Failed to get consumer identifier for request")
175
177
  return None
apitally/flask.py CHANGED
@@ -115,7 +115,11 @@ class ApitallyMiddleware:
115
115
  return environ["PATH_INFO"], False
116
116
 
117
117
  def get_consumer(self) -> Optional[str]:
118
- return str(g.consumer_identifier) if "consumer_identifier" in g else None
118
+ if "apitally_consumer" in g:
119
+ return str(g.apitally_consumer)
120
+ if "consumer_identifier" in g: # Keeping this for legacy support
121
+ return str(g.consumer_identifier)
122
+ return None
119
123
 
120
124
 
121
125
  def _get_app_info(app: Flask, app_version: Optional[str] = None, openapi_url: Optional[str] = None) -> Dict[str, Any]:
apitally/litestar.py CHANGED
@@ -168,12 +168,14 @@ class ApitallyPlugin(InitPluginProtocol):
168
168
  return False # pragma: no cover
169
169
 
170
170
  def get_consumer(self, request: Request) -> Optional[str]:
171
- if hasattr(request.state, "consumer_identifier"):
171
+ if hasattr(request.state, "apitally_consumer"):
172
+ return str(request.state.apitally_consumer)
173
+ if hasattr(request.state, "consumer_identifier"): # Keeping this for legacy support
172
174
  return str(request.state.consumer_identifier)
173
175
  if self.identify_consumer_callback is not None:
174
- consumer_identifier = self.identify_consumer_callback(request)
175
- if consumer_identifier is not None:
176
- return str(consumer_identifier)
176
+ consumer = self.identify_consumer_callback(request)
177
+ if consumer is not None:
178
+ return str(consumer)
177
179
  return None
178
180
 
179
181
 
apitally/starlette.py CHANGED
@@ -142,12 +142,14 @@ class ApitallyMiddleware(BaseHTTPMiddleware):
142
142
  return request.url.path, False
143
143
 
144
144
  def get_consumer(self, request: Request) -> Optional[str]:
145
- if hasattr(request.state, "consumer_identifier"):
145
+ if hasattr(request.state, "apitally_consumer"):
146
+ return str(request.state.apitally_consumer)
147
+ if hasattr(request.state, "consumer_identifier"): # Keeping this for legacy support
146
148
  return str(request.state.consumer_identifier)
147
149
  if self.identify_consumer_callback is not None:
148
- consumer_identifier = self.identify_consumer_callback(request)
149
- if consumer_identifier is not None:
150
- return str(consumer_identifier)
150
+ consumer = self.identify_consumer_callback(request)
151
+ if consumer is not None:
152
+ return str(consumer)
151
153
  return None
152
154
 
153
155
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: apitally
3
- Version: 0.8.0
4
- Summary: API monitoring for REST APIs built with FastAPI, Flask, Django, Starlette and Litestar.
3
+ Version: 0.10.0
4
+ Summary: Simple API monitoring & analytics for REST APIs built with FastAPI, Flask, Django, Starlette and Litestar.
5
5
  Home-page: https://apitally.io
6
6
  License: MIT
7
7
  Author: Apitally
@@ -26,6 +26,7 @@ Provides-Extra: django-rest-framework
26
26
  Provides-Extra: fastapi
27
27
  Provides-Extra: flask
28
28
  Provides-Extra: litestar
29
+ Provides-Extra: sentry
29
30
  Provides-Extra: starlette
30
31
  Requires-Dist: backoff (>=2.0.0)
31
32
  Requires-Dist: django (>=2.2) ; extra == "django-ninja" or extra == "django-rest-framework"
@@ -37,23 +38,24 @@ Requires-Dist: httpx (>=0.22.0) ; extra == "fastapi" or extra == "litestar" or e
37
38
  Requires-Dist: inflection (>=0.5.1) ; extra == "django-rest-framework"
38
39
  Requires-Dist: litestar (>=2.0.0) ; extra == "litestar"
39
40
  Requires-Dist: requests (>=2.26.0) ; extra == "django-ninja" or extra == "django-rest-framework" or extra == "flask"
41
+ Requires-Dist: sentry-sdk (>=2.2.0) ; extra == "sentry"
40
42
  Requires-Dist: starlette (>=0.21.0,<1.0.0) ; extra == "fastapi" or extra == "starlette"
41
43
  Requires-Dist: uritemplate (>=3.0.0) ; extra == "django-rest-framework"
42
44
  Project-URL: Documentation, https://docs.apitally.io
43
- Project-URL: Repository, https://github.com/apitally/python-client
45
+ Project-URL: Repository, https://github.com/apitally/apitally-py
44
46
  Description-Content-Type: text/markdown
45
47
 
46
48
  <p align="center">
47
49
  <picture>
48
50
  <source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-vertical-dark.png">
49
51
  <source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-vertical-light.png">
50
- <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-vertical-light.png">
52
+ <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-vertical-light.png" width="150">
51
53
  </picture>
52
54
  </p>
53
55
 
54
56
  <p align="center"><b>API monitoring made easy.</b></p>
55
57
 
56
- <p align="center"><i>Apitally is a simple and affordable API monitoring tool with a focus on data privacy.<br>It is super easy to use for API projects in Python or Node.js and never collects sensitive data.</i></p>
58
+ <p align="center"><i>Apitally is a simple API monitoring & analytics tool with a focus on data privacy.<br>It is super easy to use for API projects in Python or Node.js and never collects sensitive data.</i></p>
57
59
 
58
60
  <p align="center">🔗 <b><a href="https://apitally.io" target="_blank">apitally.io</a></b></p>
59
61
 
@@ -63,8 +65,8 @@ Description-Content-Type: text/markdown
63
65
 
64
66
  # Apitally client library for Python
65
67
 
66
- [![Tests](https://github.com/apitally/python-client/actions/workflows/tests.yaml/badge.svg?event=push)](https://github.com/apitally/python-client/actions)
67
- [![Codecov](https://codecov.io/gh/apitally/python-client/graph/badge.svg?token=UNLYBY4Y3V)](https://codecov.io/gh/apitally/python-client)
68
+ [![Tests](https://github.com/apitally/apitally-py/actions/workflows/tests.yaml/badge.svg?event=push)](https://github.com/apitally/apitally-py/actions)
69
+ [![Codecov](https://codecov.io/gh/apitally/apitally-py/graph/badge.svg?token=UNLYBY4Y3V)](https://codecov.io/gh/apitally/apitally-py)
68
70
  [![PyPI](https://img.shields.io/pypi/v/apitally?logo=pypi&logoColor=white&color=%23006dad)](https://pypi.org/project/apitally/)
69
71
 
70
72
  This client library for Apitally currently supports the following Python web
@@ -0,0 +1,19 @@
1
+ apitally/__init__.py,sha256=v4zmKjsKOPZbp6BrWoz7iK4ST0sdZdUh9bQSJmluZ5o,23
2
+ apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ apitally/client/asyncio.py,sha256=uR5JlH37G6gZvAJ7A1gYOGkjn3zjC-4I6avA1fncXHs,4433
4
+ apitally/client/base.py,sha256=VWtHTkA6UT5aV37i_CXUvcfmYrabjHGBhzbDU6EeH1A,12785
5
+ apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
6
+ apitally/client/threading.py,sha256=ihQzUStrSQFynpqXgFpseAXrHuc5Et1QvG-YHlzqDr8,4831
7
+ apitally/common.py,sha256=GbVmnXxhRvV30d7CfCQ9r0AeXj14Mv9Jm_Yd1bRWP28,1088
8
+ apitally/django.py,sha256=ZsCKTUq4V3nCwIcrO8_3mPQFuPRiriqRSxQ8IPMRoOQ,12922
9
+ apitally/django_ninja.py,sha256=iMvZd7j04nbOLpJgYxs7tpbsyXlZuhmHjcswXMvyUlU,82
10
+ apitally/django_rest_framework.py,sha256=iMvZd7j04nbOLpJgYxs7tpbsyXlZuhmHjcswXMvyUlU,82
11
+ apitally/fastapi.py,sha256=Q3n2bVREKQ_V_2yCQ48ngPtr-NJxDskpT_l20xhSbpM,85
12
+ apitally/flask.py,sha256=iD2mbFqrWoEFFmDNXUtR32OYwlHob4I3tJT90-kwcnw,5691
13
+ apitally/litestar.py,sha256=fgpdIlOpXS7TYFM02GUXk78H8WuJgDWMePPsQf-Pw1I,8092
14
+ apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ apitally/starlette.py,sha256=e0PoLTrihHioLGDF1TThjlsiyYtusa6Or6GKCX7Y9NQ,7881
16
+ apitally-0.10.0.dist-info/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
17
+ apitally-0.10.0.dist-info/METADATA,sha256=qS7p_ikNFk2H474nD7yI6KdZ5jPM3IuC7EQ5F9Nrbas,6834
18
+ apitally-0.10.0.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
19
+ apitally-0.10.0.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- apitally/__init__.py,sha256=iPlYCcIzuzW7T2HKDkmYlMkRI51dBLfNRxPPiWrfw9U,22
2
- apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- apitally/client/asyncio.py,sha256=uR5JlH37G6gZvAJ7A1gYOGkjn3zjC-4I6avA1fncXHs,4433
4
- apitally/client/base.py,sha256=-2Bw_zFq6de-mSQSYsTa0NSRoMcHwiZqpSCQnjjIZ2M,11351
5
- apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
6
- apitally/client/threading.py,sha256=ihQzUStrSQFynpqXgFpseAXrHuc5Et1QvG-YHlzqDr8,4831
7
- apitally/common.py,sha256=GbVmnXxhRvV30d7CfCQ9r0AeXj14Mv9Jm_Yd1bRWP28,1088
8
- apitally/django.py,sha256=Ym590Tiz1Qnk-IgFcna8r-XkSgcln2riziSPZCwD9nw,12812
9
- apitally/django_ninja.py,sha256=iMvZd7j04nbOLpJgYxs7tpbsyXlZuhmHjcswXMvyUlU,82
10
- apitally/django_rest_framework.py,sha256=iMvZd7j04nbOLpJgYxs7tpbsyXlZuhmHjcswXMvyUlU,82
11
- apitally/fastapi.py,sha256=Q3n2bVREKQ_V_2yCQ48ngPtr-NJxDskpT_l20xhSbpM,85
12
- apitally/flask.py,sha256=Utn92aXXl_1f4bKvuf4iZDB4v1vVLpeW5p1tF57Kf-8,5552
13
- apitally/litestar.py,sha256=1-skfFDKjYa7y6mOdNvjR4YGtsQbNA0iGQP1jyre41Q,7978
14
- apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- apitally/starlette.py,sha256=kyXgzw0L90GUOZCgjuwM3q0uOpPX-hD0TQb2Wgqbqb8,7767
16
- apitally-0.8.0.dist-info/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
17
- apitally-0.8.0.dist-info/METADATA,sha256=jl6_hdLWMu-CStH70iJ-4-Lt94328tX_X33avx-PIqI,6736
18
- apitally-0.8.0.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
19
- apitally-0.8.0.dist-info/RECORD,,