apitally 0.16.3__py3-none-any.whl → 0.17.0__py3-none-any.whl

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/django.py CHANGED
@@ -10,8 +10,10 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Unio
10
10
  from warnings import warn
11
11
 
12
12
  from django.conf import settings
13
+ from django.contrib.admindocs.views import extract_views_from_urlpatterns, simplify_regex
13
14
  from django.urls import URLPattern, URLResolver, get_resolver
14
15
  from django.utils.module_loading import import_string
16
+ from django.views.generic.base import View
15
17
 
16
18
  from apitally.client.client_threading import ApitallyClient
17
19
  from apitally.client.consumers import Consumer as ApitallyConsumer
@@ -41,6 +43,7 @@ class ApitallyMiddlewareConfig:
41
43
  request_logging_config: Optional[RequestLoggingConfig]
42
44
  app_version: Optional[str]
43
45
  identify_consumer_callback: Optional[Callable[[HttpRequest], Union[str, ApitallyConsumer, None]]]
46
+ include_django_views: bool
44
47
  urlconfs: List[Optional[str]]
45
48
  proxy: Optional[str]
46
49
 
@@ -53,6 +56,7 @@ class ApitallyMiddleware:
53
56
  self.drf_available = _check_import("rest_framework")
54
57
  self.drf_endpoint_enumerator = None
55
58
  self.ninja_available = _check_import("ninja")
59
+ self.include_django_views = False
56
60
  self.callbacks = set()
57
61
 
58
62
  if self.config is None:
@@ -68,6 +72,9 @@ class ApitallyMiddleware:
68
72
  self.callbacks.update(_get_drf_callbacks(self.config.urlconfs))
69
73
  if self.ninja_available and None not in self.config.urlconfs:
70
74
  self.callbacks.update(_get_ninja_callbacks(self.config.urlconfs))
75
+ if self.config.include_django_views:
76
+ self.callbacks.update(_get_django_callbacks(self.config.urlconfs))
77
+ self.include_django_views = True
71
78
 
72
79
  self.client = ApitallyClient(
73
80
  client_id=self.config.client_id,
@@ -98,6 +105,7 @@ class ApitallyMiddleware:
98
105
  request_logging_config: Optional[RequestLoggingConfig] = None,
99
106
  app_version: Optional[str] = None,
100
107
  identify_consumer_callback: Optional[str] = None,
108
+ include_django_views: bool = False,
101
109
  urlconf: Optional[Union[List[Optional[str]], str]] = None,
102
110
  proxy: Optional[str] = None,
103
111
  ) -> None:
@@ -109,6 +117,7 @@ class ApitallyMiddleware:
109
117
  identify_consumer_callback=import_string(identify_consumer_callback)
110
118
  if identify_consumer_callback
111
119
  else None,
120
+ include_django_views=include_django_views,
112
121
  urlconfs=[urlconf] if urlconf is None or isinstance(urlconf, str) else urlconf,
113
122
  proxy=proxy,
114
123
  )
@@ -116,6 +125,7 @@ class ApitallyMiddleware:
116
125
  def __call__(self, request: HttpRequest) -> HttpResponse:
117
126
  if self.client.enabled and request.method is not None and request.method != "OPTIONS":
118
127
  timestamp = time.time()
128
+
119
129
  request_size = parse_int(request.headers.get("Content-Length"))
120
130
  request_body = b""
121
131
  if self.capture_request_body:
@@ -128,32 +138,35 @@ class ApitallyMiddleware:
128
138
  start_time = time.perf_counter()
129
139
  response = self.get_response(request)
130
140
  response_time = time.perf_counter() - start_time
131
- response_size = (
132
- parse_int(response["Content-Length"])
133
- if response.has_header("Content-Length")
134
- else (len(response.content) if not response.streaming else None)
135
- )
136
- response_body = b""
137
- response_content_type = response.get("Content-Type")
138
- if (
139
- self.capture_response_body
140
- and not response.streaming
141
- and RequestLogger.is_supported_content_type(response_content_type)
142
- ):
143
- response_body = (
144
- response.content if response_size is not None and response_size <= MAX_BODY_SIZE else BODY_TOO_LARGE
145
- )
146
-
147
- try:
148
- consumer = self.get_consumer(request)
149
- consumer_identifier = consumer.identifier if consumer else None
150
- self.client.consumer_registry.add_or_update_consumer(consumer)
151
- except Exception: # pragma: no cover
152
- logger.exception("Failed to get consumer for request")
153
- consumer_identifier = None
154
-
155
141
  path = self.get_path(request)
142
+
156
143
  if path is not None:
