apitally 0.16.2__tar.gz → 0.16.3__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.2 → apitally-0.16.3}/.github/workflows/tests.yaml +1 -11
  2. {apitally-0.16.2 → apitally-0.16.3}/.pre-commit-config.yaml +2 -2
  3. {apitally-0.16.2 → apitally-0.16.3}/PKG-INFO +4 -4
  4. {apitally-0.16.2 → apitally-0.16.3}/apitally/starlette.py +37 -10
  5. {apitally-0.16.2 → apitally-0.16.3}/pyproject.toml +2 -2
  6. {apitally-0.16.2 → apitally-0.16.3}/tests/test_starlette.py +103 -102
  7. {apitally-0.16.2 → apitally-0.16.3}/uv.lock +1574 -1574
  8. {apitally-0.16.2 → apitally-0.16.3}/.github/workflows/publish.yaml +0 -0
  9. {apitally-0.16.2 → apitally-0.16.3}/.github/workflows/summary.yaml +0 -0
  10. {apitally-0.16.2 → apitally-0.16.3}/.gitignore +0 -0
  11. {apitally-0.16.2 → apitally-0.16.3}/LICENSE +0 -0
  12. {apitally-0.16.2 → apitally-0.16.3}/Makefile +0 -0
  13. {apitally-0.16.2 → apitally-0.16.3}/README.md +0 -0
  14. {apitally-0.16.2 → apitally-0.16.3}/apitally/__init__.py +0 -0
  15. {apitally-0.16.2 → apitally-0.16.3}/apitally/blacksheep.py +0 -0
  16. {apitally-0.16.2 → apitally-0.16.3}/apitally/client/__init__.py +0 -0
  17. {apitally-0.16.2 → apitally-0.16.3}/apitally/client/client_asyncio.py +0 -0
  18. {apitally-0.16.2 → apitally-0.16.3}/apitally/client/client_base.py +0 -0
  19. {apitally-0.16.2 → apitally-0.16.3}/apitally/client/client_threading.py +0 -0
  20. {apitally-0.16.2 → apitally-0.16.3}/apitally/client/consumers.py +0 -0
  21. {apitally-0.16.2 → apitally-0.16.3}/apitally/client/logging.py +0 -0
  22. {apitally-0.16.2 → apitally-0.16.3}/apitally/client/request_logging.py +0 -0
  23. {apitally-0.16.2 → apitally-0.16.3}/apitally/client/requests.py +0 -0
  24. {apitally-0.16.2 → apitally-0.16.3}/apitally/client/sentry.py +0 -0
  25. {apitally-0.16.2 → apitally-0.16.3}/apitally/client/server_errors.py +0 -0
  26. {apitally-0.16.2 → apitally-0.16.3}/apitally/client/validation_errors.py +0 -0
  27. {apitally-0.16.2 → apitally-0.16.3}/apitally/common.py +0 -0
  28. {apitally-0.16.2 → apitally-0.16.3}/apitally/django.py +0 -0
  29. {apitally-0.16.2 → apitally-0.16.3}/apitally/django_ninja.py +0 -0
  30. {apitally-0.16.2 → apitally-0.16.3}/apitally/django_rest_framework.py +0 -0
  31. {apitally-0.16.2 → apitally-0.16.3}/apitally/fastapi.py +0 -0
  32. {apitally-0.16.2 → apitally-0.16.3}/apitally/flask.py +0 -0
  33. {apitally-0.16.2 → apitally-0.16.3}/apitally/litestar.py +0 -0
  34. {apitally-0.16.2 → apitally-0.16.3}/apitally/py.typed +0 -0
  35. {apitally-0.16.2 → apitally-0.16.3}/renovate.json +0 -0
  36. {apitally-0.16.2 → apitally-0.16.3}/tests/__init__.py +0 -0
  37. {apitally-0.16.2 → apitally-0.16.3}/tests/conftest.py +0 -0
  38. {apitally-0.16.2 → apitally-0.16.3}/tests/constants.py +0 -0
  39. {apitally-0.16.2 → apitally-0.16.3}/tests/django_ninja_urls.py +0 -0
  40. {apitally-0.16.2 → apitally-0.16.3}/tests/django_rest_framework_urls.py +0 -0
  41. {apitally-0.16.2 → apitally-0.16.3}/tests/test_blacksheep.py +0 -0
  42. {apitally-0.16.2 → apitally-0.16.3}/tests/test_client_asyncio.py +0 -0
  43. {apitally-0.16.2 → apitally-0.16.3}/tests/test_client_consumers.py +0 -0
  44. {apitally-0.16.2 → apitally-0.16.3}/tests/test_client_request_logging.py +0 -0
  45. {apitally-0.16.2 → apitally-0.16.3}/tests/test_client_requests.py +0 -0
  46. {apitally-0.16.2 → apitally-0.16.3}/tests/test_client_server_errors.py +0 -0
  47. {apitally-0.16.2 → apitally-0.16.3}/tests/test_client_threading.py +0 -0
  48. {apitally-0.16.2 → apitally-0.16.3}/tests/test_client_validation_errors.py +0 -0
  49. {apitally-0.16.2 → apitally-0.16.3}/tests/test_django_ninja.py +0 -0
  50. {apitally-0.16.2 → apitally-0.16.3}/tests/test_django_rest_framework.py +0 -0
  51. {apitally-0.16.2 → apitally-0.16.3}/tests/test_fastapi.py +0 -0
  52. {apitally-0.16.2 → apitally-0.16.3}/tests/test_flask.py +0 -0
  53. {apitally-0.16.2 → apitally-0.16.3}/tests/test_litestar.py +0 -0
