apitally 0.14.2__tar.gz → 0.14.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.
- {apitally-0.14.2 → apitally-0.14.3}/.pre-commit-config.yaml +1 -1
- {apitally-0.14.2 → apitally-0.14.3}/PKG-INFO +6 -5
- {apitally-0.14.2 → apitally-0.14.3}/README.md +3 -3
- {apitally-0.14.2 → apitally-0.14.3}/apitally/starlette.py +12 -6
- {apitally-0.14.2 → apitally-0.14.3}/pyproject.toml +1 -1
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_starlette.py +55 -38
- {apitally-0.14.2 → apitally-0.14.3}/uv.lock +786 -375
- {apitally-0.14.2 → apitally-0.14.3}/.github/workflows/publish.yaml +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/.github/workflows/summary.yaml +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/.github/workflows/tests.yaml +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/.gitignore +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/LICENSE +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/Makefile +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/__init__.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/client/__init__.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/client/client_asyncio.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/client/client_base.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/client/client_threading.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/client/consumers.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/client/logging.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/client/request_logging.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/client/requests.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/client/server_errors.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/client/validation_errors.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/common.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/django.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/django_ninja.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/django_rest_framework.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/fastapi.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/flask.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/litestar.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/apitally/py.typed +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/renovate.json +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/__init__.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/conftest.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/constants.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/django_ninja_urls.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/django_rest_framework_urls.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_client_asyncio.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_client_consumers.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_client_request_logging.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_client_requests.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_client_server_errors.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_client_threading.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_client_validation_errors.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_django_ninja.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_django_rest_framework.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_fastapi.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_flask.py +0 -0
- {apitally-0.14.2 → apitally-0.14.3}/tests/test_litestar.py +0 -0
@@ -1,12 +1,13 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: apitally
|
3
|
-
Version: 0.14.
|
3
|
+
Version: 0.14.3
|
4
4
|
Summary: Simple API monitoring & analytics for REST APIs built with FastAPI, Flask, Django, Starlette and Litestar.
|
5
5
|
Project-URL: Homepage, https://apitally.io
|
6
6
|
Project-URL: Documentation, https://docs.apitally.io
|
7
7
|
Project-URL: Repository, https://github.com/apitally/apitally-py
|
8
8
|
Author-email: Apitally <hello@apitally.io>
|
9
9
|
License: MIT License
|
10
|
+
License-File: LICENSE
|
10
11
|
Classifier: Development Status :: 5 - Production/Stable
|
11
12
|
Classifier: Environment :: Web Environment
|
12
13
|
Classifier: Framework :: Django
|
@@ -68,9 +69,9 @@ Description-Content-Type: text/markdown
|
|
68
69
|
</picture>
|
69
70
|
</p>
|
70
71
|
|
71
|
-
<p align="center"><b>
|
72
|
+
<p align="center"><b>Analytics, logging & monitoring for REST APIs.</b></p>
|
72
73
|
|
73
|
-
<p align="center"><i>Apitally
|
74
|
+
<p align="center"><i>Apitally helps you understand how your APIs are being used and alerts you when things go wrong.<br>It's super easy to use and designed to protect your data privacy.</i></p>
|
74
75
|
|
75
76
|
<p align="center">🔗 <b><a href="https://apitally.io" target="_blank">apitally.io</a></b></p>
|
76
77
|
|
@@ -100,7 +101,7 @@ the 📚 [documentation](https://docs.apitally.io).
|
|
100
101
|
## Key features
|
101
102
|
|
102
103
|
- Middleware for different frameworks to capture metadata about API endpoints,
|
103
|
-
requests and responses
|
104
|
+
requests and responses
|
104
105
|
- Non-blocking clients that aggregate and send captured data to Apitally in
|
105
106
|
regular intervals
|
106
107
|
|
@@ -6,9 +6,9 @@
|
|
6
6
|
</picture>
|
7
7
|
</p>
|
8
8
|
|
9
|
-
<p align="center"><b>
|
9
|
+
<p align="center"><b>Analytics, logging & monitoring for REST APIs.</b></p>
|
10
10
|
|
11
|
-
<p align="center"><i>Apitally
|
11
|
+
<p align="center"><i>Apitally helps you understand how your APIs are being used and alerts you when things go wrong.<br>It's super easy to use and designed to protect your data privacy.</i></p>
|
12
12
|
|
13
13
|
<p align="center">🔗 <b><a href="https://apitally.io" target="_blank">apitally.io</a></b></p>
|
14
14
|
|
@@ -38,7 +38,7 @@ the 📚 [documentation](https://docs.apitally.io).
|
|
38
38
|
## Key features
|
39
39
|
|
40
40
|
- Middleware for different frameworks to capture metadata about API endpoints,
|
41
|
-
requests and responses
|
41
|
+
requests and responses
|
42
42
|
- Non-blocking clients that aggregate and send captured data to Apitally in
|
43
43
|
regular intervals
|
44
44
|
|
@@ -222,12 +222,18 @@ class ApitallyMiddleware:
|
|
222
222
|
},
|
223
223
|
)
|
224
224
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
if
|
230
|
-
|
225
|
+
def get_path(self, request: Request, routes: Optional[list[BaseRoute]] = None) -> Optional[str]:
|
226
|
+
if routes is None:
|
227
|
+
routes = request.app.routes
|
228
|
+
for route in routes:
|
229
|
+
if hasattr(route, "routes"):
|
230
|
+
path = self.get_path(request, routes=route.routes)
|
231
|
+
if path is not None:
|
232
|
+
return path
|
233
|
+
elif hasattr(route, "path"):
|
234
|
+
match, _ = route.matches(request.scope)
|
235
|
+
if match == Match.FULL:
|
236
|
+
return request.scope.get("root_path", "") + route.path
|
231
237
|
return None
|
232
238
|
|
233
239
|
def get_consumer(self, request: Request) -> Optional[ApitallyConsumer]:
|
@@ -80,7 +80,7 @@ Repository = "https://github.com/apitally/apitally-py"
|
|
80
80
|
[dependency-groups]
|
81
81
|
dev = [
|
82
82
|
"ipykernel~=6.29.0",
|
83
|
-
"mypy~=1.
|
83
|
+
"mypy~=1.14.0",
|
84
84
|
"pre-commit~=3.5.0; python_version<'3.9'",
|
85
85
|
"pre-commit~=4.0.1; python_version>='3.9'",
|
86
86
|
"ruff~=0.8.0",
|
@@ -40,7 +40,7 @@ async def app(request: FixtureRequest, module_mocker: MockerFixture) -> Starlett
|
|
40
40
|
def get_starlette_app() -> Starlette:
|
41
41
|
from starlette.applications import Starlette
|
42
42
|
from starlette.responses import PlainTextResponse, StreamingResponse
|
43
|
-
from starlette.routing import Route
|
43
|
+
from starlette.routing import Mount, Route
|
44
44
|
|
45
45
|
from apitally.starlette import ApitallyConsumer, ApitallyMiddleware, RequestLoggingConfig
|
46
46
|
|
@@ -79,16 +79,27 @@ def get_starlette_app() -> Starlette:
|
|
79
79
|
def identify_consumer(request: Request) -> Optional[ApitallyConsumer]:
|
80
80
|
return ApitallyConsumer("test", name="Test")
|
81
81
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
app = Starlette(
|
82
|
+
sub_app = Starlette(
|
83
|
+
routes=[
|
84
|
+
Route("/foo", foo),
|
85
|
+
Route("/foo/{bar}", foo_bar),
|
86
|
+
Route("/bar", bar, methods=["POST"]),
|
87
|
+
Route("/baz", baz, methods=["POST"]),
|
88
|
+
Route("/val", val),
|
89
|
+
]
|
90
|
+
)
|
91
|
+
app = Starlette(
|
92
|
+
routes=[
|
93
|
+
Mount("/api", sub_app),
|
94
|
+
Mount(
|
95
|
+
"/test",
|
96
|
+
routes=[
|
97
|
+
Route("/task", task, methods=["POST"]),
|
98
|
+
],
|
99
|
+
),
|
100
|
+
Route("/stream", stream),
|
101
|
+
]
|
102
|
+
)
|
92
103
|
app.add_middleware(
|
93
104
|
ApitallyMiddleware,
|
94
105
|
client_id=CLIENT_ID,
|
@@ -104,7 +115,7 @@ def get_starlette_app() -> Starlette:
|
|
104
115
|
|
105
116
|
|
106
117
|
def get_fastapi_app() -> Starlette:
|
107
|
-
from fastapi import FastAPI, Query
|
118
|
+
from fastapi import APIRouter, FastAPI, Query
|
108
119
|
from fastapi.responses import PlainTextResponse, StreamingResponse
|
109
120
|
|
110
121
|
from apitally.fastapi import ApitallyConsumer, ApitallyMiddleware, RequestLoggingConfig
|
@@ -125,28 +136,30 @@ def get_fastapi_app() -> Starlette:
|
|
125
136
|
identify_consumer_callback=identify_consumer,
|
126
137
|
)
|
127
138
|
|
128
|
-
|
139
|
+
router = APIRouter()
|
140
|
+
|
141
|
+
@router.get("/foo")
|
129
142
|
def foo():
|
130
143
|
return "foo"
|
131
144
|
|
132
|
-
@
|
145
|
+
@router.get("/foo/{bar}")
|
133
146
|
def foo_bar(bar: str):
|
134
147
|
return PlainTextResponse(f"foo: {bar}")
|
135
148
|
|
136
|
-
@
|
149
|
+
@router.post("/bar")
|
137
150
|
async def bar(request: Request):
|
138
151
|
body = await request.body()
|
139
152
|
return PlainTextResponse("bar: " + body.decode())
|
140
153
|
|
141
|
-
@
|
154
|
+
@router.post("/baz")
|
142
155
|
def baz():
|
143
156
|
raise ValueError("baz")
|
144
157
|
|
145
|
-
@
|
158
|
+
@router.get("/val")
|
146
159
|
def val(foo: int = Query()):
|
147
160
|
return "val"
|
148
161
|
|
149
|
-
@app.get("/stream
|
162
|
+
@app.get("/stream")
|
150
163
|
def stream():
|
151
164
|
def stream_response():
|
152
165
|
yield b"foo"
|
@@ -154,7 +167,7 @@ def get_fastapi_app() -> Starlette:
|
|
154
167
|
|
155
168
|
return StreamingResponse(stream_response())
|
156
169
|
|
157
|
-
@app.post("/task
|
170
|
+
@app.post("/test/task")
|
158
171
|
def task(background_tasks: BackgroundTasks):
|
159
172
|
def task_func_with_error():
|
160
173
|
raise ValueError("task")
|
@@ -162,6 +175,8 @@ def get_fastapi_app() -> Starlette:
|
|
162
175
|
background_tasks.add_task(task_func_with_error)
|
163
176
|
return "ok"
|
164
177
|
|
178
|
+
app.include_router(router, prefix="/api")
|
179
|
+
|
165
180
|
return app
|
166
181
|
|
167
182
|
|
@@ -171,29 +186,29 @@ def test_middleware_requests_ok(app: Starlette, mocker: MockerFixture):
|
|
171
186
|
mock = mocker.patch("apitally.client.requests.RequestCounter.add_request")
|
172
187
|
client = TestClient(app)
|
173
188
|
|
174
|
-
response = client.get("/foo
|
189
|
+
response = client.get("/api/foo")
|
175
190
|
assert response.status_code == 200
|
176
191
|
mock.assert_called_once()
|
177
192
|
assert mock.call_args is not None
|
178
193
|
assert mock.call_args.kwargs["consumer"] == "test"
|
179
194
|
assert mock.call_args.kwargs["method"] == "GET"
|
180
|
-
assert mock.call_args.kwargs["path"] == "/foo
|
195
|
+
assert mock.call_args.kwargs["path"] == "/api/foo"
|
181
196
|
assert mock.call_args.kwargs["status_code"] == 200
|
182
197
|
assert mock.call_args.kwargs["response_time"] > 0
|
183
198
|
|
184
|
-
response = client.get("/foo/123
|
199
|
+
response = client.get("/api/foo/123")
|
185
200
|
assert response.status_code == 200
|
186
201
|
assert mock.call_count == 2
|
187
202
|
assert mock.call_args is not None
|
188
|
-
assert mock.call_args.kwargs["path"] == "/foo/{bar}
|
203
|
+
assert mock.call_args.kwargs["path"] == "/api/foo/{bar}"
|
189
204
|
|
190
|
-
response = client.post("/bar
|
205
|
+
response = client.post("/api/bar")
|
191
206
|
assert response.status_code == 200
|
192
207
|
assert mock.call_count == 3
|
193
208
|
assert mock.call_args is not None
|
194
209
|
assert mock.call_args.kwargs["method"] == "POST"
|
195
210
|
|
196
|
-
response = client.get("/stream
|
211
|
+
response = client.get("/stream")
|
197
212
|
assert response.status_code == 200
|
198
213
|
assert mock.call_count == 4
|
199
214
|
assert mock.call_args is not None
|
@@ -207,12 +222,12 @@ def test_middleware_requests_error(app: Starlette, mocker: MockerFixture):
|
|
207
222
|
mock2 = mocker.patch("apitally.client.server_errors.ServerErrorCounter.add_server_error")
|
208
223
|
client = TestClient(app, raise_server_exceptions=False)
|
209
224
|
|
210
|
-
response = client.post("/baz
|
225
|
+
response = client.post("/api/baz")
|
211
226
|
assert response.status_code == 500
|
212
227
|
mock1.assert_called_once()
|
213
228
|
assert mock1.call_args is not None
|
214
229
|
assert mock1.call_args.kwargs["method"] == "POST"
|
215
|
-
assert mock1.call_args.kwargs["path"] == "/baz
|
230
|
+
assert mock1.call_args.kwargs["path"] == "/api/baz"
|
216
231
|
assert mock1.call_args.kwargs["status_code"] == 500
|
217
232
|
assert mock1.call_args.kwargs["response_time"] > 0
|
218
233
|
|
@@ -222,7 +237,7 @@ def test_middleware_requests_error(app: Starlette, mocker: MockerFixture):
|
|
222
237
|
assert isinstance(exception, ValueError)
|
223
238
|
|
224
239
|
# Throws a ValueError in a background task, but returns 200
|
225
|
-
response = client.post("/task
|
240
|
+
response = client.post("/test/task")
|
226
241
|
assert response.status_code == 200
|
227
242
|
assert mock1.call_count == 2
|
228
243
|
assert mock1.call_args is not None
|
@@ -236,7 +251,7 @@ def test_middleware_requests_unhandled(app: Starlette, mocker: MockerFixture):
|
|
236
251
|
mock = mocker.patch("apitally.client.requests.RequestCounter.add_request")
|
237
252
|
client = TestClient(app)
|
238
253
|
|
239
|
-
response = client.post("/xxx
|
254
|
+
response = client.post("/xxx")
|
240
255
|
assert response.status_code == 404
|
241
256
|
mock.assert_not_called()
|
242
257
|
|
@@ -248,7 +263,7 @@ def test_middleware_validation_error(app: Starlette, mocker: MockerFixture):
|
|
248
263
|
client = TestClient(app)
|
249
264
|
|
250
265
|
# Validation error as foo must be an integer
|
251
|
-
response = client.get("/val?foo=bar")
|
266
|
+
response = client.get("/api/val?foo=bar")
|
252
267
|
assert response.status_code == 422
|
253
268
|
|
254
269
|
# FastAPI only
|
@@ -256,7 +271,7 @@ def test_middleware_validation_error(app: Starlette, mocker: MockerFixture):
|
|
256
271
|
mock.assert_called_once()
|
257
272
|
assert mock.call_args is not None
|
258
273
|
assert mock.call_args.kwargs["method"] == "GET"
|
259
|
-
assert mock.call_args.kwargs["path"] == "/val
|
274
|
+
assert mock.call_args.kwargs["path"] == "/api/val"
|
260
275
|
assert len(mock.call_args.kwargs["detail"]) == 1
|
261
276
|
assert mock.call_args.kwargs["detail"][0]["loc"] == ["query", "foo"]
|
262
277
|
|
@@ -269,13 +284,13 @@ def test_middleware_request_logging(app: Starlette, mocker: MockerFixture):
|
|
269
284
|
mock = mocker.patch("apitally.client.request_logging.RequestLogger.log_request")
|
270
285
|
client = TestClient(app)
|
271
286
|
|
272
|
-
response = client.get("/foo/123
|
287
|
+
response = client.get("/api/foo/123?foo=bar", headers={"Test-Header": "test"})
|
273
288
|
assert response.status_code == 200
|
274
289
|
mock.assert_called_once()
|
275
290
|
assert mock.call_args is not None
|
276
291
|
assert mock.call_args.kwargs["request"]["method"] == "GET"
|
277
|
-
assert mock.call_args.kwargs["request"]["path"] == "/foo/{bar}
|
278
|
-
assert mock.call_args.kwargs["request"]["url"] == "http://testserver/foo/123
|
292
|
+
assert mock.call_args.kwargs["request"]["path"] == "/api/foo/{bar}"
|
293
|
+
assert mock.call_args.kwargs["request"]["url"] == "http://testserver/api/foo/123?foo=bar"
|
279
294
|
assert ("test-header", "test") in mock.call_args.kwargs["request"]["headers"]
|
280
295
|
assert mock.call_args.kwargs["request"]["consumer"] == "test"
|
281
296
|
assert mock.call_args.kwargs["response"]["status_code"] == 200
|
@@ -284,18 +299,18 @@ def test_middleware_request_logging(app: Starlette, mocker: MockerFixture):
|
|
284
299
|
assert mock.call_args.kwargs["response"]["size"] > 0
|
285
300
|
assert mock.call_args.kwargs["response"]["body"] == b"foo: 123"
|
286
301
|
|
287
|
-
response = client.post("/bar
|
302
|
+
response = client.post("/api/bar", content=b"foo")
|
288
303
|
assert response.status_code == 200
|
289
304
|
assert mock.call_count == 2
|
290
305
|
assert mock.call_args is not None
|
291
306
|
assert mock.call_args.kwargs["request"]["method"] == "POST"
|
292
|
-
assert mock.call_args.kwargs["request"]["path"] == "/bar
|
293
|
-
assert mock.call_args.kwargs["request"]["url"] == "http://testserver/bar
|
307
|
+
assert mock.call_args.kwargs["request"]["path"] == "/api/bar"
|
308
|
+
assert mock.call_args.kwargs["request"]["url"] == "http://testserver/api/bar"
|
294
309
|
assert mock.call_args.kwargs["request"]["body"] == b"foo"
|
295
310
|
assert mock.call_args.kwargs["response"]["body"] == b"bar: foo"
|
296
311
|
|
297
312
|
mocker.patch("apitally.starlette.MAX_BODY_SIZE", 2)
|
298
|
-
response = client.post("/bar
|
313
|
+
response = client.post("/api/bar", content=b"foo")
|
299
314
|
assert response.status_code == 200
|
300
315
|
assert mock.call_count == 3
|
301
316
|
assert mock.call_args is not None
|
@@ -312,6 +327,8 @@ def test_get_startup_data(app: Starlette, mocker: MockerFixture):
|
|
312
327
|
|
313
328
|
data = _get_startup_data(app=app.middleware_stack, app_version="1.2.3", openapi_url=None)
|
314
329
|
assert len(data["paths"]) == 7
|
330
|
+
assert {"method": "get", "path": "/api/foo"} in data["paths"]
|
331
|
+
assert {"method": "get", "path": "/stream"} in data["paths"]
|
315
332
|
assert data["versions"]["starlette"]
|
316
333
|
assert data["versions"]["app"] == "1.2.3"
|
317
334
|
assert data["client"] == "python:starlette"
|