clientity 0.1.6__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.
- clientity-0.1.6/.gitignore +51 -0
- clientity-0.1.6/PKG-INFO +318 -0
- clientity-0.1.6/docs/CHANGELOG.md +129 -0
- clientity-0.1.6/docs/README.md +302 -0
- clientity-0.1.6/pyproject.toml +34 -0
- clientity-0.1.6/src/clientity/__init__.py +9 -0
- clientity-0.1.6/src/clientity/core/__init__.py +19 -0
- clientity-0.1.6/src/clientity/core/adapters/__init__.py +17 -0
- clientity-0.1.6/src/clientity/core/adapters/_aiohttp.py +77 -0
- clientity-0.1.6/src/clientity/core/adapters/_httpx.py +58 -0
- clientity-0.1.6/src/clientity/core/adapters/_requests.py +53 -0
- clientity-0.1.6/src/clientity/core/adapters/base.py +37 -0
- clientity-0.1.6/src/clientity/core/client.py +112 -0
- clientity-0.1.6/src/clientity/core/endpoint.py +148 -0
- clientity-0.1.6/src/clientity/core/grouping/__init__.py +4 -0
- clientity-0.1.6/src/clientity/core/grouping/base.py +60 -0
- clientity-0.1.6/src/clientity/core/grouping/namespace.py +74 -0
- clientity-0.1.6/src/clientity/core/grouping/resource.py +54 -0
- clientity-0.1.6/src/clientity/core/hints.py +35 -0
- clientity-0.1.6/src/clientity/core/primitives/__init__.py +15 -0
- clientity-0.1.6/src/clientity/core/primitives/bound.py +26 -0
- clientity-0.1.6/src/clientity/core/primitives/instructions.py +79 -0
- clientity-0.1.6/src/clientity/core/primitives/method.py +25 -0
- clientity-0.1.6/src/clientity/core/primitives/url.py +104 -0
- clientity-0.1.6/src/clientity/core/protocols/__init__.py +16 -0
- clientity-0.1.6/src/clientity/core/protocols/interface.py +44 -0
- clientity-0.1.6/src/clientity/core/protocols/located.py +10 -0
- clientity-0.1.6/src/clientity/core/protocols/models.py +19 -0
- clientity-0.1.6/src/clientity/core/utils/__init__.py +9 -0
- clientity-0.1.6/src/clientity/core/utils/calls.py +160 -0
- clientity-0.1.6/src/clientity/core/utils/data.py +1 -0
- clientity-0.1.6/src/clientity/core/utils/http.py +95 -0
- clientity-0.1.6/src/clientity/core/utils/misc.py +3 -0
- clientity-0.1.6/src/clientity/core/utils/models.py +134 -0
- clientity-0.1.6/src/clientity/core/utils/typers.py +17 -0
- clientity-0.1.6/src/clientity/exc/__init__.py +6 -0
- clientity-0.1.6/src/clientity/exc/base.py +7 -0
- clientity-0.1.6/src/clientity/exc/http.py +6 -0
- clientity-0.1.6/src/clientity/exc/models.py +7 -0
- clientity-0.1.6/src/clientity/logs.py +26 -0
- clientity-0.1.6/tests/__init__.py +1 -0
- clientity-0.1.6/tests/conftest.py +73 -0
- clientity-0.1.6/tests/integration/__init__.py +1 -0
- clientity-0.1.6/tests/integration/test_client.py +297 -0
- clientity-0.1.6/tests/test_respall.py +199 -0
- clientity-0.1.6/tests/unit/__init__.py +1 -0
- clientity-0.1.6/tests/unit/test_adapters.py +139 -0
- clientity-0.1.6/tests/unit/test_endpoint.py +152 -0
- clientity-0.1.6/tests/unit/test_grouping.py +122 -0
- clientity-0.1.6/tests/unit/test_primitives.py +213 -0
- clientity-0.1.6/tests/unit/test_utils.py +199 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# scaffold/contents/GITIGNORE
|
|
2
|
+
|
|
3
|
+
.gitignore
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*$py.class
|
|
7
|
+
*.so
|
|
8
|
+
.Python
|
|
9
|
+
build/
|
|
10
|
+
develop-eggs/
|
|
11
|
+
dist/
|
|
12
|
+
downloads/
|
|
13
|
+
eggs/
|
|
14
|
+
.eggs/
|
|
15
|
+
lib/
|
|
16
|
+
lib64/
|
|
17
|
+
parts/
|
|
18
|
+
sdist/
|
|
19
|
+
var/
|
|
20
|
+
wheels/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.cfg
|
|
23
|
+
*.egg
|
|
24
|
+
.pytest_cache/
|
|
25
|
+
.coverage
|
|
26
|
+
htmlcov/
|
|
27
|
+
.tox/
|
|
28
|
+
.venv
|
|
29
|
+
venv/
|
|
30
|
+
ENV/
|
|
31
|
+
.mypy_cache/
|
|
32
|
+
.dmypy.json
|
|
33
|
+
dmypy.json
|
|
34
|
+
.DS_Store
|
|
35
|
+
*.swp
|
|
36
|
+
*.swo
|
|
37
|
+
*~
|
|
38
|
+
.idea/
|
|
39
|
+
.vscode/.gitignore
|
|
40
|
+
.DS_Store
|
|
41
|
+
venv.command
|
|
42
|
+
pyrightconfig.json
|
|
43
|
+
|
|
44
|
+
.venv/
|
|
45
|
+
__pycache__/
|
|
46
|
+
.pytest_cache/
|
|
47
|
+
temp/
|
|
48
|
+
sandbox/
|
|
49
|
+
notes/
|
|
50
|
+
handlers/
|
|
51
|
+
contents/
|
clientity-0.1.6/PKG-INFO
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clientity
|
|
3
|
+
Version: 0.1.6
|
|
4
|
+
Summary: rapid http clients
|
|
5
|
+
Author-email: Joel Yisrael <schizoprada@gmail.com>
|
|
6
|
+
Requires-Dist: loguru
|
|
7
|
+
Requires-Dist: pydantic
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
11
|
+
Provides-Extra: engines
|
|
12
|
+
Requires-Dist: aiohttp; extra == 'engines'
|
|
13
|
+
Requires-Dist: httpx; extra == 'engines'
|
|
14
|
+
Requires-Dist: requests; extra == 'engines'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# clientity
|
|
18
|
+
|
|
19
|
+
Rapid HTTP clients with operator-based endpoint definitions.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install clientity
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
With your preferred HTTP library:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install clientity[httpx] # or requests, aiohttp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import httpx
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from clientity import client, endpoint, Resource
|
|
39
|
+
|
|
40
|
+
# Define response models
|
|
41
|
+
@dataclass
|
|
42
|
+
class User:
|
|
43
|
+
id: int
|
|
44
|
+
name: str
|
|
45
|
+
email: str
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class Status:
|
|
49
|
+
ok: bool
|
|
50
|
+
version: str
|
|
51
|
+
|
|
52
|
+
# Create client
|
|
53
|
+
api = client(httpx.AsyncClient()) @ "https://api.example.com"
|
|
54
|
+
|
|
55
|
+
# Define endpoints with operators
|
|
56
|
+
api.status = endpoint.get @ "/status" >> Status
|
|
57
|
+
api.user = endpoint.get @ "/users/{id}" >> User
|
|
58
|
+
api.users = endpoint.get @ "/users" >> list[User]
|
|
59
|
+
|
|
60
|
+
# Use it
|
|
61
|
+
async def main():
|
|
62
|
+
status = await api.status()
|
|
63
|
+
user = await api.user(id=123)
|
|
64
|
+
users = await api.users()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Operators
|
|
68
|
+
|
|
69
|
+
| Operator | Method | Description |
|
|
70
|
+
|----------|--------|-------------|
|
|
71
|
+
| `@` | `.at(path)` | Set endpoint path |
|
|
72
|
+
| `%` | `.queries(model)` | Set query parameter model |
|
|
73
|
+
| `<<` | `.requests(model)` | Set request body model |
|
|
74
|
+
| `>>` | `.responds(model)` | Set response model |
|
|
75
|
+
| `&` | `.prehook(fn)` | Add pre-request hook |
|
|
76
|
+
| `\|` | `.posthook(fn)` | Add post-response hook |
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Full example
|
|
80
|
+
api.search = (
|
|
81
|
+
endpoint.post
|
|
82
|
+
@ "/search"
|
|
83
|
+
% SearchQuery # query params
|
|
84
|
+
<< SearchBody # request body
|
|
85
|
+
>> list[Result] # response model
|
|
86
|
+
& log_request # pre-hook [(Request) -> Request]
|
|
87
|
+
| log_response # post-hook [(Response) -> Response]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
results = await api.search(q="python", limit=10, filters={"type": "code"})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Path Parameters
|
|
94
|
+
|
|
95
|
+
Path parameters are extracted from `{param}` syntax and can be passed positionally or by name:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
api.user = endpoint.get @ "/users/{user_id}/posts/{post_id}"
|
|
99
|
+
|
|
100
|
+
# Both work:
|
|
101
|
+
post = await api.user(123, 456)
|
|
102
|
+
post = await api.user(user_id=123, post_id=456)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Query & Body Models
|
|
106
|
+
|
|
107
|
+
Use dataclasses or Pydantic models for query parameters and request bodies:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from dataclasses import dataclass
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class SearchQuery:
|
|
114
|
+
q: str
|
|
115
|
+
limit: int = 10
|
|
116
|
+
offset: int = 0
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class CreateUser:
|
|
120
|
+
name: str
|
|
121
|
+
email: str
|
|
122
|
+
|
|
123
|
+
api.search = endpoint.get @ "/search" % SearchQuery
|
|
124
|
+
api.create_user = endpoint.post @ "/users" << CreateUser
|
|
125
|
+
|
|
126
|
+
# Query params from model fields
|
|
127
|
+
results = await api.search(q="python", limit=20)
|
|
128
|
+
|
|
129
|
+
# Body from model fields
|
|
130
|
+
user = await api.create_user(name="Joel", email="joel@example.com")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Response Models
|
|
134
|
+
|
|
135
|
+
### Single Model
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
@dataclass
|
|
139
|
+
class User:
|
|
140
|
+
id: int
|
|
141
|
+
name: str
|
|
142
|
+
|
|
143
|
+
api.user = endpoint.get @ "/users/{id}" >> User
|
|
144
|
+
user = await api.user(id=1) # Returns User instance
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### List Response
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
api.users = endpoint.get @ "/users" >> list[User]
|
|
151
|
+
users = await api.users() # Returns list[User]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Custom Response Handling
|
|
155
|
+
|
|
156
|
+
Implement `__respond__` for custom single-item parsing:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
@dataclass
|
|
160
|
+
class User:
|
|
161
|
+
id: int
|
|
162
|
+
name: str
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def __respond__(cls, response) -> 'User':
|
|
166
|
+
data = response.json()["data"]["user"]
|
|
167
|
+
return cls(**data)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Implement `__respondall__` for custom list parsing:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
@dataclass
|
|
174
|
+
class User:
|
|
175
|
+
id: int
|
|
176
|
+
name: str
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def __respondall__(cls, response) -> list['User']:
|
|
180
|
+
data = response.json()
|
|
181
|
+
return [cls(**u) for u in data["results"]]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Resources
|
|
185
|
+
|
|
186
|
+
Group related endpoints under a path prefix:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from clientity import Resource
|
|
190
|
+
|
|
191
|
+
users = Resource("users")
|
|
192
|
+
users.list = endpoint.get @ "" >> list[User]
|
|
193
|
+
users.get = endpoint.get @ "{id}" >> User
|
|
194
|
+
users.create = endpoint.post @ "" << CreateUser >> User
|
|
195
|
+
users.delete = endpoint.delete @ "{id}"
|
|
196
|
+
|
|
197
|
+
api.users = users
|
|
198
|
+
|
|
199
|
+
# Usage
|
|
200
|
+
all_users = await api.users.list()
|
|
201
|
+
user = await api.users.get(id=123)
|
|
202
|
+
new_user = await api.users.create(name="Joel", email="joel@example.com")
|
|
203
|
+
await api.users.delete(id=123)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Nested Resources
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
api_resource = Resource("api")
|
|
210
|
+
v1 = Resource("v1")
|
|
211
|
+
v1.users = endpoint.get @ "users" >> list[User]
|
|
212
|
+
v1.posts = endpoint.get @ "posts" >> list[Post]
|
|
213
|
+
api_resource.v1 = v1
|
|
214
|
+
|
|
215
|
+
api.api = api_resource
|
|
216
|
+
|
|
217
|
+
# Resolves to /api/v1/users
|
|
218
|
+
users = await api.api.v1.users()
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Namespaces
|
|
222
|
+
|
|
223
|
+
For endpoints with different base URLs or independent HTTP clients:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from clientity import Namespace
|
|
227
|
+
|
|
228
|
+
# Independent namespace with own client
|
|
229
|
+
search = Namespace(
|
|
230
|
+
base="https://search.example.com",
|
|
231
|
+
interface=httpx.AsyncClient()
|
|
232
|
+
)
|
|
233
|
+
search.query = endpoint.post @ "/query" >> list[Result]
|
|
234
|
+
|
|
235
|
+
api.search = search
|
|
236
|
+
|
|
237
|
+
# Uses search.example.com, not api.example.com
|
|
238
|
+
results = await api.search.query(q="test")
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Dependent namespace (uses parent client):
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
auth = Namespace(name="auth")
|
|
245
|
+
auth.login = endpoint.post @ "/auth/login" << Credentials >> Token
|
|
246
|
+
|
|
247
|
+
api.auth = auth
|
|
248
|
+
|
|
249
|
+
# Uses api.example.com/auth/login
|
|
250
|
+
token = await api.auth.login(username="joel", password="secret")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Hooks
|
|
254
|
+
|
|
255
|
+
Pre-hooks modify the request before sending:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
def add_auth(request):
|
|
259
|
+
request.headers["Authorization"] = "Bearer token123"
|
|
260
|
+
return request
|
|
261
|
+
|
|
262
|
+
api.secure = endpoint.get @ "/secure" & add_auth
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Post-hooks modify the response after receiving:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
def log_response(response):
|
|
269
|
+
print(f"Status: {response.status_code}")
|
|
270
|
+
return response
|
|
271
|
+
|
|
272
|
+
api.logged = endpoint.get @ "/data" | log_response
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Async hooks work too:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
async def refresh_token(request):
|
|
279
|
+
token = await get_fresh_token()
|
|
280
|
+
request.headers["Authorization"] = f"Bearer {token}"
|
|
281
|
+
return request
|
|
282
|
+
|
|
283
|
+
api.secure = endpoint.get @ "/secure" & refresh_token
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Supported HTTP Libraries
|
|
287
|
+
|
|
288
|
+
clientity adapts to your preferred HTTP library:
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
# httpx (sync or async)
|
|
292
|
+
import httpx
|
|
293
|
+
api = client(httpx.AsyncClient()) @ "https://api.example.com"
|
|
294
|
+
api = client(httpx.Client()) @ "https://api.example.com"
|
|
295
|
+
|
|
296
|
+
# requests
|
|
297
|
+
import requests
|
|
298
|
+
api = client(requests.Session()) @ "https://api.example.com"
|
|
299
|
+
|
|
300
|
+
# aiohttp
|
|
301
|
+
import aiohttp
|
|
302
|
+
api = client(aiohttp.ClientSession()) @ "https://api.example.com"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Lazy Interface
|
|
306
|
+
|
|
307
|
+
Pass a callable to defer client creation:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
def make_client():
|
|
311
|
+
return httpx.AsyncClient(headers={"X-API-Key": os.environ["API_KEY"]})
|
|
312
|
+
|
|
313
|
+
api = client(make_client) @ "https://api.example.com"
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
MIT
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# `clientity` -- changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.6] -- Jan. 6th, 2026
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
+ **Execution utilities**: `http.execute` and `http.respond` extracted from Client/Namespace
|
|
7
|
+
+ **`groupings()` iterator**: On `Grouping` base class for iterating nested Resource/Namespace
|
|
8
|
+
+ **Integration test suite**: Full flow tests for Client → Resource → Namespace → Endpoint
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
* **Operator mappings**: `%` for query model, `&` for prehook, `|` for posthook (precedence fix)
|
|
12
|
+
* **Phantom types for DX**: Operators return `Bound[Endpoint]` to type checkers via `TYPE_CHECKING` block
|
|
13
|
+
* **`embody()` fallback**: Now calls `dictate()` for unknown object types
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
* `Resource.__nest__` / `Namespace.__nest__` double-prepend bug (strips child location prefix)
|
|
17
|
+
* `Client.__sourced` nested grouping re-nesting (bypass `__setattr__` with `object.__setattr__`)
|
|
18
|
+
* `Grouping.__setattr__` changed `if` to `elif` for Grouping check
|
|
19
|
+
* Various type hints: `Stringable` for URL params, `respond` overloads, `AsyncInterface` return types
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Current Agenda
|
|
24
|
+
|
|
25
|
+
### Immediate
|
|
26
|
+
1. Revisit DX typing for nested resource/namespace attribute access (`client.users.list` shows `Any`)
|
|
27
|
+
|
|
28
|
+
### Pinned for Later
|
|
29
|
+
1. IDE typing / signatures (`Unpack[TypedDict]` for kwargs autocomplete)
|
|
30
|
+
2. Iterable response models
|
|
31
|
+
3. WebSocket clients
|
|
32
|
+
4. CLI generation
|
|
33
|
+
|
|
34
|
+
## [0.1.5] -- Jan. 4th, 2026
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
+ **Grouping system**: Base `Grouping` class for organizing endpoints
|
|
38
|
+
- `Resource`: Dependent grouping with relative `location`, prepends path to nested endpoints
|
|
39
|
+
- `Namespace`: Independent grouping with absolute `base`, optional own `interface`/`adapter`
|
|
40
|
+
- Nested resource support with `__nest__` and `Located` protocol
|
|
41
|
+
- `endpoints()` iterator utility on base `Grouping`
|
|
42
|
+
|
|
43
|
+
+ **Adapters**: Library-specific request building and sending
|
|
44
|
+
- `Adapter` ABC with `build()` and `send()` methods
|
|
45
|
+
- `RequestsAdapter` for `requests` library
|
|
46
|
+
- `HttpxAdapter` for `httpx` library
|
|
47
|
+
- `AiohttpAdapter` for `aiohttp` library (fire-and-forget or session reuse modes)
|
|
48
|
+
- `adapt()` factory function with `Compatible()` detection
|
|
49
|
+
|
|
50
|
+
+ **Utilities**:
|
|
51
|
+
- `synced`: Inverse of `asynced` - wraps async callables to sync with `eval` option
|
|
52
|
+
- `dictate`: Extract dict from model instances (pydantic, dataclass, etc.)
|
|
53
|
+
- `sift.instructions()`: Convenience method for sifting with `Instructions`
|
|
54
|
+
- `domain()`: Extract domain name from URL
|
|
55
|
+
- `bound()`: Moved to utils/typers.py
|
|
56
|
+
|
|
57
|
+
+ **Type hints**:
|
|
58
|
+
- `Located` protocol for objects with `location` attribute
|
|
59
|
+
- `Requested` hint for embody input
|
|
60
|
+
- `Responded` hint for execution return type
|
|
61
|
+
- `WrappedEndpoint` = `Union[Endpoint, Bound[Endpoint]]`
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
* `Location.__new__`: Now idempotent - returns existing Location unchanged
|
|
65
|
+
* `Location.name`: Property returning PascalCase name from path segments (excluding params)
|
|
66
|
+
* `Endpoint.mutate()`: Public method exposing `__copy` for modification
|
|
67
|
+
* `Client.__init__`: Now accepts `Interfacing` (interface or callable returning interface)
|
|
68
|
+
* `embody()`: Parameter type changed from `Requesting` to `Optional[Requested]`
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
* `synced`/`asynced` overload signatures for proper type inference
|
|
72
|
+
* Name mangling issues with abstract methods (`__wrap__` -> `__wrap__`)
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Current Agenda
|
|
77
|
+
|
|
78
|
+
### Immediate
|
|
79
|
+
1. Update `Client` to handle `Resource` and `Namespace` assignment in `__setattr__`
|
|
80
|
+
2. Extract shared execution logic from `Client.__x` and `Namespace.__x` into utility
|
|
81
|
+
3. Test full flow: Client -> Resource -> Endpoint -> execution
|
|
82
|
+
|
|
83
|
+
### Pending
|
|
84
|
+
- IDE typing / signatures (`Unpack[TypedDict]` for kwargs autocomplete)
|
|
85
|
+
- `domain()` utility implementation (currently raises)
|
|
86
|
+
|
|
87
|
+
### Pinned for Later
|
|
88
|
+
1. **Iterable response models**: Handle `list[Item]` responses, sequence parsing, `__responds__` for collections
|
|
89
|
+
2. **WebSocket clients**: Different protocol handling, maybe `ws_endpoint` or separate client type
|
|
90
|
+
3. **CLI generation**: `clientity generate --source /path/to/spec --destination /path/to/client.py` from OpenAPI/FastAPI/Flask specs
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## [0.1.0] -- Dec. 29, 2025
|
|
95
|
+
* Initial project scaffolding and core primitives
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
+ **Project Structure**: Established base package layout with `core/`, `exc/`, and logging infrastructure
|
|
99
|
+
+ **Logging**: Configurable logging via `CLIENTITYLOGS` environment variable with `devnull` fallback
|
|
100
|
+
+ **Exceptions**: Base `ClientityError` exception class
|
|
101
|
+
|
|
102
|
+
+ **Protocols**:
|
|
103
|
+
- `SyncInterface` / `AsyncInterface`: Runtime-checkable protocols for HTTP engines (requests, httpx, aiohttp, etc.)
|
|
104
|
+
- `Requestable`: Protocol for request models with `__request__()` method and optional `RequestKey` class var
|
|
105
|
+
- `Responsive`: Protocol for response models with `__respond__()` classmethod
|
|
106
|
+
|
|
107
|
+
+ **Primitives**:
|
|
108
|
+
- `Location`: Path fragment class with parameter extraction (`{id}`), `/` operator for joining, and `resolve()` for substitution
|
|
109
|
+
- `URL`: Full URL class combining base + location, with `resolve()` for final string output
|
|
110
|
+
- `MethodType`: Literal type for HTTP methods with constants (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`)
|
|
111
|
+
- `Hooks`: Container for pre/post request hooks with async conversion via `before`/`after` properties
|
|
112
|
+
- `Instructions`: Core instruction set containing method, location, hooks, and models (querying, requesting, responding) with `merge()` and `prepend()` methods
|
|
113
|
+
|
|
114
|
+
+ **Endpoint**:
|
|
115
|
+
- `Endpoint`: Builder class for constructing `Instructions` via method chaining
|
|
116
|
+
- `endpoint` factory with method-specific properties (`.get`, `.post`, `.put`, etc.)
|
|
117
|
+
- Operator overloads: `@` (location), `|` (hooks), `<<` (request models), `>>` (response model)
|
|
118
|
+
|
|
119
|
+
+ **Utilities**:
|
|
120
|
+
- `asynced`: Utility for wrapping sync callables as async
|
|
121
|
+
- `embody`: Utility for encoding request objects to `(key, data)` tuples
|
|
122
|
+
|
|
123
|
+
### Architecture
|
|
124
|
+
+ **Reverse-cascade design**: Endpoints return instruction sets that bubble up to client for execution
|
|
125
|
+
+ **Engine agnostic**: Interface protocols accept any HTTP library that quacks correctly
|
|
126
|
+
+ **Sift pattern** (pending): kwargs distributed to path/query/body based on model field introspection
|
|
127
|
+
|
|
128
|
+
## [0.0.0] -- Dec. 28, 2025
|
|
129
|
+
* Project Initialized
|