pantech 0.1.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.
- pantech-0.1.0/.gitignore +19 -0
- pantech-0.1.0/CHANGELOG.md +20 -0
- pantech-0.1.0/LICENSE +21 -0
- pantech-0.1.0/PKG-INFO +218 -0
- pantech-0.1.0/README.md +185 -0
- pantech-0.1.0/examples/async_list.py +35 -0
- pantech-0.1.0/examples/create_instance.py +55 -0
- pantech-0.1.0/examples/webhook_server.py +59 -0
- pantech-0.1.0/openapi/public-api.yaml +17668 -0
- pantech-0.1.0/pyproject.toml +92 -0
- pantech-0.1.0/scripts/_codegen.py +358 -0
- pantech-0.1.0/scripts/_spec.py +62 -0
- pantech-0.1.0/scripts/generate_resources.py +381 -0
- pantech-0.1.0/scripts/generate_types.py +54 -0
- pantech-0.1.0/scripts/sync_spec.py +32 -0
- pantech-0.1.0/src/pantech/__init__.py +71 -0
- pantech-0.1.0/src/pantech/_base_client.py +548 -0
- pantech-0.1.0/src/pantech/_client.py +105 -0
- pantech-0.1.0/src/pantech/_exceptions.py +270 -0
- pantech-0.1.0/src/pantech/_operations.py +346 -0
- pantech-0.1.0/src/pantech/_resource.py +234 -0
- pantech-0.1.0/src/pantech/_streaming.py +55 -0
- pantech-0.1.0/src/pantech/_types.py +38 -0
- pantech-0.1.0/src/pantech/_version.py +1 -0
- pantech-0.1.0/src/pantech/_webhooks.py +72 -0
- pantech-0.1.0/src/pantech/py.typed +0 -0
- pantech-0.1.0/src/pantech/resources/__init__.py +6 -0
- pantech-0.1.0/src/pantech/resources/_async.py +6082 -0
- pantech-0.1.0/src/pantech/resources/_sync.py +6082 -0
- pantech-0.1.0/src/pantech/types/__init__.py +10 -0
- pantech-0.1.0/src/pantech/types/_generated.py +2447 -0
- pantech-0.1.0/tests/__init__.py +0 -0
- pantech-0.1.0/tests/conftest.py +92 -0
- pantech-0.1.0/tests/test_client.py +150 -0
- pantech-0.1.0/tests/test_errors.py +88 -0
- pantech-0.1.0/tests/test_pagination.py +59 -0
- pantech-0.1.0/tests/test_retries.py +134 -0
- pantech-0.1.0/tests/test_spec_coverage.py +167 -0
- pantech-0.1.0/tests/test_wait.py +114 -0
- pantech-0.1.0/tests/test_webhooks.py +84 -0
pantech-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `pantech` package. The newest version heading
|
|
4
|
+
(`## X.Y.Z (YYYY-MM-DD)`) is released when it reaches `main`: see
|
|
5
|
+
`.github/workflows/release.yml`. Put work in progress under `## Unreleased`.
|
|
6
|
+
|
|
7
|
+
## 0.1.0 (2026-10-24)
|
|
8
|
+
|
|
9
|
+
FEATURES:
|
|
10
|
+
|
|
11
|
+
- First release of the official Python SDK for the Pantech Dynamics public API (`/public/v1`).
|
|
12
|
+
- `pantech.Pantech` (blocking) and `pantech.AsyncPantech` (asyncio) clients with the same methods, as context managers.
|
|
13
|
+
- Every operation in the public API, grouped by family (`client.instances`, `client.networks.subnets`, `client.kubernetes.node_pools`, `client.billing.receipts`…), with typed keyword arguments and TypedDict results in `pantech.types`.
|
|
14
|
+
- `api_key` and `base_url` from arguments or `PANTECH_API_KEY` / `PANTECH_API_URL`.
|
|
15
|
+
- An automatic `Idempotency-Key` on every write, reused across retries. Retries (default 3) on connection errors, timeouts, 429, 500, 502, 503 and 504 with jittered backoff and `Retry-After`; writes the API does not replay are sent once.
|
|
16
|
+
- Errors as `APIError` subclasses carrying `status`, `code`, `title`, `detail`, `request_id` and field `errors`.
|
|
17
|
+
- `list_all()` iterators that follow `next_cursor`, sync and async.
|
|
18
|
+
- `client.operations.wait(...)`, `client.operations.stream(...)` (Server-Sent Events), and `instances.wait_for_order(...)` / `databases.wait_for_order(...)`.
|
|
19
|
+
- `client.me.get()` reports the key's `kind` and `client_name`; `client.me.revoke()` revokes the calling key.
|
|
20
|
+
- `pantech.verify_webhook(secret, header, body, tolerance=300)` for `Pantech-Signature`.
|
pantech-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pantech Dynamics
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
pantech-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pantech
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The official Python SDK for the Pantech Dynamics public API.
|
|
5
|
+
Project-URL: Homepage, https://pantechdynamics.com
|
|
6
|
+
Project-URL: Documentation, https://docs.pantechdynamics.com/sdks/python
|
|
7
|
+
Project-URL: Repository, https://github.com/Pantech-Dynamics/pantech-python
|
|
8
|
+
Project-URL: Issues, https://github.com/Pantech-Dynamics/pantech-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/Pantech-Dynamics/pantech-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author: Pantech Dynamics
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,cloud,kubernetes,pantech,pantech dynamics,sdk,vps
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
28
|
+
Classifier: Topic :: System :: Systems Administration
|
|
29
|
+
Classifier: Typing :: Typed
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Requires-Dist: httpx<1,>=0.25
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Pantech Dynamics Python SDK
|
|
35
|
+
|
|
36
|
+
[](https://github.com/Pantech-Dynamics/pantech-python/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/pantech/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
|
|
40
|
+
The official Python library for the [Pantech Dynamics public API](https://docs.pantechdynamics.com/api):
|
|
41
|
+
instances, VPC networks, firewalls, public IPs, load balancers, volumes, snapshots, managed databases,
|
|
42
|
+
managed Kubernetes, SSH keys, webhooks, billing and usage.
|
|
43
|
+
|
|
44
|
+
- Sync (`Pantech`) and asyncio (`AsyncPantech`) clients with the same methods
|
|
45
|
+
- Every operation in the public API, with typed keyword arguments and TypedDict results
|
|
46
|
+
- Automatic `Idempotency-Key`s, safe retries, cursor pagination, operation and order waiting
|
|
47
|
+
- Webhook signature verification
|
|
48
|
+
- One dependency: `httpx`. Python 3.10+.
|
|
49
|
+
|
|
50
|
+
Full documentation: https://docs.pantechdynamics.com/sdks/python
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
pip install pantech
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
Create an API key in the console under **Organization › API keys**, then:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
export PANTECH_API_KEY=PAN_...
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import pantech
|
|
68
|
+
|
|
69
|
+
with pantech.Pantech() as client:
|
|
70
|
+
me = client.me.get()
|
|
71
|
+
print("Organization", me["organization_id"])
|
|
72
|
+
|
|
73
|
+
for instance in client.instances.list_all():
|
|
74
|
+
print(instance["id"], instance["name"], instance["observed_state"])
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Create an instance and wait for it
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
order = client.instances.create(
|
|
81
|
+
name="web-1",
|
|
82
|
+
plan_slug="starter",
|
|
83
|
+
image_slug="ubuntu-24-04",
|
|
84
|
+
ssh_key_id="sshk_...",
|
|
85
|
+
)
|
|
86
|
+
client.instances.wait_for_order(order["order_id"]) # raises OrderFailedError if it fails
|
|
87
|
+
instance = client.instances.get(order["instance_id"])
|
|
88
|
+
print(instance["public_ipv4"])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Most changes return `202 Accepted` with an operation. Wait for it:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
accepted = client.instances.stop("vm_...")
|
|
95
|
+
client.operations.wait(accepted["operation_id"], timeout=300) # raises OperationFailedError on failure
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Async
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import asyncio
|
|
102
|
+
import pantech
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def main() -> None:
|
|
106
|
+
async with pantech.AsyncPantech() as client:
|
|
107
|
+
async for volume in client.volumes.list_all():
|
|
108
|
+
print(volume["name"])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
asyncio.run(main())
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Resources
|
|
115
|
+
|
|
116
|
+
Families are attributes of the client and nest where the API does:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
client.networks.create(name="prod", cidr="10.10.0.0/16")
|
|
120
|
+
client.networks.subnets.list("net_...")
|
|
121
|
+
client.networks.firewall_rules.create("snet_...", ...)
|
|
122
|
+
client.public_ips.port_forwarding_rules.list("pip_...")
|
|
123
|
+
client.databases.snapshots.create("db_...", name="before-migration")
|
|
124
|
+
client.kubernetes.node_pools.scale("k8s_...", "k8spool_...", workers=3)
|
|
125
|
+
client.catalog.list_plans(placement="vpc")
|
|
126
|
+
client.billing.receipts.get_pdf("rcpt_...") # bytes
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Arguments are keyword-only, except the ids in the path. Optional arguments you leave out are not
|
|
130
|
+
sent; passing `None` sends JSON `null`. Results are plain dicts typed with the TypedDicts in
|
|
131
|
+
`pantech.types`. `client.request("GET", "/me")` calls any route directly.
|
|
132
|
+
|
|
133
|
+
## Configuration
|
|
134
|
+
|
|
135
|
+
| Option | Environment | Default |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `api_key` | `PANTECH_API_KEY` | (required) |
|
|
138
|
+
| `base_url` | `PANTECH_API_URL` | `https://api.pantechdynamics.com/public/v1` |
|
|
139
|
+
| `timeout` | | 60 s per attempt (10 s to connect); a number or an `httpx.Timeout` |
|
|
140
|
+
| `max_retries` | | 3 |
|
|
141
|
+
| `default_headers`, `user_agent`, `http_client` | | |
|
|
142
|
+
|
|
143
|
+
Every method also takes `timeout=`, `extra_headers=`, `extra_query=` and (with a body)
|
|
144
|
+
`extra_body=`. Writes take `idempotency_key=`.
|
|
145
|
+
|
|
146
|
+
## Retries and idempotency
|
|
147
|
+
|
|
148
|
+
Every POST, PUT, PATCH and DELETE gets a fresh UUIDv4 `Idempotency-Key`, and retries resend the
|
|
149
|
+
same key, so the API returns the first result instead of repeating the change. Connection errors,
|
|
150
|
+
timeouts, `429`, `500`, `502`, `503` and `504` are retried up to `max_retries` times with jittered
|
|
151
|
+
exponential backoff, or after the `Retry-After` the API sends.
|
|
152
|
+
|
|
153
|
+
A few writes are not replayed by the API, so the SDK sends them once and retries only a `429`:
|
|
154
|
+
creating and deleting SSH keys, opening a console, issuing a volume key credential, revoking the
|
|
155
|
+
calling key (`client.me.revoke()`), and the webhook test, delete, secret rotation and redelivery calls.
|
|
156
|
+
|
|
157
|
+
## Errors
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
try:
|
|
161
|
+
client.networks.subnets.get("snet_...")
|
|
162
|
+
except pantech.NotFoundError as err:
|
|
163
|
+
print(err.detail, err.request_id)
|
|
164
|
+
except pantech.ValidationError as err:
|
|
165
|
+
for field in err.errors:
|
|
166
|
+
print(field["field"], field["code"], field["message"])
|
|
167
|
+
except pantech.APIError as err:
|
|
168
|
+
print(err.status, err.code, err.detail)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`APIError` carries `status`, `code` (stable: branch on it), `title`, `detail` (safe to show),
|
|
172
|
+
`request_id` (quote it to support) and `errors`. Subclasses: `BadRequestError` (400),
|
|
173
|
+
`AuthenticationError` (401), `PaymentRequiredError` (402), `PermissionDeniedError` (403),
|
|
174
|
+
`NotFoundError` (404), `ConflictError` (409), `ValidationError` (422), `RateLimitError` (429),
|
|
175
|
+
`InternalServerError` (5xx). No answer at all is `APIConnectionError` (or `APITimeoutError`).
|
|
176
|
+
Waiting raises `OperationFailedError`, `OrderFailedError` or `WaitTimeoutError`. All of them are
|
|
177
|
+
`PantechError`.
|
|
178
|
+
|
|
179
|
+
## Webhooks
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
import pantech
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def handle(headers, raw_body: bytes) -> None:
|
|
186
|
+
pantech.verify_webhook(
|
|
187
|
+
secret="whsec_...",
|
|
188
|
+
header=headers.get("Pantech-Signature"),
|
|
189
|
+
body=raw_body, # the raw bytes, before JSON parsing
|
|
190
|
+
tolerance=300, # seconds; the default
|
|
191
|
+
) # raises WebhookVerificationError
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The comparison is constant-time, and any of several `v1` signatures may match (there are two for
|
|
195
|
+
24 hours after a secret rotation). See `examples/webhook_server.py`.
|
|
196
|
+
|
|
197
|
+
## Examples
|
|
198
|
+
|
|
199
|
+
- [`examples/create_instance.py`](examples/create_instance.py): order an instance and wait for it
|
|
200
|
+
- [`examples/async_list.py`](examples/async_list.py): concurrent listing with `AsyncPantech`
|
|
201
|
+
- [`examples/webhook_server.py`](examples/webhook_server.py): a verified webhook receiver on `http.server`
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```sh
|
|
206
|
+
python3 -m venv .venv && .venv/bin/pip install -e . pytest pytest-asyncio mypy ruff pyyaml types-PyYAML
|
|
207
|
+
.venv/bin/python scripts/sync_spec.py # download the published spec into openapi/public-api.yaml
|
|
208
|
+
.venv/bin/python scripts/generate_types.py && .venv/bin/python scripts/generate_resources.py
|
|
209
|
+
.venv/bin/ruff check . && .venv/bin/mypy && .venv/bin/pytest -q
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The SDK is generated from the published [OpenAPI spec](https://docs.pantechdynamics.com/openapi/public-api.yaml).
|
|
213
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow and how releases are made. Report security
|
|
214
|
+
issues as described in [SECURITY.md](SECURITY.md).
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT. See [LICENSE](LICENSE).
|
pantech-0.1.0/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Pantech Dynamics Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Pantech-Dynamics/pantech-python/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/pantech/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
The official Python library for the [Pantech Dynamics public API](https://docs.pantechdynamics.com/api):
|
|
8
|
+
instances, VPC networks, firewalls, public IPs, load balancers, volumes, snapshots, managed databases,
|
|
9
|
+
managed Kubernetes, SSH keys, webhooks, billing and usage.
|
|
10
|
+
|
|
11
|
+
- Sync (`Pantech`) and asyncio (`AsyncPantech`) clients with the same methods
|
|
12
|
+
- Every operation in the public API, with typed keyword arguments and TypedDict results
|
|
13
|
+
- Automatic `Idempotency-Key`s, safe retries, cursor pagination, operation and order waiting
|
|
14
|
+
- Webhook signature verification
|
|
15
|
+
- One dependency: `httpx`. Python 3.10+.
|
|
16
|
+
|
|
17
|
+
Full documentation: https://docs.pantechdynamics.com/sdks/python
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pip install pantech
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
Create an API key in the console under **Organization › API keys**, then:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
export PANTECH_API_KEY=PAN_...
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import pantech
|
|
35
|
+
|
|
36
|
+
with pantech.Pantech() as client:
|
|
37
|
+
me = client.me.get()
|
|
38
|
+
print("Organization", me["organization_id"])
|
|
39
|
+
|
|
40
|
+
for instance in client.instances.list_all():
|
|
41
|
+
print(instance["id"], instance["name"], instance["observed_state"])
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Create an instance and wait for it
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
order = client.instances.create(
|
|
48
|
+
name="web-1",
|
|
49
|
+
plan_slug="starter",
|
|
50
|
+
image_slug="ubuntu-24-04",
|
|
51
|
+
ssh_key_id="sshk_...",
|
|
52
|
+
)
|
|
53
|
+
client.instances.wait_for_order(order["order_id"]) # raises OrderFailedError if it fails
|
|
54
|
+
instance = client.instances.get(order["instance_id"])
|
|
55
|
+
print(instance["public_ipv4"])
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Most changes return `202 Accepted` with an operation. Wait for it:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
accepted = client.instances.stop("vm_...")
|
|
62
|
+
client.operations.wait(accepted["operation_id"], timeout=300) # raises OperationFailedError on failure
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Async
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import asyncio
|
|
69
|
+
import pantech
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def main() -> None:
|
|
73
|
+
async with pantech.AsyncPantech() as client:
|
|
74
|
+
async for volume in client.volumes.list_all():
|
|
75
|
+
print(volume["name"])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Resources
|
|
82
|
+
|
|
83
|
+
Families are attributes of the client and nest where the API does:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
client.networks.create(name="prod", cidr="10.10.0.0/16")
|
|
87
|
+
client.networks.subnets.list("net_...")
|
|
88
|
+
client.networks.firewall_rules.create("snet_...", ...)
|
|
89
|
+
client.public_ips.port_forwarding_rules.list("pip_...")
|
|
90
|
+
client.databases.snapshots.create("db_...", name="before-migration")
|
|
91
|
+
client.kubernetes.node_pools.scale("k8s_...", "k8spool_...", workers=3)
|
|
92
|
+
client.catalog.list_plans(placement="vpc")
|
|
93
|
+
client.billing.receipts.get_pdf("rcpt_...") # bytes
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Arguments are keyword-only, except the ids in the path. Optional arguments you leave out are not
|
|
97
|
+
sent; passing `None` sends JSON `null`. Results are plain dicts typed with the TypedDicts in
|
|
98
|
+
`pantech.types`. `client.request("GET", "/me")` calls any route directly.
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
| Option | Environment | Default |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `api_key` | `PANTECH_API_KEY` | (required) |
|
|
105
|
+
| `base_url` | `PANTECH_API_URL` | `https://api.pantechdynamics.com/public/v1` |
|
|
106
|
+
| `timeout` | | 60 s per attempt (10 s to connect); a number or an `httpx.Timeout` |
|
|
107
|
+
| `max_retries` | | 3 |
|
|
108
|
+
| `default_headers`, `user_agent`, `http_client` | | |
|
|
109
|
+
|
|
110
|
+
Every method also takes `timeout=`, `extra_headers=`, `extra_query=` and (with a body)
|
|
111
|
+
`extra_body=`. Writes take `idempotency_key=`.
|
|
112
|
+
|
|
113
|
+
## Retries and idempotency
|
|
114
|
+
|
|
115
|
+
Every POST, PUT, PATCH and DELETE gets a fresh UUIDv4 `Idempotency-Key`, and retries resend the
|
|
116
|
+
same key, so the API returns the first result instead of repeating the change. Connection errors,
|
|
117
|
+
timeouts, `429`, `500`, `502`, `503` and `504` are retried up to `max_retries` times with jittered
|
|
118
|
+
exponential backoff, or after the `Retry-After` the API sends.
|
|
119
|
+
|
|
120
|
+
A few writes are not replayed by the API, so the SDK sends them once and retries only a `429`:
|
|
121
|
+
creating and deleting SSH keys, opening a console, issuing a volume key credential, revoking the
|
|
122
|
+
calling key (`client.me.revoke()`), and the webhook test, delete, secret rotation and redelivery calls.
|
|
123
|
+
|
|
124
|
+
## Errors
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
try:
|
|
128
|
+
client.networks.subnets.get("snet_...")
|
|
129
|
+
except pantech.NotFoundError as err:
|
|
130
|
+
print(err.detail, err.request_id)
|
|
131
|
+
except pantech.ValidationError as err:
|
|
132
|
+
for field in err.errors:
|
|
133
|
+
print(field["field"], field["code"], field["message"])
|
|
134
|
+
except pantech.APIError as err:
|
|
135
|
+
print(err.status, err.code, err.detail)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`APIError` carries `status`, `code` (stable: branch on it), `title`, `detail` (safe to show),
|
|
139
|
+
`request_id` (quote it to support) and `errors`. Subclasses: `BadRequestError` (400),
|
|
140
|
+
`AuthenticationError` (401), `PaymentRequiredError` (402), `PermissionDeniedError` (403),
|
|
141
|
+
`NotFoundError` (404), `ConflictError` (409), `ValidationError` (422), `RateLimitError` (429),
|
|
142
|
+
`InternalServerError` (5xx). No answer at all is `APIConnectionError` (or `APITimeoutError`).
|
|
143
|
+
Waiting raises `OperationFailedError`, `OrderFailedError` or `WaitTimeoutError`. All of them are
|
|
144
|
+
`PantechError`.
|
|
145
|
+
|
|
146
|
+
## Webhooks
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
import pantech
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def handle(headers, raw_body: bytes) -> None:
|
|
153
|
+
pantech.verify_webhook(
|
|
154
|
+
secret="whsec_...",
|
|
155
|
+
header=headers.get("Pantech-Signature"),
|
|
156
|
+
body=raw_body, # the raw bytes, before JSON parsing
|
|
157
|
+
tolerance=300, # seconds; the default
|
|
158
|
+
) # raises WebhookVerificationError
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The comparison is constant-time, and any of several `v1` signatures may match (there are two for
|
|
162
|
+
24 hours after a secret rotation). See `examples/webhook_server.py`.
|
|
163
|
+
|
|
164
|
+
## Examples
|
|
165
|
+
|
|
166
|
+
- [`examples/create_instance.py`](examples/create_instance.py): order an instance and wait for it
|
|
167
|
+
- [`examples/async_list.py`](examples/async_list.py): concurrent listing with `AsyncPantech`
|
|
168
|
+
- [`examples/webhook_server.py`](examples/webhook_server.py): a verified webhook receiver on `http.server`
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
python3 -m venv .venv && .venv/bin/pip install -e . pytest pytest-asyncio mypy ruff pyyaml types-PyYAML
|
|
174
|
+
.venv/bin/python scripts/sync_spec.py # download the published spec into openapi/public-api.yaml
|
|
175
|
+
.venv/bin/python scripts/generate_types.py && .venv/bin/python scripts/generate_resources.py
|
|
176
|
+
.venv/bin/ruff check . && .venv/bin/mypy && .venv/bin/pytest -q
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The SDK is generated from the published [OpenAPI spec](https://docs.pantechdynamics.com/openapi/public-api.yaml).
|
|
180
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow and how releases are made. Report security
|
|
181
|
+
issues as described in [SECURITY.md](SECURITY.md).
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""List instances and volumes concurrently with the asyncio client.
|
|
2
|
+
|
|
3
|
+
export PANTECH_API_KEY=PAN_...
|
|
4
|
+
python examples/async_list.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
|
|
11
|
+
import pantech
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def main() -> None:
|
|
15
|
+
async with pantech.AsyncPantech() as client:
|
|
16
|
+
me, instances, volumes = await asyncio.gather(
|
|
17
|
+
client.me.get(),
|
|
18
|
+
collect(client.instances.list_all()),
|
|
19
|
+
collect(client.volumes.list_all()),
|
|
20
|
+
)
|
|
21
|
+
print(f"Organization {me['organization_id']}")
|
|
22
|
+
print(f"{len(instances)} instances:")
|
|
23
|
+
for inst in instances:
|
|
24
|
+
print(f" {inst['id']} {inst['name']:<24} {inst.get('observed_state', '')}")
|
|
25
|
+
print(f"{len(volumes)} volumes:")
|
|
26
|
+
for vol in volumes:
|
|
27
|
+
print(f" {vol['id']} {vol['name']:<24} {vol.get('size_gb', '')} GB")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def collect(items): # type: ignore[no-untyped-def]
|
|
31
|
+
return [item async for item in items]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Create an instance, wait until it runs, and print its address.
|
|
2
|
+
|
|
3
|
+
export PANTECH_API_KEY=PAN_...
|
|
4
|
+
python examples/create_instance.py web-1 --plan starter --image ubuntu-24-04 --ssh-key sshk_...
|
|
5
|
+
|
|
6
|
+
Creating an instance costs money: the order is paid from your credit.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import pantech
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> int:
|
|
18
|
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
19
|
+
parser.add_argument("name")
|
|
20
|
+
parser.add_argument("--plan", default="starter", help="a plan slug from catalog.list_plans()")
|
|
21
|
+
parser.add_argument("--image", default="ubuntu-24-04", help="an image slug from catalog.list_images()")
|
|
22
|
+
parser.add_argument("--ssh-key", required=True, help="an SSH key id (sshk_...) from ssh_keys.list()")
|
|
23
|
+
parser.add_argument("--region", default=None)
|
|
24
|
+
args = parser.parse_args()
|
|
25
|
+
|
|
26
|
+
with pantech.Pantech() as client:
|
|
27
|
+
try:
|
|
28
|
+
order = client.instances.create(
|
|
29
|
+
name=args.name,
|
|
30
|
+
plan_slug=args.plan,
|
|
31
|
+
image_slug=args.image,
|
|
32
|
+
ssh_key_id=args.ssh_key,
|
|
33
|
+
region=args.region or pantech.NOT_GIVEN,
|
|
34
|
+
)
|
|
35
|
+
print(f"Ordered {order['instance_id']} (order {order['order_id']}), waiting for it to be provisioned...")
|
|
36
|
+
client.instances.wait_for_order(order["order_id"], timeout=900)
|
|
37
|
+
instance = client.instances.get(order["instance_id"])
|
|
38
|
+
except pantech.ValidationError as err:
|
|
39
|
+
for field in err.errors:
|
|
40
|
+
print(f"{field['field']}: {field['message']}", file=sys.stderr)
|
|
41
|
+
return 1
|
|
42
|
+
except pantech.APIError as err:
|
|
43
|
+
print(f"{err.detail or err.title} (request_id: {err.request_id})", file=sys.stderr)
|
|
44
|
+
return 1
|
|
45
|
+
except pantech.PantechError as err:
|
|
46
|
+
print(err, file=sys.stderr)
|
|
47
|
+
return 1
|
|
48
|
+
|
|
49
|
+
address = instance.get("public_ipv4") or instance.get("private_ipv4")
|
|
50
|
+
print(f"{instance['name']} is {instance.get('observed_state')} at {address}")
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
sys.exit(main())
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""A webhook receiver on the standard library's http.server.
|
|
2
|
+
|
|
3
|
+
export PANTECH_WEBHOOK_SECRET=whsec_...
|
|
4
|
+
python examples/webhook_server.py 8000
|
|
5
|
+
|
|
6
|
+
Point a webhook at https://<your host>/pantech (behind TLS in production).
|
|
7
|
+
Each delivery is verified against its raw body before it is trusted; a bad
|
|
8
|
+
signature gets 400 and Pantech retries later.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
17
|
+
from typing import Set
|
|
18
|
+
|
|
19
|
+
import pantech
|
|
20
|
+
|
|
21
|
+
SECRET = os.environ.get("PANTECH_WEBHOOK_SECRET", "")
|
|
22
|
+
seen_events: Set[str] = set() # deduplicate on the event id: deliveries can repeat
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Handler(BaseHTTPRequestHandler):
|
|
26
|
+
def do_POST(self) -> None:
|
|
27
|
+
if self.path != "/pantech":
|
|
28
|
+
self.send_error(404)
|
|
29
|
+
return
|
|
30
|
+
length = int(self.headers.get("Content-Length") or 0)
|
|
31
|
+
body = self.rfile.read(length) # the raw bytes, exactly as signed
|
|
32
|
+
try:
|
|
33
|
+
pantech.verify_webhook(SECRET, self.headers.get(pantech.SIGNATURE_HEADER), body)
|
|
34
|
+
except pantech.WebhookVerificationError as err:
|
|
35
|
+
self.send_error(400, str(err))
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
event = json.loads(body)
|
|
39
|
+
if event.get("id") not in seen_events:
|
|
40
|
+
seen_events.add(event.get("id"))
|
|
41
|
+
print(f"{self.headers.get('Pantech-Event-Type')}: {event.get('id')}")
|
|
42
|
+
# Answer 2xx within 10 seconds; do slow work after responding.
|
|
43
|
+
self.send_response(204)
|
|
44
|
+
self.end_headers()
|
|
45
|
+
|
|
46
|
+
def log_message(self, format: str, *args: object) -> None: # quieter than the default
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main() -> None:
|
|
51
|
+
if not SECRET:
|
|
52
|
+
sys.exit("Set PANTECH_WEBHOOK_SECRET to the webhook's signing secret.")
|
|
53
|
+
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
|
|
54
|
+
print(f"Listening on http://0.0.0.0:{port}/pantech")
|
|
55
|
+
HTTPServer(("0.0.0.0", port), Handler).serve_forever()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
main()
|