apitally 0.14.2__tar.gz → 0.14.4__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 (50) hide show
  1. {apitally-0.14.2 → apitally-0.14.4}/.github/workflows/publish.yaml +2 -2
  2. {apitally-0.14.2 → apitally-0.14.4}/.github/workflows/tests.yaml +5 -5
  3. {apitally-0.14.2 → apitally-0.14.4}/.pre-commit-config.yaml +1 -1
  4. {apitally-0.14.2 → apitally-0.14.4}/PKG-INFO +6 -5
  5. {apitally-0.14.2 → apitally-0.14.4}/README.md +3 -3
  6. {apitally-0.14.2 → apitally-0.14.4}/apitally/client/client_asyncio.py +7 -8
  7. {apitally-0.14.2 → apitally-0.14.4}/apitally/client/client_base.py +1 -0
  8. {apitally-0.14.2 → apitally-0.14.4}/apitally/client/client_threading.py +7 -8
  9. {apitally-0.14.2 → apitally-0.14.4}/apitally/starlette.py +12 -6
  10. {apitally-0.14.2 → apitally-0.14.4}/pyproject.toml +1 -1
  11. {apitally-0.14.2 → apitally-0.14.4}/tests/test_starlette.py +56 -38
  12. {apitally-0.14.2 → apitally-0.14.4}/uv.lock +798 -381
  13. {apitally-0.14.2 → apitally-0.14.4}/.github/workflows/summary.yaml +0 -0
  14. {apitally-0.14.2 → apitally-0.14.4}/.gitignore +0 -0
  15. {apitally-0.14.2 → apitally-0.14.4}/LICENSE +0 -0
  16. {apitally-0.14.2 → apitally-0.14.4}/Makefile +0 -0
  17. {apitally-0.14.2 → apitally-0.14.4}/apitally/__init__.py +0 -0
  18. {apitally-0.14.2 → apitally-0.14.4}/apitally/client/__init__.py +0 -0
  19. {apitally-0.14.2 → apitally-0.14.4}/apitally/client/consumers.py +0 -0
  20. {apitally-0.14.2 → apitally-0.14.4}/apitally/client/logging.py +0 -0
  21. {apitally-0.14.2 → apitally-0.14.4}/apitally/client/request_logging.py +0 -0
  22. {apitally-0.14.2 → apitally-0.14.4}/apitally/client/requests.py +0 -0
  23. {apitally-0.14.2 → apitally-0.14.4}/apitally/client/server_errors.py +0 -0
  24. {apitally-0.14.2 → apitally-0.14.4}/apitally/client/validation_errors.py +0 -0
  25. {apitally-0.14.2 → apitally-0.14.4}/apitally/common.py +0 -0
  26. {apitally-0.14.2 → apitally-0.14.4}/apitally/django.py +0 -0
  27. {apitally-0.14.2 → apitally-0.14.4}/apitally/django_ninja.py +0 -0
  28. {apitally-0.14.2 → apitally-0.14.4}/apitally/django_rest_framework.py +0 -0
  29. {apitally-0.14.2 → apitally-0.14.4}/apitally/fastapi.py +0 -0
  30. {apitally-0.14.2 → apitally-0.14.4}/apitally/flask.py +0 -0
  31. {apitally-0.14.2 → apitally-0.14.4}/apitally/litestar.py +0 -0
  32. {apitally-0.14.2 → apitally-0.14.4}/apitally/py.typed +0 -0
  33. {apitally-0.14.2 → apitally-0.14.4}/renovate.json +0 -0
  34. {apitally-0.14.2 → apitally-0.14.4}/tests/__init__.py +0 -0
  35. {apitally-0.14.2 → apitally-0.14.4}/tests/conftest.py +0 -0
  36. {apitally-0.14.2 → apitally-0.14.4}/tests/constants.py +0 -0
  37. {apitally-0.14.2 → apitally-0.14.4}/tests/django_ninja_urls.py +0 -0
  38. {apitally-0.14.2 → apitally-0.14.4}/tests/django_rest_framework_urls.py +0 -0
  39. {apitally-0.14.2 → apitally-0.14.4}/tests/test_client_asyncio.py +0 -0
  40. {apitally-0.14.2 → apitally-0.14.4}/tests/test_client_consumers.py +0 -0
  41. {apitally-0.14.2 → apitally-0.14.4}/tests/test_client_request_logging.py +0 -0
  42. {apitally-0.14.2 → apitally-0.14.4}/tests/test_client_requests.py +0 -0
  43. {apitally-0.14.2 → apitally-0.14.4}/tests/test_client_server_errors.py +0 -0
  44. {apitally-0.14.2 → apitally-0.14.4}/tests/test_client_threading.py +0 -0
  45. {apitally-0.14.2 → apitally-0.14.4}/tests/test_client_validation_errors.py +0 -0
  46. {apitally-0.14.2 → apitally-0.14.4}/tests/test_django_ninja.py +0 -0
  47. {apitally-0.14.2 → apitally-0.14.4}/tests/test_django_rest_framework.py +0 -0
  48. {apitally-0.14.2 → apitally-0.14.4}/tests/test_fastapi.py +0 -0
  49. {apitally-0.14.2 → apitally-0.14.4}/tests/test_flask.py +0 -0
  50. {apitally-0.14.2 → apitally-0.14.4}/tests/test_litestar.py +0 -0
