apitally 0.14.5__py3-none-any.whl → 0.14.6__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.
@@ -81,6 +81,7 @@ class ApitallyClient(ApitallyClientBase):
81
81
  self._stop_sync_loop = True
82
82
 
83
83
  async def handle_shutdown(self) -> None:
84
+ self.enabled = False
84
85
  if self._sync_loop_task is not None:
85
86
  self._sync_loop_task.cancel()
86
87
  # Send any remaining data before exiting
@@ -164,6 +165,7 @@ class ApitallyClient(ApitallyClientBase):
164
165
 
165
166
  def _handle_hub_response(self, response: httpx.Response) -> None:
166
167
  if response.status_code == 404:
168
+ self.enabled = False
167
169
  self.stop_sync_loop()
168
170
  logger.error("Invalid Apitally client ID: %s", self.client_id)
169
171
  elif response.status_code == 422:
@@ -52,6 +52,7 @@ class ApitallyClientBase(ABC):
52
52
 
53
53
  self.client_id = client_id
54
54
  self.env = env
55
+ self.enabled = True
55
56
  self.instance_uuid = str(uuid4())
56
57
  self.request_counter = RequestCounter()
57
58
  self.validation_error_counter = ValidationErrorCounter()
@@ -190,6 +190,7 @@ class ApitallyClient(ApitallyClientBase):
190
190
 
191
191
  def _handle_hub_response(self, response: requests.Response) -> None:
192
192
  if response.status_code == 404:
193
+ self.enabled = False
193
194
  self.stop_sync_loop()
194
195
  logger.error("Invalid Apitally client ID: %s", self.client_id)
195
196
  elif response.status_code == 422:
@@ -177,8 +177,6 @@ class RequestLogger:
177
177
 
178
178
  query = self._mask_query_params(parsed_url.query) if self.config.log_query_params else ""
179
179
  request["url"] = urlunparse(parsed_url._replace(query=query))
180
- request["headers"] = self._mask_headers(request["headers"]) if self.config.log_request_headers else []
181
- response["headers"] = self._mask_headers(response["headers"]) if self.config.log_response_headers else []
182
180
 
183
181
  if not self.config.log_request_body or not self._has_supported_content_type(request["headers"]):
184
182
  request["body"] = None
@@ -214,6 +212,9 @@ class RequestLogger:
214
212
  if response["body"] is not None and len(response["body"]) > MAX_BODY_SIZE:
215
213
  response["body"] = BODY_TOO_LARGE
216
214
 
215
+ request["headers"] = self._mask_headers(request["headers"]) if self.config.log_request_headers else []
216
+ response["headers"] = self._mask_headers(response["headers"]) if self.config.log_response_headers else []
217
+
217
218
  item = {
218
219
  "uuid": str(uuid4()),
219
220
  "request": _skip_empty_values(request),
@@ -310,14 +311,18 @@ class RequestLogger:
310
311
  return True
311
312
  return False
312
313
 
314
+ @staticmethod
315
+ def _get_user_agent(headers: List[Tuple[str, str]]) -> Optional[str]:
316
+ return next((v for k, v in headers if k.lower() == "user-agent"), None)
317
+
313
318
  @staticmethod
314
319
  def _has_supported_content_type(headers: List[Tuple[str, str]]) -> bool:
315
320
  content_type = next((v for k, v in headers if k.lower() == "content-type"), None)
316
- return content_type is not None and any(content_type.startswith(t) for t in ALLOWED_CONTENT_TYPES)
321
+ return RequestLogger.is_supported_content_type(content_type)
317
322
 
318
323
  @staticmethod
319
- def _get_user_agent(headers: List[Tuple[str, str]]) -> Optional[str]:
320
- return next((v for k, v in headers if k.lower() == "user-agent"), None)
324
+ def is_supported_content_type(content_type: Optional[str]) -> bool:
325
+ return content_type is not None and any(content_type.startswith(t) for t in ALLOWED_CONTENT_TYPES)
321
326
 
322
327
 
323
328
  def _check_writable_fs() -> bool:
apitally/django.py CHANGED
@@ -19,6 +19,7 @@ from apitally.client.logging import get_logger
19
19
  from apitally.client.request_logging import (
20
20
  BODY_TOO_LARGE,
21
21
  MAX_BODY_SIZE,
22
+ RequestLogger,
22
23
  RequestLoggingConfig,
23
24
  )
24
25
  from apitally.common import get_versions, parse_int
@@ -113,7 +114,7 @@ class ApitallyMiddleware:
113
114
  )
114
115
 
