apitally 0.16.1__tar.gz → 0.16.2__tar.gz

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.
Files changed (53) hide show
  1. {apitally-0.16.1 → apitally-0.16.2}/.github/workflows/tests.yaml +0 -7
  2. {apitally-0.16.1 → apitally-0.16.2}/PKG-INFO +1 -1
  3. {apitally-0.16.1 → apitally-0.16.2}/apitally/client/client_asyncio.py +1 -7
  4. {apitally-0.16.1 → apitally-0.16.2}/apitally/client/client_threading.py +1 -3
  5. {apitally-0.16.1 → apitally-0.16.2}/apitally/starlette.py +10 -17
  6. {apitally-0.16.1 → apitally-0.16.2}/tests/test_client_asyncio.py +3 -2
  7. {apitally-0.16.1 → apitally-0.16.2}/tests/test_client_threading.py +3 -1
  8. {apitally-0.16.1 → apitally-0.16.2}/tests/test_fastapi.py +0 -2
  9. {apitally-0.16.1 → apitally-0.16.2}/tests/test_litestar.py +0 -1
  10. {apitally-0.16.1 → apitally-0.16.2}/tests/test_starlette.py +0 -2
  11. {apitally-0.16.1 → apitally-0.16.2}/.github/workflows/publish.yaml +0 -0
  12. {apitally-0.16.1 → apitally-0.16.2}/.github/workflows/summary.yaml +0 -0
  13. {apitally-0.16.1 → apitally-0.16.2}/.gitignore +0 -0
  14. {apitally-0.16.1 → apitally-0.16.2}/.pre-commit-config.yaml +0 -0
  15. {apitally-0.16.1 → apitally-0.16.2}/LICENSE +0 -0
  16. {apitally-0.16.1 → apitally-0.16.2}/Makefile +0 -0
  17. {apitally-0.16.1 → apitally-0.16.2}/README.md +0 -0
  18. {apitally-0.16.1 → apitally-0.16.2}/apitally/__init__.py +0 -0
  19. {apitally-0.16.1 → apitally-0.16.2}/apitally/blacksheep.py +0 -0
  20. {apitally-0.16.1 → apitally-0.16.2}/apitally/client/__init__.py +0 -0
  21. {apitally-0.16.1 → apitally-0.16.2}/apitally/client/client_base.py +0 -0
  22. {apitally-0.16.1 → apitally-0.16.2}/apitally/client/consumers.py +0 -0
  23. {apitally-0.16.1 → apitally-0.16.2}/apitally/client/logging.py +0 -0
  24. {apitally-0.16.1 → apitally-0.16.2}/apitally/client/request_logging.py +0 -0
  25. {apitally-0.16.1 → apitally-0.16.2}/apitally/client/requests.py +0 -0
  26. {apitally-0.16.1 → apitally-0.16.2}/apitally/client/sentry.py +0 -0
  27. {apitally-0.16.1 → apitally-0.16.2}/apitally/client/server_errors.py +0 -0
  28. {apitally-0.16.1 → apitally-0.16.2}/apitally/client/validation_errors.py +0 -0
  29. {apitally-0.16.1 → apitally-0.16.2}/apitally/common.py +0 -0
  30. {apitally-0.16.1 → apitally-0.16.2}/apitally/django.py +1 -1
  31. {apitally-0.16.1 → apitally-0.16.2}/apitally/django_ninja.py +0 -0
  32. {apitally-0.16.1 → apitally-0.16.2}/apitally/django_rest_framework.py +0 -0
  33. {apitally-0.16.1 → apitally-0.16.2}/apitally/fastapi.py +0 -0
  34. {apitally-0.16.1 → apitally-0.16.2}/apitally/flask.py +1 -1
  35. {apitally-0.16.1 → apitally-0.16.2}/apitally/litestar.py +0 -0
  36. {apitally-0.16.1 → apitally-0.16.2}/apitally/py.typed +0 -0
  37. {apitally-0.16.1 → apitally-0.16.2}/pyproject.toml +0 -0
  38. {apitally-0.16.1 → apitally-0.16.2}/renovate.json +0 -0
  39. {apitally-0.16.1 → apitally-0.16.2}/tests/__init__.py +0 -0
  40. {apitally-0.16.1 → apitally-0.16.2}/tests/conftest.py +0 -0
  41. {apitally-0.16.1 → apitally-0.16.2}/tests/constants.py +0 -0
  42. {apitally-0.16.1 → apitally-0.16.2}/tests/django_ninja_urls.py +0 -0
  43. {apitally-0.16.1 → apitally-0.16.2}/tests/django_rest_framework_urls.py +0 -0
  44. {apitally-0.16.1 → apitally-0.16.2}/tests/test_blacksheep.py +0 -0
  45. {apitally-0.16.1 → apitally-0.16.2}/tests/test_client_consumers.py +0 -0
  46. {apitally-0.16.1 → apitally-0.16.2}/tests/test_client_request_logging.py +0 -0
  47. {apitally-0.16.1 → apitally-0.16.2}/tests/test_client_requests.py +0 -0
  48. {apitally-0.16.1 → apitally-0.16.2}/tests/test_client_server_errors.py +0 -0
  49. {apitally-0.16.1 → apitally-0.16.2}/tests/test_client_validation_errors.py +0 -0
  50. {apitally-0.16.1 → apitally-0.16.2}/tests/test_django_ninja.py +0 -0
  51. {apitally-0.16.1 → apitally-0.16.2}/tests/test_django_rest_framework.py +0 -0
  52. {apitally-0.16.1 → apitally-0.16.2}/tests/test_flask.py +0 -0
  53. {apitally-0.16.1 → apitally-0.16.2}/uv.lock +0 -0
