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.
Files changed (40) hide show
  1. pantech-0.1.0/.gitignore +19 -0
  2. pantech-0.1.0/CHANGELOG.md +20 -0
  3. pantech-0.1.0/LICENSE +21 -0
  4. pantech-0.1.0/PKG-INFO +218 -0
  5. pantech-0.1.0/README.md +185 -0
  6. pantech-0.1.0/examples/async_list.py +35 -0
  7. pantech-0.1.0/examples/create_instance.py +55 -0
  8. pantech-0.1.0/examples/webhook_server.py +59 -0
  9. pantech-0.1.0/openapi/public-api.yaml +17668 -0
  10. pantech-0.1.0/pyproject.toml +92 -0
  11. pantech-0.1.0/scripts/_codegen.py +358 -0
  12. pantech-0.1.0/scripts/_spec.py +62 -0
  13. pantech-0.1.0/scripts/generate_resources.py +381 -0
  14. pantech-0.1.0/scripts/generate_types.py +54 -0
  15. pantech-0.1.0/scripts/sync_spec.py +32 -0
  16. pantech-0.1.0/src/pantech/__init__.py +71 -0
  17. pantech-0.1.0/src/pantech/_base_client.py +548 -0
  18. pantech-0.1.0/src/pantech/_client.py +105 -0
  19. pantech-0.1.0/src/pantech/_exceptions.py +270 -0
  20. pantech-0.1.0/src/pantech/_operations.py +346 -0
  21. pantech-0.1.0/src/pantech/_resource.py +234 -0
  22. pantech-0.1.0/src/pantech/_streaming.py +55 -0
  23. pantech-0.1.0/src/pantech/_types.py +38 -0
  24. pantech-0.1.0/src/pantech/_version.py +1 -0
  25. pantech-0.1.0/src/pantech/_webhooks.py +72 -0
  26. pantech-0.1.0/src/pantech/py.typed +0 -0
  27. pantech-0.1.0/src/pantech/resources/__init__.py +6 -0
  28. pantech-0.1.0/src/pantech/resources/_async.py +6082 -0
  29. pantech-0.1.0/src/pantech/resources/_sync.py +6082 -0
  30. pantech-0.1.0/src/pantech/types/__init__.py +10 -0
  31. pantech-0.1.0/src/pantech/types/_generated.py +2447 -0
  32. pantech-0.1.0/tests/__init__.py +0 -0
  33. pantech-0.1.0/tests/conftest.py +92 -0
  34. pantech-0.1.0/tests/test_client.py +150 -0
  35. pantech-0.1.0/tests/test_errors.py +88 -0
  36. pantech-0.1.0/tests/test_pagination.py +59 -0
  37. pantech-0.1.0/tests/test_retries.py +134 -0
  38. pantech-0.1.0/tests/test_spec_coverage.py +167 -0
  39. pantech-0.1.0/tests/test_wait.py +114 -0
  40. pantech-0.1.0/tests/test_webhooks.py +84 -0
@@ -0,0 +1,19 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .env
13
+ .DS_Store
14
+ * 2.py
15
+ * 2
16
+ * 2.*
17
+ coverage.xml
18
+ .tox/
19
+ .nox/
@@ -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
+ [![CI](https://github.com/Pantech-Dynamics/pantech-python/actions/workflows/ci.yml/badge.svg)](https://github.com/Pantech-Dynamics/pantech-python/actions/workflows/ci.yml)
37
+ [![PyPI](https://img.shields.io/pypi/v/pantech.svg)](https://pypi.org/project/pantech/)
38
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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).
@@ -0,0 +1,185 @@
1
+ # Pantech Dynamics Python SDK
2
+
3
+ [![CI](https://github.com/Pantech-Dynamics/pantech-python/actions/workflows/ci.yml/badge.svg)](https://github.com/Pantech-Dynamics/pantech-python/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/pantech.svg)](https://pypi.org/project/pantech/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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()