@@ -49,7 +49,7 @@ jobs:
49
49
  python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
50
50
  deps:
51
51
  - fastapi starlette
52
- - fastapi==0.88.0 starlette
52
+ - fastapi==0.94.1 starlette
53
53
  - flask
54
54
  - flask==2.3.*
55
55
  - flask==2.0.3 Werkzeug==2.*
@@ -66,24 +66,14 @@ jobs:
66
66
  exclude:
67
67
  - python: "3.8"
68
68
  deps: blacksheep
69
- - python: "3.8"
70
- deps: blacksheep==2.2.0
71
69
  - python: "3.8"
72
70
  deps: blacksheep==2.1.0
73
- - python: "3.12"
74
- deps: fastapi==0.100.1 starlette
75
- - python: "3.12"
76
- deps: fastapi==0.87.0 starlette
77
71
  - python: "3.12"
78
72
  deps: djangorestframework==3.12.* django==3.2.* uritemplate
79
73
  - python: "3.12"
80
74
  deps: djangorestframework==3.10.* django==2.2.* uritemplate
81
75
  - python: "3.12"
82
76
  deps: litestar==2.0.1
83
- - python: "3.13"
84
- deps: fastapi==0.100.1 starlette
85
- - python: "3.13"
86
- deps: fastapi==0.87.0 starlette
87
77
  - python: "3.13"
88
78
  deps: djangorestframework==3.12.* django==3.2.* uritemplate
89
79
  - python: "3.13"
@@ -8,12 +8,12 @@ repos:
8
8
  - id: trailing-whitespace
9
9
  - id: mixed-line-ending
10
10
  - repo: https://github.com/charliermarsh/ruff-pre-commit
11
- rev: v0.11.7
11
+ rev: v0.11.8
12
12
  hooks:
13
13
  - id: ruff
14
14
  args: ["--fix", "--exit-non-zero-on-fix"]
15
15
  - id: ruff-format
16
16
  - repo: https://github.com/astral-sh/uv-pre-commit
17
- rev: 0.6.17
17
+ rev: 0.7.2
18
18
  hooks:
19
19
  - id: uv-lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apitally
3
- Version: 0.16.2
3
+ Version: 0.16.3
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
@@ -49,9 +49,9 @@ Requires-Dist: inflection>=0.5.1; extra == 'django-rest-framework'
49
49
  Requires-Dist: requests>=2.26.0; extra == 'django-rest-framework'
50
50
  Requires-Dist: uritemplate>=3.0.0; extra == 'django-rest-framework'
51
51
  Provides-Extra: fastapi
52
- Requires-Dist: fastapi>=0.88.0; extra == 'fastapi'
52
+ Requires-Dist: fastapi>=0.94.1; extra == 'fastapi'
53
53
  Requires-Dist: httpx>=0.22.0; extra == 'fastapi'
54
- Requires-Dist: starlette<1.0.0,>=0.22.0; extra == 'fastapi'
54
+ Requires-Dist: starlette<1.0.0,>=0.26.1; extra == 'fastapi'
55
55
  Provides-Extra: flask
56
56
  Requires-Dist: flask>=2.0.0; extra == 'flask'
57
57
  Requires-Dist: requests>=2.26.0; extra == 'flask'