@@ -48,10 +48,7 @@ jobs:
48
48
  matrix:
49
49
  python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
50
50
  deps:
51
- - starlette
52
51
  - fastapi starlette
53
- - fastapi==0.109.0 starlette
54
- - fastapi==0.100.1 starlette
55
52
  - fastapi==0.88.0 starlette
56
53
  - flask
57
54
  - flask==2.3.*
@@ -61,14 +58,10 @@ jobs:
61
58
  - djangorestframework==3.12.* django==3.2.* uritemplate
62
59
  - djangorestframework==3.10.* django==2.2.* uritemplate
63
60
  - django-ninja django
64
- - django-ninja==0.22.* django
65
61
  - django-ninja==0.18.0 django
66
62
  - litestar
67
- - litestar==2.6.1
68
- - litestar==2.3.0
69
63
  - litestar==2.0.1
70
64
  - blacksheep
71
- - blacksheep==2.2.0
72
65
  - blacksheep==2.1.0
73
66
  exclude:
74
67
  - python: "3.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apitally
3
- Version: 0.16.1
3
+ Version: 0.16.2
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
@@ -41,7 +41,6 @@ class ApitallyClient(ApitallyClientBase):
41
41
  self._stop_sync_loop = False
42
42
  self._sync_loop_task: Optional[asyncio.Task] = None
43
43
  self._sync_data_queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue()
44
- self._set_startup_data_task: Optional[asyncio.Task] = None
45
44
 
46
45
  def get_http_client(self) -> httpx.AsyncClient:
47
46
  if httpx.__version__ >= "0.26.0":
@@ -67,7 +66,7 @@ class ApitallyClient(ApitallyClientBase):
67
66
  try:
68
67
  async with self.get_http_client() as client:
69
68
  tasks = [self.send_sync_data(client), self.send_log_data(client)]
70
- if not self._startup_data_sent and last_sync_time > 0: # not on first sync
69
+ if not self._startup_data_sent:
71
70
  tasks.append(self.send_startup_data(client))
72
71
  await asyncio.gather(*tasks)
73
72
  last_sync_time = now
@@ -92,11 +91,6 @@ class ApitallyClient(ApitallyClientBase):
92
91
  def set_startup_data(self, data: Dict[str, Any]) -> None:
93
92
  self._startup_data_sent = False
94
93
  self._startup_data = self.add_uuids_to_data(data)
95
- self._set_startup_data_task = asyncio.create_task(self._set_startup_data())
96
-
97
- async def _set_startup_data(self) -> None:
98
- async with self.get_http_client() as client:
99
- await self.send_startup_data(client)
100
94
 
101
95
  async def send_startup_data(self, client: httpx.AsyncClient) -> None:
102
96
  if self._startup_data is not None:
@@ -80,7 +80,7 @@ class ApitallyClient(ApitallyClientBase):
80
80
  if (now - last_sync_time) >= self.sync_interval:
81
81
  try:
82
82
  with requests.Session() as session:
83
- if not self._startup_data_sent and last_sync_time > 0: # not on first sync
83
+ if not self._startup_data_sent:
84
84
  self.send_startup_data(session)
85
85
  self.send_sync_data(session)
86
86
  self.send_log_data(session)
@@ -105,8 +105,6 @@ class ApitallyClient(ApitallyClientBase):
105
105
  def set_startup_data(self, data: Dict[str, Any]) -> None:
106
106
  self._startup_data_sent = False
107
107
  self._startup_data = self.add_uuids_to_data(data)
