apitally 0.13.0a1__py3-none-any.whl → 0.14.0rc1__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/litestar.py CHANGED
@@ -13,12 +13,17 @@ from litestar.handlers import HTTPRouteHandler
13
13
  from litestar.plugins import InitPluginProtocol
14
14
  from litestar.types import ASGIApp, Message, Receive, Scope, Send
15
15
 
16
- from apitally.client.asyncio import ApitallyClient
17
- from apitally.client.base import Consumer as ApitallyConsumer
18
- from apitally.common import get_versions
16
+ from apitally.client.client_asyncio import ApitallyClient
17
+ from apitally.client.consumers import Consumer as ApitallyConsumer
18
+ from apitally.client.request_logging import (
19
+ BODY_TOO_LARGE,
20
+ MAX_BODY_SIZE,
21
+ RequestLoggingConfig,
22
+ )
23
+ from apitally.common import get_versions, parse_int
19
24
 
20
25
 
21
- __all__ = ["ApitallyPlugin", "ApitallyConsumer"]
26
+ __all__ = ["ApitallyPlugin", "ApitallyConsumer", "RequestLoggingConfig"]
22
27
 
23
28
 
24
29
  class ApitallyPlugin(InitPluginProtocol):
@@ -26,15 +31,23 @@ class ApitallyPlugin(InitPluginProtocol):
26
31
  self,
27
32
  client_id: str,
28
33
  env: str = "dev",
34
+ request_logging_config: Optional[RequestLoggingConfig] = None,
29
35
  app_version: Optional[str] = None,
30
36
  filter_openapi_paths: bool = True,
31
37
  identify_consumer_callback: Optional[Callable[[Request], Union[str, ApitallyConsumer, None]]] = None,
32
38
  ) -> None:
33
- self.client = ApitallyClient(client_id=client_id, env=env)
39
+ self.client = ApitallyClient(client_id=client_id, env=env, request_logging_config=request_logging_config)
34
40
  self.app_version = app_version
35
41
  self.filter_openapi_paths = filter_openapi_paths
36
42
  self.identify_consumer_callback = identify_consumer_callback
43
+
37
44
  self.openapi_path = "/schema"
45
+ self.capture_request_body = (
46
+ self.client.request_logger.config.enabled and self.client.request_logger.config.log_request_body
47
+ )
48
+ self.capture_response_body = (
49
+ self.client.request_logger.config.enabled and self.client.request_logger.config.log_response_body
50
+ )
38
51
 
39
52
  def on_app_init(self, app_config: AppConfig) -> AppConfig:
40
53
  app_config.on_startup.append(self.on_startup)
@@ -67,23 +80,39 @@ class ApitallyPlugin(InitPluginProtocol):
67
80
  def middleware_factory(self, app: ASGIApp) -> ASGIApp:
68
81
  async def middleware(scope: Scope, receive: Receive, send: Send) -> None:
69
82
  if scope["type"] == "http" and scope["method"] != "OPTIONS":
83
+ timestamp = time.time()
70
84
  request = Request(scope)
85
+ request_size = parse_int(request.headers.get("Content-Length"))
86
+ request_body = b""
87
+ request_body_too_large = request_size is not None and request_size > MAX_BODY_SIZE
71
88
  response_status = 0
72
89
  response_time = 0.0
73
90
  response_headers = Headers()
74
91
  response_body = b""
75
- response_size = 0
92
+ response_body_too_large = False
93
+ response_size: Optional[int] = None
76
94
  response_chunked = False
77
95
  start_time = time.perf_counter()
78
96
 
97
+ async def receive_wrapper() -> Message:
98
+ nonlocal request_body, request_body_too_large
99
+ message = await receive()
100
+ if message["type"] == "http.request" and self.capture_request_body and not request_body_too_large:
101
+ request_body += message.get("body", b"")
102
+ if len(request_body) > MAX_BODY_SIZE:
103
+ request_body_too_large = True
104
+ request_body = b""
105
+ return message
106
+
79
107
  async def send_wrapper(message: Message) -> None:
80
108
  nonlocal \
81
109
  response_time, \
82
110
  response_status, \
83
111
  response_headers, \
84
112
  response_body, \
85
- response_size, \
86
- response_chunked
113
+ response_body_too_large, \
114
+ response_chunked, \
115
+ response_size
87
116
  if message["type"] == "http.response.start":
88
117
  response_time = time.perf_counter() - start_time
89
118
  response_status = message["status"]
@@ -92,20 +121,28 @@ class ApitallyPlugin(InitPluginProtocol):
92
121
  response_headers.get("Transfer-Encoding") == "chunked"
93
122
  or "Content-Length" not in response_headers
94
123
  )
124
+ response_size = parse_int(response_headers.get("Content-Length")) if not response_chunked else 0
125
+ response_body_too_large = response_size is not None and response_size > MAX_BODY_SIZE
95
126
  elif message["type"] == "http.response.body":
96
- if response_chunked:
127
+ if response_chunked and response_size is not None:
97
128
  response_size += len(message.get("body", b""))
98
- if response_status == 400:
129
+ if (self.capture_response_body or response_status == 400) and not response_body_too_large:
99
130
  response_body += message.get("body", b"")
131
+ if len(response_body) > MAX_BODY_SIZE:
132
+ response_body_too_large = True
133
+ response_body = b""
100
134
  await send(message)
101
135
 
102
- await app(scope, receive, send_wrapper)
136
+ await app(scope, receive_wrapper, send_wrapper)
103
137
  self.add_request(
138
+ timestamp=timestamp,
104
139
  request=request,
140
+ request_body=request_body if not request_body_too_large else BODY_TOO_LARGE,
141
+ request_size=request_size,
105
142
  response_status=response_status,
106
143
  response_time=response_time,
107
144
  response_headers=response_headers,
108
- response_body=response_body,
145
+ response_body=response_body if not response_body_too_large else BODY_TOO_LARGE,
109
146
  response_size=response_size,
110
147
  )
111
148
  else:
@@ -115,64 +152,95 @@ class ApitallyPlugin(InitPluginProtocol):
115
152
 
116
153
  def add_request(
117
154
  self,
155
+ timestamp: float,
118
156
  request: Request,
157
+ request_body: bytes,
158
+ request_size: Optional[int],
119
159
  response_status: int,
120
160
  response_time: float,
121
161
  response_headers: Headers,
122
162
  response_body: bytes,
123
- response_size: int = 0,
163
+ response_size: Optional[int],
124
164
  ) -> None:
125
- if response_status < 100 or not request.route_handler.paths:
165
+ if response_status < 100:
126
166
  return # pragma: no cover
127
167
  path = self.get_path(request)
128
- if path is None or self.filter_path(path):
168
+ if self.filter_path(path):
129
169
  return
170
+
130
171
  consumer = self.get_consumer(request)
131
172
  consumer_identifier = consumer.identifier if consumer else None
132
173
  self.client.consumer_registry.add_or_update_consumer(consumer)