@@ -14,9 +14,9 @@ jobs:
14
14
  with:
15
15
  fetch-depth: 0
16
16
  - name: Install uv
17
- uses: astral-sh/setup-uv@v4
17
+ uses: astral-sh/setup-uv@v5
18
18
  with:
19
- version: "0.5.1"
19
+ version: "0.5.14"
20
20
  enable-cache: true
21
21
  - name: Build package
22
22
  run: uv build
@@ -24,12 +24,12 @@ jobs:
24
24
  steps:
25
25
  - uses: actions/checkout@v4
26
26
  - name: Install uv
27
- uses: astral-sh/setup-uv@v4
27
+ uses: astral-sh/setup-uv@v5
28
28
  with:
29
- version: "0.5.1"
29
+ version: "0.5.14"
30
30
  enable-cache: true
31
31
  - name: Install Python
32
- run: uv python install 3.12
32
+ run: uv python install 3.13
33
33
  - name: Install dependencies
34
34
  run: uv sync --all-extras --frozen
35
35
  - name: Run checks
@@ -81,9 +81,9 @@ jobs:
81
81
  steps:
82
82
  - uses: actions/checkout@v4
83
83
  - name: Install uv
84
- uses: astral-sh/setup-uv@v4
84
+ uses: astral-sh/setup-uv@v5
85
85
  with:
86
- version: "0.5.1"
86
+ version: "0.5.14"
87
87
  enable-cache: true
88
88
  - name: Install Python
89
89
  run: uv python install ${{ matrix.python }}
@@ -8,7 +8,7 @@ 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.8.2
11
+ rev: v0.8.6
12
12
  hooks:
13
13
  - id: ruff
14
14
  args: ["--fix", "--exit-non-zero-on-fix"]
@@ -1,12 +1,13 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: apitally
3
- Version: 0.14.2
3
+ Version: 0.14.4
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>API monitoring made easy.</b></p>
72
+ <p align="center"><b>Analytics, logging & monitoring for REST APIs.</b></p>
72
73
 
73
- <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>
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 (no sensitive data is captured)
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>API monitoring made easy.</b></p>
9
+ <p align="center"><b>Analytics, logging & monitoring for REST APIs.</b></p>
10
10
 
11
- <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>
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 (no sensitive data is captured)
41
+ requests and responses
42
42
  - Non-blocking clients that aggregate and send captured data to Apitally in
43
43
  regular intervals
44
44
 
@@ -6,7 +6,7 @@ import random
6
6
  import time
7
7
  from contextlib import suppress
8
8
  from functools import partial
9
- from typing import Any, AsyncIterator, Dict, Optional, Tuple, Union
9
+ from typing import Any, AsyncIterator, Dict, Optional, Union
10
10
  from uuid import UUID
11
11
 
12
12
  import backoff
@@ -40,7 +40,7 @@ class ApitallyClient(ApitallyClientBase):
40
40
  self.proxy = proxy
41
41
  self._stop_sync_loop = False
42
42
  self._sync_loop_task: Optional[asyncio.Task] = None
43
- self._sync_data_queue: asyncio.Queue[Tuple[float, Dict[str, Any]]] = asyncio.Queue()
43
+ self._sync_data_queue: asyncio.Queue[Dict[str, Any]] = asyncio.Queue()
44
44
  self._set_startup_data_task: Optional[asyncio.Task] = None
45
45
 
46
46
  def get_http_client(self) -> httpx.AsyncClient:
@@ -103,20 +103,19 @@ class ApitallyClient(ApitallyClientBase):
103
103
 
104
104
  async def send_sync_data(self, client: httpx.AsyncClient) -> None:
105
105
  data = self.get_sync_data()
106
- self._sync_data_queue.put_nowait((time.time(), data))
106
+ self._sync_data_queue.put_nowait(data)
107
107
 
108
108
  i = 0
109
109
  while not self._sync_data_queue.empty():
110
- timestamp, data = self._sync_data_queue.get_nowait()
110
+ data = self._sync_data_queue.get_nowait()
111
111
  try:
112
- if (time_offset := time.time() - timestamp) <= MAX_QUEUE_TIME:
112
+ if time.time() - data["timestamp"] <= MAX_QUEUE_TIME:
113
113
  if i > 0:
114
- await asyncio.sleep(random.uniform(0.1, 0.3))
115
- data["time_offset"] = time_offset
114
+ await asyncio.sleep(random.uniform(0.1, 0.5))
116
115
  await self._send_sync_data(client, data)