@@ -62,7 +62,7 @@ Provides-Extra: sentry
62
62
  Requires-Dist: sentry-sdk>=2.2.0; extra == 'sentry'
63
63
  Provides-Extra: starlette
64
64
  Requires-Dist: httpx>=0.22.0; extra == 'starlette'
65
- Requires-Dist: starlette<1.0.0,>=0.21.0; extra == 'starlette'
65
+ Requires-Dist: starlette<1.0.0,>=0.26.1; extra == 'starlette'
66
66
  Description-Content-Type: text/markdown
67
67
 
68
68
  <p align="center">
@@ -1,16 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import time
4
- from typing import Any, Callable, Dict, List, Optional, Union
4
+ from contextlib import asynccontextmanager
5
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
5
6
  from warnings import warn
6
7
 
7
8
  from httpx import HTTPStatusError, Proxy
9
+ from starlette.applications import Starlette
8
10
  from starlette.datastructures import Headers
9
11
  from starlette.requests import Request
10
12
  from starlette.routing import BaseRoute, Match, Router
11
13
  from starlette.schemas import EndpointInfo, SchemaGenerator
12
14
  from starlette.testclient import TestClient
13
- from starlette.types import ASGIApp, Message, Receive, Scope, Send
15
+ from starlette.types import ASGIApp, Lifespan, Message, Receive, Scope, Send
14
16
 
15
17
  from apitally.client.client_asyncio import ApitallyClient
16
18
  from apitally.client.consumers import Consumer as ApitallyConsumer
@@ -56,10 +58,13 @@ class ApitallyMiddleware:
56
58
  self.client.request_logger.config.enabled and self.client.request_logger.config.log_response_body
57
59
  )
58
60
 
59
- _register_event_handler(app, "startup", self.on_startup)
60
- _register_event_handler(app, "shutdown", self.client.handle_shutdown)
61
+ _inject_lifespan_handlers(
62
+ app,
63
+ on_startup=self.on_startup,
64
+ on_shutdown=self.client.handle_shutdown,
65
+ )
61
66
 
62
- def on_startup(self) -> None:
67
+ async def on_startup(self) -> None:
63
68
  data = _get_startup_data(self.app, app_version=self.app_version, openapi_url=self.openapi_url)
64
69
  self.client.set_startup_data(data)
65
70
  self.client.start_sync_loop()
@@ -289,8 +294,30 @@ def _get_routes(app: Union[ASGIApp, Router]) -> List[BaseRoute]:
289
294
  return [] # pragma: no cover
290
295
 
291
296
 
292
- def _register_event_handler(app: Union[ASGIApp, Router], event: str, handler: Callable[[], Any]) -> None:
293
- if isinstance(app, Router):
294
- app.add_event_handler(event, handler)
295
- elif hasattr(app, "app"):
296
- _register_event_handler(app.app, event, handler)
297
+ def _inject_lifespan_handlers(
298
+ app: Union[ASGIApp, Router],
299
+ on_startup: Callable[[], Awaitable[Any]],
300
+ on_shutdown: Callable[[], Awaitable[Any]],
301
+ ) -> None:
302
+ """
303
+ Ensures the given startup and shutdown functions are called as part of the app's lifespan context manager.
304
+ """
305
+ router = app
306
+ while not isinstance(router, Router) and hasattr(router, "app"):
307
+ router = router.app
308
+ if not isinstance(router, Router):
309
+ raise TypeError("app must be a Starlette or Router instance")
310
+
311
+ lifespan: Optional[Lifespan] = getattr(router, "lifespan_context", None)
312
+
313
+ @asynccontextmanager
314
+ async def wrapped_lifespan(app: Starlette):
315
+ await on_startup()
316
+ if lifespan is not None:
317
+ async with lifespan(app):
318
+ yield
319
+ else:
320
+ yield
321
+ await on_shutdown()
322
+
323
+ router.lifespan_context = wrapped_lifespan
@@ -49,11 +49,11 @@ django_rest_framework = [
49
49
  "inflection>=0.5.1", # required for schema generation
50
50
  "requests>=2.26.0",
51
51
  ]
52
- fastapi = ["fastapi>=0.88.0", "starlette>=0.22.0,<1.0.0", "httpx>=0.22.0"]
52
+ fastapi = ["fastapi>=0.94.1", "starlette>=0.26.1,<1.0.0", "httpx>=0.22.0"]
53
53
  flask = ["flask>=2.0.0", "requests>=2.26.0"]