133
- self.client.request_counter.add_request(
134
- consumer=consumer_identifier,
135
- method=request.method,
136
- path=path,
137
- status_code=response_status,
138
- response_time=response_time,
139
- request_size=request.headers.get("Content-Length"),
140
- response_size=response_size or response_headers.get("Content-Length"),
141
- )
142
- if response_status == 400 and response_body and len(response_body) < 4096:
143
- with contextlib.suppress(json.JSONDecodeError):
144
- parsed_body = json.loads(response_body)
145
- if (
146
- isinstance(parsed_body, dict)
147
- and "detail" in parsed_body
148
- and isinstance(parsed_body["detail"], str)
149
- and "validation" in parsed_body["detail"].lower()
150
- and "extra" in parsed_body
151
- and isinstance(parsed_body["extra"], list)
152
- ):
153
- self.client.validation_error_counter.add_validation_errors(
154
- consumer=consumer_identifier,
155
- method=request.method,
156
- path=path,
157
- detail=[
158
- {
159
- "loc": [error.get("source", "body")] + error["key"].split("."),
160
- "msg": error["message"],
161
- "type": "",
162
- }
163
- for error in parsed_body["extra"]
164
- if "key" in error and "message" in error
165
- ],
166
- )
167
- if response_status == 500 and "exception" in request.state:
168
- self.client.server_error_counter.add_server_error(
174
+
175
+ if path is not None:
176
+ self.client.request_counter.add_request(
169
177
  consumer=consumer_identifier,
170
178
  method=request.method,
171
179
  path=path,
172
- exception=request.state["exception"],
180
+ status_code=response_status,
181
+ response_time=response_time,
182
+ request_size=request_size,
183
+ response_size=response_size,
184
+ )
185
+
186
+ if response_status == 400 and response_body and len(response_body) < 4096:
187
+ with contextlib.suppress(json.JSONDecodeError):
188
+ parsed_body = json.loads(response_body)
189
+ if (
190
+ isinstance(parsed_body, dict)
191
+ and "detail" in parsed_body
192
+ and isinstance(parsed_body["detail"], str)
193
+ and "validation" in parsed_body["detail"].lower()
194
+ and "extra" in parsed_body
195
+ and isinstance(parsed_body["extra"], list)
196
+ ):
197
+ self.client.validation_error_counter.add_validation_errors(
198
+ consumer=consumer_identifier,
199
+ method=request.method,
200
+ path=path,
201
+ detail=[
202
+ {
203
+ "loc": [error.get("source", "body")] + error["key"].split("."),
204
+ "msg": error["message"],
205
+ "type": "",
206
+ }
207
+ for error in parsed_body["extra"]
208
+ if "key" in error and "message" in error
209
+ ],
210
+ )
211
+
212
+ if response_status == 500 and "exception" in request.state:
213
+ self.client.server_error_counter.add_server_error(
214
+ consumer=consumer_identifier,
215
+ method=request.method,
216
+ path=path,
217
+ exception=request.state["exception"],
218
+ )
219
+
220
+ if self.client.request_logger.enabled:
221
+ self.client.request_logger.log_request(
222
+ request={
223
+ "timestamp": timestamp,
224
+ "method": request.method,
225
+ "path": path,
226
+ "url": str(request.url),
227
+ "headers": [(k, v) for k, v in request.headers.items()],
228
+ "size": request_size,
229
+ "consumer": consumer_identifier,
230
+ "body": request_body,
231
+ },
232
+ response={
233
+ "status_code": response_status,
234
+ "response_time": response_time,
235
+ "headers": [(k, v) for k, v in response_headers.items()],
236
+ "size": response_size,
237
+ "body": response_body,
238
+ },
173
239
  )
174
240
 
175
241
  def get_path(self, request: Request) -> Optional[str]:
242
+ if not request.route_handler.paths:
243
+ return None
176
244
  path: List[str] = []
177
245
  for layer in request.route_handler.ownership_layers:
178
246
  if isinstance(layer, HTTPRouteHandler):
@@ -183,8 +251,8 @@ class ApitallyPlugin(InitPluginProtocol):
183
251
  path.append(layer.path.lstrip("/"))
184
252
  return "/" + "/".join(filter(None, path))
185
253
 
186
- def filter_path(self, path: str) -> bool:
187
- if self.filter_openapi_paths and self.openapi_path:
254
+ def filter_path(self, path: Optional[str]) -> bool:
255
+ if path is not None and self.filter_openapi_paths and self.openapi_path:
188
256
  return path == self.openapi_path or path.startswith(self.openapi_path + "/")
189
257
  return False # pragma: no cover
190
258
 
apitally/starlette.py CHANGED
@@ -4,7 +4,7 @@ import asyncio
4
4
  import contextlib
5
5
  import json
6
6
  import time
7
- from typing import Any, Callable, Dict, List, Optional, Tuple, Union
7
+ from typing import Any, Callable, Dict, List, Optional, Union
8
8
  from warnings import warn
9
9
 
10
10
  from httpx import HTTPStatusError
@@ -15,12 +15,17 @@ from starlette.schemas import EndpointInfo, SchemaGenerator
15
15
  from starlette.testclient import TestClient
16
16
  from starlette.types import ASGIApp, Message, Receive, Scope, Send
17
17
 
18
- from apitally.client.asyncio import ApitallyClient
19
- from apitally.client.base import Consumer as ApitallyConsumer
20
- from apitally.common import get_versions
18
+ from apitally.client.client_asyncio import ApitallyClient
19
+ from apitally.client.consumers import Consumer as ApitallyConsumer
20
+ from apitally.client.request_logging import (
21
+ BODY_TOO_LARGE,
22
+ MAX_BODY_SIZE,
23
+ RequestLoggingConfig,
24
+ )
25
+ from apitally.common import get_versions, parse_int
21
26
 
22
27
 
23
- __all__ = ["ApitallyMiddleware", "ApitallyConsumer"]
28
+ __all__ = ["ApitallyMiddleware", "ApitallyConsumer", "RequestLoggingConfig"]
24
29
 
25
30
 
26
31
  class ApitallyMiddleware:
@@ -29,20 +34,26 @@ class ApitallyMiddleware:
29
34
  app: ASGIApp,
30
35
  client_id: str,
31
36
  env: str = "dev",
37
+ request_logging_config: Optional[RequestLoggingConfig] = None,
32
38
  app_version: Optional[str] = None,
33
39
  openapi_url: Optional[str] = "/openapi.json",
34
- filter_unhandled_paths: bool = True,
35
40
  identify_consumer_callback: Optional[Callable[[Request], Union[str, ApitallyConsumer, None]]] = None,
36
41
  ) -> None:
