wrapfast 0.0.2__tar.gz

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.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.13
wrapfast-0.0.2/LICENSE ADDED
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2026 wrapfast contributors
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted.
5
+
6
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
7
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
8
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
9
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
10
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
11
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
12
+ THIS SOFTWARE.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: wrapfast
3
+ Version: 0.0.2
4
+ Summary: Composable HTTP API client: pluggable transport, session, and presentation layers for maintainable wrappers.
5
+ Project-URL: Repository, https://github.com/UnknownAPI/wrapfast
6
+ Project-URL: Issues, https://github.com/UnknownAPI/wrapfast/issues
7
+ Author: wrapfast contributors
8
+ License-Expression: 0BSD
9
+ License-File: LICENSE
10
+ Keywords: api,api-client,http,http-client,rest,typing,wrapper
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.13
21
+ Provides-Extra: examples
22
+ Requires-Dist: pydantic>=2; extra == 'examples'
23
+ Requires-Dist: requests>=2.31; extra == 'examples'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # wrapfast
27
+
28
+ **wrapfast** is a small Python library with a big opinion: API clients stay maintainable when you **separate concerns** instead of growing a single “do everything” class.
29
+
30
+ It exists to promote **good practice**, **clear organisation**, and **real flexibility** when you wrap REST (or HTTP-shaped) APIs in Python. You compose a pipeline from a few roles—each one easy to test, swap, or extend—instead of hard‑coding `requests.get` next to auth logic next to JSON parsing next to URL strings scattered across the codebase.
31
+
32
+ ---
33
+
34
+ ## The idea in one glance
35
+
36
+ | Piece | Responsibility |
37
+ |--------|----------------|
38
+ | **`Transport`** | How a request leaves your process and bytes come back (`requests`, `httpx`, a mock, async later). |
39
+ | **`Session`** | Cross‑cutting behaviour around the wire call: tokens, headers, cookies, tracing, optional response handling. |
40
+ | **`PresentationCodec`** | How typed domain objects become bytes and back (JSON + Pydantic, `msgspec`, plain `dict`, …). |
41
+ | **`Endpoint`** | A named operation: HTTP method, path, and the request/response types you expect. |
42
+ | **`HttpClient`** | The thin orchestrator: build `HttpRequest` → session → transport → session → decode. |
43
+
44
+ That split is the point: **organisation** (each type has one job), **good practice** (test transports and codecs without the network; test sessions without JSON details), and **flexibility** (change transport or codec without rewriting your endpoints).
45
+
46
+ ---
47
+
48
+ ## Code that shows the shape
49
+
50
+ This is intentionally dense: it is the whole architecture on one screen, using **Pydantic** for request/response models and JSON.
51
+
52
+ ```python
53
+ import requests
54
+ from pydantic import BaseModel, ConfigDict
55
+
56
+ from wrapfast import (
57
+ Endpoint,
58
+ HttpClient,
59
+ HttpRequest,
60
+ HttpResponse,
61
+ PresentationCodec,
62
+ Session,
63
+ Transport,
64
+ )
65
+
66
+
67
+ class User(BaseModel):
68
+ model_config = ConfigDict(extra="ignore")
69
+
70
+ id: int
71
+ name: str
72
+
73
+
74
+ GET_USER = Endpoint("GET", "users/1", type(None), User)
75
+
76
+
77
+ class RequestsTransport(Transport):
78
+ def send(self, request: HttpRequest) -> HttpResponse:
79
+ r = requests.request(
80
+ request.method,
81
+ request.url,
82
+ headers=request.headers,
83
+ data=request.data or None,
84
+ timeout=30,
85
+ )
86
+ return HttpResponse(r.status_code, {k.lower(): v for k, v in r.headers.items()}, r.content)
87
+
88
+
89
+ class BearerSession(Session):
90
+ def __init__(self, token: str) -> None:
91
+ self._token = token
92
+
93
+ def wrap_request(self, request: HttpRequest) -> HttpRequest:
94
+ h = {**request.headers, "authorization": f"Bearer {self._token}"}
95
+ return HttpRequest(request.method, request.url, h, request.data)
96
+
97
+ def unwrap_response(self, response: HttpResponse) -> HttpResponse:
98
+ return response # e.g. 401 → refresh token, logging, metrics
99
+
100
+
101
+ class PydanticJsonCodec(PresentationCodec):
102
+ def get_content_type(self) -> str:
103
+ return "application/json"
104
+
105
+ def encode(self, obj: object) -> bytes:
106
+ if obj is None:
107
+ return b""
108
+ if isinstance(obj, BaseModel):
109
+ return obj.model_dump_json(exclude_none=True).encode("utf-8")
110
+ raise TypeError("encode expects None or a Pydantic model")
111
+
112
+ def decode(self, data: bytes, target: type):
113
+ if not isinstance(target, type) or not issubclass(target, BaseModel):
114
+ raise TypeError("decode target must be a BaseModel subclass")
115
+ return target.model_validate_json(data)
116
+
117
+
118
+ client = HttpClient(
119
+ base_url="https://api.example.com/",
120
+ transport=RequestsTransport(),
121
+ session=BearerSession("<access token>"),
122
+ presentation_codec=PydanticJsonCodec(),
123
+ )
124
+
125
+ user = client.send(GET_USER, None) # User: validated model, not raw JSON
126
+ ```
127
+
128
+ Add **`pydantic`** and **`requests`** to your environment when using this pattern (also bundled as the optional **`examples`** extra in this repo).
129
+
130
+ **`HttpClient`** is the spine: it does not know *which* HTTP library you use, *how* you authenticate, or *how* bodies are serialised. Those are **policies** you inject. Your API surface becomes a set of **`Endpoint`** values plus **Pydantic models** (or other types you teach the codec)—easier to read, review, and reuse.
131
+
132
+ `PresentationCodec`, `Transport`, `Session`, and async `AsyncTransport` are abstract bases (`abc.ABC`). Codecs implement `get_content_type()` (used for the outbound `Content-Type` header), `encode`, and `decode`; transports and sessions implement the `send` / `wrap_request` / `unwrap_response` hooks shown above.
133
+
134
+ ---
135
+
136
+ ## Why it matters
137
+
138
+ - **Tests**: fake `Transport` returns canned `HttpResponse`; no sockets.
139
+ - **Auth**: evolve `Session` (login, refresh, header rules) without touching codecs.
140
+ - **Formats**: swap JSON for another codec at the edge without renaming your domain models’ usage sites.
141
+ - **Readability**: endpoints read like a table of operations; the “how we call HTTP” story lives in a few small classes.
142
+
143
+ ---
144
+
145
+ ## Project layout & example
146
+
147
+ | Path | Role |
148
+ |------|------|
149
+ | `src/wrapfast/` | Installable package: `HttpClient`, protocols, `Endpoint`. |
150
+ | `examples/dummyjson_requests.py` | End‑to‑end sample: `requests`, Pydantic, bearer **session** (login, `/auth/me`, refresh). |
151
+
152
+ After `pip install wrapfast`, use `import wrapfast`. From a clone without installing, add the `src` directory to `PYTHONPATH` (see **`examples/dummyjson_requests.py`**). A fuller DummyJSON walkthrough lives in that example.
153
+
154
+ ---
155
+
156
+ ## Requirements
157
+
158
+ Python **3.13+** (see `pyproject.toml`). The library itself has no required runtime dependencies; pair it with **your** transport and codec. The README snippet and **`examples`** extra use **Pydantic** and **`requests`**.
159
+
160
+ ---
161
+
162
+ ## License
163
+
164
+ This project is released under the [**0BSD**](https://opensource.org/licenses/0BSD) license (see [`LICENSE`](LICENSE)): use it for anything, with no attribution requirement and minimal legal boilerplate. It is one of the most permissive widely used open-source terms for software.
@@ -0,0 +1,139 @@
1
+ # wrapfast
2
+
3
+ **wrapfast** is a small Python library with a big opinion: API clients stay maintainable when you **separate concerns** instead of growing a single “do everything” class.
4
+
5
+ It exists to promote **good practice**, **clear organisation**, and **real flexibility** when you wrap REST (or HTTP-shaped) APIs in Python. You compose a pipeline from a few roles—each one easy to test, swap, or extend—instead of hard‑coding `requests.get` next to auth logic next to JSON parsing next to URL strings scattered across the codebase.
6
+
7
+ ---
8
+
9
+ ## The idea in one glance
10
+
11
+ | Piece | Responsibility |
12
+ |--------|----------------|
13
+ | **`Transport`** | How a request leaves your process and bytes come back (`requests`, `httpx`, a mock, async later). |
14
+ | **`Session`** | Cross‑cutting behaviour around the wire call: tokens, headers, cookies, tracing, optional response handling. |
15
+ | **`PresentationCodec`** | How typed domain objects become bytes and back (JSON + Pydantic, `msgspec`, plain `dict`, …). |
16
+ | **`Endpoint`** | A named operation: HTTP method, path, and the request/response types you expect. |
17
+ | **`HttpClient`** | The thin orchestrator: build `HttpRequest` → session → transport → session → decode. |
18
+
19
+ That split is the point: **organisation** (each type has one job), **good practice** (test transports and codecs without the network; test sessions without JSON details), and **flexibility** (change transport or codec without rewriting your endpoints).
20
+
21
+ ---
22
+
23
+ ## Code that shows the shape
24
+
25
+ This is intentionally dense: it is the whole architecture on one screen, using **Pydantic** for request/response models and JSON.
26
+
27
+ ```python
28
+ import requests
29
+ from pydantic import BaseModel, ConfigDict
30
+
31
+ from wrapfast import (
32
+ Endpoint,
33
+ HttpClient,
34
+ HttpRequest,
35
+ HttpResponse,
36
+ PresentationCodec,
37
+ Session,
38
+ Transport,
39
+ )
40
+
41
+
42
+ class User(BaseModel):
43
+ model_config = ConfigDict(extra="ignore")
44
+
45
+ id: int
46
+ name: str
47
+
48
+
49
+ GET_USER = Endpoint("GET", "users/1", type(None), User)
50
+
51
+
52
+ class RequestsTransport(Transport):
53
+ def send(self, request: HttpRequest) -> HttpResponse:
54
+ r = requests.request(
55
+ request.method,
56
+ request.url,
57
+ headers=request.headers,
58
+ data=request.data or None,
59
+ timeout=30,
60
+ )
61
+ return HttpResponse(r.status_code, {k.lower(): v for k, v in r.headers.items()}, r.content)
62
+
63
+
64
+ class BearerSession(Session):
65
+ def __init__(self, token: str) -> None:
66
+ self._token = token
67
+
68
+ def wrap_request(self, request: HttpRequest) -> HttpRequest:
69
+ h = {**request.headers, "authorization": f"Bearer {self._token}"}
70
+ return HttpRequest(request.method, request.url, h, request.data)
71
+
72
+ def unwrap_response(self, response: HttpResponse) -> HttpResponse:
73
+ return response # e.g. 401 → refresh token, logging, metrics
74
+
75
+
76
+ class PydanticJsonCodec(PresentationCodec):
77
+ def get_content_type(self) -> str:
78
+ return "application/json"
79
+
80
+ def encode(self, obj: object) -> bytes:
81
+ if obj is None:
82
+ return b""
83
+ if isinstance(obj, BaseModel):
84
+ return obj.model_dump_json(exclude_none=True).encode("utf-8")
85
+ raise TypeError("encode expects None or a Pydantic model")
86
+
87
+ def decode(self, data: bytes, target: type):
88
+ if not isinstance(target, type) or not issubclass(target, BaseModel):
89
+ raise TypeError("decode target must be a BaseModel subclass")
90
+ return target.model_validate_json(data)
91
+
92
+
93
+ client = HttpClient(
94
+ base_url="https://api.example.com/",
95
+ transport=RequestsTransport(),
96
+ session=BearerSession("<access token>"),
97
+ presentation_codec=PydanticJsonCodec(),
98
+ )
99
+
100
+ user = client.send(GET_USER, None) # User: validated model, not raw JSON
101
+ ```
102
+
103
+ Add **`pydantic`** and **`requests`** to your environment when using this pattern (also bundled as the optional **`examples`** extra in this repo).
104
+
105
+ **`HttpClient`** is the spine: it does not know *which* HTTP library you use, *how* you authenticate, or *how* bodies are serialised. Those are **policies** you inject. Your API surface becomes a set of **`Endpoint`** values plus **Pydantic models** (or other types you teach the codec)—easier to read, review, and reuse.
106
+
107
+ `PresentationCodec`, `Transport`, `Session`, and async `AsyncTransport` are abstract bases (`abc.ABC`). Codecs implement `get_content_type()` (used for the outbound `Content-Type` header), `encode`, and `decode`; transports and sessions implement the `send` / `wrap_request` / `unwrap_response` hooks shown above.
108
+
109
+ ---
110
+
111
+ ## Why it matters
112
+
113
+ - **Tests**: fake `Transport` returns canned `HttpResponse`; no sockets.
114
+ - **Auth**: evolve `Session` (login, refresh, header rules) without touching codecs.
115
+ - **Formats**: swap JSON for another codec at the edge without renaming your domain models’ usage sites.
116
+ - **Readability**: endpoints read like a table of operations; the “how we call HTTP” story lives in a few small classes.
117
+
118
+ ---
119
+
120
+ ## Project layout & example
121
+
122
+ | Path | Role |
123
+ |------|------|
124
+ | `src/wrapfast/` | Installable package: `HttpClient`, protocols, `Endpoint`. |
125
+ | `examples/dummyjson_requests.py` | End‑to‑end sample: `requests`, Pydantic, bearer **session** (login, `/auth/me`, refresh). |
126
+
127
+ After `pip install wrapfast`, use `import wrapfast`. From a clone without installing, add the `src` directory to `PYTHONPATH` (see **`examples/dummyjson_requests.py`**). A fuller DummyJSON walkthrough lives in that example.
128
+
129
+ ---
130
+
131
+ ## Requirements
132
+
133
+ Python **3.13+** (see `pyproject.toml`). The library itself has no required runtime dependencies; pair it with **your** transport and codec. The README snippet and **`examples`** extra use **Pydantic** and **`requests`**.
134
+
135
+ ---
136
+
137
+ ## License
138
+
139
+ This project is released under the [**0BSD**](https://opensource.org/licenses/0BSD) license (see [`LICENSE`](LICENSE)): use it for anything, with no attribution requirement and minimal legal boilerplate. It is one of the most permissive widely used open-source terms for software.
@@ -0,0 +1,267 @@
1
+ """
2
+ Example: wrapfast HttpClient + DummyJSON (https://dummyjson.com) using `requests`,
3
+ Pydantic models, and a **Session** that injects ``Authorization: Bearer …`` after login
4
+ and updates tokens after refresh.
5
+
6
+ Install from the repo root::
7
+
8
+ pip install requests pydantic
9
+ # or: uv venv && uv sync --extra examples
10
+
11
+ Run::
12
+
13
+ python examples/dummyjson_requests.py
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sys
19
+ from pathlib import Path
20
+ from urllib.parse import urlparse
21
+
22
+ import requests
23
+ from pydantic import BaseModel, ConfigDict
24
+
25
+ # Repo layout: package lives under src/wrapfast/ (add ``src`` so ``import wrapfast`` works without install).
26
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
27
+
28
+ from wrapfast import (
29
+ Endpoint,
30
+ HttpClient,
31
+ HttpRequest,
32
+ HttpResponse,
33
+ PresentationCodec,
34
+ Session,
35
+ Transport,
36
+ )
37
+
38
+
39
+ def _url_resource_path(url: str) -> str:
40
+ """``https://dummyjson.com/auth/me`` -> ``auth/me``."""
41
+ return urlparse(url).path.lstrip("/")
42
+
43
+
44
+ class RequestsTransport(Transport):
45
+ """Bridge wrapfast's HttpRequest/HttpResponse to the `requests` library."""
46
+
47
+ def __init__(self, timeout: float = 30.0) -> None:
48
+ self._timeout = timeout
49
+
50
+ def send(self, request: HttpRequest) -> HttpResponse:
51
+ resp = requests.request(
52
+ method=request.method,
53
+ url=request.url,
54
+ headers=request.headers,
55
+ data=request.data or None,
56
+ timeout=self._timeout,
57
+ )
58
+ return HttpResponse(
59
+ status_code=resp.status_code,
60
+ headers={k.lower(): v for k, v in resp.headers.items()},
61
+ data=resp.content,
62
+ )
63
+
64
+
65
+ # --- DummyJSON shapes (subset of real responses; extra JSON keys ignored) ---
66
+
67
+
68
+ class LoginRequest(BaseModel):
69
+ model_config = ConfigDict(extra="forbid")
70
+
71
+ username: str
72
+ password: str
73
+
74
+
75
+ class LoginResponse(BaseModel):
76
+ model_config = ConfigDict(extra="ignore")
77
+
78
+ id: int
79
+ username: str
80
+ email: str
81
+ firstName: str
82
+ lastName: str
83
+ gender: str
84
+ image: str
85
+ accessToken: str
86
+ refreshToken: str
87
+
88
+
89
+ class MeResponse(BaseModel):
90
+ """Subset of ``GET /auth/me`` (current user)."""
91
+
92
+ model_config = ConfigDict(extra="ignore")
93
+
94
+ id: int
95
+ username: str
96
+ email: str
97
+ firstName: str
98
+ lastName: str
99
+
100
+
101
+ class RefreshRequest(BaseModel):
102
+ model_config = ConfigDict(extra="forbid")
103
+
104
+ refreshToken: str
105
+ expiresInMins: int | None = None
106
+
107
+
108
+ class RefreshResponse(BaseModel):
109
+ model_config = ConfigDict(extra="ignore")
110
+
111
+ accessToken: str
112
+ refreshToken: str
113
+
114
+
115
+ class Product(BaseModel):
116
+ model_config = ConfigDict(extra="ignore")
117
+
118
+ id: int
119
+ title: str
120
+ description: str | None = None
121
+ price: float | None = None
122
+ brand: str | None = None
123
+
124
+
125
+ class DummyJsonBearerSession(Session):
126
+ """Session that attaches a bearer access token to outbound requests.
127
+
128
+ DummyJSON expects ``Authorization: Bearer <accessToken>`` for endpoints such as
129
+ ``GET /auth/me``. Password login must **not** send a stale bearer token, so
130
+ ``POST /auth/login`` is excluded from injection.
131
+
132
+ Use :meth:`bind_login` after a successful login and :meth:`apply_refresh` after
133
+ ``POST /auth/refresh`` so subsequent calls use the latest tokens.
134
+ """
135
+
136
+ #: Path prefixes that must not receive ``Authorization`` (password login).
137
+ _no_bearer_paths: frozenset[str] = frozenset({"auth/login"})
138
+
139
+ def __init__(self) -> None:
140
+ self._access_token: str | None = None
141
+ self._refresh_token: str | None = None
142
+
143
+ @property
144
+ def access_token(self) -> str | None:
145
+ return self._access_token
146
+
147
+ @property
148
+ def refresh_token(self) -> str | None:
149
+ return self._refresh_token
150
+
151
+ def bind_login(self, login: LoginResponse) -> None:
152
+ """Store tokens from ``POST /auth/login``."""
153
+ self._access_token = login.accessToken
154
+ self._refresh_token = login.refreshToken
155
+
156
+ def apply_refresh(self, tokens: RefreshResponse) -> None:
157
+ """Replace tokens after ``POST /auth/refresh``."""
158
+ self._access_token = tokens.accessToken
159
+ self._refresh_token = tokens.refreshToken
160
+
161
+ def clear(self) -> None:
162
+ """Drop tokens (e.g. logout)."""
163
+ self._access_token = None
164
+ self._refresh_token = None
165
+
166
+ def wrap_request(self, request: HttpRequest) -> HttpRequest:
167
+ path = _url_resource_path(request.url)
168
+ if self._access_token is None or path in self._no_bearer_paths:
169
+ return request
170
+ headers = {**request.headers, "authorization": f"Bearer {self._access_token}"}
171
+ return HttpRequest(
172
+ method=request.method,
173
+ url=request.url,
174
+ headers=headers,
175
+ data=request.data,
176
+ )
177
+
178
+ def unwrap_response(self, response: HttpResponse) -> HttpResponse:
179
+ """Hook for logging, retry, or error mapping; pass-through here."""
180
+ return response
181
+
182
+
183
+ class PydanticJsonCodec(PresentationCodec):
184
+ """JSON codec using Pydantic ``model_dump_json`` / ``model_validate_json``."""
185
+
186
+ def get_content_type(self) -> str:
187
+ return "application/json"
188
+
189
+ def encode(self, obj: object) -> bytes:
190
+ if obj is None:
191
+ return b""
192
+ if isinstance(obj, BaseModel):
193
+ return obj.model_dump_json(exclude_none=True, by_alias=True).encode("utf-8")
194
+ raise TypeError(
195
+ f"PydanticJsonCodec.encode expects None or BaseModel, got {type(obj).__name__}"
196
+ )
197
+
198
+ def decode(self, data: bytes, target: type):
199
+ if not isinstance(target, type) or not issubclass(target, BaseModel):
200
+ raise TypeError(
201
+ f"PydanticJsonCodec.decode expects a BaseModel subclass, got {target!r}"
202
+ )
203
+ return target.model_validate_json(data)
204
+
205
+
206
+ # Paths must not start with "/" if ``base_url`` ends with "/" (see HttpClient URL rules).
207
+ AUTH_LOGIN = Endpoint("POST", "auth/login", LoginRequest, LoginResponse)
208
+ AUTH_ME = Endpoint("GET", "auth/me", type(None), MeResponse)
209
+ AUTH_REFRESH = Endpoint("POST", "auth/refresh", RefreshRequest, RefreshResponse)
210
+ PRODUCT_BY_ID = Endpoint("GET", "products/1", type(None), Product)
211
+
212
+
213
+ def main() -> None:
214
+ base_url = "https://dummyjson.com/"
215
+ session = DummyJsonBearerSession()
216
+ codec = PydanticJsonCodec()
217
+ client = HttpClient(
218
+ base_url=base_url,
219
+ transport=RequestsTransport(),
220
+ session=session,
221
+ presentation_codec=codec,
222
+ )
223
+
224
+ # Public data: no bearer required (session has no token yet).
225
+ product = client.send(PRODUCT_BY_ID, None)
226
+ print("1) GET /products/1 (no session token) ->", product.title)
227
+
228
+ # Login: session skips Authorization on auth/login (see wrap_request).
229
+ logged_in = client.send(
230
+ AUTH_LOGIN,
231
+ LoginRequest(username="emilys", password="emilyspass"),
232
+ )
233
+ session.bind_login(logged_in)
234
+ print("2) POST /auth/login -> tokens stored on session")
235
+
236
+ # Authenticated route: HttpClient builds the request; session adds Bearer.
237
+ me = client.send(AUTH_ME, None)
238
+ print(
239
+ "3) GET /auth/me (Bearer from session) ->",
240
+ me.firstName,
241
+ me.lastName,
242
+ f"<{me.email}>",
243
+ )
244
+
245
+ # Refresh: body carries refreshToken; session still adds current access Bearer.
246
+ refreshed = client.send(
247
+ AUTH_REFRESH,
248
+ RefreshRequest(refreshToken=session.refresh_token or ""),
249
+ )
250
+ session.apply_refresh(refreshed)
251
+ print("4) POST /auth/refresh -> session updated with new access + refresh tokens")
252
+
253
+ me_again = client.send(AUTH_ME, None)
254
+ print(
255
+ "5) GET /auth/me again (new bearer) -> still",
256
+ me_again.username,
257
+ )
258
+
259
+ session.clear()
260
+ print(
261
+ "6) session.clear() - without new login, /auth/me would return 401 "
262
+ "and decoding would fail unless you handle errors."
263
+ )
264
+
265
+
266
+ if __name__ == "__main__":
267
+ main()
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.26.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "wrapfast"
7
+ version = "0.0.2"
8
+ description = "Composable HTTP API client: pluggable transport, session, and presentation layers for maintainable wrappers."
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ license = "0BSD"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "wrapfast contributors" }]
14
+ keywords = [
15
+ "api",
16
+ "api-client",
17
+ "http",
18
+ "http-client",
19
+ "rest",
20
+ "typing",
21
+ "wrapper",
22
+ ]
23
+ classifiers = [
24
+ "Development Status :: 4 - Beta",
25
+ "Intended Audience :: Developers",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.13",
29
+ "Programming Language :: Python :: 3 :: Only",
30
+ "Topic :: Internet :: WWW/HTTP",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ "Typing :: Typed",
33
+ ]
34
+ dependencies = []
35
+
36
+ [project.urls]
37
+ Repository = "https://github.com/UnknownAPI/wrapfast"
38
+ Issues = "https://github.com/UnknownAPI/wrapfast/issues"
39
+
40
+ [project.optional-dependencies]
41
+ examples = ["pydantic>=2", "requests>=2.31"]
@@ -0,0 +1,19 @@
1
+ """wrapfast — pluggable HTTP client: transport, session, and presentation layers."""
2
+
3
+ from .application import Endpoint, HttpClient
4
+ from .presentation import PresentationCodec
5
+ from .session import Session
6
+ from .transport import AsyncTransport, HttpRequest, HttpResponse, Transport
7
+
8
+ __all__ = [
9
+ "AsyncTransport",
10
+ "Endpoint",
11
+ "HttpClient",
12
+ "HttpRequest",
13
+ "HttpResponse",
14
+ "PresentationCodec",
15
+ "Session",
16
+ "Transport",
17
+ ]
18
+
19
+ __version__ = "0.1.0"
@@ -0,0 +1,43 @@
1
+ from dataclasses import dataclass
2
+
3
+ from .presentation import PresentationCodec
4
+ from .session import Session
5
+ from .transport import HttpRequest, Transport
6
+
7
+
8
+ @dataclass
9
+ class Endpoint[T_Request, T_Response]:
10
+ method: str
11
+ path: str
12
+ request_type: type[T_Request]
13
+ response_type: type[T_Response]
14
+
15
+
16
+ class HttpClient:
17
+ def __init__(
18
+ self,
19
+ base_url: str,
20
+ transport: Transport,
21
+ session: Session,
22
+ presentation_codec: PresentationCodec,
23
+ ) -> None:
24
+ self._base_url = base_url
25
+ self._transport = transport
26
+ self._session = session
27
+ self._presentation_codec = presentation_codec
28
+
29
+ def send[T_Request, T_Response](
30
+ self, endpoint: Endpoint[T_Request, T_Response], request: T_Request
31
+ ) -> T_Response:
32
+ http_req = HttpRequest(
33
+ method=endpoint.method,
34
+ url=f"{self._base_url}{endpoint.path}"
35
+ if not endpoint.path.startswith("/")
36
+ else endpoint.path,
37
+ headers={"Content-Type": self._presentation_codec.get_content_type()},
38
+ data=self._presentation_codec.encode(request),
39
+ )
40
+ http_req = self._session.wrap_request(http_req)
41
+ http_resp = self._transport.send(http_req)
42
+ http_resp = self._session.unwrap_response(http_resp)
43
+ return self._presentation_codec.decode(http_resp.data, endpoint.response_type)
@@ -0,0 +1,13 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+
5
+ class PresentationCodec(ABC):
6
+ @abstractmethod
7
+ def get_content_type(self) -> str: ...
8
+
9
+ @abstractmethod
10
+ def encode(self, obj: Any) -> bytes: ...
11
+
12
+ @abstractmethod
13
+ def decode(self, data: bytes, target: type) -> Any: ...
File without changes
@@ -0,0 +1,11 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from .transport import HttpRequest, HttpResponse
4
+
5
+
6
+ class Session(ABC):
7
+ @abstractmethod
8
+ def wrap_request(self, request: HttpRequest) -> HttpRequest: ...
9
+
10
+ @abstractmethod
11
+ def unwrap_response(self, response: HttpResponse) -> HttpResponse: ...
@@ -0,0 +1,28 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from typing import Mapping
4
+
5
+
6
+ @dataclass
7
+ class HttpRequest:
8
+ method: str
9
+ url: str
10
+ headers: Mapping[str, str]
11
+ data: bytes
12
+
13
+
14
+ @dataclass
15
+ class HttpResponse:
16
+ status_code: int
17
+ headers: Mapping[str, str]
18
+ data: bytes
19
+
20
+
21
+ class Transport(ABC):
22
+ @abstractmethod
23
+ def send(self, request: HttpRequest) -> HttpResponse: ...
24
+
25
+
26
+ class AsyncTransport(ABC):
27
+ @abstractmethod
28
+ async def send(self, request: HttpRequest) -> HttpResponse: ...
wrapfast-0.0.2/uv.lock ADDED
@@ -0,0 +1,221 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "annotated-types"
7
+ version = "0.7.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "certifi"
16
+ version = "2026.4.22"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
21
+ ]
22
+
23
+ [[package]]
24
+ name = "charset-normalizer"
25
+ version = "3.4.7"
26
+ source = { registry = "https://pypi.org/simple" }
27
+ sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
30
+ { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
31
+ { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
32
+ { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
33
+ { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
34
+ { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
35
+ { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
36
+ { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
37
+ { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
38
+ { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
39
+ { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
40
+ { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
41
+ { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
42
+ { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
43
+ { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
44
+ { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
45
+ { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
46
+ { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
47
+ { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
48
+ { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
49
+ { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
50
+ { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
51
+ { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
52
+ { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
53
+ { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
54
+ { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
55
+ { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
56
+ { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
57
+ { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
58
+ { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
59
+ { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
60
+ { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
61
+ { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
62
+ { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
63
+ { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
64
+ { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
65
+ { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
66
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
67
+ { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
68
+ { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
69
+ { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
70
+ { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
71
+ { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
72
+ { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
73
+ { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
74
+ { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
75
+ { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
76
+ { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
77
+ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
78
+ ]
79
+
80
+ [[package]]
81
+ name = "idna"
82
+ version = "3.14"
83
+ source = { registry = "https://pypi.org/simple" }
84
+ sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" }
85
+ wheels = [
86
+ { url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" },
87
+ ]
88
+
89
+ [[package]]
90
+ name = "pydantic"
91
+ version = "2.13.4"
92
+ source = { registry = "https://pypi.org/simple" }
93
+ dependencies = [
94
+ { name = "annotated-types" },
95
+ { name = "pydantic-core" },
96
+ { name = "typing-extensions" },
97
+ { name = "typing-inspection" },
98
+ ]
99
+ sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
100
+ wheels = [
101
+ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
102
+ ]
103
+
104
+ [[package]]
105
+ name = "pydantic-core"
106
+ version = "2.46.4"
107
+ source = { registry = "https://pypi.org/simple" }
108
+ dependencies = [
109
+ { name = "typing-extensions" },
110
+ ]
111
+ sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
112
+ wheels = [
113
+ { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
114
+ { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
115
+ { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
116
+ { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
117
+ { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
118
+ { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
119
+ { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
120
+ { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
121
+ { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
122
+ { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
123
+ { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
124
+ { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
125
+ { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
126
+ { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
127
+ { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
128
+ { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
129
+ { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
130
+ { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
131
+ { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
132
+ { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
133
+ { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
134
+ { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
135
+ { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
136
+ { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
137
+ { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
138
+ { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
139
+ { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
140
+ { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
141
+ { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
142
+ { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
143
+ { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
144
+ { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
145
+ { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
146
+ { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
147
+ { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
148
+ { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
149
+ { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
150
+ { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
151
+ { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
152
+ { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
153
+ { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
154
+ { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
155
+ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
156
+ { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
157
+ { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
158
+ ]
159
+
160
+ [[package]]
161
+ name = "requests"
162
+ version = "2.34.0"
163
+ source = { registry = "https://pypi.org/simple" }
164
+ dependencies = [
165
+ { name = "certifi" },
166
+ { name = "charset-normalizer" },
167
+ { name = "idna" },
168
+ { name = "urllib3" },
169
+ ]
170
+ sdist = { url = "https://files.pythonhosted.org/packages/43/b8/7a707d60fea4c49094e40262cc0e2ca6c768cca21587e34d3f705afec47e/requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a", size = 142436, upload-time = "2026-05-11T19:29:51.717Z" }
171
+ wheels = [
172
+ { url = "https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60", size = 73021, upload-time = "2026-05-11T19:29:49.923Z" },
173
+ ]
174
+
175
+ [[package]]
176
+ name = "typing-extensions"
177
+ version = "4.15.0"
178
+ source = { registry = "https://pypi.org/simple" }
179
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
180
+ wheels = [
181
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
182
+ ]
183
+
184
+ [[package]]
185
+ name = "typing-inspection"
186
+ version = "0.4.2"
187
+ source = { registry = "https://pypi.org/simple" }
188
+ dependencies = [
189
+ { name = "typing-extensions" },
190
+ ]
191
+ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
192
+ wheels = [
193
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
194
+ ]
195
+
196
+ [[package]]
197
+ name = "urllib3"
198
+ version = "2.7.0"
199
+ source = { registry = "https://pypi.org/simple" }
200
+ sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
201
+ wheels = [
202
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
203
+ ]
204
+
205
+ [[package]]
206
+ name = "wrapfast"
207
+ version = "0.0.2"
208
+ source = { editable = "." }
209
+
210
+ [package.optional-dependencies]
211
+ examples = [
212
+ { name = "pydantic" },
213
+ { name = "requests" },
214
+ ]
215
+
216
+ [package.metadata]
217
+ requires-dist = [
218
+ { name = "pydantic", marker = "extra == 'examples'", specifier = ">=2" },
219
+ { name = "requests", marker = "extra == 'examples'", specifier = ">=2.31" },
220
+ ]
221
+ provides-extras = ["examples"]