108
- with requests.Session() as session:
109
- self.send_startup_data(session)
110
108
 
111
109
  def send_startup_data(self, session: requests.Session) -> None:
112
110
  if self._startup_data is not None:
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
4
3
  import time
5
4
  from typing import Any, Callable, Dict, List, Optional, Union
6
5
  from warnings import warn
@@ -40,6 +39,8 @@ class ApitallyMiddleware:
40
39
  proxy: Optional[Union[str, Proxy]] = None,
41
40
  ) -> None:
42
41
  self.app = app
42
+ self.app_version = app_version
43
+ self.openapi_url = openapi_url
43
44
  self.identify_consumer_callback = identify_consumer_callback
44
45
  self.client = ApitallyClient(
45
46
  client_id=client_id,
@@ -47,10 +48,6 @@ class ApitallyMiddleware:
47
48
  request_logging_config=request_logging_config,
48
49
  proxy=proxy,
49
50
  )
50
- self.client.start_sync_loop()
51
- self._delayed_set_startup_data_task: Optional[asyncio.Task] = None
52
- self.delayed_set_startup_data(app_version, openapi_url)
53
- _register_shutdown_handler(app, self.client.handle_shutdown)
54
51
 
55
52
  self.capture_request_body = (
56
53
  self.client.request_logger.config.enabled and self.client.request_logger.config.log_request_body
@@ -59,17 +56,13 @@ class ApitallyMiddleware:
59
56
  self.client.request_logger.config.enabled and self.client.request_logger.config.log_response_body
60
57
  )
61
58
 
62
- def delayed_set_startup_data(self, app_version: Optional[str] = None, openapi_url: Optional[str] = None) -> None:
63
- self._delayed_set_startup_data_task = asyncio.create_task(
64
- self._delayed_set_startup_data(app_version, openapi_url)
65
- )
59
+ _register_event_handler(app, "startup", self.on_startup)
60
+ _register_event_handler(app, "shutdown", self.client.handle_shutdown)
66
61
 
67
- async def _delayed_set_startup_data(
68
- self, app_version: Optional[str] = None, openapi_url: Optional[str] = None
69
- ) -> None:
70
- await asyncio.sleep(1.0) # Short delay to allow app routes to be registered first
71
- data = _get_startup_data(self.app, app_version, openapi_url)
62
+ def on_startup(self) -> None:
63
+ data = _get_startup_data(self.app, app_version=self.app_version, openapi_url=self.openapi_url)
72
64
  self.client.set_startup_data(data)
65
+ self.client.start_sync_loop()
73
66
 
74
67
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
75
68
  if self.client.enabled and scope["type"] == "http" and scope["method"] != "OPTIONS":
@@ -296,8 +289,8 @@ def _get_routes(app: Union[ASGIApp, Router]) -> List[BaseRoute]:
296
289
  return [] # pragma: no cover
297
290
 
298
291
 
299
- def _register_shutdown_handler(app: Union[ASGIApp, Router], shutdown_handler: Callable[[], Any]) -> None:
292
+ def _register_event_handler(app: Union[ASGIApp, Router], event: str, handler: Callable[[], Any]) -> None:
300
293
  if isinstance(app, Router):
301
- app.add_event_handler("shutdown", shutdown_handler)
294
+ app.add_event_handler(event, handler)
302
295
  elif hasattr(app, "app"):
303
- _register_shutdown_handler(app.app, shutdown_handler)
296
+ _register_event_handler(app.app, event, handler)
@@ -156,13 +156,14 @@ async def test_send_log_data(client: ApitallyClient, httpx_mock: HTTPXMock):
156
156
  assert len(client.request_logger.file_deque) == 0
157
157
 
158
158
 
159
- async def test_set_startup_data(client: ApitallyClient, httpx_mock: HTTPXMock):
159
+ async def test_send_startup_data(client: ApitallyClient, httpx_mock: HTTPXMock):
160
160
  from apitally.client.client_base import HUB_BASE_URL, HUB_VERSION
161
161
 
162
162
  httpx_mock.add_response()
163
163
  data = {"paths": [], "client_version": "1.0.0", "starlette_version": "0.28.0", "python_version": "3.11.4"}
164
164
  client.set_startup_data(data)
165
- await asyncio.sleep(0.01)
165
+ async with client.get_http_client() as http_client:
166
+ await client.send_startup_data(client=http_client)
166
167
 
167
168
  request = httpx_mock.get_request(url=f"{HUB_BASE_URL}/{HUB_VERSION}/{CLIENT_ID}/{ENV}/startup")
168
169
  assert request is not None