37
42
  self.app = app
38
- self.filter_unhandled_paths = filter_unhandled_paths
39
43
  self.identify_consumer_callback = identify_consumer_callback
40
- self.client = ApitallyClient(client_id=client_id, env=env)
44
+ self.client = ApitallyClient(client_id=client_id, env=env, request_logging_config=request_logging_config)
41
45
  self.client.start_sync_loop()
42
46
  self._delayed_set_startup_data_task: Optional[asyncio.Task] = None
43
47
  self.delayed_set_startup_data(app_version, openapi_url)
44
48
  _register_shutdown_handler(app, self.client.handle_shutdown)
45
49
 
50
+ self.capture_request_body = (
51
+ self.client.request_logger.config.enabled and self.client.request_logger.config.log_request_body
52
+ )
53
+ self.capture_response_body = (
54
+ self.client.request_logger.config.enabled and self.client.request_logger.config.log_response_body
55
+ )
56
+
46
57
  def delayed_set_startup_data(self, app_version: Optional[str] = None, openapi_url: Optional[str] = None) -> None:
47
58
  self._delayed_set_startup_data_task = asyncio.create_task(
48
59
  self._delayed_set_startup_data(app_version, openapi_url)
@@ -57,24 +68,40 @@ class ApitallyMiddleware:
57
68
 
58
69
  async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
59
70
  if scope["type"] == "http" and scope["method"] != "OPTIONS":
71
+ timestamp = time.time()
60
72
  request = Request(scope)
73
+ request_size = parse_int(request.headers.get("Content-Length"))
74
+ request_body = b""
75
+ request_body_too_large = request_size is not None and request_size > MAX_BODY_SIZE
61
76
  response_status = 0
62
77
  response_time: Optional[float] = None
63
78
  response_headers = Headers()
64
79
  response_body = b""
65
- response_size = 0
80
+ response_body_too_large = False
81
+ response_size: Optional[int] = None
66
82
  response_chunked = False
67
83
  exception: Optional[BaseException] = None
68
84
  start_time = time.perf_counter()
69
85
 
86
+ async def receive_wrapper() -> Message:
87
+ nonlocal request_body, request_body_too_large
88
+ message = await receive()
89
+ if message["type"] == "http.request" and self.capture_request_body and not request_body_too_large:
90
+ request_body += message.get("body", b"")
91
+ if len(request_body) > MAX_BODY_SIZE:
92
+ request_body_too_large = True
93
+ request_body = b""
94
+ return message
95
+
70
96
  async def send_wrapper(message: Message) -> None:
71
97
  nonlocal \
72
98
  response_time, \
73
99
  response_status, \
74
100
  response_headers, \
75
101
  response_body, \
76
- response_size, \
77
- response_chunked
102
+ response_body_too_large, \
103
+ response_chunked, \
104
+ response_size
78
105
  if message["type"] == "http.response.start":
79
106
  response_time = time.perf_counter() - start_time
80
107
  response_status = message["status"]
@@ -83,15 +110,20 @@ class ApitallyMiddleware:
83
110
  response_headers.get("Transfer-Encoding") == "chunked"
84
111
  or "Content-Length" not in response_headers
85
112
  )
113
+ response_size = parse_int(response_headers.get("Content-Length")) if not response_chunked else 0
114
+ response_body_too_large = response_size is not None and response_size > MAX_BODY_SIZE
86
115
  elif message["type"] == "http.response.body":
87
- if response_chunked:
116
+ if response_chunked and response_size is not None:
88
117
  response_size += len(message.get("body", b""))
89
- if response_status == 422:
118
+ if (self.capture_response_body or response_status == 422) and not response_body_too_large:
90
119
  response_body += message.get("body", b"")
120
+ if len(response_body) > MAX_BODY_SIZE:
121
+ response_body_too_large = True
122
+ response_body = b""
91
123
  await send(message)
92
124
 
93
125
  try:
94
- await self.app(scope, receive, send_wrapper)
126
+ await self.app(scope, receive_wrapper, send_wrapper)
95
127
  except BaseException as e:
96
128
  exception = e
97
129
  raise e from None
@@ -99,11 +131,14 @@ class ApitallyMiddleware:
99
131
  if response_time is None:
100
132
  response_time = time.perf_counter() - start_time
101
133
  self.add_request(
134
+ timestamp=timestamp,
102
135
  request=request,
136
+ request_body=request_body if not request_body_too_large else BODY_TOO_LARGE,
137
+ request_size=request_size,
103
138
  response_status=response_status,
104
139
  response_time=response_time,
105
140
  response_headers=response_headers,
106
- response_body=response_body,
141
+ response_body=response_body if not response_body_too_large else BODY_TOO_LARGE,
107
142
  response_size=response_size,
108
143
  exception=exception,
109
144
  )
@@ -112,29 +147,34 @@ class ApitallyMiddleware:
112
147
 
113
148
  def add_request(
114
149
  self,
150
+ timestamp: float,
115
151
  request: Request,
152
+ request_body: bytes,
153
+ request_size: Optional[int],
116
154
  response_status: int,
117
155
  response_time: float,
118
156
  response_headers: Headers,
119
157
  response_body: bytes,
120
- response_size: int = 0,
158
+ response_size: Optional[int],
121
159
  exception: Optional[BaseException] = None,
122
160
  ) -> None:
123
- path_template, is_handled_path = self.get_path_template(request)
124
- if is_handled_path or not self.filter_unhandled_paths:
125
- consumer = self.get_consumer(request)
126
- consumer_identifier = consumer.identifier if consumer else None
127
- self.client.consumer_registry.add_or_update_consumer(consumer)
161
+ path = self.get_path(request)
162
+
163
+ consumer = self.get_consumer(request)
164
+ consumer_identifier = consumer.identifier if consumer else None
165
+ self.client.consumer_registry.add_or_update_consumer(consumer)
166
+
167
+ if path is not None:
128
168
  if response_status == 0 and exception is not None:
