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.
Files changed (51) hide show
  1. clientity-0.1.6/.gitignore +51 -0
  2. clientity-0.1.6/PKG-INFO +318 -0
  3. clientity-0.1.6/docs/CHANGELOG.md +129 -0
  4. clientity-0.1.6/docs/README.md +302 -0
  5. clientity-0.1.6/pyproject.toml +34 -0
  6. clientity-0.1.6/src/clientity/__init__.py +9 -0
  7. clientity-0.1.6/src/clientity/core/__init__.py +19 -0
  8. clientity-0.1.6/src/clientity/core/adapters/__init__.py +17 -0
  9. clientity-0.1.6/src/clientity/core/adapters/_aiohttp.py +77 -0
  10. clientity-0.1.6/src/clientity/core/adapters/_httpx.py +58 -0
  11. clientity-0.1.6/src/clientity/core/adapters/_requests.py +53 -0
  12. clientity-0.1.6/src/clientity/core/adapters/base.py +37 -0
  13. clientity-0.1.6/src/clientity/core/client.py +112 -0
  14. clientity-0.1.6/src/clientity/core/endpoint.py +148 -0
  15. clientity-0.1.6/src/clientity/core/grouping/__init__.py +4 -0
  16. clientity-0.1.6/src/clientity/core/grouping/base.py +60 -0
  17. clientity-0.1.6/src/clientity/core/grouping/namespace.py +74 -0
  18. clientity-0.1.6/src/clientity/core/grouping/resource.py +54 -0
  19. clientity-0.1.6/src/clientity/core/hints.py +35 -0
  20. clientity-0.1.6/src/clientity/core/primitives/__init__.py +15 -0
  21. clientity-0.1.6/src/clientity/core/primitives/bound.py +26 -0
  22. clientity-0.1.6/src/clientity/core/primitives/instructions.py +79 -0
  23. clientity-0.1.6/src/clientity/core/primitives/method.py +25 -0
  24. clientity-0.1.6/src/clientity/core/primitives/url.py +104 -0
  25. clientity-0.1.6/src/clientity/core/protocols/__init__.py +16 -0
  26. clientity-0.1.6/src/clientity/core/protocols/interface.py +44 -0
  27. clientity-0.1.6/src/clientity/core/protocols/located.py +10 -0
  28. clientity-0.1.6/src/clientity/core/protocols/models.py +19 -0
  29. clientity-0.1.6/src/clientity/core/utils/__init__.py +9 -0
  30. clientity-0.1.6/src/clientity/core/utils/calls.py +160 -0
  31. clientity-0.1.6/src/clientity/core/utils/data.py +1 -0
  32. clientity-0.1.6/src/clientity/core/utils/http.py +95 -0
  33. clientity-0.1.6/src/clientity/core/utils/misc.py +3 -0
  34. clientity-0.1.6/src/clientity/core/utils/models.py +134 -0
  35. clientity-0.1.6/src/clientity/core/utils/typers.py +17 -0
  36. clientity-0.1.6/src/clientity/exc/__init__.py +6 -0
  37. clientity-0.1.6/src/clientity/exc/base.py +7 -0
  38. clientity-0.1.6/src/clientity/exc/http.py +6 -0
  39. clientity-0.1.6/src/clientity/exc/models.py +7 -0
  40. clientity-0.1.6/src/clientity/logs.py +26 -0
  41. clientity-0.1.6/tests/__init__.py +1 -0
  42. clientity-0.1.6/tests/conftest.py +73 -0
  43. clientity-0.1.6/tests/integration/__init__.py +1 -0
  44. clientity-0.1.6/tests/integration/test_client.py +297 -0
  45. clientity-0.1.6/tests/test_respall.py +199 -0
  46. clientity-0.1.6/tests/unit/__init__.py +1 -0
  47. clientity-0.1.6/tests/unit/test_adapters.py +139 -0
  48. clientity-0.1.6/tests/unit/test_endpoint.py +152 -0
  49. clientity-0.1.6/tests/unit/test_grouping.py +122 -0
  50. clientity-0.1.6/tests/unit/test_primitives.py +213 -0
  51. 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/
@@ -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