haveapi-client 0.30.0__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.
- haveapi_client-0.30.0/LICENSE.txt +21 -0
- haveapi_client-0.30.0/PKG-INFO +198 -0
- haveapi_client-0.30.0/README.md +176 -0
- haveapi_client-0.30.0/pyproject.toml +65 -0
- haveapi_client-0.30.0/pyproject.toml.orig +56 -0
- haveapi_client-0.30.0/src/haveapi_client/__init__.py +62 -0
- haveapi_client-0.30.0/src/haveapi_client/_auth/__init__.py +139 -0
- haveapi_client-0.30.0/src/haveapi_client/_protocol/__init__.py +52 -0
- haveapi_client-0.30.0/src/haveapi_client/_protocol/description.py +176 -0
- haveapi_client-0.30.0/src/haveapi_client/_protocol/execution.py +61 -0
- haveapi_client-0.30.0/src/haveapi_client/_protocol/input.py +161 -0
- haveapi_client-0.30.0/src/haveapi_client/_protocol/models.py +166 -0
- haveapi_client-0.30.0/src/haveapi_client/_protocol/request.py +345 -0
- haveapi_client-0.30.0/src/haveapi_client/_protocol/response.py +97 -0
- haveapi_client-0.30.0/src/haveapi_client/_resources/__init__.py +196 -0
- haveapi_client-0.30.0/src/haveapi_client/_resources/state.py +266 -0
- haveapi_client-0.30.0/src/haveapi_client/_transport/__init__.py +155 -0
- haveapi_client-0.30.0/src/haveapi_client/async_client.py +209 -0
- haveapi_client-0.30.0/src/haveapi_client/client.py +203 -0
- haveapi_client-0.30.0/src/haveapi_client/exceptions.py +88 -0
- haveapi_client-0.30.0/src/haveapi_client/operations.py +190 -0
- haveapi_client-0.30.0/src/haveapi_client/pagination.py +202 -0
- haveapi_client-0.30.0/src/haveapi_client/py.typed +0 -0
- haveapi_client-0.30.0/src/haveapi_client/response.py +303 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014 Jakub Skokan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: haveapi-client
|
|
3
|
+
Version: 0.30.0
|
|
4
|
+
Summary: Dynamic synchronous and asynchronous clients for HaveAPI APIs
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE.txt
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Dist: httpx>=0.28.1,<1
|
|
16
|
+
Requires-Dist: anyio>=4.15,<5
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Project-URL: Homepage, https://github.com/vpsfreecz/haveapi
|
|
19
|
+
Project-URL: Source, https://github.com/vpsfreecz/haveapi/tree/master/clients/python
|
|
20
|
+
Project-URL: Issues, https://github.com/vpsfreecz/haveapi/issues
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# HaveAPI Python client
|
|
24
|
+
|
|
25
|
+
`haveapi-client` discovers resources, actions and parameter types from any
|
|
26
|
+
HaveAPI API using protocol 2.x. It provides synchronous and asynchronous clients
|
|
27
|
+
with the same dynamic interface. Python 3.11 or newer is required.
|
|
28
|
+
|
|
29
|
+
[vpsAdmin](https://github.com/vpsfreecz/vpsadmin) is the control panel used by
|
|
30
|
+
vpsFree.cz to manage virtual private servers (VPSes). It is HaveAPI's main user,
|
|
31
|
+
so the examples in this README and the `examples/` directory use its API.
|
|
32
|
+
|
|
33
|
+
Install the package with `uv add haveapi-client` or `pip install haveapi-client`
|
|
34
|
+
once a Python release is available. To try a checkout before publication, run
|
|
35
|
+
`uv add /path/to/haveapi/clients/python` in your application.
|
|
36
|
+
|
|
37
|
+
## Read a page of VPSes
|
|
38
|
+
|
|
39
|
+
The examples use an existing vpsAdmin API token. Set `VPSADMIN_URL` to the API
|
|
40
|
+
base URL and `VPSADMIN_TOKEN` to your token. Keep credentials out of source files.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import os
|
|
44
|
+
|
|
45
|
+
from haveapi_client import Client, TokenAuth
|
|
46
|
+
|
|
47
|
+
with Client(
|
|
48
|
+
os.environ["VPSADMIN_URL"], auth=TokenAuth(os.environ["VPSADMIN_TOKEN"])
|
|
49
|
+
) as api:
|
|
50
|
+
result = api.vps.list.call(params={"limit": 100}, meta={"count": True})
|
|
51
|
+
for vps in result.data:
|
|
52
|
+
print(vps["id"], vps["hostname"])
|
|
53
|
+
print(result.meta.get("total_count"))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `AsyncClient` with `async with` and await action calls for async applications.
|
|
57
|
+
It works with asyncio and Trio. Complete runnable examples are in
|
|
58
|
+
[`examples/list_vps.py`](examples/list_vps.py) and
|
|
59
|
+
[`examples/list_vps_async.py`](examples/list_vps_async.py).
|
|
60
|
+
|
|
61
|
+
## Discovery and calls
|
|
62
|
+
|
|
63
|
+
Constructing a client and inspecting resource or action properties sends no
|
|
64
|
+
requests. Call `connect()` explicitly or enter the client context to fetch its
|
|
65
|
+
description. An authenticated connection refreshes discovery with the selected
|
|
66
|
+
identity, so available actions and parameters reflect server authorization.
|
|
67
|
+
The server still authorizes every request.
|
|
68
|
+
|
|
69
|
+
The base URL includes the API mount, for example `https://api.example/mount/`.
|
|
70
|
+
Select the API version with the client's `version` argument. Discovery uses that
|
|
71
|
+
mount and version; described action paths already contain their routing prefix
|
|
72
|
+
and resolve from the origin. The client preserves the server's ordinary,
|
|
73
|
+
authentication and action-state routes without repeating the mount or adding
|
|
74
|
+
a version to a global authentication route.
|
|
75
|
+
|
|
76
|
+
Action destinations must remain within the configured origin and mount before
|
|
77
|
+
credentials are attached. Applications that fabricate or cache API-relative
|
|
78
|
+
action descriptions for a mounted API must refresh them from the server's
|
|
79
|
+
OPTIONS description. Root-mounted APIs keep the same URLs.
|
|
80
|
+
|
|
81
|
+
`api.vps.list(limit=10)` is shorthand for
|
|
82
|
+
`api.vps.list.call(params={"limit": 10})`. Use `.call()` to pass action metadata,
|
|
83
|
+
explicit path parameters or per-request `RequestOptions`. Input values are
|
|
84
|
+
validated and encoded from the description. Omitted parameters and explicit
|
|
85
|
+
`None` have different meanings; `None` is accepted only where advertised.
|
|
86
|
+
|
|
87
|
+
POST, PUT and DELETE send JSON input and metadata. The Ruby server requires
|
|
88
|
+
the JSON root to be an object: list input needs a described namespace around
|
|
89
|
+
it. An unnamespaced list body is rejected locally. JSON retains empty
|
|
90
|
+
containers, sparse object rows, and finite numbers, booleans and nulls in
|
|
91
|
+
Custom values.
|
|
92
|
+
|
|
93
|
+
GET uses Rack bracket-query encoding. It accepts only shapes that can be
|
|
94
|
+
represented without changing their meaning:
|
|
95
|
+
|
|
96
|
+
- Described scalar values use their normal type conversion, including the
|
|
97
|
+
empty query value for a permitted nullable scalar.
|
|
98
|
+
- Nonempty mappings can nest. Every key component, including described
|
|
99
|
+
namespaces and parameter names, must be nonempty and contain no `[` or `]`.
|
|
100
|
+
- Nonempty arrays of strings retain their order. Arrays of flat objects must
|
|
101
|
+
have the same nonempty key set in every row, with no container-valued fields.
|
|
102
|
+
The client emits each row's keys in the same order. Described row fields use
|
|
103
|
+
their declared types; Custom row fields must contain strings.
|
|
104
|
+
- Nested Custom values may contain strings, mappings and these permitted
|
|
105
|
+
arrays. Nested numbers, booleans and nulls are rejected because a query
|
|
106
|
+
cannot preserve their JSON types. A nullable top-level Custom value follows
|
|
107
|
+
the normal nullable-scalar rule.
|
|
108
|
+
- Sparse rows, empty rows, nested arrays, explicit empty lists or mappings,
|
|
109
|
+
and object arrays with container-valued fields are rejected. Omitted input
|
|
110
|
+
remains omitted; structural input or metadata wrappers with no supplied
|
|
111
|
+
fields do not require a query pair.
|
|
112
|
+
|
|
113
|
+
The same GET restrictions apply to metadata. Invalid shapes raise
|
|
114
|
+
`ValidationError` before an action request is sent. The client follows the
|
|
115
|
+
action's described method; it does not replace GET with JSON. Only the literal
|
|
116
|
+
`Custom` parameter type accepts arbitrary finite JSON values. Unknown parameter
|
|
117
|
+
type names are rejected rather than treated as Custom.
|
|
118
|
+
|
|
119
|
+
For names that collide with Python methods or keywords, use exact-name maps:
|
|
120
|
+
`api.resources["vps"].actions["index"]`. Indexing a resource binds one path
|
|
121
|
+
argument, such as `api.vps[vps_id].show()`. Follow the discovered hierarchy for
|
|
122
|
+
nested resources: `api.parent[parent_id].child.list()` applies when that API
|
|
123
|
+
describes a child resource under its parent. In vpsAdmin, network interfaces
|
|
124
|
+
are top-level: use `api.network_interface.list(vps=vps_id)`.
|
|
125
|
+
|
|
126
|
+
An action returns `Response`: `.data` is a finite result, `.meta` contains action
|
|
127
|
+
metadata, and `.status_code`, `.headers` and `.envelope` expose response context.
|
|
128
|
+
Returned objects are read-only mappings with optional attribute access to
|
|
129
|
+
fields. Mapping access remains reliable when a field name collides with a method.
|
|
130
|
+
Association handles can be inspected without fetching them; call `.fetch()`
|
|
131
|
+
explicitly to retrieve an association. Refreshing discovery or changing
|
|
132
|
+
authentication invalidates previously obtained resource and object handles.
|
|
133
|
+
|
|
134
|
+
## Authentication
|
|
135
|
+
|
|
136
|
+
Use `BasicAuth(username, password)`, `TokenAuth(token)` or
|
|
137
|
+
`OAuth2Auth(access_token)` for an existing credential. OAuth2Auth sends a bearer
|
|
138
|
+
token; browser authorization and token refresh are outside this package.
|
|
139
|
+
|
|
140
|
+
For password-based token login, call
|
|
141
|
+
`api.begin_token_login({"user": username, "password": password, ...})` with
|
|
142
|
+
fields accepted by that API. The result is `Authenticated` or an
|
|
143
|
+
`AuthenticationChallenge`. Read a challenge's `.next_action` and `.input`, then
|
|
144
|
+
call `.submit({...})` with the required fields. Continue until authenticated;
|
|
145
|
+
this supports API-specific chains such as TOTP followed by a password reset.
|
|
146
|
+
`AsyncClient` returns `AsyncAuthenticationChallenge`, whose submit is awaited.
|
|
147
|
+
The continuation token is kept by the challenge and cannot be overridden.
|
|
148
|
+
|
|
149
|
+
`renew_token()` and `revoke_token()` use the provider's advertised actions.
|
|
150
|
+
`logout()` clears local authentication and rediscovers the public API. Closing a
|
|
151
|
+
client does not revoke its server token. Credentials are kept in memory; the
|
|
152
|
+
package does not write a credential cache.
|
|
153
|
+
|
|
154
|
+
## Pagination and operations
|
|
155
|
+
|
|
156
|
+
Normal action calls fetch one page. Use `action.iter_pages(...)` or
|
|
157
|
+
`action.iter_all(...)` for explicit iteration. Async clients return async
|
|
158
|
+
iterators. Supply a cursor strategy that matches the endpoint's ordering:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from haveapi_client import IdCursor
|
|
162
|
+
|
|
163
|
+
for vps in api.vps.list.iter_all(strategy=IdCursor(direction="asc"), page_size=100):
|
|
164
|
+
print(vps["id"])
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`IdCursor` requires integer IDs in the requested monotonic order and rejects a
|
|
168
|
+
repeated or reversed cursor. The endpoint must advertise `limit` and the cursor
|
|
169
|
+
parameter. HaveAPI pagination metadata alone does not promise this ordering;
|
|
170
|
+
select the strategy from the API's documented behavior.
|
|
171
|
+
|
|
172
|
+
When a response includes an operation, use `response.operation.show()`,
|
|
173
|
+
`.wait(timeout=300, interval=1)` or `.cancel()`. Await these methods on an async
|
|
174
|
+
operation. Waiting checks the described action-state resource and raises
|
|
175
|
+
`OperationFailedError` for failure or `OperationTimeoutError` at the deadline.
|
|
176
|
+
Timeout and coroutine cancellation do not cancel the server operation. Explicit
|
|
177
|
+
cancel requires advertised cancellation support.
|
|
178
|
+
|
|
179
|
+
Async waits enforce the overall deadline. A synchronous wait checks its budget
|
|
180
|
+
between requests and bounds each HTTP phase by the remaining time; an in-flight
|
|
181
|
+
synchronous request can finish after the overall deadline.
|
|
182
|
+
|
|
183
|
+
## Errors and connection ownership
|
|
184
|
+
|
|
185
|
+
Catch `HaveAPIError` for client errors, or its more specific `TransportError`,
|
|
186
|
+
`ProtocolError`, `AuthenticationError`, `ValidationError` and `ActionError`
|
|
187
|
+
subclasses. Errors provide status and structured details when available. Treat
|
|
188
|
+
response payloads and diagnostics as potentially private application data.
|
|
189
|
+
|
|
190
|
+
The default HTTP timeout is 30 seconds and can be overridden per request with
|
|
191
|
+
`RequestOptions(timeout=...)`. Requests are not retried or redirected. Clients
|
|
192
|
+
use independent request headers and bodies; authentication changes are rejected
|
|
193
|
+
while requests are active. Injected HTTPX clients remain caller-owned and must
|
|
194
|
+
be closed by the caller. Their default authentication, headers and cookies are
|
|
195
|
+
not inherited by HaveAPI requests.
|
|
196
|
+
|
|
197
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for checks and packaging, and the shared
|
|
198
|
+
[release procedure](../../doc/agent-instructions/releases.md) for publication.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# HaveAPI Python client
|
|
2
|
+
|
|
3
|
+
`haveapi-client` discovers resources, actions and parameter types from any
|
|
4
|
+
HaveAPI API using protocol 2.x. It provides synchronous and asynchronous clients
|
|
5
|
+
with the same dynamic interface. Python 3.11 or newer is required.
|
|
6
|
+
|
|
7
|
+
[vpsAdmin](https://github.com/vpsfreecz/vpsadmin) is the control panel used by
|
|
8
|
+
vpsFree.cz to manage virtual private servers (VPSes). It is HaveAPI's main user,
|
|
9
|
+
so the examples in this README and the `examples/` directory use its API.
|
|
10
|
+
|
|
11
|
+
Install the package with `uv add haveapi-client` or `pip install haveapi-client`
|
|
12
|
+
once a Python release is available. To try a checkout before publication, run
|
|
13
|
+
`uv add /path/to/haveapi/clients/python` in your application.
|
|
14
|
+
|
|
15
|
+
## Read a page of VPSes
|
|
16
|
+
|
|
17
|
+
The examples use an existing vpsAdmin API token. Set `VPSADMIN_URL` to the API
|
|
18
|
+
base URL and `VPSADMIN_TOKEN` to your token. Keep credentials out of source files.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import os
|
|
22
|
+
|
|
23
|
+
from haveapi_client import Client, TokenAuth
|
|
24
|
+
|
|
25
|
+
with Client(
|
|
26
|
+
os.environ["VPSADMIN_URL"], auth=TokenAuth(os.environ["VPSADMIN_TOKEN"])
|
|
27
|
+
) as api:
|
|
28
|
+
result = api.vps.list.call(params={"limit": 100}, meta={"count": True})
|
|
29
|
+
for vps in result.data:
|
|
30
|
+
print(vps["id"], vps["hostname"])
|
|
31
|
+
print(result.meta.get("total_count"))
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use `AsyncClient` with `async with` and await action calls for async applications.
|
|
35
|
+
It works with asyncio and Trio. Complete runnable examples are in
|
|
36
|
+
[`examples/list_vps.py`](examples/list_vps.py) and
|
|
37
|
+
[`examples/list_vps_async.py`](examples/list_vps_async.py).
|
|
38
|
+
|
|
39
|
+
## Discovery and calls
|
|
40
|
+
|
|
41
|
+
Constructing a client and inspecting resource or action properties sends no
|
|
42
|
+
requests. Call `connect()` explicitly or enter the client context to fetch its
|
|
43
|
+
description. An authenticated connection refreshes discovery with the selected
|
|
44
|
+
identity, so available actions and parameters reflect server authorization.
|
|
45
|
+
The server still authorizes every request.
|
|
46
|
+
|
|
47
|
+
The base URL includes the API mount, for example `https://api.example/mount/`.
|
|
48
|
+
Select the API version with the client's `version` argument. Discovery uses that
|
|
49
|
+
mount and version; described action paths already contain their routing prefix
|
|
50
|
+
and resolve from the origin. The client preserves the server's ordinary,
|
|
51
|
+
authentication and action-state routes without repeating the mount or adding
|
|
52
|
+
a version to a global authentication route.
|
|
53
|
+
|
|
54
|
+
Action destinations must remain within the configured origin and mount before
|
|
55
|
+
credentials are attached. Applications that fabricate or cache API-relative
|
|
56
|
+
action descriptions for a mounted API must refresh them from the server's
|
|
57
|
+
OPTIONS description. Root-mounted APIs keep the same URLs.
|
|
58
|
+
|
|
59
|
+
`api.vps.list(limit=10)` is shorthand for
|
|
60
|
+
`api.vps.list.call(params={"limit": 10})`. Use `.call()` to pass action metadata,
|
|
61
|
+
explicit path parameters or per-request `RequestOptions`. Input values are
|
|
62
|
+
validated and encoded from the description. Omitted parameters and explicit
|
|
63
|
+
`None` have different meanings; `None` is accepted only where advertised.
|
|
64
|
+
|
|
65
|
+
POST, PUT and DELETE send JSON input and metadata. The Ruby server requires
|
|
66
|
+
the JSON root to be an object: list input needs a described namespace around
|
|
67
|
+
it. An unnamespaced list body is rejected locally. JSON retains empty
|
|
68
|
+
containers, sparse object rows, and finite numbers, booleans and nulls in
|
|
69
|
+
Custom values.
|
|
70
|
+
|
|
71
|
+
GET uses Rack bracket-query encoding. It accepts only shapes that can be
|
|
72
|
+
represented without changing their meaning:
|
|
73
|
+
|
|
74
|
+
- Described scalar values use their normal type conversion, including the
|
|
75
|
+
empty query value for a permitted nullable scalar.
|
|
76
|
+
- Nonempty mappings can nest. Every key component, including described
|
|
77
|
+
namespaces and parameter names, must be nonempty and contain no `[` or `]`.
|
|
78
|
+
- Nonempty arrays of strings retain their order. Arrays of flat objects must
|
|
79
|
+
have the same nonempty key set in every row, with no container-valued fields.
|
|
80
|
+
The client emits each row's keys in the same order. Described row fields use
|
|
81
|
+
their declared types; Custom row fields must contain strings.
|
|
82
|
+
- Nested Custom values may contain strings, mappings and these permitted
|
|
83
|
+
arrays. Nested numbers, booleans and nulls are rejected because a query
|
|
84
|
+
cannot preserve their JSON types. A nullable top-level Custom value follows
|
|
85
|
+
the normal nullable-scalar rule.
|
|
86
|
+
- Sparse rows, empty rows, nested arrays, explicit empty lists or mappings,
|
|
87
|
+
and object arrays with container-valued fields are rejected. Omitted input
|
|
88
|
+
remains omitted; structural input or metadata wrappers with no supplied
|
|
89
|
+
fields do not require a query pair.
|
|
90
|
+
|
|
91
|
+
The same GET restrictions apply to metadata. Invalid shapes raise
|
|
92
|
+
`ValidationError` before an action request is sent. The client follows the
|
|
93
|
+
action's described method; it does not replace GET with JSON. Only the literal
|
|
94
|
+
`Custom` parameter type accepts arbitrary finite JSON values. Unknown parameter
|
|
95
|
+
type names are rejected rather than treated as Custom.
|
|
96
|
+
|
|
97
|
+
For names that collide with Python methods or keywords, use exact-name maps:
|
|
98
|
+
`api.resources["vps"].actions["index"]`. Indexing a resource binds one path
|
|
99
|
+
argument, such as `api.vps[vps_id].show()`. Follow the discovered hierarchy for
|
|
100
|
+
nested resources: `api.parent[parent_id].child.list()` applies when that API
|
|
101
|
+
describes a child resource under its parent. In vpsAdmin, network interfaces
|
|
102
|
+
are top-level: use `api.network_interface.list(vps=vps_id)`.
|
|
103
|
+
|
|
104
|
+
An action returns `Response`: `.data` is a finite result, `.meta` contains action
|
|
105
|
+
metadata, and `.status_code`, `.headers` and `.envelope` expose response context.
|
|
106
|
+
Returned objects are read-only mappings with optional attribute access to
|
|
107
|
+
fields. Mapping access remains reliable when a field name collides with a method.
|
|
108
|
+
Association handles can be inspected without fetching them; call `.fetch()`
|
|
109
|
+
explicitly to retrieve an association. Refreshing discovery or changing
|
|
110
|
+
authentication invalidates previously obtained resource and object handles.
|
|
111
|
+
|
|
112
|
+
## Authentication
|
|
113
|
+
|
|
114
|
+
Use `BasicAuth(username, password)`, `TokenAuth(token)` or
|
|
115
|
+
`OAuth2Auth(access_token)` for an existing credential. OAuth2Auth sends a bearer
|
|
116
|
+
token; browser authorization and token refresh are outside this package.
|
|
117
|
+
|
|
118
|
+
For password-based token login, call
|
|
119
|
+
`api.begin_token_login({"user": username, "password": password, ...})` with
|
|
120
|
+
fields accepted by that API. The result is `Authenticated` or an
|
|
121
|
+
`AuthenticationChallenge`. Read a challenge's `.next_action` and `.input`, then
|
|
122
|
+
call `.submit({...})` with the required fields. Continue until authenticated;
|
|
123
|
+
this supports API-specific chains such as TOTP followed by a password reset.
|
|
124
|
+
`AsyncClient` returns `AsyncAuthenticationChallenge`, whose submit is awaited.
|
|
125
|
+
The continuation token is kept by the challenge and cannot be overridden.
|
|
126
|
+
|
|
127
|
+
`renew_token()` and `revoke_token()` use the provider's advertised actions.
|
|
128
|
+
`logout()` clears local authentication and rediscovers the public API. Closing a
|
|
129
|
+
client does not revoke its server token. Credentials are kept in memory; the
|
|
130
|
+
package does not write a credential cache.
|
|
131
|
+
|
|
132
|
+
## Pagination and operations
|
|
133
|
+
|
|
134
|
+
Normal action calls fetch one page. Use `action.iter_pages(...)` or
|
|
135
|
+
`action.iter_all(...)` for explicit iteration. Async clients return async
|
|
136
|
+
iterators. Supply a cursor strategy that matches the endpoint's ordering:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from haveapi_client import IdCursor
|
|
140
|
+
|
|
141
|
+
for vps in api.vps.list.iter_all(strategy=IdCursor(direction="asc"), page_size=100):
|
|
142
|
+
print(vps["id"])
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`IdCursor` requires integer IDs in the requested monotonic order and rejects a
|
|
146
|
+
repeated or reversed cursor. The endpoint must advertise `limit` and the cursor
|
|
147
|
+
parameter. HaveAPI pagination metadata alone does not promise this ordering;
|
|
148
|
+
select the strategy from the API's documented behavior.
|
|
149
|
+
|
|
150
|
+
When a response includes an operation, use `response.operation.show()`,
|
|
151
|
+
`.wait(timeout=300, interval=1)` or `.cancel()`. Await these methods on an async
|
|
152
|
+
operation. Waiting checks the described action-state resource and raises
|
|
153
|
+
`OperationFailedError` for failure or `OperationTimeoutError` at the deadline.
|
|
154
|
+
Timeout and coroutine cancellation do not cancel the server operation. Explicit
|
|
155
|
+
cancel requires advertised cancellation support.
|
|
156
|
+
|
|
157
|
+
Async waits enforce the overall deadline. A synchronous wait checks its budget
|
|
158
|
+
between requests and bounds each HTTP phase by the remaining time; an in-flight
|
|
159
|
+
synchronous request can finish after the overall deadline.
|
|
160
|
+
|
|
161
|
+
## Errors and connection ownership
|
|
162
|
+
|
|
163
|
+
Catch `HaveAPIError` for client errors, or its more specific `TransportError`,
|
|
164
|
+
`ProtocolError`, `AuthenticationError`, `ValidationError` and `ActionError`
|
|
165
|
+
subclasses. Errors provide status and structured details when available. Treat
|
|
166
|
+
response payloads and diagnostics as potentially private application data.
|
|
167
|
+
|
|
168
|
+
The default HTTP timeout is 30 seconds and can be overridden per request with
|
|
169
|
+
`RequestOptions(timeout=...)`. Requests are not retried or redirected. Clients
|
|
170
|
+
use independent request headers and bodies; authentication changes are rejected
|
|
171
|
+
while requests are active. Injected HTTPX clients remain caller-owned and must
|
|
172
|
+
be closed by the caller. Their default authentication, headers and cookies are
|
|
173
|
+
not inherited by HaveAPI requests.
|
|
174
|
+
|
|
175
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for checks and packaging, and the shared
|
|
176
|
+
[release procedure](../../doc/agent-instructions/releases.md) for publication.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "haveapi-client"
|
|
3
|
+
version = "0.30.0"
|
|
4
|
+
description = "Dynamic synchronous and asynchronous clients for HaveAPI APIs"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE.txt"]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"httpx>=0.28.1,<1",
|
|
11
|
+
"anyio>=4.15,<5",
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Programming Language :: Python :: 3.15",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/vpsfreecz/haveapi"
|
|
26
|
+
Source = "https://github.com/vpsfreecz/haveapi/tree/master/clients/python"
|
|
27
|
+
Issues = "https://github.com/vpsfreecz/haveapi/issues"
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["uv_build>=0.13.0,<0.14"]
|
|
31
|
+
build-backend = "uv_build"
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"mypy>=2.4,<3",
|
|
36
|
+
"pytest>=9.1,<10",
|
|
37
|
+
"ruff==0.17.1",
|
|
38
|
+
"trio>=0.34,<1",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
target-version = "py311"
|
|
43
|
+
line-length = 88
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = [
|
|
47
|
+
"E",
|
|
48
|
+
"F",
|
|
49
|
+
"I",
|
|
50
|
+
"UP",
|
|
51
|
+
"B",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint.isort]
|
|
55
|
+
known-first-party = ["haveapi_client"]
|
|
56
|
+
|
|
57
|
+
[tool.mypy]
|
|
58
|
+
python_version = "3.11"
|
|
59
|
+
strict = true
|
|
60
|
+
files = ["src"]
|
|
61
|
+
|
|
62
|
+
[tool.pytest.ini_options]
|
|
63
|
+
testpaths = ["tests/unit"]
|
|
64
|
+
addopts = "--strict-markers"
|
|
65
|
+
markers = ["integration: starts a local real Ruby HaveAPI server"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "haveapi-client"
|
|
3
|
+
version = "0.30.0"
|
|
4
|
+
description = "Dynamic synchronous and asynchronous clients for HaveAPI APIs"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE.txt"]
|
|
9
|
+
dependencies = ["httpx>=0.28.1,<1", "anyio>=4.15,<5"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Programming Language :: Python :: 3.15",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/vpsfreecz/haveapi"
|
|
23
|
+
Source = "https://github.com/vpsfreecz/haveapi/tree/master/clients/python"
|
|
24
|
+
Issues = "https://github.com/vpsfreecz/haveapi/issues"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["uv_build>=0.13.0,<0.14"]
|
|
28
|
+
build-backend = "uv_build"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"mypy>=2.4,<3",
|
|
33
|
+
"pytest>=9.1,<10",
|
|
34
|
+
"ruff==0.17.1",
|
|
35
|
+
"trio>=0.34,<1",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
target-version = "py311"
|
|
40
|
+
line-length = 88
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint.isort]
|
|
46
|
+
known-first-party = ["haveapi_client"]
|
|
47
|
+
|
|
48
|
+
[tool.mypy]
|
|
49
|
+
python_version = "3.11"
|
|
50
|
+
strict = true
|
|
51
|
+
files = ["src"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests/unit"]
|
|
55
|
+
addopts = "--strict-markers"
|
|
56
|
+
markers = ["integration: starts a local real Ruby HaveAPI server"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Dynamic clients for self-describing HaveAPI APIs."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from ._auth import (
|
|
6
|
+
AsyncAuthenticationChallenge,
|
|
7
|
+
Authenticated,
|
|
8
|
+
AuthenticationChallenge,
|
|
9
|
+
BasicAuth,
|
|
10
|
+
OAuth2Auth,
|
|
11
|
+
TokenAuth,
|
|
12
|
+
)
|
|
13
|
+
from ._protocol import RequestOptions
|
|
14
|
+
from .async_client import AsyncClient
|
|
15
|
+
from .client import Client
|
|
16
|
+
from .exceptions import (
|
|
17
|
+
ActionError,
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
HaveAPIError,
|
|
20
|
+
NotConnectedError,
|
|
21
|
+
OperationFailedError,
|
|
22
|
+
OperationTimeoutError,
|
|
23
|
+
PaginationError,
|
|
24
|
+
ProtocolError,
|
|
25
|
+
StaleDescriptionError,
|
|
26
|
+
TransportError,
|
|
27
|
+
ValidationError,
|
|
28
|
+
)
|
|
29
|
+
from .operations import AsyncOperation, Operation
|
|
30
|
+
from .pagination import IdCursor
|
|
31
|
+
from .response import ResourceObject, Response
|
|
32
|
+
|
|
33
|
+
__version__ = version("haveapi-client")
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"ActionError",
|
|
37
|
+
"AsyncAuthenticationChallenge",
|
|
38
|
+
"AsyncClient",
|
|
39
|
+
"AsyncOperation",
|
|
40
|
+
"Authenticated",
|
|
41
|
+
"AuthenticationChallenge",
|
|
42
|
+
"AuthenticationError",
|
|
43
|
+
"BasicAuth",
|
|
44
|
+
"Client",
|
|
45
|
+
"HaveAPIError",
|
|
46
|
+
"IdCursor",
|
|
47
|
+
"NotConnectedError",
|
|
48
|
+
"OAuth2Auth",
|
|
49
|
+
"Operation",
|
|
50
|
+
"OperationFailedError",
|
|
51
|
+
"OperationTimeoutError",
|
|
52
|
+
"PaginationError",
|
|
53
|
+
"ProtocolError",
|
|
54
|
+
"RequestOptions",
|
|
55
|
+
"ResourceObject",
|
|
56
|
+
"Response",
|
|
57
|
+
"StaleDescriptionError",
|
|
58
|
+
"TokenAuth",
|
|
59
|
+
"TransportError",
|
|
60
|
+
"ValidationError",
|
|
61
|
+
"__version__",
|
|
62
|
+
]
|