openrelay-sdk 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.
- openrelay_sdk-0.1.0/.gitignore +164 -0
- openrelay_sdk-0.1.0/PKG-INFO +101 -0
- openrelay_sdk-0.1.0/README.md +70 -0
- openrelay_sdk-0.1.0/pyproject.toml +61 -0
- openrelay_sdk-0.1.0/src/openrelay/__init__.py +54 -0
- openrelay_sdk-0.1.0/src/openrelay/_base_client.py +173 -0
- openrelay_sdk-0.1.0/src/openrelay/_client.py +233 -0
- openrelay_sdk-0.1.0/src/openrelay/_constants.py +25 -0
- openrelay_sdk-0.1.0/src/openrelay/_exceptions.py +72 -0
- openrelay_sdk-0.1.0/src/openrelay/_types.py +9 -0
- openrelay_sdk-0.1.0/src/openrelay/_version.py +1 -0
- openrelay_sdk-0.1.0/src/openrelay/py.typed +0 -0
- openrelay_sdk-0.1.0/src/openrelay/resources/__init__.py +9 -0
- openrelay_sdk-0.1.0/src/openrelay/resources/gpus.py +109 -0
- openrelay_sdk-0.1.0/src/openrelay/resources/vms.py +352 -0
- openrelay_sdk-0.1.0/src/openrelay/types/__init__.py +21 -0
- openrelay_sdk-0.1.0/src/openrelay/types/gpus.py +48 -0
- openrelay_sdk-0.1.0/src/openrelay/types/shared.py +14 -0
- openrelay_sdk-0.1.0/src/openrelay/types/vms.py +77 -0
- openrelay_sdk-0.1.0/tests/__init__.py +0 -0
- openrelay_sdk-0.1.0/tests/conftest.py +34 -0
- openrelay_sdk-0.1.0/tests/test_client.py +114 -0
- openrelay_sdk-0.1.0/tests/test_exceptions.py +200 -0
- openrelay_sdk-0.1.0/tests/test_gpus.py +101 -0
- openrelay_sdk-0.1.0/tests/test_vms.py +223 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules
|
|
3
|
+
.pnpm-store
|
|
4
|
+
|
|
5
|
+
# Next.js
|
|
6
|
+
.next
|
|
7
|
+
out
|
|
8
|
+
|
|
9
|
+
# Build
|
|
10
|
+
dist
|
|
11
|
+
build
|
|
12
|
+
|
|
13
|
+
# Environment
|
|
14
|
+
.env
|
|
15
|
+
.env.local
|
|
16
|
+
.env.*.local
|
|
17
|
+
# scripts/local-stack.sh backs up an existing .env.local here on its first run,
|
|
18
|
+
# so the copy needs the same guard as the original.
|
|
19
|
+
.env.local.bak
|
|
20
|
+
|
|
21
|
+
# Local stack state (scripts/local-stack.sh): ops org id, JWT secret, logs
|
|
22
|
+
.local-stack/
|
|
23
|
+
# Legacy state dir from the pre-rename script; migrated to .local-stack on `up`.
|
|
24
|
+
# Kept ignored so its JWT secret is never swept into a commit before migration.
|
|
25
|
+
.local-toolbox/
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
.idea
|
|
29
|
+
.vscode
|
|
30
|
+
*.swp
|
|
31
|
+
*.swo
|
|
32
|
+
.DS_Store
|
|
33
|
+
|
|
34
|
+
# Claude Code per-developer state (MCP server enablement, permission approvals):
|
|
35
|
+
# differs per machine, never commit
|
|
36
|
+
.claude/settings.local.json
|
|
37
|
+
# Agent-local state: worktrees (`git add -A` would commit each one as a gitlink),
|
|
38
|
+
# task lists and caches
|
|
39
|
+
.claude/worktrees/
|
|
40
|
+
.claude/tasks/
|
|
41
|
+
.claude/cache/
|
|
42
|
+
|
|
43
|
+
# A saved terraform plan holds sensitive values in cleartext and must never be
|
|
44
|
+
# committed. infra/terraform/scripts/tf-run.sh writes its plans outside the
|
|
45
|
+
# worktree entirely; this catches one run by hand with -out= inside it.
|
|
46
|
+
*.tfplan
|
|
47
|
+
|
|
48
|
+
# Logs
|
|
49
|
+
*.log
|
|
50
|
+
npm-debug.log*
|
|
51
|
+
yarn-debug.log*
|
|
52
|
+
yarn-error.log*
|
|
53
|
+
|
|
54
|
+
# TypeScript
|
|
55
|
+
*.tsbuildinfo
|
|
56
|
+
|
|
57
|
+
# Vercel
|
|
58
|
+
.vercel
|
|
59
|
+
|
|
60
|
+
.env.prod-local
|
|
61
|
+
|
|
62
|
+
# Secrets
|
|
63
|
+
secrets/
|
|
64
|
+
infra/.secrets/
|
|
65
|
+
**/.secrets/
|
|
66
|
+
*-key.pem
|
|
67
|
+
# age private decryption key: NEVER commit (only the encrypted .env.claude.* files + the public .age-recipients are)
|
|
68
|
+
.env.keys
|
|
69
|
+
# local Stripe API keys (holds sk_live/pk_live): NEVER commit
|
|
70
|
+
.stripe.keys
|
|
71
|
+
# local Cerebras API key (csk-... bearer token; lives in SSM in beta/prod). NEVER commit
|
|
72
|
+
.cerebras.key
|
|
73
|
+
|
|
74
|
+
# Python SDK
|
|
75
|
+
sdks/python/.venv/
|
|
76
|
+
sdks/python/dist/
|
|
77
|
+
sdks/python/*.egg-info/
|
|
78
|
+
sdks/python/dev.py
|
|
79
|
+
__pycache__/
|
|
80
|
+
*.pyc
|
|
81
|
+
.pytest_cache/
|
|
82
|
+
|
|
83
|
+
# Test results
|
|
84
|
+
e2e/test-results/
|
|
85
|
+
|
|
86
|
+
# Schemathesis local example database, generated config, and reports
|
|
87
|
+
# (created by `st run` / `schemathesis run`; schemathesis.toml is written
|
|
88
|
+
# fresh by .github/workflows/schemathesis.yml on every run, never committed)
|
|
89
|
+
.schemathesis/
|
|
90
|
+
schemathesis.toml
|
|
91
|
+
schemathesis-report/
|
|
92
|
+
|
|
93
|
+
# Compiled binaries
|
|
94
|
+
infra/nomad-plugins/vm-templates/vm-templates
|
|
95
|
+
|
|
96
|
+
# Cloudflare
|
|
97
|
+
.wrangler/
|
|
98
|
+
|
|
99
|
+
# Google service-account keys (never commit)
|
|
100
|
+
scripts/.google-sheets-key.json
|
|
101
|
+
*.google-sheets-key.json
|
|
102
|
+
|
|
103
|
+
# Outreach recipient lists (PII: never commit)
|
|
104
|
+
**/recipients*.txt
|
|
105
|
+
*.recipients.txt
|
|
106
|
+
# local GLM-5.2 provider API keys (never commit)
|
|
107
|
+
.glm52.keys
|
|
108
|
+
|
|
109
|
+
# Lead lists / customer data (never commit)
|
|
110
|
+
gpu_customers_over_100.csv
|
|
111
|
+
email_flags.csv
|
|
112
|
+
customers_segmented.csv
|
|
113
|
+
campaign_exclusions.csv
|
|
114
|
+
attio_import.csv
|
|
115
|
+
|
|
116
|
+
# Terraform (never commit state or real tfvars)
|
|
117
|
+
**/.terraform/*
|
|
118
|
+
*.tfstate
|
|
119
|
+
*.tfstate.*
|
|
120
|
+
*.tfvars
|
|
121
|
+
!*.tfvars.example
|
|
122
|
+
# Module test fixtures (fake values), loaded by terraform test.
|
|
123
|
+
!infra/terraform/modules/*/tests/terraform.tfvars
|
|
124
|
+
crash.log
|
|
125
|
+
|
|
126
|
+
# Cloudflare Worker local dev secrets
|
|
127
|
+
**/.dev.vars
|
|
128
|
+
.nebius
|
|
129
|
+
# Lambda Cloud API key (both spellings; never commit)
|
|
130
|
+
.lambda
|
|
131
|
+
.lamda
|
|
132
|
+
|
|
133
|
+
# built Go binaries (never commit)
|
|
134
|
+
/services/fabric/fabric-nodeagent
|
|
135
|
+
/tools/brownout/brownout
|
|
136
|
+
|
|
137
|
+
# patch/merge backups (never commit)
|
|
138
|
+
*.orig
|
|
139
|
+
*.rej
|
|
140
|
+
|
|
141
|
+
# Generated by `next dev` (node_modules/next/dist/server/lib/generate-agent-files.js).
|
|
142
|
+
apps/dashboard/AGENTS.md
|
|
143
|
+
apps/dashboard/CLAUDE.md
|
|
144
|
+
apps/toolbox/AGENTS.md
|
|
145
|
+
apps/toolbox/CLAUDE.md
|
|
146
|
+
apps/linkedin-outreach/AGENTS.md
|
|
147
|
+
apps/linkedin-outreach/CLAUDE.md
|
|
148
|
+
|
|
149
|
+
# Regenerated by `next typegen` (which every app's `typecheck` runs first), `next build` and `next dev`, with import paths that follow the dist dir of whichever ran last.
|
|
150
|
+
apps/*/next-env.d.ts
|
|
151
|
+
|
|
152
|
+
# Generated from services/control-plane-go/openapi.yaml by `pnpm --filter <app> openapi:gen`, which each app's typecheck, dev and build all run first.
|
|
153
|
+
apps/toolbox/src/lib/api-types.gen.d.ts
|
|
154
|
+
apps/dashboard/src/lib/api-types.gen.d.ts
|
|
155
|
+
|
|
156
|
+
# Local Google OAuth client creds (never commit)
|
|
157
|
+
.google.creds
|
|
158
|
+
.eve
|
|
159
|
+
.output
|
|
160
|
+
|
|
161
|
+
# Lean build output (formal/)
|
|
162
|
+
.lake/
|
|
163
|
+
.abuse-harness/
|
|
164
|
+
e2e/abuse/artifacts/
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: openrelay-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The official Python SDK for the OpenRelay API
|
|
5
|
+
Project-URL: Homepage, https://openrelay.inc
|
|
6
|
+
Project-URL: Documentation, https://docs.openrelay.inc
|
|
7
|
+
Project-URL: Repository, https://github.com/OpenRelayInc/OpenRelay
|
|
8
|
+
Project-URL: Issues, https://github.com/OpenRelayInc/OpenRelay/issues
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Requires-Dist: httpx<1,>=0.25.0
|
|
23
|
+
Requires-Dist: pydantic<3,>=2.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: respx>=0.20; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# OpenRelay Python SDK
|
|
33
|
+
|
|
34
|
+
The official Python SDK for the [OpenRelay](https://openrelay.inc) API.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install openrelay-sdk
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The package imports as `openrelay`. The unrelated `openrelay` distribution on
|
|
43
|
+
PyPI also provides a top-level `openrelay` module, so install `openrelay-sdk`,
|
|
44
|
+
and do not install both into the same environment.
|
|
45
|
+
|
|
46
|
+
From a checkout of this repository: `pip install ./sdks/python`.
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import openrelay
|
|
52
|
+
|
|
53
|
+
client = openrelay.Client(api_key="or_your_api_key")
|
|
54
|
+
|
|
55
|
+
# List available GPUs
|
|
56
|
+
gpus = client.gpus.list()
|
|
57
|
+
|
|
58
|
+
# Create a VM
|
|
59
|
+
vm = client.vms.create(
|
|
60
|
+
name="dev-box",
|
|
61
|
+
ssh_key_ids=["your-ssh-key-id"],
|
|
62
|
+
gpu_type="rtx-4090",
|
|
63
|
+
gpu_count=1,
|
|
64
|
+
disk_size_gb=100,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Wait for it to be ready
|
|
68
|
+
vm = client.vms.wait_for_status(vm.id)
|
|
69
|
+
print(f"VM running at: {vm.endpoint_url}")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Async Usage
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import asyncio
|
|
76
|
+
import openrelay
|
|
77
|
+
|
|
78
|
+
async def main():
|
|
79
|
+
async with openrelay.AsyncClient(api_key="or_your_api_key") as client:
|
|
80
|
+
vm = await client.vms.get("your-vm-id")
|
|
81
|
+
print(vm.status)
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
The client accepts these parameters:
|
|
89
|
+
|
|
90
|
+
| Parameter | Env Variable | Default |
|
|
91
|
+
|-----------|-------------|---------|
|
|
92
|
+
| `api_key` | `OPENRELAY_API_KEY` | Required |
|
|
93
|
+
| `base_url` | `OPENRELAY_API_URL` | `https://api.openrelay.inc` |
|
|
94
|
+
| `timeout` | (none) | `60.0` seconds |
|
|
95
|
+
| `max_retries` | (none) | `2` |
|
|
96
|
+
|
|
97
|
+
The `orl` CLI reads the same two variables.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# OpenRelay Python SDK
|
|
2
|
+
|
|
3
|
+
The official Python SDK for the [OpenRelay](https://openrelay.inc) API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install openrelay-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package imports as `openrelay`. The unrelated `openrelay` distribution on
|
|
12
|
+
PyPI also provides a top-level `openrelay` module, so install `openrelay-sdk`,
|
|
13
|
+
and do not install both into the same environment.
|
|
14
|
+
|
|
15
|
+
From a checkout of this repository: `pip install ./sdks/python`.
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import openrelay
|
|
21
|
+
|
|
22
|
+
client = openrelay.Client(api_key="or_your_api_key")
|
|
23
|
+
|
|
24
|
+
# List available GPUs
|
|
25
|
+
gpus = client.gpus.list()
|
|
26
|
+
|
|
27
|
+
# Create a VM
|
|
28
|
+
vm = client.vms.create(
|
|
29
|
+
name="dev-box",
|
|
30
|
+
ssh_key_ids=["your-ssh-key-id"],
|
|
31
|
+
gpu_type="rtx-4090",
|
|
32
|
+
gpu_count=1,
|
|
33
|
+
disk_size_gb=100,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Wait for it to be ready
|
|
37
|
+
vm = client.vms.wait_for_status(vm.id)
|
|
38
|
+
print(f"VM running at: {vm.endpoint_url}")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Async Usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import asyncio
|
|
45
|
+
import openrelay
|
|
46
|
+
|
|
47
|
+
async def main():
|
|
48
|
+
async with openrelay.AsyncClient(api_key="or_your_api_key") as client:
|
|
49
|
+
vm = await client.vms.get("your-vm-id")
|
|
50
|
+
print(vm.status)
|
|
51
|
+
|
|
52
|
+
asyncio.run(main())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
The client accepts these parameters:
|
|
58
|
+
|
|
59
|
+
| Parameter | Env Variable | Default |
|
|
60
|
+
|-----------|-------------|---------|
|
|
61
|
+
| `api_key` | `OPENRELAY_API_KEY` | Required |
|
|
62
|
+
| `base_url` | `OPENRELAY_API_URL` | `https://api.openrelay.inc` |
|
|
63
|
+
| `timeout` | (none) | `60.0` seconds |
|
|
64
|
+
| `max_retries` | (none) | `2` |
|
|
65
|
+
|
|
66
|
+
The `orl` CLI reads the same two variables.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "openrelay-sdk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "The official Python SDK for the OpenRelay API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.25.0,<1",
|
|
14
|
+
"pydantic>=2.0.0,<3",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# PyPI renders these as links on the package page, the page's route back to
|
|
31
|
+
# the site.
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://openrelay.inc"
|
|
34
|
+
Documentation = "https://docs.openrelay.inc"
|
|
35
|
+
Repository = "https://github.com/OpenRelayInc/OpenRelay"
|
|
36
|
+
Issues = "https://github.com/OpenRelayInc/OpenRelay/issues"
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=7.0",
|
|
41
|
+
"pytest-asyncio>=0.21",
|
|
42
|
+
"respx>=0.20",
|
|
43
|
+
"ruff>=0.1",
|
|
44
|
+
"mypy>=1.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.version]
|
|
48
|
+
path = "src/openrelay/_version.py"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/openrelay"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
asyncio_mode = "auto"
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
target-version = "py38"
|
|
58
|
+
|
|
59
|
+
[tool.mypy]
|
|
60
|
+
python_version = "3.8"
|
|
61
|
+
strict = true
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""OpenRelay Python SDK."""
|
|
2
|
+
|
|
3
|
+
from ._client import AsyncClient, Client
|
|
4
|
+
from ._exceptions import (
|
|
5
|
+
APIConnectionError,
|
|
6
|
+
APIError,
|
|
7
|
+
APITimeoutError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
InternalServerError,
|
|
10
|
+
InvalidRequestError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
OpenRelayError,
|
|
13
|
+
PermissionDeniedError,
|
|
14
|
+
RateLimitError,
|
|
15
|
+
)
|
|
16
|
+
from ._version import __version__
|
|
17
|
+
from .types.gpus import (
|
|
18
|
+
GpuAvailability,
|
|
19
|
+
GpuAvailabilityRegion,
|
|
20
|
+
GpuModel,
|
|
21
|
+
GpuModelAvailability,
|
|
22
|
+
GpuPricing,
|
|
23
|
+
GpuPricingTier,
|
|
24
|
+
)
|
|
25
|
+
from .types.vms import Vm, VmActionResponse, VmCreateResponse
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"__version__",
|
|
29
|
+
# Clients
|
|
30
|
+
"Client",
|
|
31
|
+
"AsyncClient",
|
|
32
|
+
# Exceptions
|
|
33
|
+
"OpenRelayError",
|
|
34
|
+
"APIError",
|
|
35
|
+
"AuthenticationError",
|
|
36
|
+
"PermissionDeniedError",
|
|
37
|
+
"NotFoundError",
|
|
38
|
+
"InvalidRequestError",
|
|
39
|
+
"RateLimitError",
|
|
40
|
+
"InternalServerError",
|
|
41
|
+
"APIConnectionError",
|
|
42
|
+
"APITimeoutError",
|
|
43
|
+
# GPU types
|
|
44
|
+
"GpuModel",
|
|
45
|
+
"GpuModelAvailability",
|
|
46
|
+
"GpuAvailability",
|
|
47
|
+
"GpuAvailabilityRegion",
|
|
48
|
+
"GpuPricing",
|
|
49
|
+
"GpuPricingTier",
|
|
50
|
+
# VM types
|
|
51
|
+
"Vm",
|
|
52
|
+
"VmCreateResponse",
|
|
53
|
+
"VmActionResponse",
|
|
54
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from ._constants import (
|
|
12
|
+
API_KEY_ENV_VAR,
|
|
13
|
+
CAPACITY_CODES,
|
|
14
|
+
BASE_URL_ENV_VAR,
|
|
15
|
+
DEFAULT_BASE_URL,
|
|
16
|
+
DEFAULT_MAX_RETRIES,
|
|
17
|
+
DEFAULT_TIMEOUT,
|
|
18
|
+
INITIAL_RETRY_DELAY,
|
|
19
|
+
MAX_RETRY_DELAY,
|
|
20
|
+
)
|
|
21
|
+
from ._exceptions import (
|
|
22
|
+
APIError,
|
|
23
|
+
AuthenticationError,
|
|
24
|
+
InternalServerError,
|
|
25
|
+
InvalidRequestError,
|
|
26
|
+
NotFoundError,
|
|
27
|
+
OpenRelayError,
|
|
28
|
+
PermissionDeniedError,
|
|
29
|
+
RateLimitError,
|
|
30
|
+
)
|
|
31
|
+
from ._version import __version__
|
|
32
|
+
|
|
33
|
+
T = TypeVar("T", bound=BaseModel)
|
|
34
|
+
|
|
35
|
+
_UUID_RE = re.compile(
|
|
36
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_ERROR_TYPE_MAP: Dict[str, Type[APIError]] = {
|
|
40
|
+
"authentication_error": AuthenticationError,
|
|
41
|
+
"permission_error": PermissionDeniedError,
|
|
42
|
+
"not_found_error": NotFoundError,
|
|
43
|
+
"invalid_request_error": InvalidRequestError,
|
|
44
|
+
"rate_limit_error": RateLimitError,
|
|
45
|
+
"server_error": InternalServerError,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_uuid(value: str) -> bool:
|
|
50
|
+
return bool(_UUID_RE.match(value))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# The error type a flat control-plane body implies, which carries no type field.
|
|
54
|
+
_STATUS_ERROR_TYPE: Dict[int, str] = {
|
|
55
|
+
400: "invalid_request_error",
|
|
56
|
+
401: "authentication_error",
|
|
57
|
+
403: "permission_error",
|
|
58
|
+
404: "not_found_error",
|
|
59
|
+
429: "rate_limit_error",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _parse_error(response: httpx.Response) -> Tuple[str, str, Optional[str]]:
|
|
64
|
+
"""(type, message, code) from an error response. The control plane writes a
|
|
65
|
+
flat {"error": "<message>", "code": "<CODE>"}, which carries no type, so the
|
|
66
|
+
type follows from the status. The nested {"error": {"type", "message",
|
|
67
|
+
"code"}} shape is read as it comes. A body that is neither yields a
|
|
68
|
+
server_error with a status-only message and no code."""
|
|
69
|
+
try:
|
|
70
|
+
body = response.json()
|
|
71
|
+
except Exception:
|
|
72
|
+
body = None
|
|
73
|
+
if isinstance(body, dict):
|
|
74
|
+
error = body.get("error", {})
|
|
75
|
+
if isinstance(error, str):
|
|
76
|
+
status = response.status_code
|
|
77
|
+
code = body.get("code")
|
|
78
|
+
return (
|
|
79
|
+
"server_error" if status >= 500 else _STATUS_ERROR_TYPE.get(status, "api_error"),
|
|
80
|
+
error,
|
|
81
|
+
code if isinstance(code, str) else None,
|
|
82
|
+
)
|
|
83
|
+
if isinstance(error, dict):
|
|
84
|
+
code = error.get("code")
|
|
85
|
+
return (
|
|
86
|
+
error.get("type", "server_error"),
|
|
87
|
+
error.get("message", "Unknown error"),
|
|
88
|
+
code if isinstance(code, str) else None,
|
|
89
|
+
)
|
|
90
|
+
return "server_error", f"HTTP {response.status_code}", None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BaseClient:
|
|
94
|
+
"""Shared configuration for sync and async clients."""
|
|
95
|
+
|
|
96
|
+
_gpu_name_cache: Optional[Dict[str, str]]
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
api_key: Optional[str] = None,
|
|
102
|
+
base_url: Optional[str] = None,
|
|
103
|
+
timeout: Optional[float] = None,
|
|
104
|
+
max_retries: Optional[int] = None,
|
|
105
|
+
) -> None:
|
|
106
|
+
self.api_key = api_key or os.environ.get(API_KEY_ENV_VAR)
|
|
107
|
+
if not self.api_key:
|
|
108
|
+
raise OpenRelayError(
|
|
109
|
+
f"API key is required. Pass api_key= or set {API_KEY_ENV_VAR}."
|
|
110
|
+
)
|
|
111
|
+
self.base_url = (
|
|
112
|
+
base_url or os.environ.get(BASE_URL_ENV_VAR) or DEFAULT_BASE_URL
|
|
113
|
+
).rstrip("/")
|
|
114
|
+
self.timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
|
|
115
|
+
self.max_retries = (
|
|
116
|
+
max_retries if max_retries is not None else DEFAULT_MAX_RETRIES
|
|
117
|
+
)
|
|
118
|
+
self._gpu_name_cache = None
|
|
119
|
+
|
|
120
|
+
def _build_headers(self) -> Dict[str, str]:
|
|
121
|
+
return {
|
|
122
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
123
|
+
"Content-Type": "application/json",
|
|
124
|
+
"User-Agent": f"openrelay-python/{__version__}",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def _build_url(self, path: str) -> str:
|
|
128
|
+
return f"{self.base_url}{path}"
|
|
129
|
+
|
|
130
|
+
def _should_retry(self, response: httpx.Response) -> bool:
|
|
131
|
+
if response.status_code == 503 and _parse_error(response)[2] in CAPACITY_CODES:
|
|
132
|
+
return False
|
|
133
|
+
return response.status_code in (429, 500, 502, 503, 504)
|
|
134
|
+
|
|
135
|
+
def _retry_delay(
|
|
136
|
+
self, attempt: int, response_headers: Optional[Dict[str, str]] = None
|
|
137
|
+
) -> float:
|
|
138
|
+
if response_headers:
|
|
139
|
+
retry_after = response_headers.get("retry-after")
|
|
140
|
+
if retry_after:
|
|
141
|
+
try:
|
|
142
|
+
return float(retry_after)
|
|
143
|
+
except (ValueError, TypeError):
|
|
144
|
+
pass
|
|
145
|
+
delay = min(INITIAL_RETRY_DELAY * (2**attempt), MAX_RETRY_DELAY)
|
|
146
|
+
jitter = random.random() * 0.5 * delay
|
|
147
|
+
return delay + jitter
|
|
148
|
+
|
|
149
|
+
def _raise_for_status(self, response: httpx.Response) -> None:
|
|
150
|
+
if response.is_success:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
error_type, message, code = _parse_error(response)
|
|
154
|
+
exc_class = _ERROR_TYPE_MAP.get(error_type, APIError)
|
|
155
|
+
raise exc_class(
|
|
156
|
+
message,
|
|
157
|
+
status_code=response.status_code,
|
|
158
|
+
type=error_type,
|
|
159
|
+
code=code,
|
|
160
|
+
headers=dict(response.headers),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def _parse_response(self, response: httpx.Response, model: Type[T]) -> T:
|
|
164
|
+
body = response.json()
|
|
165
|
+
data = body.get("data", body)
|
|
166
|
+
return model.model_validate(data)
|
|
167
|
+
|
|
168
|
+
def _parse_response_list(
|
|
169
|
+
self, response: httpx.Response, model: Type[T]
|
|
170
|
+
) -> List[T]:
|
|
171
|
+
body = response.json()
|
|
172
|
+
data = body.get("data", [])
|
|
173
|
+
return [model.model_validate(item) for item in data]
|