129
169
  response_status = 500
130
170
  self.client.request_counter.add_request(
131
171
  consumer=consumer_identifier,
132
172
  method=request.method,
133
- path=path_template,
173
+ path=path,
134
174
  status_code=response_status,
135
175
  response_time=response_time,
136
- request_size=request.headers.get("Content-Length"),
137
- response_size=response_size or response_headers.get("Content-Length"),
176
+ request_size=request_size,
177
+ response_size=response_size,
138
178
  )
139
179
  if response_status == 422 and response_body and response_headers.get("Content-Type") == "application/json":
140
180
  with contextlib.suppress(json.JSONDecodeError):
@@ -144,24 +184,45 @@ class ApitallyMiddleware:
144
184
  self.client.validation_error_counter.add_validation_errors(
145
185
  consumer=consumer_identifier,
146
186
  method=request.method,
147
- path=path_template,
187
+ path=path,
148
188
  detail=body["detail"],
149
189
  )
150
190
  if response_status == 500 and exception is not None:
151
191
  self.client.server_error_counter.add_server_error(
152
192
  consumer=consumer_identifier,
153
193
  method=request.method,
154
- path=path_template,
194
+ path=path,
155
195
  exception=exception,
156
196
  )
157
197
 
198
+ if self.client.request_logger.enabled:
199
+ self.client.request_logger.log_request(
200
+ request={
201
+ "timestamp": timestamp,
202
+ "method": request.method,
203
+ "path": path,
204
+ "url": str(request.url),
205
+ "headers": request.headers.items(),
206
+ "size": request_size,
207
+ "consumer": consumer_identifier,
208
+ "body": request_body,
209
+ },
210
+ response={
211
+ "status_code": response_status,
212
+ "response_time": response_time,
213
+ "headers": response_headers.items(),
214
+ "size": response_size,
215
+ "body": response_body,
216
+ },
217
+ )
218
+
158
219
  @staticmethod
159
- def get_path_template(request: Request) -> Tuple[str, bool]:
220
+ def get_path(request: Request) -> Optional[str]:
160
221
  for route in request.app.routes:
161
222
  match, _ = route.matches(request.scope)
162
223
  if match == Match.FULL:
163
- return route.path, True
164
- return request.url.path, False
224
+ return route.path
225
+ return None
165
226
 
166
227
  def get_consumer(self, request: Request) -> Optional[ApitallyConsumer]:
167
228
  if hasattr(request.state, "apitally_consumer") and request.state.apitally_consumer:
@@ -1,34 +1,33 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: apitally
3
- Version: 0.13.0a1
3
+ Version: 0.14.0rc1
4
4
  Summary: Simple API monitoring & analytics for REST APIs built with FastAPI, Flask, Django, Starlette and Litestar.
5
- Project-URL: homepage, https://apitally.io
6
- Project-URL: documentation, https://docs.apitally.io
7
- Project-URL: source, https://github.com/apitally/apitally-py
5
+ Project-URL: Homepage, https://apitally.io
6
+ Project-URL: Documentation, https://docs.apitally.io
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
11
- Classifier: Development Status :: 4 - Beta
10
+ Classifier: Development Status :: 5 - Production/Stable
12
11
  Classifier: Environment :: Web Environment
13
12
  Classifier: Framework :: Django
14
13
  Classifier: Framework :: FastAPI
15
14
  Classifier: Framework :: Flask
16
15
  Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Information Technology
17
17
  Classifier: License :: OSI Approved :: MIT License
18
18
  Classifier: Programming Language :: Python
19
19
  Classifier: Programming Language :: Python :: 3
20
20
  Classifier: Programming Language :: Python :: 3 :: Only