54
54
  litestar = ["litestar>=2.0.0", "httpx>=0.22.0"]
55
55
  sentry = ["sentry-sdk>=2.2.0"]
56
- starlette = ["starlette>=0.21.0,<1.0.0", "httpx>=0.22.0"]
56
+ starlette = ["starlette>=0.26.1,<1.0.0", "httpx>=0.22.0"]
57
57
 
58
58
  [project.urls]
59
59
  Homepage = "https://apitally.io"
@@ -28,6 +28,7 @@ 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.handle_shutdown")
31
32
  if request.param == "starlette":
32
33
  return get_starlette_app()
33
34
  elif request.param == "fastapi":
@@ -182,35 +183,35 @@ def test_middleware_requests_ok(app: Starlette, mocker: MockerFixture):
182
183
  from starlette.testclient import TestClient
183
184
 
184
185
  mock = mocker.patch("apitally.client.requests.RequestCounter.add_request")
185
- client = TestClient(app)
186
-
187
- response = client.get("/api/foo")
188
- assert response.status_code == 200
189
- mock.assert_called_once()
190
- assert mock.call_args is not None
191
- assert mock.call_args.kwargs["consumer"] == "test"
192
- assert mock.call_args.kwargs["method"] == "GET"
193
- assert mock.call_args.kwargs["path"] == "/api/foo"
194
- assert mock.call_args.kwargs["status_code"] == 200
195
- assert mock.call_args.kwargs["response_time"] > 0
196
-
197
- response = client.get("/api/foo/123")
198
- assert response.status_code == 200
199
- assert mock.call_count == 2
200
- assert mock.call_args is not None
201
- assert mock.call_args.kwargs["path"] == "/api/foo/{bar}"
202
-
203
- response = client.post("/api/bar")
204
- assert response.status_code == 200
205
- assert mock.call_count == 3
206
- assert mock.call_args is not None
207
- assert mock.call_args.kwargs["method"] == "POST"
208
-
209
- response = client.get("/stream")
210
- assert response.status_code == 200
211
- assert mock.call_count == 4
212
- assert mock.call_args is not None
213
- assert mock.call_args.kwargs["response_size"] == 6
186
+
187
+ with TestClient(app) as client:
188
+ response = client.get("/api/foo")
189
+ assert response.status_code == 200
190
+ mock.assert_called_once()
191
+ assert mock.call_args is not None
192
+ assert mock.call_args.kwargs["consumer"] == "test"
193
+ assert mock.call_args.kwargs["method"] == "GET"
194
+ assert mock.call_args.kwargs["path"] == "/api/foo"
195
+ assert mock.call_args.kwargs["status_code"] == 200
196
+ assert mock.call_args.kwargs["response_time"] > 0
197
+
198
+ response = client.get("/api/foo/123")
199
+ assert response.status_code == 200
200
+ assert mock.call_count == 2
201
+ assert mock.call_args is not None
202
+ assert mock.call_args.kwargs["path"] == "/api/foo/{bar}"
203
+
204
+ response = client.post("/api/bar")
205
+ assert response.status_code == 200
206
+ assert mock.call_count == 3
207
+ assert mock.call_args is not None
208
+ assert mock.call_args.kwargs["method"] == "POST"
209
+
210
+ response = client.get("/stream")
211
+ assert response.status_code == 200
212
+ assert mock.call_count == 4
213
+ assert mock.call_args is not None
214
+ assert mock.call_args.kwargs["response_size"] == 6
214
215
 
215
216
 
216
217
  def test_middleware_requests_error(app: Starlette, mocker: MockerFixture):
@@ -218,60 +219,60 @@ def test_middleware_requests_error(app: Starlette, mocker: MockerFixture):
218
219
 
219
220
  mock1 = mocker.patch("apitally.client.requests.RequestCounter.add_request")
220
221
  mock2 = mocker.patch("apitally.client.server_errors.ServerErrorCounter.add_server_error")
221
- client = TestClient(app, raise_server_exceptions=False)
222
-
223
- response = client.post("/api/baz")
224
- assert response.status_code == 500
225
- mock1.assert_called_once()
226
- assert mock1.call_args is not None
227
- assert mock1.call_args.kwargs["method"] == "POST"
228
- assert mock1.call_args.kwargs["path"] == "/api/baz"
229
- assert mock1.call_args.kwargs["status_code"] == 500
230
- assert mock1.call_args.kwargs["response_time"] > 0
231
-
232
- mock2.assert_called_once()
233
- assert mock2.call_args is not None
234
- exception = mock2.call_args.kwargs["exception"]
235
- assert isinstance(exception, ValueError)
236
-
237
- # Throws a ValueError in a background task, but returns 200
238
- response = client.post("/test/task")
239
- assert response.status_code == 200
240
- assert mock1.call_count == 2
241
- assert mock1.call_args is not None
242
- assert mock1.call_args.kwargs["status_code"] == 200
243
- mock2.assert_called_once() # Not called again
222
+
223
+ with TestClient(app, raise_server_exceptions=False) as client:
224
+ response = client.post("/api/baz")
225
+ assert response.status_code == 500
226
+ mock1.assert_called_once()
227
+ assert mock1.call_args is not None
228
+ assert mock1.call_args.kwargs["method"] == "POST"
229
+ assert mock1.call_args.kwargs["path"] == "/api/baz"
230
+ assert mock1.call_args.kwargs["status_code"] == 500
231
+ assert mock1.call_args.kwargs["response_time"] > 0
232
+
233
+ mock2.assert_called_once()
234
+ assert mock2.call_args is not None
235
+ exception = mock2.call_args.kwargs["exception"]
236
+ assert isinstance(exception, ValueError)
237
+
238
+ # Throws a ValueError in a background task, but returns 200
239
+ response = client.post("/test/task")
240
+ assert response.status_code == 200
241
+ assert mock1.call_count == 2
242
+ assert mock1.call_args is not None
243
+ assert mock1.call_args.kwargs["status_code"] == 200
244
+ mock2.assert_called_once() # Not called again
244
245
 
245
246
 
246
247
  def test_middleware_requests_unhandled(app: Starlette, mocker: MockerFixture):
247
248
  from starlette.testclient import TestClient
248
249
 
249
250
  mock = mocker.patch("apitally.client.requests.RequestCounter.add_request")
250
- client = TestClient(app)
251
251
 
252
- response = client.post("/xxx")
253
- assert response.status_code == 404
254
- mock.assert_not_called()
252
+ with TestClient(app) as client:
253
+ response = client.post("/xxx")
254
+ assert response.status_code == 404
255
+ mock.assert_not_called()
255
256
 
256
257
 
257
258
  def test_middleware_validation_error(app: Starlette, mocker: MockerFixture):
258
259
  from starlette.testclient import TestClient
259
260
 
260
261
  mock = mocker.patch("apitally.client.validation_errors.ValidationErrorCounter.add_validation_errors")
261
- client = TestClient(app)
262
262
 
263
- # Validation error as foo must be an integer
264
- response = client.get("/api/val?foo=bar")
265
- assert response.status_code == 422
263
+ with TestClient(app) as client:
264
+ # Validation error as foo must be an integer
265
+ response = client.get("/api/val?foo=bar")
266
+ assert response.status_code == 422
266
267
 
267
- # FastAPI only
268
- if response.headers["Content-Type"] == "application/json":
269
- mock.assert_called_once()
270
- assert mock.call_args is not None
271
- assert mock.call_args.kwargs["method"] == "GET"
272
- assert mock.call_args.kwargs["path"] == "/api/val"
273
- assert len(mock.call_args.kwargs["detail"]) == 1
274
- assert mock.call_args.kwargs["detail"][0]["loc"] == ["query", "foo"]
268
+ # FastAPI only
269
+ if response.headers["Content-Type"] == "application/json":
270
+ mock.assert_called_once()
271
+ assert mock.call_args is not None
272
+ assert mock.call_args.kwargs["method"] == "GET"
273
+ assert mock.call_args.kwargs["path"] == "/api/val"
274
+ assert len(mock.call_args.kwargs["detail"]) == 1
275
+ assert mock.call_args.kwargs["detail"][0]["loc"] == ["query", "foo"]
275
276
 
276
277
 
277
278
  def test_middleware_request_logging(app: Starlette, mocker: MockerFixture):
@@ -280,40 +281,40 @@ def test_middleware_request_logging(app: Starlette, mocker: MockerFixture):
280
281
  from apitally.client.request_logging import BODY_TOO_LARGE
281
282
 
282
283
  mock = mocker.patch("apitally.client.request_logging.RequestLogger.log_request")