115
116
  def __call__(self, request: HttpRequest) -> HttpResponse:
116
- if request.method is not None and request.method != "OPTIONS":
117
+ if self.client.enabled and request.method is not None and request.method != "OPTIONS":
117
118
  timestamp = time.time()
118
119
  request_size = parse_int(request.headers.get("Content-Length"))
119
120
  request_body = b""
@@ -133,7 +134,12 @@ class ApitallyMiddleware:
133
134
  else (len(response.content) if not response.streaming else None)
134
135
  )
135
136
  response_body = b""
136
- if self.capture_response_body and not response.streaming:
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
+ ):
137
143
  response_body = (
138
144
  response.content if response_size is not None and response_size <= MAX_BODY_SIZE else BODY_TOO_LARGE
139
145
  )
apitally/flask.py CHANGED
@@ -17,6 +17,7 @@ from apitally.client.consumers import Consumer as ApitallyConsumer
17
17
  from apitally.client.request_logging import (
18
18
  BODY_TOO_LARGE,
19
19
  MAX_BODY_SIZE,
20
+ RequestLogger,
20
21
  RequestLoggingConfig,
21
22
  )
22
23
  from apitally.common import get_versions
@@ -74,6 +75,9 @@ class ApitallyMiddleware:
74
75
  self.client.set_startup_data(data)
75
76
 
76
77
  def __call__(self, environ: WSGIEnvironment, start_response: StartResponse) -> Iterable[bytes]:
78
+ if not self.client.enabled:
79
+ return self.wsgi_app(environ, start_response)
80
+
77
81
  timestamp = time.time()
78
82
  response_headers = Headers([])
79
83
  status_code = 0
@@ -100,7 +104,8 @@ class ApitallyMiddleware:
100
104
  response_time = time.perf_counter() - start_time
101
105
 
102
106
  response_body = b""
103
- if self.capture_response_body:
107
+ response_content_type = response_headers.get("Content-Type")
108
+ if self.capture_response_body and RequestLogger.is_supported_content_type(response_content_type):
104
109
  response_size = response_headers.get("Content-Length", type=int)
105
110
  if response_size is not None and response_size > MAX_BODY_SIZE:
106
111
  response_body = BODY_TOO_LARGE
apitally/litestar.py CHANGED
@@ -19,6 +19,7 @@ from apitally.client.consumers import Consumer as ApitallyConsumer
19
19
  from apitally.client.request_logging import (
20
20
  BODY_TOO_LARGE,
21
21
  MAX_BODY_SIZE,
22
+ RequestLogger,
22
23
  RequestLoggingConfig,
23
24
  )
24
25
  from apitally.common import get_versions, parse_int
@@ -86,7 +87,7 @@ class ApitallyPlugin(InitPluginProtocol):
86
87
 
87
88
  def middleware_factory(self, app: ASGIApp) -> ASGIApp:
88
89
  async def middleware(scope: Scope, receive: Receive, send: Send) -> None:
89
- if scope["type"] == "http" and scope["method"] != "OPTIONS":
90
+ if self.client.enabled and scope["type"] == "http" and scope["method"] != "OPTIONS":
90
91
  timestamp = time.time()
91
92
  request = Request(scope)
92
93
  request_size = parse_int(request.headers.get("Content-Length"))
@@ -99,6 +100,7 @@ class ApitallyPlugin(InitPluginProtocol):
99
100
  response_body_too_large = False
100
101
  response_size: Optional[int] = None
101
102
  response_chunked = False
103
+ response_content_type: Optional[str] = None
102
104
  start_time = time.perf_counter()
103
105
 
104
106
  async def receive_wrapper() -> Message:
@@ -119,6 +121,7 @@ class ApitallyPlugin(InitPluginProtocol):
119
121
  response_body, \
120
122
  response_body_too_large, \
121
123
  response_chunked, \
124
+ response_content_type, \
122
125
  response_size
123
126
  if message["type"] == "http.response.start":
124
127
  response_time = time.perf_counter() - start_time
@@ -128,12 +131,17 @@ class ApitallyPlugin(InitPluginProtocol):
128
131
  response_headers.get("Transfer-Encoding") == "chunked"
129
132
  or "Content-Length" not in response_headers
130
133
  )
134
+ response_content_type = response_headers.get("Content-Type")
131
135
  response_size = parse_int(response_headers.get("Content-Length")) if not response_chunked else 0
132
136
  response_body_too_large = response_size is not None and response_size > MAX_BODY_SIZE
133
137
  elif message["type"] == "http.response.body":
134
138
  if response_chunked and response_size is not None:
135
139
  response_size += len(message.get("body", b""))
136
- if (self.capture_response_body or response_status == 400) and not response_body_too_large:
140
+ if (
141
+ (self.capture_response_body or response_status == 400)
142
+ and RequestLogger.is_supported_content_type(response_content_type)
143
+ and not response_body_too_large
144
+ ):
137
145
  response_body += message.get("body", b"")
138
146
  if len(response_body) > MAX_BODY_SIZE:
139
147
  response_body_too_large = True
apitally/starlette.py CHANGED
@@ -20,6 +20,7 @@ from apitally.client.consumers import Consumer as ApitallyConsumer
20
20
  from apitally.client.request_logging import (
21
21
  BODY_TOO_LARGE,
22
22
  MAX_BODY_SIZE,
23
+ RequestLogger,
23
24
  RequestLoggingConfig,
24
25
  )
25
26
  from apitally.common import get_versions, parse_int
@@ -73,7 +74,7 @@ class ApitallyMiddleware:
73
74
  self.client.set_startup_data(data)
74
75
 
75
76
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
76
- if scope["type"] == "http" and scope["method"] != "OPTIONS":
77
+ if self.client.enabled and scope["type"] == "http" and scope["method"] != "OPTIONS":
77
78
  timestamp = time.time()
78
79
  request = Request(scope)
79
80
  request_size = parse_int(request.headers.get("Content-Length"))
@@ -86,6 +87,7 @@ class ApitallyMiddleware:
86
87
  response_body_too_large = False
87
88
  response_size: Optional[int] = None
88
89
  response_chunked = False
90
+ response_content_type: Optional[str] = None
89
91
  exception: Optional[BaseException] = None
90
92
  start_time = time.perf_counter()
91
93
 
@@ -107,6 +109,7 @@ class ApitallyMiddleware:
107
109
  response_body, \
108
110
  response_body_too_large, \
109
111
  response_chunked, \
112
+ response_content_type, \
110
113
  response_size
111
114
  if message["type"] == "http.response.start":
112
115
  response_time = time.perf_counter() - start_time
@@ -116,12 +119,17 @@ class ApitallyMiddleware:
116
119
  response_headers.get("Transfer-Encoding") == "chunked"
117
120
  or "Content-Length" not in response_headers
118
121
  )
122
+ response_content_type = response_headers.get("Content-Type")
119
123
  response_size = parse_int(response_headers.get("Content-Length")) if not response_chunked else 0
120
124
  response_body_too_large = response_size is not None and response_size > MAX_BODY_SIZE
121
125
  elif message["type"] == "http.response.body":
122
126
  if response_chunked and response_size is not None:
123
127
  response_size += len(message.get("body", b""))
124
- if (self.capture_response_body or response_status == 422) and not response_body_too_large:
128
+ if (
129
+ (self.capture_response_body or response_status == 422)
130
+ and RequestLogger.is_supported_content_type(response_content_type)
131
+ and not response_body_too_large
132
+ ):
125
133
  response_body += message.get("body", b"")
126
134
  if len(response_body) > MAX_BODY_SIZE:
127
135
  response_body_too_large = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: apitally
3
- Version: 0.14.5
3
+ Version: 0.14.6
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
@@ -62,18 +62,19 @@ Requires-Dist: starlette<1.0.0,>=0.21.0; extra == 'starlette'
62
62
  Description-Content-Type: text/markdown
63
63
 
64
64
  <p align="center">
65
- <picture>
66
- <source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-vertical-dark.png">
67
- <source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-vertical-light.png">
68
- <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-vertical-light.png" width="150">
69
- </picture>
65
+ <a href="https://apitally.io" target="_blank">
66
+ <picture>
67
+ <source media="(prefers-color-scheme: dark)" srcset="https://assets.apitally.io/logos/logo-vertical-dark.png">
68
+ <source media="(prefers-color-scheme: light)" srcset="https://assets.apitally.io/logos/logo-vertical-light.png">
69
+ <img alt="Apitally logo" src="https://assets.apitally.io/logos/logo-vertical-light.png" width="150">
70
+ </picture>
71
+ </a>
70
72
  </p>
71
73
 
72
- <p align="center"><b>Analytics, logging & monitoring for REST APIs.</b></p>
74
+ <p align="center"><b>Simple, privacy-focused API monitoring & analytics</b></p>
73
75
 
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>
75
-
76
- <p align="center">🔗 <b><a href="https://apitally.io" target="_blank">apitally.io</a></b></p>
76
+ <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
+ <br>
77
78
 
