apitally 0.18.0__py3-none-any.whl → 0.18.2__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.
@@ -97,8 +97,8 @@ class RequestLoggingConfig:
97
97
  log_exception: Whether to log unhandled exceptions in case of server errors
98
98
  mask_query_params: Query parameter names to mask in logs. Expects regular expressions.
99
99
  mask_headers: Header names to mask in logs. Expects regular expressions.
100
- mask_request_body_callback: Callback to mask the request body. Expects (method, path, body) and returns the masked body as bytes or None.
101
- mask_response_body_callback: Callback to mask the response body. Expects (method, path, body) and returns the masked body as bytes or None.
100
+ mask_request_body_callback: Callback to mask the request body. Expects `request: RequestDict` as argument and returns the masked body as bytes or None.
101
+ mask_response_body_callback: Callback to mask the response body. Expects `request: RequestDict` and `response: ResponseDict` as arguments and returns the masked body as bytes or None.
102
102
  exclude_paths: Paths to exclude from logging. Expects regular expressions.
103
103
  exclude_callback: Callback to exclude requests from logging. Should expect two arguments, `request: RequestDict` and `response: ResponseDict`, and return True to exclude the request.
104
104
  """
apitally/django.py CHANGED
@@ -123,115 +123,114 @@ class ApitallyMiddleware:
123
123
  )
124
124
 
125
125
  def __call__(self, request: HttpRequest) -> HttpResponse:
126
- if self.client.enabled and request.method is not None and request.method != "OPTIONS":
127
- timestamp = time.time()
128
-
129
- request_size = parse_int(request.headers.get("Content-Length"))
130
- request_body = b""
131
- if self.capture_request_body:
132
- request_body = (
133
- request.body
134
- if request_size is not None and request_size <= MAX_BODY_SIZE and len(request.body) <= MAX_BODY_SIZE
135
- else BODY_TOO_LARGE
136
- )
126
+ if not self.client.enabled or request.method is None or request.method == "OPTIONS":
127
+ return self.get_response(request)
128
+
129
+ timestamp = time.time()
130
+ request_size = parse_int(request.headers.get("Content-Length"))
131
+ request_body = b""
132
+ if self.capture_request_body:
133
+ request_body = (
134
+ request.body
135
+ if request_size is not None and request_size <= MAX_BODY_SIZE and len(request.body) <= MAX_BODY_SIZE
136
+ else BODY_TOO_LARGE
137
+ )
137
138
 
138
- start_time = time.perf_counter()
139
- response = self.get_response(request)
140
- response_time = time.perf_counter() - start_time
141
- path = self.get_path(request)
142
-
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
- )
139
+ start_time = time.perf_counter()
140
+ response = self.get_response(request)
141
+ response_time = time.perf_counter() - start_time
142
+ path = self.get_path(request)
143
+
144
+ if path is None:
145
+ return response
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
+ response_size = (
156
+ parse_int(response["Content-Length"])
157
+ if response.has_header("Content-Length")
158
+ else (len(response.content) if not response.streaming else None)
159
+ )
160
+ response_body = b""
161
+ response_content_type = response.get("Content-Type")
162
+ if (
163
+ self.capture_response_body
164
+ and not response.streaming
165
+ and RequestLogger.is_supported_content_type(response_content_type)
166
+ ):
167
+ response_body = (
168
+ response.content if response_size is not None and response_size <= MAX_BODY_SIZE else BODY_TOO_LARGE
169
+ )
169
170
 
170
- try:
171
- self.client.request_counter.add_request(
171
+ try:
172
+ self.client.request_counter.add_request(
173
+ consumer=consumer_identifier,
174
+ method=request.method,
175
+ path=path,
176
+ status_code=response.status_code,
177
+ response_time=response_time,
178
+ request_size=request_size,
179
+ response_size=response_size,
180
+ )
181
+ except Exception: # pragma: no cover
182
+ logger.exception("Failed to log request metadata")
183
+
184
+ if (
185
+ response.status_code == 422
186
+ and (content_type := response.get("Content-Type")) is not None
187
+ and content_type.startswith("application/json")
188
+ ):
189
+ try:
190
+ body = try_json_loads(response.content, encoding=response.get("Content-Encoding"))
191
+ if isinstance(body, dict) and "detail" in body and isinstance(body["detail"], list):
192
+ # Log Django Ninja / Pydantic validation errors
193
+ self.client.validation_error_counter.add_validation_errors(
172
194
  consumer=consumer_identifier,
173
195
  method=request.method,
174
196
  path=path,
175
- status_code=response.status_code,
176
- response_time=response_time,
177
- request_size=request_size,
178
- response_size=response_size,
197
+ detail=body["detail"],
179
198
  )
180
- except Exception: # pragma: no cover
181
- logger.exception("Failed to log request metadata")
182
-
183
- if (
184
- response.status_code == 422
185
- and (content_type := response.get("Content-Type")) is not None
186
- and content_type.startswith("application/json")
187
- ):
188
- try:
189
- body = try_json_loads(response.content, encoding=response.get("Content-Encoding"))
190
- if isinstance(body, dict) and "detail" in body and isinstance(body["detail"], list):
191
- # Log Django Ninja / Pydantic validation errors
192
- self.client.validation_error_counter.add_validation_errors(
193
- consumer=consumer_identifier,
194
- method=request.method,
195
- path=path,
196
- detail=body["detail"],
197
- )
198
- except Exception: # pragma: no cover
199
- logger.exception("Failed to log validation errors")
200
-
201
- if response.status_code == 500 and hasattr(request, "unhandled_exception"):
202
- try:
203
- self.client.server_error_counter.add_server_error(
204
- consumer=consumer_identifier,
205
- method=request.method,
206
- path=path,
207
- exception=getattr(request, "unhandled_exception"),
208
- )
209
- except Exception: # pragma: no cover
210
- logger.exception("Failed to log server error")
211
-
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
- )
233
- else:
234
- response = self.get_response(request)
199
+ except Exception: # pragma: no cover
200
+ logger.exception("Failed to log validation errors")
201
+
202
+ if response.status_code == 500 and hasattr(request, "unhandled_exception"):
203
+ try:
204
+ self.client.server_error_counter.add_server_error(
205
+ consumer=consumer_identifier,
206
+ method=request.method,
207
+ path=path,
208
+ exception=getattr(request, "unhandled_exception"),
209
+ )
210
+ except Exception: # pragma: no cover
211
+ logger.exception("Failed to log server error")
212
+
213
+ if self.client.request_logger.enabled:
214
+ self.client.request_logger.log_request(
215
+ request={
216
+ "timestamp": timestamp,
217
+ "method": request.method,
218
+ "path": path,
219
+ "url": request.build_absolute_uri(),
220
+ "headers": list(request.headers.items()),
221
+ "size": request_size,
222
+ "consumer": consumer_identifier,
223
+ "body": request_body,
224
+ },
225
+ response={
226
+ "status_code": response.status_code,
227
+ "response_time": response_time,
228
+ "headers": list(response.items()),
229
+ "size": response_size,
230
+ "body": response_body,
231
+ },
232
+ exception=getattr(request, "unhandled_exception", None),
233
+ )
235
234
 
236
235
  return response
237
236
 
apitally/starlette.py CHANGED
@@ -91,6 +91,7 @@ class ApitallyMiddleware:
91
91
 
92
92
  async def receive_wrapper() -> Message:
93
93
  nonlocal request_body, request_body_too_large
94
+
94
95
  message = await receive()
95
96
  if message["type"] == "http.request" and self.capture_request_body and not request_body_too_large:
96
97
  request_body += message.get("body", b"")
@@ -109,6 +110,7 @@ class ApitallyMiddleware:
109
110
  response_chunked, \
110
111
  response_content_type, \
111
112
  response_size
113
+
112
114
  if message["type"] == "http.response.start":
113
115
  response_time = time.perf_counter() - start_time
114
116
  response_status = message["status"]
@@ -120,9 +122,11 @@ class ApitallyMiddleware:
120
122
  response_content_type = response_headers.get("Content-Type")
121
123
  response_size = parse_int(response_headers.get("Content-Length")) if not response_chunked else 0
122
124
  response_body_too_large = response_size is not None and response_size > MAX_BODY_SIZE
125
+
123
126
  elif message["type"] == "http.response.body":
124
127
  if response_chunked and response_size is not None:
125
128
  response_size += len(message.get("body", b""))
129
+
126
130
  if (
127
131
  (self.capture_response_body or response_status == 422)
128
132
  and RequestLogger.is_supported_content_type(response_content_type)
@@ -132,6 +136,11 @@ class ApitallyMiddleware:
132
136
  if len(response_body) > MAX_BODY_SIZE:
133
137
  response_body_too_large = True
134
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
+
135
144
  await send(message)
136
145
 
137
146
  try:
@@ -142,9 +151,6 @@ class ApitallyMiddleware:
142
151
  finally:
143
152
  if response_time is None:
144
153
  response_time = time.perf_counter() - start_time
145
- if self.capture_client_disconnects and await request.is_disconnected():
146
- # Client closed connection (report NGINX specific status code)
147
- response_status = 499
148
154
  self.add_request(
149
155
  timestamp=timestamp,
150
156
  request=request,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apitally
3
- Version: 0.18.0
3
+ Version: 0.18.2
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
@@ -67,21 +67,17 @@ Description-Content-Type: text/markdown
67
67
  <p align="center">
68
68
  <a href="https://apitally.io" target="_blank">
69
69
  <picture>
70
- <source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-vertical-dark.png">
71
- <source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-vertical-light.png">
72
- <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-vertical-light.png" width="150">
70
+ <source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-horizontal-new-dark.png">
71
+ <source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-horizontal-new-light.png">
72
+ <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-horizontal-new-light.png" width="220">
73
73
  </picture>
74
74
  </a>
75
75
  </p>
76
-
77
76
  <p align="center"><b>Simple, privacy-focused API monitoring & analytics</b></p>
78
-
79
- <p align="center"><i>Apitally helps you understand how your APIs are being used and alerts you when things go wrong.<br>Just add two lines of code to your project to get started.</i></p>
77
+ <p align="center" style="color: #ccc;">Apitally gives you the visibility you need to build better APIs – with just a few lines of code.</p>
78
+ <br>
79
+ <img alt="Apitally screenshots" src="https://assets.apitally.io/screenshots/overview.png">
80
80
  <br>
81
-
82
- ![Apitally screenshots](https://assets.apitally.io/screenshots/overview.png)
83
-
84
- ---
85
81
 
86
82
  # Apitally SDK for Python
87
83
 
@@ -138,7 +134,7 @@ pip install apitally[fastapi]
138
134
  ```
139
135
 
140
136
  The available extras are: `fastapi`, `flask`, `django_rest_framework`,
141
- `django_ninja`, `starlette` and `litestar`.
137
+ `django_ninja`, `starlette`, `litestar` and `blacksheep`.
142
138
 
143
139
  ## Usage
144
140
 
@@ -1,26 +1,26 @@
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=MnyL6ntMYj1WndjLqpDZ-8BrbDWCeVV2dpZbe8Fnm-Y,19254
4
+ apitally/django.py,sha256=7eSh1tzuu0wRl9PxJgXMBSz9DfyohCDIqd4ecQTOJ1M,18499
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
8
8
  apitally/flask.py,sha256=OoCEnjtnD51GUGq-adK80ebuiLj-5HXubxffCv5XTCM,9622
9
9
  apitally/litestar.py,sha256=mHoMqBO_gyoopeHljY8e8GTcV29UDf3uhQMxY3GeNpA,13451
10
10
  apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- apitally/starlette.py,sha256=IJymf3Gvhnls5B8LEGAcVyDS7x_hba93PfxSCuSKhFI,13872
11
+ apitally/starlette.py,sha256=Ep7n_yAqoleFpuLk43kMONiFJVRsFsu-G2_TIjfiCHQ,13878
12
12
  apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  apitally/client/client_asyncio.py,sha256=rTsH5wlLHK3RmyIuEiT6vzjquU-l2OPC34JnC2U6uYw,6658
14
14
  apitally/client/client_base.py,sha256=DvivGeHd3dyOASRvkIo44Zh8RzdBMfH8_rROa2lFbgw,3799
15
15
  apitally/client/client_threading.py,sha256=sxMRcxRgk1SxJjSq-qpIcDVmD3Q7Kv4CVT5zEUVt0KM,7257
16
16
  apitally/client/consumers.py,sha256=w_AFQhVgdtJVt7pVySBvSZwQg-2JVqmD2JQtVBoMkus,2626
17
17
  apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
18
- apitally/client/request_logging.py,sha256=SMvQd3WDolnb8u9rHVh2_OgXwFjL2jLZt-GpZNQ1XGk,14115
18
+ apitally/client/request_logging.py,sha256=FIJfbc4fYZnBIKebpJtrk19DcAxJylltghY7RFitWsE,14173
19
19
  apitally/client/requests.py,sha256=SDptGOg9XvaEKFj2o3oxJz-JAuZzUrqpHnbOQixf99o,3794
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.18.0.dist-info/METADATA,sha256=P_79lUjux4JjQZXrAU4LMPsZq00_KyLTc8sZEoONXqg,9271
24
- apitally-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- apitally-0.18.0.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
26
- apitally-0.18.0.dist-info/RECORD,,
23
+ apitally-0.18.2.dist-info/METADATA,sha256=J2Mr1Y-82i7_Batr5bwlTCEhMzcnirozN83VNZgIJ4U,9269
24
+ apitally-0.18.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
+ apitally-0.18.2.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
26
+ apitally-0.18.2.dist-info/RECORD,,