144
+ try:
145
+ consumer = self.get_consumer(request)
146
+ consumer_identifier = consumer.identifier if consumer else None
147
+ self.client.consumer_registry.add_or_update_consumer(consumer)
148
+ except Exception: # pragma: no cover
149
+ logger.exception("Failed to get consumer for request")
150
+ consumer_identifier = None
151
+
152
+ response_size = (
153
+ parse_int(response["Content-Length"])
154
+ if response.has_header("Content-Length")
155
+ else (len(response.content) if not response.streaming else None)
156
+ )
157
+ response_body = b""
158
+ response_content_type = response.get("Content-Type")
159
+ if (
160
+ self.capture_response_body
161
+ and not response.streaming
162
+ and RequestLogger.is_supported_content_type(response_content_type)
163
+ ):
164
+ response_body = (
165
+ response.content
166
+ if response_size is not None and response_size <= MAX_BODY_SIZE
167
+ else BODY_TOO_LARGE
168
+ )
169
+
157
170
  try:
158
171
  self.client.request_counter.add_request(
159
172
  consumer=consumer_identifier,
@@ -196,27 +209,27 @@ class ApitallyMiddleware:
196
209
  except Exception: # pragma: no cover
197
210
  logger.exception("Failed to log server error")
198
211
 
199
- if self.client.request_logger.enabled:
200
- self.client.request_logger.log_request(
201
- request={
202
- "timestamp": timestamp,
203
- "method": request.method,
204
- "path": path,
205
- "url": request.build_absolute_uri(),
206
- "headers": list(request.headers.items()),
207
- "size": request_size,
208
- "consumer": consumer_identifier,
209
- "body": request_body,
210
- },
211
- response={
212
- "status_code": response.status_code,
213
- "response_time": response_time,
214
- "headers": list(response.items()),
215
- "size": response_size,
216
- "body": response_body,
217
- },
218
- exception=getattr(request, "unhandled_exception", None),
219
- )
212
+ if self.client.request_logger.enabled:
213
+ self.client.request_logger.log_request(
214
+ request={
215
+ "timestamp": timestamp,
216
+ "method": request.method,
217
+ "path": path,
218
+ "url": request.build_absolute_uri(),
219
+ "headers": list(request.headers.items()),
220
+ "size": request_size,
221
+ "consumer": consumer_identifier,
222
+ "body": request_body,
223
+ },
224
+ response={
225
+ "status_code": response.status_code,
226
+ "response_time": response_time,
227
+ "headers": list(response.items()),
228
+ "size": response_size,
229
+ "body": response_body,
230
+ },
231
+ exception=getattr(request, "unhandled_exception", None),
232
+ )
220
233
  else:
221
234
  response = self.get_response(request)
222
235
 
@@ -240,8 +253,9 @@ class ApitallyMiddleware:
240
253
  from ninja.operation import PathView
241
254
 
242
255
  if hasattr(match.func, "__self__") and isinstance(match.func.__self__, PathView):
243
- path = "/" + match.route.lstrip("/")
244
- return re.sub(r"<(?:[^:]+:)?([^>:]+)>", r"{\1}", path)
256
+ return _transform_path(match.route)
257
+ if self.include_django_views:
258
+ return _transform_path(match.route)
245
259
  except Exception: # pragma: no cover
246
260
  logger.exception("Failed to get path for request")
247
261
  return None
@@ -263,10 +277,12 @@ class ApitallyMiddleware:
263
277
  return None
264
278
 
265
279
 
266
- def _get_startup_data(app_version: Optional[str], urlconfs: List[Optional[str]]) -> Dict[str, Any]:
280
+ def _get_startup_data(
281
+ app_version: Optional[str], urlconfs: List[Optional[str]], include_django_views: bool = False
282
+ ) -> Dict[str, Any]:
267
283
  data: Dict[str, Any] = {}
268
284
  try:
269
- data["paths"] = _get_paths(urlconfs)
285
+ data["paths"] = _get_paths(urlconfs, include_django_views=include_django_views)
270
286
  except Exception: # pragma: no cover
271
287
  data["paths"] = []
272
288
  logger.exception("Failed to get paths")
@@ -293,13 +309,26 @@ def _get_openapi(urlconfs: List[Optional[str]]) -> Optional[str]:
293
309
  return None # pragma: no cover
294
310
 
295
311
 
296
- def _get_paths(urlconfs: List[Optional[str]]) -> List[Dict[str, str]]:
312
+ def _get_paths(urlconfs: List[Optional[str]], include_django_views: bool = False) -> List[Dict[str, str]]:
297
313
  paths = []
298
314
  with contextlib.suppress(ImportError):
299
315
  paths.extend(_get_drf_paths(urlconfs))
300
316
  with contextlib.suppress(ImportError):
301
317
  paths.extend(_get_ninja_paths(urlconfs))
302
- return paths
318
+ if include_django_views:
319
+ paths.extend(_get_django_paths(urlconfs))
320
+ return _deduplicate_paths(paths)
321
+
322
+
323
+ def _deduplicate_paths(paths: List[Dict[str, str]]) -> List[Dict[str, str]]:
324
+ seen = set()
325
+ deduplicated_paths = []
326
+ for path in paths:
327
+ key = (path["method"], path["path"])
328
+ if key not in seen:
329
+ seen.add(key)
330
+ deduplicated_paths.append(path)
331
+ return deduplicated_paths
303
332
 
304
333
 
305
334
  def _get_drf_paths(urlconfs: List[Optional[str]]) -> List[Dict[str, str]]:
@@ -405,6 +434,37 @@ def _get_ninja_api_instances(
405
434
  return apis
406
435
 
407
436
 
437
+ def _get_django_paths(urlconfs: Optional[List[Optional[str]]] = None) -> List[Dict[str, str]]:
438
+ if urlconfs is None:
439
+ urlconfs = [None]
440
+ return [
441
+ {
442
+ "method": method.upper(),
443
+ "path": _transform_path(regex),
444
+ }
445
+ for urlconf in urlconfs
446
+ for callback, regex, _, _ in extract_views_from_urlpatterns(get_resolver(urlconf).url_patterns)
447
+ if hasattr(callback, "view_class") and issubclass(callback.view_class, View)
448
+ for method in callback.view_class.http_method_names
449
+ if method != "options" and hasattr(callback.view_class, method)
450
+ ]
451
+
452
+
453
+ def _get_django_callbacks(urlconfs: Optional[List[Optional[str]]] = None) -> Set[Callable]:
454
+ if urlconfs is None:
455
+ urlconfs = [None]
456
+ return {
457
+ callback
458
+ for urlconf in urlconfs
459
+ for callback, _, _, _ in extract_views_from_urlpatterns(get_resolver(urlconf).url_patterns)
460
+ }
461
+
462
+
463
+ def _transform_path(path: str) -> str:
464
+ path = simplify_regex(path)
465
+ return re.sub(r"<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>", r"{\g<parameter>}", path)
466
+
467
+
408
468
  def _check_import(name: str) -> bool:
409
469
  try:
410
470
  import_module(name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apitally
3
- Version: 0.16.3
3
+ Version: 0.17.0
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
@@ -1,7 +1,7 @@
1
1
  apitally/__init__.py,sha256=ShXQBVjyiSOHxoQJS2BvNG395W4KZfqMxZWBAR0MZrE,22
2
2
  apitally/blacksheep.py,sha256=KvcPFeiwQgWZmRglbm8SLaN6_WRs5kZ3SymB1IuLR-A,9616
3
3
  apitally/common.py,sha256=azDxepViH0QW0MuufTHxeSQyLGzCkocAX_KPziWTx8A,1605
4
- apitally/django.py,sha256=f_k7yYlvvvhJMR53NcXCfmlLxLX3CeLO9ephF4bzKbo,16892
4
+ apitally/django.py,sha256=MnyL6ntMYj1WndjLqpDZ-8BrbDWCeVV2dpZbe8Fnm-Y,19254
5
5
  apitally/django_ninja.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
6
6
  apitally/django_rest_framework.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
7
7
  apitally/fastapi.py,sha256=IfKfgsmIY8_AtnuMTW2sW4qnkya61CAE2vBoIpcc9tk,169
@@ -20,7 +20,7 @@ apitally/client/requests.py,sha256=SDptGOg9XvaEKFj2o3oxJz-JAuZzUrqpHnbOQixf99o,3
20
20
  apitally/client/sentry.py,sha256=qMjHdI0V7c50ruo1WjmjWc8g6oGDv724vSCvcuZ8G9k,1188
21
21
  apitally/client/server_errors.py,sha256=4B2BKDFoIpoWc55UVH6AIdYSgzj6zxCdMNUW77JjhZw,3423
22
22
  apitally/client/validation_errors.py,sha256=6G8WYWFgJs9VH9swvkPXJGuOJgymj5ooWA9OwjUTbuM,1964
23
- apitally-0.16.3.dist-info/METADATA,sha256=NbeWFQBfMfdu1Ao_0zGhBTMsNO3m1xmWZ6KNhGe2qNQ,9321
24
- apitally-0.16.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- apitally-0.16.3.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
26
- apitally-0.16.3.dist-info/RECORD,,
23
+ apitally-0.17.0.dist-info/METADATA,sha256=ERuU63GwG_N8quZ137RNkkTjfF0r3hAzoM514YjKfvs,9321
24
+ apitally-0.17.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ apitally-0.17.0.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
26
+ apitally-0.17.0.dist-info/RECORD,,