78
79
  ![Apitally screenshots](https://assets.apitally.io/screenshots/overview.png)
79
80
 
@@ -89,10 +90,10 @@ This client library for Apitally currently supports the following Python web
89
90
  frameworks:
90
91
 
91
92
  - [FastAPI](https://docs.apitally.io/frameworks/fastapi)
92
- - [Starlette](https://docs.apitally.io/frameworks/starlette)
93
- - [Flask](https://docs.apitally.io/frameworks/flask)
94
- - [Django Ninja](https://docs.apitally.io/frameworks/django-ninja)
95
93
  - [Django REST Framework](https://docs.apitally.io/frameworks/django-rest-framework)
94
+ - [Django Ninja](https://docs.apitally.io/frameworks/django-ninja)
95
+ - [Flask](https://docs.apitally.io/frameworks/flask)
96
+ - [Starlette](https://docs.apitally.io/frameworks/starlette)
96
97
  - [Litestar](https://docs.apitally.io/frameworks/litestar)
97
98
 
98
99
  Learn more about Apitally on our 🌎 [website](https://apitally.io) or check out
@@ -100,10 +101,21 @@ the 📚 [documentation](https://docs.apitally.io).
100
101
 
101
102
  ## Key features
102
103
 
103
- - Middleware for different frameworks to capture metadata about API endpoints,
104
- requests and responses
105
- - Non-blocking clients that aggregate and send captured data to Apitally in
106
- regular intervals
104
+ ### API analytics
105
+
106
+ Track traffic, error and performance metrics for your API, each endpoint and individual API consumers, allowing you to make informed, data-driven engineering and product decisions.
107
+
108
+ ### Error tracking
109
+
110
+ Understand which validation rules in your endpoints cause client errors. Capture error details and stack traces for 500 error responses, and have them linked to Sentry issues automatically.
111
+
112
+ ### Request logging
113
+
114
+ Drill down from insights to individual requests or use powerful filtering to understand how consumers have interacted with your API. Configure exactly what is included in the logs to meet your requirements.
115
+
116
+ ### API monitoring & alerting
117
+
118
+ Get notified immediately if something isn't right using custom alerts, synthetic uptime checks and heartbeat monitoring. Notifications can be delivered via email, Slack or Microsoft Teams.
107
119
 
108
120
  ## Install
109
121
 
@@ -140,6 +152,25 @@ app.add_middleware(
140
152
  )
141
153
  ```
142
154
 
155
+ ### Django
156
+
157
+ This is an example of how to add the Apitally middleware to a Django Ninja or
158
+ Django REST Framework application. For further instructions, see our
159
+ [setup guide for Django](https://docs.apitally.io/frameworks/django).
160
+
161
+ In your Django `settings.py` file:
162
+
163
+ ```python
164
+ MIDDLEWARE = [
165
+ "apitally.django.ApitallyMiddleware",
166
+ # Other middleware ...
167
+ ]
168
+ APITALLY_MIDDLEWARE = {
169
+ "client_id": "your-client-id",
170
+ "env": "dev", # or "prod" etc.
171
+ }
172
+ ```
173
+
143
174
  ### Flask
144
175
 
145
176
  This is an example of how to add the Apitally middleware to a Flask application.
@@ -158,23 +189,22 @@ app.wsgi_app = ApitallyMiddleware(
158
189
  )
159
190
  ```
160
191
 
161
- ### Django
162
-
163
- This is an example of how to add the Apitally middleware to a Django Ninja or
164
- Django REST Framework application. For further instructions, see our
165
- [setup guide for Django](https://docs.apitally.io/frameworks/django).
192
+ ### Starlette
166
193
 
167
- In your Django `settings.py` file:
194
+ This is an example of how to add the Apitally middleware to a Starlette application.
195
+ For further instructions, see our
196
+ [setup guide for Starlette](https://docs.apitally.io/frameworks/starlette).
168
197
 
169
198
  ```python
170
- MIDDLEWARE = [
171
- "apitally.django.ApitallyMiddleware",
172
- # Other middleware ...
173
- ]
174
- APITALLY_MIDDLEWARE = {
175
- "client_id": "your-client-id",
176
- "env": "dev", # or "prod" etc.
177
- }
199
+ from starlette.applications import Starlette
200
+ from apitally.starlette import ApitallyMiddleware
201
+
202
+ app = Starlette(routes=[...])
203
+ app.add_middleware(
204
+ ApitallyMiddleware,
205
+ client_id="your-client-id",
206
+ env="dev", # or "prod" etc.
207
+ )
178
208
  ```
179
209
 
180
210
  ### Litestar
@@ -1,24 +1,24 @@
1
1
  apitally/__init__.py,sha256=ShXQBVjyiSOHxoQJS2BvNG395W4KZfqMxZWBAR0MZrE,22
2
2
  apitally/common.py,sha256=Y8MRuTUHFUeQkcDrCLUxnqIPRpYIiW8S43T0QUab-_A,1267
3
- apitally/django.py,sha256=xM8zyH8LYr4BxAlhvfpUyau4OF5i_TMcISs3eJ7xvpY,16621
3
+ apitally/django.py,sha256=Tsw3uSM6EcfoCKpJuqliMFbc6eX0sVzJR9UyZck613M,16860
4
4
  apitally/django_ninja.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
5
5
  apitally/django_rest_framework.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
6
6
  apitally/fastapi.py,sha256=IfKfgsmIY8_AtnuMTW2sW4qnkya61CAE2vBoIpcc9tk,169
7
- apitally/flask.py,sha256=Th5LsMsTKkWERPrKfSWPhzrp99tg0pDtKXgtlVLx3eo,9279
8
- apitally/litestar.py,sha256=hAH2-OVVXBDVY8LopfIGv30yYwi-71tSEsKd6648CYc,13098
7
+ apitally/flask.py,sha256=kOFkAZj62Zr-l5eJWRr8lmsMHfFW_kfi1kflH6mZseQ,9533
8
+ apitally/litestar.py,sha256=qV89DXFlG619-20294OLg52vGv4C4GxIm9IPoyY21L4,13514
9
9
  apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- apitally/starlette.py,sha256=VaT4-QVSYC0YX1U5kVI-dGROEd64IbjYU5lx5N16yf8,12852
10
+ apitally/starlette.py,sha256=cMUUcQSqZ_meoxjyi8doZGarwP8RwNjFaFDmjoWvVK4,13240
11
11
  apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- apitally/client/client_asyncio.py,sha256=2RibaadLAEdl2i0yAb4dSEDq_r46w6HPFpZVzvt59aQ,6941
13
- apitally/client/client_base.py,sha256=dXsaB7scd0wCd_DcdiUvNlDLZ2pWbWsGDnJ4fLYvJGg,3771
14
- apitally/client/client_threading.py,sha256=Y8LlA_8LFHAuXym-T4bY_XFqXxiIDw9qFqO6K3FbZy0,7356
12
+ apitally/client/client_asyncio.py,sha256=9mdi9Hmb6-xn7dNdwP84e4PNAHGg2bYdMEgIfPUAtcQ,7003
13
+ apitally/client/client_base.py,sha256=DvivGeHd3dyOASRvkIo44Zh8RzdBMfH8_rROa2lFbgw,3799
14
+ apitally/client/client_threading.py,sha256=7JPu2Uulev7X2RiSLx4HJYfvAP6Z5zB_yuSevMfQC7I,7389
15
15
  apitally/client/consumers.py,sha256=w_AFQhVgdtJVt7pVySBvSZwQg-2JVqmD2JQtVBoMkus,2626
16
16
  apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
17
- apitally/client/request_logging.py,sha256=kKEPfNVK-T-maz4pkeyu805rZ4B2MFsjWjyu0PeW1WE,12973
17
+ apitally/client/request_logging.py,sha256=tVTMbTGtdf8OVULrnRPKNZxLuYqiMvO1NviRIfSNKnw,13134
18
18
  apitally/client/requests.py,sha256=RdJyvIqQGVHvS-wjpAPUwcO7byOJ6jO8dYqNTU2Furg,3685
19
19
  apitally/client/server_errors.py,sha256=axEhOxqV5SWjk0QCZTLVv2UMIaTfqPc81Typ4DXt66A,4646
20
20
  apitally/client/validation_errors.py,sha256=6G8WYWFgJs9VH9swvkPXJGuOJgymj5ooWA9OwjUTbuM,1964
21
- apitally-0.14.5.dist-info/METADATA,sha256=YlHcMcEDL-0mhhfXhQPS1PbbsobvZJhn8Ie8jiPfUpA,7570
22
- apitally-0.14.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- apitally-0.14.5.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
24
- apitally-0.14.5.dist-info/RECORD,,
21
+ apitally-0.14.6.dist-info/METADATA,sha256=veFQGVno3OtfK3J6RLBTyfLxqUzNXR0YtJVpcBhWz4I,8665
22
+ apitally-0.14.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ apitally-0.14.6.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
24
+ apitally-0.14.6.dist-info/RECORD,,