@@ -141,12 +141,14 @@ def test_send_log_data(client: ApitallyClient, requests_mock: Mocker):
141
141
  assert len(client.request_logger.file_deque) == 0
142
142
 
143
143
 
144
- def test_set_startup_data(client: ApitallyClient, requests_mock: Mocker):
144
+ def test_send_startup_data(client: ApitallyClient, requests_mock: Mocker):
145
145
  from apitally.client.client_base import HUB_BASE_URL, HUB_VERSION
146
146
 
147
147
  mock = requests_mock.register_uri("POST", f"{HUB_BASE_URL}/{HUB_VERSION}/{CLIENT_ID}/{ENV}/startup")
148
148
  data = {"paths": [], "client_version": "1.0.0", "starlette_version": "0.28.0", "python_version": "3.11.4"}
149
149
  client.set_startup_data(data)
150
+ with requests.Session() as session:
151
+ client.send_startup_data(session)
150
152
 
151
153
  assert len(mock.request_history) == 1
152
154
  request_data = mock.request_history[0].json()
@@ -30,8 +30,6 @@ def app(module_mocker: MockerFixture) -> FastAPI:
30
30
 
31
31
  module_mocker.patch("apitally.client.client_asyncio.ApitallyClient._instance", None)
32
32
  module_mocker.patch("apitally.client.client_asyncio.ApitallyClient.start_sync_loop")
33
- module_mocker.patch("apitally.client.client_asyncio.ApitallyClient.set_startup_data")
34
- module_mocker.patch("apitally.starlette.ApitallyMiddleware.delayed_set_startup_data")
35
33
 
36
34
  def identify_consumer(request: Request) -> Optional[str]:
37
35
  if consumer := request.query_params.get("consumer"):
@@ -37,7 +37,6 @@ async def app(module_mocker: MockerFixture) -> Litestar:
37
37
 
38
38
  module_mocker.patch("apitally.client.client_asyncio.ApitallyClient._instance", None)
39
39
  module_mocker.patch("apitally.client.client_asyncio.ApitallyClient.start_sync_loop")
40
- module_mocker.patch("apitally.client.client_asyncio.ApitallyClient.set_startup_data")
41
40
  module_mocker.patch("apitally.client.client_asyncio.ApitallyClient.handle_shutdown", mocked_handle_shutdown)
42
41
 
43
42
  @get("/foo")
@@ -28,8 +28,6 @@ if TYPE_CHECKING:
28
28
  async def app(request: FixtureRequest, module_mocker: MockerFixture) -> Starlette:
29
29
  module_mocker.patch("apitally.client.client_asyncio.ApitallyClient._instance", None)
30
30
  module_mocker.patch("apitally.client.client_asyncio.ApitallyClient.start_sync_loop")
31
- module_mocker.patch("apitally.client.client_asyncio.ApitallyClient.set_startup_data")
32
- module_mocker.patch("apitally.starlette.ApitallyMiddleware.delayed_set_startup_data")
33
31
  if request.param == "starlette":
34
32
  return get_starlette_app()
35
33
  elif request.param == "fastapi":
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -75,13 +75,13 @@ class ApitallyMiddleware:
75
75
  request_logging_config=self.config.request_logging_config,
76
76
  proxy=self.config.proxy,
77
77
  )
78
- self.client.start_sync_loop()
79
78
  self.client.set_startup_data(
80
79
  _get_startup_data(
81
80
  app_version=self.config.app_version,
82
81
  urlconfs=self.config.urlconfs,
83
82
  )
84
83
  )
84
+ self.client.start_sync_loop()
85
85
 
86
86
  self.capture_request_body = (
87
87
  self.client.request_logger.config.enabled and self.client.request_logger.config.log_request_body
File without changes
@@ -51,7 +51,6 @@ class ApitallyMiddleware:
51
51
  request_logging_config=request_logging_config,
52
52
  proxy=proxy,
53
53
  )
54
- self.client.start_sync_loop()
55
54
  self.delayed_set_startup_data(app_version, openapi_url)
56
55
 
57
56
  self.capture_request_body = (
@@ -73,6 +72,7 @@ class ApitallyMiddleware:
73
72
  def _delayed_set_startup_data(self, app_version: Optional[str] = None, openapi_url: Optional[str] = None) -> None:
74
73
  data = _get_startup_data(self.app, app_version, openapi_url)
75
74
  self.client.set_startup_data(data)
75
+ self.client.start_sync_loop()
76
76
 
77
77
  def __call__(self, environ: WSGIEnvironment, start_response: StartResponse) -> Iterable[bytes]:
78
78
  if not self.client.enabled:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes