apitally 0.17.0__tar.gz → 0.18.1__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 (57) hide show
  1. {apitally-0.17.0 → apitally-0.18.1}/.github/workflows/tests.yaml +1 -5
  2. {apitally-0.17.0 → apitally-0.18.1}/.pre-commit-config.yaml +2 -2
  3. apitally-0.18.1/.tool-versions +1 -0
  4. {apitally-0.17.0 → apitally-0.18.1}/PKG-INFO +2 -3
  5. {apitally-0.17.0 → apitally-0.18.1}/apitally/starlette.py +12 -1
  6. {apitally-0.17.0 → apitally-0.18.1}/pyproject.toml +6 -17
  7. apitally-0.18.1/renovate.json +5 -0
  8. {apitally-0.17.0 → apitally-0.18.1}/tests/test_client_asyncio.py +5 -8
  9. apitally-0.18.1/uv.lock +2364 -0
  10. apitally-0.17.0/.tool-versions +0 -1
  11. apitally-0.17.0/renovate.json +0 -27
  12. apitally-0.17.0/uv.lock +0 -3639
  13. {apitally-0.17.0 → apitally-0.18.1}/.github/workflows/publish.yaml +0 -0
  14. {apitally-0.17.0 → apitally-0.18.1}/.github/workflows/summary.yaml +0 -0
  15. {apitally-0.17.0 → apitally-0.18.1}/.gitignore +0 -0
  16. {apitally-0.17.0 → apitally-0.18.1}/LICENSE +0 -0
  17. {apitally-0.17.0 → apitally-0.18.1}/Makefile +0 -0
  18. {apitally-0.17.0 → apitally-0.18.1}/README.md +0 -0
  19. {apitally-0.17.0 → apitally-0.18.1}/apitally/__init__.py +0 -0
  20. {apitally-0.17.0 → apitally-0.18.1}/apitally/blacksheep.py +0 -0
  21. {apitally-0.17.0 → apitally-0.18.1}/apitally/client/__init__.py +0 -0
  22. {apitally-0.17.0 → apitally-0.18.1}/apitally/client/client_asyncio.py +0 -0
  23. {apitally-0.17.0 → apitally-0.18.1}/apitally/client/client_base.py +0 -0
  24. {apitally-0.17.0 → apitally-0.18.1}/apitally/client/client_threading.py +0 -0
  25. {apitally-0.17.0 → apitally-0.18.1}/apitally/client/consumers.py +0 -0
  26. {apitally-0.17.0 → apitally-0.18.1}/apitally/client/logging.py +0 -0
  27. {apitally-0.17.0 → apitally-0.18.1}/apitally/client/request_logging.py +0 -0
  28. {apitally-0.17.0 → apitally-0.18.1}/apitally/client/requests.py +0 -0
  29. {apitally-0.17.0 → apitally-0.18.1}/apitally/client/sentry.py +0 -0
  30. {apitally-0.17.0 → apitally-0.18.1}/apitally/client/server_errors.py +0 -0
  31. {apitally-0.17.0 → apitally-0.18.1}/apitally/client/validation_errors.py +0 -0
  32. {apitally-0.17.0 → apitally-0.18.1}/apitally/common.py +0 -0
  33. {apitally-0.17.0 → apitally-0.18.1}/apitally/django.py +0 -0
  34. {apitally-0.17.0 → apitally-0.18.1}/apitally/django_ninja.py +0 -0
  35. {apitally-0.17.0 → apitally-0.18.1}/apitally/django_rest_framework.py +0 -0
  36. {apitally-0.17.0 → apitally-0.18.1}/apitally/fastapi.py +0 -0
  37. {apitally-0.17.0 → apitally-0.18.1}/apitally/flask.py +0 -0
  38. {apitally-0.17.0 → apitally-0.18.1}/apitally/litestar.py +0 -0
  39. {apitally-0.17.0 → apitally-0.18.1}/apitally/py.typed +0 -0
  40. {apitally-0.17.0 → apitally-0.18.1}/tests/__init__.py +0 -0
  41. {apitally-0.17.0 → apitally-0.18.1}/tests/conftest.py +0 -0
  42. {apitally-0.17.0 → apitally-0.18.1}/tests/constants.py +0 -0
  43. {apitally-0.17.0 → apitally-0.18.1}/tests/django_ninja_urls.py +0 -0
  44. {apitally-0.17.0 → apitally-0.18.1}/tests/django_rest_framework_urls.py +0 -0
  45. {apitally-0.17.0 → apitally-0.18.1}/tests/test_blacksheep.py +0 -0
  46. {apitally-0.17.0 → apitally-0.18.1}/tests/test_client_consumers.py +0 -0
  47. {apitally-0.17.0 → apitally-0.18.1}/tests/test_client_request_logging.py +0 -0
  48. {apitally-0.17.0 → apitally-0.18.1}/tests/test_client_requests.py +0 -0
  49. {apitally-0.17.0 → apitally-0.18.1}/tests/test_client_server_errors.py +0 -0
  50. {apitally-0.17.0 → apitally-0.18.1}/tests/test_client_threading.py +0 -0
  51. {apitally-0.17.0 → apitally-0.18.1}/tests/test_client_validation_errors.py +0 -0
  52. {apitally-0.17.0 → apitally-0.18.1}/tests/test_django_ninja.py +0 -0
  53. {apitally-0.17.0 → apitally-0.18.1}/tests/test_django_rest_framework.py +0 -0
  54. {apitally-0.17.0 → apitally-0.18.1}/tests/test_fastapi.py +0 -0
  55. {apitally-0.17.0 → apitally-0.18.1}/tests/test_flask.py +0 -0
  56. {apitally-0.17.0 → apitally-0.18.1}/tests/test_litestar.py +0 -0
  57. {apitally-0.17.0 → apitally-0.18.1}/tests/test_starlette.py +0 -0
@@ -46,7 +46,7 @@ jobs:
46
46
  strategy:
47
47
  fail-fast: false
48
48
  matrix:
49
- python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
49
+ python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
50
50
  deps:
51
51
  - fastapi starlette
52
52
  - fastapi==0.94.1 starlette
@@ -64,10 +64,6 @@ jobs:
64
64
  - blacksheep
65
65
  - blacksheep==2.1.0
66
66
  exclude:
67
- - python: "3.8"
68
- deps: blacksheep
69
- - python: "3.8"
70
- deps: blacksheep==2.1.0
71
67
  - python: "3.12"
72
68
  deps: djangorestframework==3.12.* django==3.2.* uritemplate
73
69
  - python: "3.12"
@@ -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.9
11
+ rev: v0.11.11
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.7.3
17
+ rev: 0.7.8
18
18
  hooks:
19
19
  - id: uv-lock
@@ -0,0 +1 @@
1
+ uv 0.7.8
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apitally
3
- Version: 0.17.0
3
+ Version: 0.18.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
@@ -19,7 +19,6 @@ Classifier: License :: OSI Approved :: MIT License
19
19
  Classifier: Programming Language :: Python
20
20
  Classifier: Programming Language :: Python :: 3
21
21
  Classifier: Programming Language :: Python :: 3 :: Only
22
- Classifier: Programming Language :: Python :: 3.8
23
22
  Classifier: Programming Language :: Python :: 3.9
24
23
  Classifier: Programming Language :: Python :: 3.10
25
24
  Classifier: Programming Language :: Python :: 3.11
@@ -31,7 +30,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
31
30
  Classifier: Topic :: Software Development
32
31
  Classifier: Topic :: System :: Monitoring
33
32
  Classifier: Typing :: Typed
34
- Requires-Python: <4.0,>=3.8
33
+ Requires-Python: <4.0,>=3.9
35
34
  Requires-Dist: backoff>=2.0.0
36
35
  Provides-Extra: blacksheep
37
36
  Requires-Dist: blacksheep>=2; extra == 'blacksheep'
@@ -38,12 +38,14 @@ 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
+ capture_client_disconnects: bool = False,
41
42
  proxy: Optional[Union[str, Proxy]] = None,
42
43
  ) -> None:
43
44
  self.app = app
44
45
  self.app_version = app_version
45
46
  self.openapi_url = openapi_url
46
47
  self.identify_consumer_callback = identify_consumer_callback
48
+ self.capture_client_disconnects = capture_client_disconnects
47
49
  self.client = ApitallyClient(
48
50
  client_id=client_id,
49
51
  env=env,
@@ -72,7 +74,7 @@ class ApitallyMiddleware:
72
74
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
73
75
  if self.client.enabled and scope["type"] == "http" and scope["method"] != "OPTIONS":
74
76
  timestamp = time.time()
75
- request = Request(scope)
77
+ request = Request(scope, receive, send)
76
78
  request_size = parse_int(request.headers.get("Content-Length"))
77
79
  request_body = b""
78
80
  request_body_too_large = request_size is not None and request_size > MAX_BODY_SIZE
@@ -89,6 +91,7 @@ class ApitallyMiddleware:
89
91
 
90
92
  async def receive_wrapper() -> Message:
91
93
  nonlocal request_body, request_body_too_large
94
+
92
95
  message = await receive()
93
96
  if message["type"] == "http.request" and self.capture_request_body and not request_body_too_large:
94
97
  request_body += message.get("body", b"")
@@ -107,6 +110,7 @@ class ApitallyMiddleware:
107
110
  response_chunked, \
108
111
  response_content_type, \
109
112
  response_size
113
+
110
114
  if message["type"] == "http.response.start":
111
115
  response_time = time.perf_counter() - start_time
112
116
  response_status = message["status"]
@@ -118,9 +122,11 @@ class ApitallyMiddleware:
118
122
  response_content_type = response_headers.get("Content-Type")
119
123
  response_size = parse_int(response_headers.get("Content-Length")) if not response_chunked else 0
120
124
  response_body_too_large = response_size is not None and response_size > MAX_BODY_SIZE
125
+
121
126
  elif message["type"] == "http.response.body":
122
127
  if response_chunked and response_size is not None:
123
128
  response_size += len(message.get("body", b""))
129
+
124
130
  if (
125
131
  (self.capture_response_body or response_status == 422)
126
132
  and RequestLogger.is_supported_content_type(response_content_type)
@@ -130,6 +136,11 @@ class ApitallyMiddleware:
130
136
  if len(response_body) > MAX_BODY_SIZE:
131
137
  response_body_too_large = True
132
138
  response_body = b""
139
+
140
+ if self.capture_client_disconnects and await request.is_disconnected():
141
+ # Client closed connection (report NGINX specific status code)
142
+ response_status = 499
143
+
133
144
  await send(message)
134
145
 
135
146
  try:
@@ -16,7 +16,6 @@ classifiers = [
16
16
  "Programming Language :: Python",
17
17
  "Programming Language :: Python :: 3",
18
18
  "Programming Language :: Python :: 3 :: Only",
19
- "Programming Language :: Python :: 3.8",
20
19
  "Programming Language :: Python :: 3.9",
21
20
  "Programming Language :: Python :: 3.10",
22
21
  "Programming Language :: Python :: 3.11",
@@ -29,7 +28,7 @@ classifiers = [
29
28
  "Topic :: System :: Monitoring",
30
29
  "Typing :: Typed",
31
30
  ]
32
- requires-python = ">=3.8,<4.0"
31
+ requires-python = ">=3.9,<4.0"
33
32
  dependencies = ["backoff>=2.0.0"]
34
33
  dynamic = ["version"]
35
34
 
@@ -61,22 +60,12 @@ Documentation = "https://docs.apitally.io"
61
60
  Repository = "https://github.com/apitally/apitally-py"
62
61
 
63
62
  [dependency-groups]
64
- dev = [
65
- "ipykernel~=6.29.0",
66
- "mypy~=1.14.0; python_version<'3.9'",
67
- "mypy~=1.15.0; python_version>='3.9'",
68
- "pre-commit~=3.5.0; python_version<'3.9'",
69
- "pre-commit~=4.2.0; python_version>='3.9'",
70
- "ruff~=0.11.0",
71
- ]
63
+ dev = ["ipykernel~=6.29.0", "mypy~=1.15.0", "pre-commit~=4.2.0", "ruff~=0.11.0"]
72
64
  test = [
73
- "pytest~=7.4.4; python_version<'3.9'",
74
- "pytest~=8.3.3; python_version>='3.9'",
65
+ "pytest~=8.3.3",
75
66
  "pytest-asyncio~=0.21.2",
76
- "pytest-cov~=5.0.0; python_version<'3.9'",
77
- "pytest-cov~=6.1.1; python_version>='3.9'",
78
- "pytest-httpx~=0.22.0; python_version<'3.9'",
79
- "pytest-httpx~=0.33.0; python_version>='3.9'",
67
+ "pytest-cov~=6.1.1",
68
+ "pytest-httpx~=0.33.0",
80
69
  "pytest-mock~=3.14.0",
81
70
  "requests-mock~=1.12.1",
82
71
  ]
@@ -115,7 +104,7 @@ select = ["E", "F", "W", "I"]
115
104
  lines-after-imports = 2
116
105
 
117
106
  [tool.mypy]
118
- python_version = "3.12"
107
+ python_version = "3.13"
119
108
  check_untyped_defs = true
120
109
 
121
110
  [tool.pytest.ini_options]
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": ["github>apitally/renovate-config"],
4
+ "ignoreDeps": ["pytest-asyncio"]
5
+ }
@@ -4,7 +4,6 @@ import asyncio
4
4
  import gzip
5
5
  import json
6
6
  import re
7
- import sys
8
7
  import time
9
8
  from typing import TYPE_CHECKING
10
9
 
@@ -127,13 +126,11 @@ async def test_send_log_data(client: ApitallyClient, httpx_mock: HTTPXMock):
127
126
  request = httpx_mock.get_request(url=url_pattern)
128
127
  assert request is not None
129
128
 
130
- if sys.version_info >= (3, 9):
131
- # This doesn't work in Python 3.8 because of a bug in httpx
132
- json_lines = gzip.decompress(request.read()).strip().split(b"\n")
133
- assert len(json_lines) == 1
134
- json_data = json.loads(json_lines[0])
135
- assert json_data["request"]["path"] == "/test"
136
- assert json_data["response"]["status_code"] == 200
129
+ json_lines = gzip.decompress(request.read()).strip().split(b"\n")
130
+ assert len(json_lines) == 1
131
+ json_data = json.loads(json_lines[0])
132
+ assert json_data["request"]["path"] == "/test"
133
+ assert json_data["response"]["status_code"] == 200
137
134
 
138
135
  if pytest_httpx.__version__ < "0.31.0":
139
136
  httpx_mock.reset(True) # type: ignore[call-arg]