117
116
  i += 1
118
117
  except httpx.HTTPError:
119
- self._sync_data_queue.put_nowait((timestamp, data))
118
+ self._sync_data_queue.put_nowait(data)
120
119
  break
121
120
  finally:
122
121
  self._sync_data_queue.task_done()
@@ -89,6 +89,7 @@ class ApitallyClientBase(ABC):
89
89
 
90
90
  def get_sync_data(self) -> Dict[str, Any]:
91
91
  data = {
92
+ "timestamp": time.time(),
92
93
  "requests": self.request_counter.get_and_reset_requests(),
93
94
  "validation_errors": self.validation_error_counter.get_and_reset_validation_errors(),
94
95
  "server_errors": self.server_error_counter.get_and_reset_server_errors(),
@@ -8,7 +8,7 @@ from functools import partial
8
8
  from io import BufferedReader
9
9
  from queue import Queue
10
10
  from threading import Event, Thread
11
- from typing import Any, Callable, Dict, Optional, Tuple
11
+ from typing import Any, Callable, Dict, Optional
12
12
  from uuid import UUID
13
13
 
14
14
  import backoff
@@ -58,7 +58,7 @@ class ApitallyClient(ApitallyClientBase):
58
58
  self.proxies = {"https": proxy} if proxy else None
59
59
  self._thread: Optional[Thread] = None
60
60
  self._stop_sync_loop = Event()
61
- self._sync_data_queue: Queue[Tuple[float, Dict[str, Any]]] = Queue()
61
+ self._sync_data_queue: Queue[Dict[str, Any]] = Queue()
62
62
 
63
63
  def start_sync_loop(self) -> None:
64
64
  self._stop_sync_loop.clear()
@@ -114,20 +114,19 @@ class ApitallyClient(ApitallyClientBase):
114
114
 
115
115
  def send_sync_data(self, session: requests.Session) -> None:
116
116
  data = self.get_sync_data()
117
- self._sync_data_queue.put_nowait((time.time(), data))
117
+ self._sync_data_queue.put_nowait(data)
118
118
 
119
119
  i = 0
120
120
  while not self._sync_data_queue.empty():
121
- timestamp, data = self._sync_data_queue.get_nowait()
121
+ data = self._sync_data_queue.get_nowait()
122
122
  try:
123
- if (time_offset := time.time() - timestamp) <= MAX_QUEUE_TIME:
123
+ if time.time() - data["timestamp"] <= MAX_QUEUE_TIME:
124
124
  if i > 0:
125
- time.sleep(random.uniform(0.1, 0.3))
126
- data["time_offset"] = time_offset
125
+ time.sleep(random.uniform(0.1, 0.5))
127
126
  self._send_sync_data(session, data)
128
127
  i += 1
129
128
  except requests.RequestException:
130
- self._sync_data_queue.put_nowait((timestamp, data))
129
+ self._sync_data_queue.put_nowait(data)
131
130
  break
132
131
  finally:
133
132
  self._sync_data_queue.task_done()
@@ -222,12 +222,18 @@ class ApitallyMiddleware:
222
222
  },
223
223
  )
224
224
 
225
- @staticmethod
226
- def get_path(request: Request) -> Optional[str]:
227
- for route in request.app.routes:
228
- match, _ = route.matches(request.scope)
229
- if match == Match.FULL:
230
- return route.path
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.13.0",
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
- routes = [
83
- Route("/foo/", foo),
84
- Route("/foo/{bar}/", foo_bar),
85
- Route("/bar/", bar, methods=["POST"]),
86
- Route("/baz/", baz, methods=["POST"]),
87
- Route("/val/", val),
88
- Route("/stream/", stream),
89
- Route("/task/", task, methods=["POST"]),
90
- ]
91
- app = Starlette(routes=routes)
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
- @app.get("/foo/")
139
+ router = APIRouter()
140
+
141
+ @router.get("/foo")
129
142
  def foo():
130
143
  return "foo"
131
144
 
132
- @app.get("/foo/{bar}/")
145
+ @router.get("/foo/{bar}")
133
146
  def foo_bar(bar: str):
134
147
  return PlainTextResponse(f"foo: {bar}")
135
148
 
136
- @app.post("/bar/")
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
- @app.post("/baz/")
154
+ @router.post("/baz")
142
155
  def baz():
143
156
  raise ValueError("baz")
144
157
 
145
- @app.get("/val/")
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/?foo=bar", headers={"Test-Header": "test"})
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/?foo=bar"
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/", content=b"foo")
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/", content=b"foo")
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,9 @@ 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": "post", "path": "/test/task"} in data["paths"]
332
+ assert {"method": "get", "path": "/stream"} in data["paths"]
315
333
  assert data["versions"]["starlette"]
316
334
  assert data["versions"]["app"] == "1.2.3"
317
335
  assert data["client"] == "python:starlette"