21
- Classifier: Programming Language :: Python :: 3.7
22
21
  Classifier: Programming Language :: Python :: 3.8
23
22
  Classifier: Programming Language :: Python :: 3.9
24
23
  Classifier: Programming Language :: Python :: 3.10
25
24
  Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
26
  Classifier: Topic :: Internet
27
27
  Classifier: Topic :: Internet :: WWW/HTTP
28
28
  Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
29
29
  Classifier: Topic :: Software Development
30
- Classifier: Topic :: Software Development :: Libraries
31
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
+ Classifier: Topic :: System :: Monitoring
32
31
  Classifier: Typing :: Typed
33
32
  Requires-Python: <4.0,>=3.8
34
33
  Requires-Dist: backoff>=2.0.0
@@ -0,0 +1,24 @@
1
+ apitally/__init__.py,sha256=ShXQBVjyiSOHxoQJS2BvNG395W4KZfqMxZWBAR0MZrE,22
2
+ apitally/common.py,sha256=Y8MRuTUHFUeQkcDrCLUxnqIPRpYIiW8S43T0QUab-_A,1267
3
+ apitally/django.py,sha256=2Wg89-NpHGm1Yc25DjtDRVdvW8H3ugSslhxPOQKiXXY,16497
4
+ apitally/django_ninja.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
5
+ apitally/django_rest_framework.py,sha256=-CmrwFFRv7thFOUK_OrOSouhHL9bm5sIBNIQlpyE_2c,166
6
+ apitally/fastapi.py,sha256=IfKfgsmIY8_AtnuMTW2sW4qnkya61CAE2vBoIpcc9tk,169
7
+ apitally/flask.py,sha256=Q-3_nrCPkinZ8QERVoa_jiZaEmZoY43oIdj5UGx2tk4,9170
8
+ apitally/litestar.py,sha256=tEaoqRJJNtbijjyrI3ue82ePhZBUk6A2yuXIjlN5oMQ,12951
9
+ apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ apitally/starlette.py,sha256=ooXr9StS8gU3y8mfyrscUdMiPIBfYMxX5Y6LfIMYQgA,12370
11
+ apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ apitally/client/client_asyncio.py,sha256=cCCtA8Lf-FOfXCstVZEAT3_0bnwH_dU1xe0YzhfLUYU,6691
13
+ apitally/client/client_base.py,sha256=w5AXAbg3hw5Qds5rovCZFtePB9bHNcJsr9l7kDgbroc,3733
14
+ apitally/client/client_threading.py,sha256=lLtr89LXrD2roDeUORJkx7QeQOPQqt162Vo8bSExPh4,7101
15
+ apitally/client/consumers.py,sha256=w_AFQhVgdtJVt7pVySBvSZwQg-2JVqmD2JQtVBoMkus,2626
16
+ apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
17
+ apitally/client/request_logging.py,sha256=Ar7bqeJbQ8gMbIt3j-wWRBH0E3OhWF0hYwiLyrBJdU0,12548
18
+ apitally/client/requests.py,sha256=RdJyvIqQGVHvS-wjpAPUwcO7byOJ6jO8dYqNTU2Furg,3685
19
+ apitally/client/server_errors.py,sha256=axEhOxqV5SWjk0QCZTLVv2UMIaTfqPc81Typ4DXt66A,4646
20
+ apitally/client/validation_errors.py,sha256=6G8WYWFgJs9VH9swvkPXJGuOJgymj5ooWA9OwjUTbuM,1964
21
+ apitally-0.14.0rc1.dist-info/METADATA,sha256=9LAy2ffwdz6zicCpyqYKct_zso_lCtZokjY0USYMc-E,7580
22
+ apitally-0.14.0rc1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
23
+ apitally-0.14.0rc1.dist-info/licenses/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
24
+ apitally-0.14.0rc1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.25.0
2
+ Generator: hatchling 1.26.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any