283
- client = TestClient(app)
284
-
285
- response = client.get("/api/foo/123?foo=bar", headers={"Test-Header": "test"})
286
- assert response.status_code == 200
287
- mock.assert_called_once()
288
- assert mock.call_args is not None
289
- assert mock.call_args.kwargs["request"]["method"] == "GET"
290
- assert mock.call_args.kwargs["request"]["path"] == "/api/foo/{bar}"
291
- assert mock.call_args.kwargs["request"]["url"] == "http://testserver/api/foo/123?foo=bar"
292
- assert ("test-header", "test") in mock.call_args.kwargs["request"]["headers"]
293
- assert mock.call_args.kwargs["request"]["consumer"] == "test"
294
- assert mock.call_args.kwargs["response"]["status_code"] == 200
295
- assert mock.call_args.kwargs["response"]["response_time"] > 0
296
- assert ("content-type", "text/plain; charset=utf-8") in mock.call_args.kwargs["response"]["headers"]
297
- assert mock.call_args.kwargs["response"]["size"] > 0
298
- assert mock.call_args.kwargs["response"]["body"] == b"foo: 123"
299
-
300
- response = client.post("/api/bar", content=b"foo")
301
- assert response.status_code == 200
302
- assert mock.call_count == 2
303
- assert mock.call_args is not None
304
- assert mock.call_args.kwargs["request"]["method"] == "POST"
305
- assert mock.call_args.kwargs["request"]["path"] == "/api/bar"
306
- assert mock.call_args.kwargs["request"]["url"] == "http://testserver/api/bar"
307
- assert mock.call_args.kwargs["request"]["body"] == b"foo"
308
- assert mock.call_args.kwargs["response"]["body"] == b"bar: foo"
309
-
310
- mocker.patch("apitally.starlette.MAX_BODY_SIZE", 2)
311
- response = client.post("/api/bar", content=b"foo")
312
- assert response.status_code == 200
313
- assert mock.call_count == 3
314
- assert mock.call_args is not None
315
- assert mock.call_args.kwargs["request"]["body"] == BODY_TOO_LARGE
316
- assert mock.call_args.kwargs["response"]["body"] == BODY_TOO_LARGE
284
+
285
+ with TestClient(app) as client:
286
+ response = client.get("/api/foo/123?foo=bar", headers={"Test-Header": "test"})
287
+ assert response.status_code == 200
288
+ mock.assert_called_once()
289
+ assert mock.call_args is not None
290
+ assert mock.call_args.kwargs["request"]["method"] == "GET"
291
+ assert mock.call_args.kwargs["request"]["path"] == "/api/foo/{bar}"
292
+ assert mock.call_args.kwargs["request"]["url"] == "http://testserver/api/foo/123?foo=bar"
293
+ assert ("test-header", "test") in mock.call_args.kwargs["request"]["headers"]
294
+ assert mock.call_args.kwargs["request"]["consumer"] == "test"
295
+ assert mock.call_args.kwargs["response"]["status_code"] == 200
296
+ assert mock.call_args.kwargs["response"]["response_time"] > 0
297
+ assert ("content-type", "text/plain; charset=utf-8") in mock.call_args.kwargs["response"]["headers"]
298
+ assert mock.call_args.kwargs["response"]["size"] > 0
299
+ assert mock.call_args.kwargs["response"]["body"] == b"foo: 123"
300
+
301
+ response = client.post("/api/bar", content=b"foo")
302
+ assert response.status_code == 200
303
+ assert mock.call_count == 2
304
+ assert mock.call_args is not None
305
+ assert mock.call_args.kwargs["request"]["method"] == "POST"
306
+ assert mock.call_args.kwargs["request"]["path"] == "/api/bar"
307
+ assert mock.call_args.kwargs["request"]["url"] == "http://testserver/api/bar"
308
+ assert mock.call_args.kwargs["request"]["body"] == b"foo"
309
+ assert mock.call_args.kwargs["response"]["body"] == b"bar: foo"
310
+
311
+ mocker.patch("apitally.starlette.MAX_BODY_SIZE", 2)
312
+ response = client.post("/api/bar", content=b"foo")
313
+ assert response.status_code == 200
314
+ assert mock.call_count == 3
315
+ assert mock.call_args is not None
316
+ assert mock.call_args.kwargs["request"]["body"] == BODY_TOO_LARGE
317
+ assert mock.call_args.kwargs["response"]["body"] == BODY_TOO_LARGE
317
318
 
318
319
 
319
320
  def test_get_startup_data(app: Starlette, mocker: MockerFixture):