wrapfast 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wrapfast/__init__.py +19 -0
- wrapfast/application.py +43 -0
- wrapfast/presentation.py +13 -0
- wrapfast/py.typed +0 -0
- wrapfast/session.py +11 -0
- wrapfast/transport.py +28 -0
- wrapfast-0.0.2.dist-info/METADATA +164 -0
- wrapfast-0.0.2.dist-info/RECORD +10 -0
- wrapfast-0.0.2.dist-info/WHEEL +4 -0
- wrapfast-0.0.2.dist-info/licenses/LICENSE +12 -0
wrapfast/__init__.py
ADDED
|
@@ -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"
|
wrapfast/application.py
ADDED
|
@@ -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)
|
wrapfast/presentation.py
ADDED
|
@@ -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: ...
|
wrapfast/py.typed
ADDED
|
File without changes
|
wrapfast/session.py
ADDED
|
@@ -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: ...
|
wrapfast/transport.py
ADDED
|
@@ -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: ...
|
|
@@ -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,10 @@
|
|
|
1
|
+
wrapfast/__init__.py,sha256=yCDfWEhNJIObr1UXPSTGhkYs6kGrf7r3KqUey9Qspbw,492
|
|
2
|
+
wrapfast/application.py,sha256=_81v0y1hKVrb5eQ3hdCjL2wNfGAxSfh4sMFghwC9a-M,1440
|
|
3
|
+
wrapfast/presentation.py,sha256=liTWdBYae-Vs8Ewvk3IKkKHjociRx6zrqGhRbTuy5nY,314
|
|
4
|
+
wrapfast/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
wrapfast/session.py,sha256=3UH9AZRB64S28cjBTKLs_4GK6Y4LqJrS_cJpyXI1GDk,304
|
|
6
|
+
wrapfast/transport.py,sha256=sUWxONdIYxsNFRrj4Wzp_OKEiuMwSH4xBCXtAg7mbYo,557
|
|
7
|
+
wrapfast-0.0.2.dist-info/METADATA,sha256=ppsmOMQTlQtM6fu07wVjB1oYSXGK0zOUmHztDfdCLvU,7088
|
|
8
|
+
wrapfast-0.0.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
wrapfast-0.0.2.dist-info/licenses/LICENSE,sha256=wlzGe99oZkutSpDmkYVZPUA3Wsz53o7QURMrn8OqsLo,661
|
|
10
|
+
wrapfast-0.0.2.dist-info/RECORD,,
|
|
@@ -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.
|