apitally 0.6.3__py3-none-any.whl → 0.7.1__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/__init__.py +1 -1
- apitally/common.py +31 -0
- apitally/django.py +216 -179
- apitally/flask.py +2 -14
- apitally/litestar.py +2 -14
- apitally/starlette.py +2 -18
- {apitally-0.6.3.dist-info → apitally-0.7.1.dist-info}/METADATA +9 -8
- {apitally-0.6.3.dist-info → apitally-0.7.1.dist-info}/RECORD +10 -9
- {apitally-0.6.3.dist-info → apitally-0.7.1.dist-info}/LICENSE +0 -0
- {apitally-0.6.3.dist-info → apitally-0.7.1.dist-info}/WHEEL +0 -0
apitally/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "0.
|
1
|
+
__version__ = "0.7.1"
|
apitally/common.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
import sys
|
2
|
+
from importlib.metadata import PackageNotFoundError, version
|
3
|
+
from typing import Dict, Optional
|
4
|
+
|
5
|
+
|
6
|
+
def get_versions(*packages, app_version: Optional[str] = None) -> Dict[str, str]:
|
7
|
+
versions = _get_common_package_versions()
|
8
|
+
for package in packages:
|
9
|
+
versions[package] = _get_package_version(package)
|
10
|
+
if app_version:
|
11
|
+
versions["app"] = app_version
|
12
|
+
return {n: v for n, v in versions.items() if v is not None}
|
13
|
+
|
14
|
+
|
15
|
+
def _get_common_package_versions() -> Dict[str, Optional[str]]:
|
16
|
+
return {
|
17
|
+
"python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
18
|
+
"apitally": _get_package_version("apitally"),
|
19
|
+
"uvicorn": _get_package_version("uvicorn"),
|
20
|
+
"hypercorn": _get_package_version("hypercorn"),
|
21
|
+
"daphne": _get_package_version("daphne"),
|
22
|
+
"gunicorn": _get_package_version("gunicorn"),
|
23
|
+
"uwsgi": _get_package_version("uwsgi"),
|
24
|
+
}
|
25
|
+
|
26
|
+
|
27
|
+
def _get_package_version(name: str) -> Optional[str]:
|
28
|
+
try:
|
29
|
+
return version(name)
|
30
|
+
except PackageNotFoundError:
|
31
|
+
return None
|
apitally/django.py
CHANGED
@@ -1,26 +1,29 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import contextlib
|
3
4
|
import json
|
4
|
-
import
|
5
|
+
import re
|
5
6
|
import time
|
6
7
|
from dataclasses import dataclass
|
7
|
-
from importlib
|
8
|
-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
|
8
|
+
from importlib import import_module
|
9
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Union
|
9
10
|
|
10
11
|
from django.conf import settings
|
11
|
-
from django.
|
12
|
-
from django.test import RequestFactory
|
13
|
-
from django.urls import Resolver404, URLPattern, URLResolver, get_resolver, resolve
|
12
|
+
from django.urls import URLPattern, URLResolver, get_resolver
|
14
13
|
from django.utils.module_loading import import_string
|
15
14
|
|
15
|
+
from apitally.client.logging import get_logger
|
16
16
|
from apitally.client.threading import ApitallyClient
|
17
|
+
from apitally.common import get_versions
|
17
18
|
|
18
19
|
|
19
20
|
if TYPE_CHECKING:
|
20
21
|
from django.http import HttpRequest, HttpResponse
|
22
|
+
from ninja import NinjaAPI
|
21
23
|
|
22
24
|
|
23
25
|
__all__ = ["ApitallyMiddleware"]
|
26
|
+
logger = get_logger(__name__)
|
24
27
|
|
25
28
|
|
26
29
|
@dataclass
|
@@ -28,8 +31,8 @@ class ApitallyMiddlewareConfig:
|
|
28
31
|
client_id: str
|
29
32
|
env: str
|
30
33
|
app_version: Optional[str]
|
31
|
-
openapi_url: Optional[str]
|
32
34
|
identify_consumer_callback: Optional[Callable[[HttpRequest], Optional[str]]]
|
35
|
+
urlconfs: List[Optional[str]]
|
33
36
|
|
34
37
|
|
35
38
|
class ApitallyMiddleware:
|
@@ -37,21 +40,31 @@ class ApitallyMiddleware:
|
|
37
40
|
|
38
41
|
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
|
39
42
|
self.get_response = get_response
|
43
|
+
self.drf_available = _check_import("rest_framework")
|
44
|
+
self.drf_endpoint_enumerator = None
|
45
|
+
self.ninja_available = _check_import("ninja")
|
46
|
+
self.callbacks = set()
|
47
|
+
|
40
48
|
if self.config is None:
|
41
49
|
config = getattr(settings, "APITALLY_MIDDLEWARE", {})
|
42
50
|
self.configure(**config)
|
43
51
|
assert self.config is not None
|
44
|
-
|
45
|
-
self.
|
46
|
-
|
47
|
-
|
52
|
+
|
53
|
+
if self.drf_available:
|
54
|
+
from rest_framework.schemas.generators import EndpointEnumerator
|
55
|
+
|
56
|
+
self.drf_endpoint_enumerator = EndpointEnumerator()
|
57
|
+
if None not in self.config.urlconfs:
|
58
|
+
self.callbacks.update(_get_drf_callbacks(self.config.urlconfs))
|
59
|
+
if self.ninja_available and None not in self.config.urlconfs:
|
60
|
+
self.callbacks.update(_get_ninja_callbacks(self.config.urlconfs))
|
61
|
+
|
48
62
|
self.client = ApitallyClient(client_id=self.config.client_id, env=self.config.env)
|
49
63
|
self.client.start_sync_loop()
|
50
64
|
self.client.set_app_info(
|
51
65
|
app_info=_get_app_info(
|
52
|
-
views=views,
|
53
66
|
app_version=self.config.app_version,
|
54
|
-
|
67
|
+
urlconfs=self.config.urlconfs,
|
55
68
|
)
|
56
69
|
)
|
57
70
|
|
@@ -61,212 +74,236 @@ class ApitallyMiddleware:
|
|
61
74
|
client_id: str,
|
62
75
|
env: str = "dev",
|
63
76
|
app_version: Optional[str] = None,
|
64
|
-
openapi_url: Optional[str] = None,
|
65
77
|
identify_consumer_callback: Optional[str] = None,
|
78
|
+
urlconf: Optional[Union[List[Optional[str]], str]] = None,
|
66
79
|
) -> None:
|
67
80
|
cls.config = ApitallyMiddlewareConfig(
|
68
81
|
client_id=client_id,
|
69
82
|
env=env,
|
70
83
|
app_version=app_version,
|
71
|
-
openapi_url=openapi_url,
|
72
84
|
identify_consumer_callback=import_string(identify_consumer_callback)
|
73
85
|
if identify_consumer_callback
|
74
86
|
else None,
|
87
|
+
urlconfs=[urlconf] if urlconf is None or isinstance(urlconf, str) else urlconf,
|
75
88
|
)
|
76
89
|
|
77
90
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
78
|
-
view = self.get_view(request)
|
79
91
|
start_time = time.perf_counter()
|
80
92
|
response = self.get_response(request)
|
81
|
-
|
93
|
+
response_time = time.perf_counter() - start_time
|
94
|
+
path = self.get_path(request)
|
95
|
+
if request.method is not None and path is not None:
|
82
96
|
consumer = self.get_consumer(request)
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
97
|
+
try:
|
98
|
+
self.client.request_counter.add_request(
|
99
|
+
consumer=consumer,
|
100
|
+
method=request.method,
|
101
|
+
path=path,
|
102
|
+
status_code=response.status_code,
|
103
|
+
response_time=response_time,
|
104
|
+
request_size=request.headers.get("Content-Length"),
|
105
|
+
response_size=response["Content-Length"]
|
106
|
+
if response.has_header("Content-Length")
|
107
|
+
else (len(response.content) if not response.streaming else None),
|
108
|
+
)
|
109
|
+
except Exception: # pragma: no cover
|
110
|
+
logger.exception("Failed to log request metadata")
|
94
111
|
if (
|
95
112
|
response.status_code == 422
|
96
113
|
and (content_type := response.get("Content-Type")) is not None
|
97
114
|
and content_type.startswith("application/json")
|
98
115
|
):
|
99
116
|
try:
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
117
|
+
with contextlib.suppress(json.JSONDecodeError):
|
118
|
+
body = json.loads(response.content)
|
119
|
+
if isinstance(body, dict) and "detail" in body and isinstance(body["detail"], list):
|
120
|
+
# Log Django Ninja / Pydantic validation errors
|
121
|
+
self.client.validation_error_counter.add_validation_errors(
|
122
|
+
consumer=consumer,
|
123
|
+
method=request.method,
|
124
|
+
path=path,
|
125
|
+
detail=body["detail"],
|
126
|
+
)
|
127
|
+
except Exception: # pragma: no cover
|
128
|
+
logger.exception("Failed to log validation errors")
|
111
129
|
return response
|
112
130
|
|
113
|
-
def
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
131
|
+
def get_path(self, request: HttpRequest) -> Optional[str]:
|
132
|
+
if (match := request.resolver_match) is not None:
|
133
|
+
try:
|
134
|
+
if self.callbacks and match.func not in self.callbacks:
|
135
|
+
return None
|
136
|
+
if self.drf_endpoint_enumerator is not None:
|
137
|
+
from rest_framework.schemas.generators import is_api_view
|
138
|
+
|
139
|
+
if is_api_view(match.func):
|
140
|
+
return self.drf_endpoint_enumerator.get_path_from_regex(match.route)
|
141
|
+
if self.ninja_available:
|
142
|
+
from ninja.operation import PathView
|
143
|
+
|
144
|
+
if hasattr(match.func, "__self__") and isinstance(match.func.__self__, PathView):
|
145
|
+
path = "/" + match.route.lstrip("/")
|
146
|
+
return re.sub(r"<(?:[^:]+:)?([^>:]+)>", r"{\1}", path)
|
147
|
+
except Exception: # pragma: no cover
|
148
|
+
logger.exception("Failed to get path for request")
|
149
|
+
return None
|
119
150
|
|
120
151
|
def get_consumer(self, request: HttpRequest) -> Optional[str]:
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
152
|
+
try:
|
153
|
+
if hasattr(request, "consumer_identifier"):
|
154
|
+
return str(request.consumer_identifier)
|
155
|
+
if self.config is not None and self.config.identify_consumer_callback is not None:
|
156
|
+
consumer_identifier = self.config.identify_consumer_callback(request)
|
157
|
+
if consumer_identifier is not None:
|
158
|
+
return str(consumer_identifier)
|
159
|
+
except Exception: # pragma: no cover
|
160
|
+
logger.exception("Failed to get consumer identifier for request")
|
127
161
|
return None
|
128
162
|
|
129
163
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
return hasattr(self.func, "view_class") and issubclass(self.func.view_class, APIView)
|
146
|
-
except ImportError: # pragma: no cover
|
147
|
-
return False
|
164
|
+
def _get_app_info(app_version: Optional[str], urlconfs: List[Optional[str]]) -> Dict[str, Any]:
|
165
|
+
app_info: Dict[str, Any] = {}
|
166
|
+
try:
|
167
|
+
app_info["paths"] = _get_paths(urlconfs)
|
168
|
+
except Exception: # pragma: no cover
|
169
|
+
app_info["paths"] = []
|
170
|
+
logger.exception("Failed to get paths")
|
171
|
+
try:
|
172
|
+
app_info["openapi"] = _get_openapi(urlconfs)
|
173
|
+
except Exception: # pragma: no cover
|
174
|
+
logger.exception("Failed to get OpenAPI schema")
|
175
|
+
app_info["versions"] = get_versions("django", "djangorestframework", "django-ninja", app_version=app_version)
|
176
|
+
app_info["client"] = "python:django"
|
177
|
+
return app_info
|
148
178
|
|
149
|
-
@property
|
150
|
-
def is_ninja_path_view(self) -> bool:
|
151
|
-
try:
|
152
|
-
from ninja.operation import PathView
|
153
179
|
|
154
|
-
|
155
|
-
|
156
|
-
|
180
|
+
def _get_openapi(urlconfs: List[Optional[str]]) -> Optional[str]:
|
181
|
+
drf_schema = None
|
182
|
+
ninja_schema = None
|
183
|
+
with contextlib.suppress(ImportError):
|
184
|
+
drf_schema = _get_drf_schema(urlconfs)
|
185
|
+
with contextlib.suppress(ImportError):
|
186
|
+
ninja_schema = _get_ninja_schema(urlconfs)
|
187
|
+
if drf_schema is not None and ninja_schema is None:
|
188
|
+
return json.dumps(drf_schema)
|
189
|
+
elif ninja_schema is not None and drf_schema is None:
|
190
|
+
return json.dumps(ninja_schema)
|
191
|
+
return None # pragma: no cover
|
157
192
|
|
158
|
-
@property
|
159
|
-
def allowed_methods(self) -> List[str]:
|
160
|
-
if hasattr(self.func, "view_class"):
|
161
|
-
return [method.upper() for method in self.func.view_class().allowed_methods]
|
162
|
-
if self.is_ninja_path_view:
|
163
|
-
assert hasattr(self.func, "__self__")
|
164
|
-
return [method.upper() for operation in self.func.__self__.operations for method in operation.methods]
|
165
|
-
return [] # pragma: no cover
|
166
193
|
|
194
|
+
def _get_paths(urlconfs: List[Optional[str]]) -> List[Dict[str, str]]:
|
195
|
+
paths = []
|
196
|
+
with contextlib.suppress(ImportError):
|
197
|
+
paths.extend(_get_drf_paths(urlconfs))
|
198
|
+
with contextlib.suppress(ImportError):
|
199
|
+
paths.extend(_get_ninja_paths(urlconfs))
|
200
|
+
return paths
|
167
201
|
|
168
|
-
def _get_app_info(
|
169
|
-
views: List[DjangoViewInfo], app_version: Optional[str] = None, openapi_url: Optional[str] = None
|
170
|
-
) -> Dict[str, Any]:
|
171
|
-
app_info: Dict[str, Any] = {}
|
172
|
-
if openapi := _get_openapi(views, openapi_url):
|
173
|
-
app_info["openapi"] = openapi
|
174
|
-
app_info["paths"] = _get_paths(views)
|
175
|
-
app_info["versions"] = _get_versions(app_version)
|
176
|
-
app_info["client"] = "python:django"
|
177
|
-
return app_info
|
178
202
|
|
203
|
+
def _get_drf_paths(urlconfs: List[Optional[str]]) -> List[Dict[str, str]]:
|
204
|
+
from rest_framework.schemas.generators import EndpointEnumerator
|
179
205
|
|
180
|
-
|
206
|
+
enumerators = [EndpointEnumerator(urlconf=urlconf) for urlconf in urlconfs]
|
181
207
|
return [
|
182
|
-
{
|
183
|
-
|
184
|
-
|
185
|
-
|
208
|
+
{
|
209
|
+
"method": method.upper(),
|
210
|
+
"path": path,
|
211
|
+
}
|
212
|
+
for enumerator in enumerators
|
213
|
+
for path, method, _ in enumerator.get_api_endpoints()
|
186
214
|
if method not in ["HEAD", "OPTIONS"]
|
187
215
|
]
|
188
216
|
|
189
217
|
|
190
|
-
def
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
for
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
)
|
236
|
-
elif hasattr(p, "_get_callback"):
|
237
|
-
try:
|
238
|
-
views.append(DjangoViewInfo(func=p._get_callback(), pattern=base + str(p.pattern), name=p.name))
|
239
|
-
except ViewDoesNotExist:
|
240
|
-
continue
|
241
|
-
elif hasattr(p, "url_patterns"):
|
242
|
-
try:
|
243
|
-
patterns = p.url_patterns
|
244
|
-
except ImportError:
|
245
|
-
continue
|
246
|
-
views.extend(
|
247
|
-
_extract_views_from_url_patterns(
|
248
|
-
patterns,
|
249
|
-
base + str(p.pattern),
|
250
|
-
namespace=namespace,
|
251
|
-
)
|
252
|
-
)
|
253
|
-
return views
|
218
|
+
def _get_drf_callbacks(urlconfs: List[Optional[str]]) -> Set[Callable]:
|
219
|
+
from rest_framework.schemas.generators import EndpointEnumerator
|
220
|
+
|
221
|
+
enumerators = [EndpointEnumerator(urlconf=urlconf) for urlconf in urlconfs]
|
222
|
+
return {callback for enumerator in enumerators for _, _, callback in enumerator.get_api_endpoints()}
|
223
|
+
|
224
|
+
|
225
|
+
def _get_drf_schema(urlconfs: List[Optional[str]]) -> Optional[Dict[str, Any]]:
|
226
|
+
from rest_framework.schemas.openapi import SchemaGenerator
|
227
|
+
|
228
|
+
schemas = []
|
229
|
+
with contextlib.suppress(AssertionError): # uritemplate and inflection must be installed for OpenAPI schema support
|
230
|
+
for urlconf in urlconfs:
|
231
|
+
generator = SchemaGenerator(urlconf=urlconf)
|
232
|
+
schema = generator.get_schema()
|
233
|
+
if schema is not None and len(schema["paths"]) > 0:
|
234
|
+
schemas.append(schema)
|
235
|
+
return None if len(schemas) != 1 else schemas[0] # type: ignore[return-value]
|
236
|
+
|
237
|
+
|
238
|
+
def _get_ninja_paths(urlconfs: List[Optional[str]]) -> List[Dict[str, str]]:
|
239
|
+
endpoints = []
|
240
|
+
for api in _get_ninja_api_instances(urlconfs=urlconfs):
|
241
|
+
schema = api.get_openapi_schema()
|
242
|
+
for path, operations in schema["paths"].items():
|
243
|
+
for method, operation in operations.items():
|
244
|
+
if method not in ["HEAD", "OPTIONS"]:
|
245
|
+
endpoints.append(
|
246
|
+
{
|
247
|
+
"method": method,
|
248
|
+
"path": path,
|
249
|
+
"summary": operation.get("summary"),
|
250
|
+
"description": operation.get("description"),
|
251
|
+
}
|
252
|
+
)
|
253
|
+
return endpoints
|
254
|
+
|
255
|
+
|
256
|
+
def _get_ninja_callbacks(urlconfs: List[Optional[str]]) -> Set[Callable]:
|
257
|
+
return {
|
258
|
+
path_view.get_view()
|
259
|
+
for api in _get_ninja_api_instances(urlconfs=urlconfs)
|
260
|
+
for _, router in api._routers
|
261
|
+
for path_view in router.path_operations.values()
|
262
|
+
}
|
254
263
|
|
255
264
|
|
256
|
-
def
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
265
|
+
def _get_ninja_schema(urlconfs: List[Optional[str]]) -> Optional[Dict[str, Any]]:
|
266
|
+
schemas = []
|
267
|
+
for api in _get_ninja_api_instances(urlconfs=urlconfs):
|
268
|
+
schema = api.get_openapi_schema()
|
269
|
+
if len(schema["paths"]) > 0:
|
270
|
+
schemas.append(schema)
|
271
|
+
return None if len(schemas) != 1 else schemas[0]
|
272
|
+
|
273
|
+
|
274
|
+
def _get_ninja_api_instances(
|
275
|
+
urlconfs: Optional[List[Optional[str]]] = None,
|
276
|
+
patterns: Optional[List[Any]] = None,
|
277
|
+
) -> Set[NinjaAPI]:
|
278
|
+
from ninja import NinjaAPI
|
279
|
+
|
280
|
+
if urlconfs is None:
|
281
|
+
urlconfs = [None]
|
282
|
+
if patterns is None:
|
283
|
+
patterns = []
|
284
|
+
for urlconf in urlconfs:
|
285
|
+
patterns.extend(get_resolver(urlconf).url_patterns)
|
286
|
+
|
287
|
+
apis: Set[NinjaAPI] = set()
|
288
|
+
for p in patterns:
|
289
|
+
if isinstance(p, URLResolver):
|
290
|
+
if p.app_name != "ninja":
|
291
|
+
apis.update(_get_ninja_api_instances(patterns=p.url_patterns))
|
292
|
+
else:
|
293
|
+
for pattern in p.url_patterns:
|
294
|
+
if isinstance(pattern, URLPattern) and pattern.lookup_str.startswith("ninja."):
|
295
|
+
callback_keywords = getattr(pattern.callback, "keywords", {})
|
296
|
+
if isinstance(callback_keywords, dict):
|
297
|
+
api = callback_keywords.get("api")
|
298
|
+
if isinstance(api, NinjaAPI):
|
299
|
+
apis.add(api)
|
300
|
+
break
|
301
|
+
return apis
|
302
|
+
|
303
|
+
|
304
|
+
def _check_import(name: str) -> bool:
|
266
305
|
try:
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
versions["app"] = app_version
|
272
|
-
return versions
|
306
|
+
import_module(name)
|
307
|
+
return True
|
308
|
+
except ImportError:
|
309
|
+
return False
|
apitally/flask.py
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import sys
|
4
3
|
import time
|
5
|
-
from importlib.metadata import version
|
6
4
|
from threading import Timer
|
7
5
|
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple
|
8
6
|
|
@@ -12,6 +10,7 @@ from werkzeug.exceptions import NotFound
|
|
12
10
|
from werkzeug.test import Client
|
13
11
|
|
14
12
|
from apitally.client.threading import ApitallyClient
|
13
|
+
from apitally.common import get_versions
|
15
14
|
|
16
15
|
|
17
16
|
if TYPE_CHECKING:
|
@@ -103,7 +102,7 @@ def _get_app_info(app: Flask, app_version: Optional[str] = None, openapi_url: Op
|
|
103
102
|
app_info["openapi"] = openapi
|
104
103
|
if paths := _get_paths(app.url_map):
|
105
104
|
app_info["paths"] = paths
|
106
|
-
app_info["versions"] =
|
105
|
+
app_info["versions"] = get_versions("flask", app_version=app_version)
|
107
106
|
app_info["client"] = "python:flask"
|
108
107
|
return app_info
|
109
108
|
|
@@ -124,14 +123,3 @@ def _get_openapi(app: WSGIApplication, openapi_url: str) -> Optional[str]:
|
|
124
123
|
if response.status_code != 200:
|
125
124
|
return None
|
126
125
|
return response.get_data(as_text=True)
|
127
|
-
|
128
|
-
|
129
|
-
def _get_versions(app_version: Optional[str]) -> Dict[str, str]:
|
130
|
-
versions = {
|
131
|
-
"python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
132
|
-
"apitally": version("apitally"),
|
133
|
-
"flask": version("flask"),
|
134
|
-
}
|
135
|
-
if app_version:
|
136
|
-
versions["app"] = app_version
|
137
|
-
return versions
|
apitally/litestar.py
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
import contextlib
|
2
2
|
import json
|
3
|
-
import sys
|
4
3
|
import time
|
5
|
-
from importlib.metadata import version
|
6
4
|
from typing import Callable, Dict, List, Optional
|
7
5
|
|
8
6
|
from litestar.app import DEFAULT_OPENAPI_CONFIG, Litestar
|
@@ -15,6 +13,7 @@ from litestar.plugins import InitPluginProtocol
|
|
15
13
|
from litestar.types import ASGIApp, Message, Receive, Scope, Send
|
16
14
|
|
17
15
|
from apitally.client.asyncio import ApitallyClient
|
16
|
+
from apitally.common import get_versions
|
18
17
|
|
19
18
|
|
20
19
|
__all__ = ["ApitallyPlugin"]
|
@@ -48,7 +47,7 @@ class ApitallyPlugin(InitPluginProtocol):
|
|
48
47
|
app_info = {
|
49
48
|
"openapi": _get_openapi(app),
|
50
49
|
"paths": [route for route in _get_routes(app) if not self.filter_path(route["path"])],
|
51
|
-
"versions":
|
50
|
+
"versions": get_versions("litestar", app_version=self.app_version),
|
52
51
|
"client": "python:litestar",
|
53
52
|
}
|
54
53
|
self.client.set_app_info(app_info)
|
@@ -174,14 +173,3 @@ def _get_routes(app: Litestar) -> List[Dict[str, str]]:
|
|
174
173
|
for method in route.methods
|
175
174
|
if route.scope_type == ScopeType.HTTP and method != "OPTIONS"
|
176
175
|
]
|
177
|
-
|
178
|
-
|
179
|
-
def _get_versions(app_version: Optional[str]) -> Dict[str, str]:
|
180
|
-
versions = {
|
181
|
-
"python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
182
|
-
"apitally": version("apitally"),
|
183
|
-
"litestar": version("litestar"),
|
184
|
-
}
|
185
|
-
if app_version:
|
186
|
-
versions["app"] = app_version
|
187
|
-
return versions
|
apitally/starlette.py
CHANGED
@@ -2,9 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
import json
|
5
|
-
import sys
|
6
5
|
import time
|
7
|
-
from importlib.metadata import PackageNotFoundError, version
|
8
6
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union
|
9
7
|
|
10
8
|
from httpx import HTTPStatusError
|
@@ -17,6 +15,7 @@ from starlette.testclient import TestClient
|
|
17
15
|
from starlette.types import ASGIApp
|
18
16
|
|
19
17
|
from apitally.client.asyncio import ApitallyClient
|
18
|
+
from apitally.common import get_versions
|
20
19
|
|
21
20
|
|
22
21
|
if TYPE_CHECKING:
|
@@ -145,7 +144,7 @@ def _get_app_info(app: ASGIApp, app_version: Optional[str] = None, openapi_url:
|
|
145
144
|
app_info["openapi"] = openapi
|
146
145
|
if endpoints := _get_endpoint_info(app):
|
147
146
|
app_info["paths"] = [{"path": endpoint.path, "method": endpoint.http_method} for endpoint in endpoints]
|
148
|
-
app_info["versions"] =
|
147
|
+
app_info["versions"] = get_versions("fastapi", "starlette", app_version=app_version)
|
149
148
|
app_info["client"] = "python:starlette"
|
150
149
|
return app_info
|
151
150
|
|
@@ -179,18 +178,3 @@ def _register_shutdown_handler(app: Union[ASGIApp, Router], shutdown_handler: Ca
|
|
179
178
|
app.add_event_handler("shutdown", shutdown_handler)
|
180
179
|
elif hasattr(app, "app"):
|
181
180
|
_register_shutdown_handler(app.app, shutdown_handler)
|
182
|
-
|
183
|
-
|
184
|
-
def _get_versions(app_version: Optional[str]) -> Dict[str, str]:
|
185
|
-
versions = {
|
186
|
-
"python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
187
|
-
"apitally": version("apitally"),
|
188
|
-
"starlette": version("starlette"),
|
189
|
-
}
|
190
|
-
try:
|
191
|
-
versions["fastapi"] = version("fastapi")
|
192
|
-
except PackageNotFoundError: # pragma: no cover
|
193
|
-
pass
|
194
|
-
if app_version:
|
195
|
-
versions["app"] = app_version
|
196
|
-
return versions
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: apitally
|
3
|
-
Version: 0.
|
4
|
-
Summary: API monitoring for REST APIs built with FastAPI, Flask, Django, and
|
3
|
+
Version: 0.7.1
|
4
|
+
Summary: API monitoring for REST APIs built with FastAPI, Flask, Django, Starlette and Litestar.
|
5
5
|
Home-page: https://apitally.io
|
6
6
|
License: MIT
|
7
7
|
Author: Apitally
|
@@ -21,7 +21,6 @@ Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Topic :: Internet :: WWW/HTTP
|
22
22
|
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
|
23
23
|
Classifier: Typing :: Typed
|
24
|
-
Provides-Extra: django
|
25
24
|
Provides-Extra: django-ninja
|
26
25
|
Provides-Extra: django-rest-framework
|
27
26
|
Provides-Extra: fastapi
|
@@ -29,15 +28,17 @@ Provides-Extra: flask
|
|
29
28
|
Provides-Extra: litestar
|
30
29
|
Provides-Extra: starlette
|
31
30
|
Requires-Dist: backoff (>=2.0.0)
|
32
|
-
Requires-Dist: django (>=2.2) ; extra == "django
|
31
|
+
Requires-Dist: django (>=2.2) ; extra == "django-ninja" or extra == "django-rest-framework"
|
33
32
|
Requires-Dist: django-ninja (>=0.18.0) ; extra == "django-ninja"
|
34
33
|
Requires-Dist: djangorestframework (>=3.10.0) ; extra == "django-rest-framework"
|
35
34
|
Requires-Dist: fastapi (>=0.87.0) ; extra == "fastapi"
|
36
35
|
Requires-Dist: flask (>=2.0.0) ; extra == "flask"
|
37
36
|
Requires-Dist: httpx (>=0.22.0) ; extra == "fastapi" or extra == "litestar" or extra == "starlette"
|
37
|
+
Requires-Dist: inflection (>=0.5.1) ; extra == "django-rest-framework"
|
38
38
|
Requires-Dist: litestar (>=2.0.0) ; extra == "litestar"
|
39
|
-
Requires-Dist: requests (>=2.26.0) ; extra == "django
|
39
|
+
Requires-Dist: requests (>=2.26.0) ; extra == "django-ninja" or extra == "django-rest-framework" or extra == "flask"
|
40
40
|
Requires-Dist: starlette (>=0.21.0,<1.0.0) ; extra == "fastapi" or extra == "starlette"
|
41
|
+
Requires-Dist: uritemplate (>=3.0.0) ; extra == "django-rest-framework"
|
41
42
|
Project-URL: Documentation, https://docs.apitally.io
|
42
43
|
Project-URL: Repository, https://github.com/apitally/python-client
|
43
44
|
Description-Content-Type: text/markdown
|
@@ -52,7 +53,7 @@ Description-Content-Type: text/markdown
|
|
52
53
|
|
53
54
|
<p align="center"><b>API monitoring made easy.</b></p>
|
54
55
|
|
55
|
-
<p align="center"><i>Apitally is a simple and affordable API monitoring
|
56
|
+
<p align="center"><i>Apitally is a simple and affordable API monitoring 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>
|
56
57
|
|
57
58
|
<p align="center">🔗 <b><a href="https://apitally.io" target="_blank">apitally.io</a></b></p>
|
58
59
|
|
@@ -95,8 +96,8 @@ example:
|
|
95
96
|
pip install apitally[fastapi]
|
96
97
|
```
|
97
98
|
|
98
|
-
The available extras are: `fastapi`, `
|
99
|
-
`litestar`.
|
99
|
+
The available extras are: `fastapi`, `flask`, `django_rest_framework`,
|
100
|
+
`django_ninja`, `starlette` and `litestar`.
|
100
101
|
|
101
102
|
## Usage
|
102
103
|
|
@@ -1,18 +1,19 @@
|
|
1
|
-
apitally/__init__.py,sha256=
|
1
|
+
apitally/__init__.py,sha256=2KJZDSMOG7KS82AxYOrZ4ZihYxX0wjfUjDsIZh3L024,22
|
2
2
|
apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
apitally/client/asyncio.py,sha256=uR5JlH37G6gZvAJ7A1gYOGkjn3zjC-4I6avA1fncXHs,4433
|
4
4
|
apitally/client/base.py,sha256=E_yUTItAtZWPOx80K3Pm55CNBgF_NZ6RMyXZTnjSV9c,8511
|
5
5
|
apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
|
6
6
|
apitally/client/threading.py,sha256=ihQzUStrSQFynpqXgFpseAXrHuc5Et1QvG-YHlzqDr8,4831
|
7
|
-
apitally/
|
7
|
+
apitally/common.py,sha256=GbVmnXxhRvV30d7CfCQ9r0AeXj14Mv9Jm_Yd1bRWP28,1088
|
8
|
+
apitally/django.py,sha256=SHSM00eRsvoJC2d4c4tK6gctKLnxLfrw4LoZ4CWPm4U,12124
|
8
9
|
apitally/django_ninja.py,sha256=iMvZd7j04nbOLpJgYxs7tpbsyXlZuhmHjcswXMvyUlU,82
|
9
10
|
apitally/django_rest_framework.py,sha256=iMvZd7j04nbOLpJgYxs7tpbsyXlZuhmHjcswXMvyUlU,82
|
10
11
|
apitally/fastapi.py,sha256=Q3n2bVREKQ_V_2yCQ48ngPtr-NJxDskpT_l20xhSbpM,85
|
11
|
-
apitally/flask.py,sha256
|
12
|
-
apitally/litestar.py,sha256=
|
12
|
+
apitally/flask.py,sha256=xxyrHchMiPuEMP3c_Us_niJn9K7x87RDvxD5GtntEvU,4769
|
13
|
+
apitally/litestar.py,sha256=Pl1tEbxve3vqrdflWdvZjPFrEVxNIr6NjuQVf4YSpzY,7171
|
13
14
|
apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
14
|
-
apitally/starlette.py,sha256=
|
15
|
-
apitally-0.
|
16
|
-
apitally-0.
|
17
|
-
apitally-0.
|
18
|
-
apitally-0.
|
15
|
+
apitally/starlette.py,sha256=9VKGdNuKPrRcnQv7GeyV5cLa7TpgidxMVnAKSxMsWjI,7345
|
16
|
+
apitally-0.7.1.dist-info/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
|
17
|
+
apitally-0.7.1.dist-info/METADATA,sha256=SO16kB-1EwZGAm28P4m4E2EMltFfmKUy4uF44X4LB30,6736
|
18
|
+
apitally-0.7.1.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
|
19
|
+
apitally-0.7.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|