spacerouter 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.
- spacerouter-0.1.0/.gitignore +25 -0
- spacerouter-0.1.0/PKG-INFO +137 -0
- spacerouter-0.1.0/README.md +121 -0
- spacerouter-0.1.0/pyproject.toml +29 -0
- spacerouter-0.1.0/src/spacerouter/__init__.py +28 -0
- spacerouter-0.1.0/src/spacerouter/admin.py +144 -0
- spacerouter-0.1.0/src/spacerouter/client.py +370 -0
- spacerouter-0.1.0/src/spacerouter/exceptions.py +61 -0
- spacerouter-0.1.0/src/spacerouter/models.py +61 -0
- spacerouter-0.1.0/tests/__init__.py +0 -0
- spacerouter-0.1.0/tests/conftest.py +24 -0
- spacerouter-0.1.0/tests/test_admin.py +171 -0
- spacerouter-0.1.0/tests/test_client.py +347 -0
- spacerouter-0.1.0/tests/test_integration.py +67 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
|
|
12
|
+
# JavaScript
|
|
13
|
+
node_modules/
|
|
14
|
+
dist/
|
|
15
|
+
*.tsbuildinfo
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.idea/
|
|
19
|
+
.vscode/
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
|
|
23
|
+
# OS
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spacerouter
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Space Router residential proxy network
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
8
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
13
|
+
Provides-Extra: socks
|
|
14
|
+
Requires-Dist: httpx[socks]<1.0,>=0.27; extra == 'socks'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# SpaceRouter Python SDK
|
|
18
|
+
|
|
19
|
+
Python SDK for routing HTTP requests through the [Space Router](../../README.md) residential proxy network.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install spacerouter
|
|
25
|
+
|
|
26
|
+
# With SOCKS5 support
|
|
27
|
+
pip install spacerouter[socks]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from spacerouter import SpaceRouter
|
|
34
|
+
|
|
35
|
+
with SpaceRouter("sr_live_YOUR_API_KEY", gateway_url="http://gateway:8080") as client:
|
|
36
|
+
response = client.get("https://httpbin.org/ip")
|
|
37
|
+
print(response.json()) # {"origin": "residential-ip"}
|
|
38
|
+
print(response.node_id) # node that handled the request
|
|
39
|
+
print(response.request_id) # unique request ID for tracing
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Async Usage
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from spacerouter import AsyncSpaceRouter
|
|
46
|
+
|
|
47
|
+
async with AsyncSpaceRouter("sr_live_YOUR_API_KEY") as client:
|
|
48
|
+
response = await client.get("https://httpbin.org/ip")
|
|
49
|
+
print(response.json())
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## IP Targeting
|
|
53
|
+
|
|
54
|
+
Route requests through specific IP types or geographic regions:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# Target residential IPs in the US
|
|
58
|
+
client = SpaceRouter("sr_live_xxx", ip_type="residential", region="US")
|
|
59
|
+
|
|
60
|
+
# Target mobile IPs in South Korea
|
|
61
|
+
client = SpaceRouter("sr_live_xxx", ip_type="mobile", region="Seoul, KR")
|
|
62
|
+
|
|
63
|
+
# Change routing on the fly
|
|
64
|
+
mobile_client = client.with_routing(ip_type="mobile", region="JP")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Available IP types: `residential`, `mobile`, `datacenter`, `business`
|
|
68
|
+
|
|
69
|
+
## SOCKS5 Proxy
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
client = SpaceRouter(
|
|
73
|
+
"sr_live_xxx",
|
|
74
|
+
protocol="socks5",
|
|
75
|
+
gateway_url="socks5://gateway:1080",
|
|
76
|
+
)
|
|
77
|
+
response = client.get("https://httpbin.org/ip")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Requires the `socks` extra: `pip install spacerouter[socks]`
|
|
81
|
+
|
|
82
|
+
## API Key Management
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from spacerouter import SpaceRouterAdmin
|
|
86
|
+
|
|
87
|
+
with SpaceRouterAdmin("http://localhost:8000") as admin:
|
|
88
|
+
# Create a key (raw value only available here)
|
|
89
|
+
key = admin.create_api_key("my-agent", rate_limit_rpm=120)
|
|
90
|
+
print(key.api_key) # sr_live_...
|
|
91
|
+
|
|
92
|
+
# List keys
|
|
93
|
+
for k in admin.list_api_keys():
|
|
94
|
+
print(k.name, k.key_prefix, k.is_active)
|
|
95
|
+
|
|
96
|
+
# Revoke a key
|
|
97
|
+
admin.revoke_api_key(key.id)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Async variant: `AsyncSpaceRouterAdmin`
|
|
101
|
+
|
|
102
|
+
## Error Handling
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from spacerouter import SpaceRouter
|
|
106
|
+
from spacerouter.exceptions import (
|
|
107
|
+
AuthenticationError, # 407 - invalid API key
|
|
108
|
+
RateLimitError, # 429 - rate limit exceeded
|
|
109
|
+
NoNodesAvailableError, # 503 - no residential nodes online
|
|
110
|
+
UpstreamError, # 502 - target unreachable via node
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
with SpaceRouter("sr_live_xxx") as client:
|
|
114
|
+
try:
|
|
115
|
+
response = client.get("https://example.com")
|
|
116
|
+
except RateLimitError as e:
|
|
117
|
+
print(f"Rate limited, retry after {e.retry_after}s")
|
|
118
|
+
except NoNodesAvailableError:
|
|
119
|
+
print("No nodes available, try again later")
|
|
120
|
+
except UpstreamError as e:
|
|
121
|
+
print(f"Node {e.node_id} could not reach target")
|
|
122
|
+
except AuthenticationError:
|
|
123
|
+
print("Check your API key")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Note: HTTP errors from the target website (e.g. 404, 500) are **not** raised as exceptions. Only proxy-layer errors produce exceptions.
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
| Parameter | Default | Description |
|
|
131
|
+
|-----------|---------|-------------|
|
|
132
|
+
| `api_key` | (required) | API key (`sr_live_...`) |
|
|
133
|
+
| `gateway_url` | `http://localhost:8080` | Proxy gateway URL |
|
|
134
|
+
| `protocol` | `http` | `http` or `socks5` |
|
|
135
|
+
| `ip_type` | `None` | IP type filter |
|
|
136
|
+
| `region` | `None` | Region filter (substring match) |
|
|
137
|
+
| `timeout` | `30.0` | Request timeout in seconds |
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# SpaceRouter Python SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for routing HTTP requests through the [Space Router](../../README.md) residential proxy network.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install spacerouter
|
|
9
|
+
|
|
10
|
+
# With SOCKS5 support
|
|
11
|
+
pip install spacerouter[socks]
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from spacerouter import SpaceRouter
|
|
18
|
+
|
|
19
|
+
with SpaceRouter("sr_live_YOUR_API_KEY", gateway_url="http://gateway:8080") as client:
|
|
20
|
+
response = client.get("https://httpbin.org/ip")
|
|
21
|
+
print(response.json()) # {"origin": "residential-ip"}
|
|
22
|
+
print(response.node_id) # node that handled the request
|
|
23
|
+
print(response.request_id) # unique request ID for tracing
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Async Usage
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from spacerouter import AsyncSpaceRouter
|
|
30
|
+
|
|
31
|
+
async with AsyncSpaceRouter("sr_live_YOUR_API_KEY") as client:
|
|
32
|
+
response = await client.get("https://httpbin.org/ip")
|
|
33
|
+
print(response.json())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## IP Targeting
|
|
37
|
+
|
|
38
|
+
Route requests through specific IP types or geographic regions:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# Target residential IPs in the US
|
|
42
|
+
client = SpaceRouter("sr_live_xxx", ip_type="residential", region="US")
|
|
43
|
+
|
|
44
|
+
# Target mobile IPs in South Korea
|
|
45
|
+
client = SpaceRouter("sr_live_xxx", ip_type="mobile", region="Seoul, KR")
|
|
46
|
+
|
|
47
|
+
# Change routing on the fly
|
|
48
|
+
mobile_client = client.with_routing(ip_type="mobile", region="JP")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Available IP types: `residential`, `mobile`, `datacenter`, `business`
|
|
52
|
+
|
|
53
|
+
## SOCKS5 Proxy
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
client = SpaceRouter(
|
|
57
|
+
"sr_live_xxx",
|
|
58
|
+
protocol="socks5",
|
|
59
|
+
gateway_url="socks5://gateway:1080",
|
|
60
|
+
)
|
|
61
|
+
response = client.get("https://httpbin.org/ip")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Requires the `socks` extra: `pip install spacerouter[socks]`
|
|
65
|
+
|
|
66
|
+
## API Key Management
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from spacerouter import SpaceRouterAdmin
|
|
70
|
+
|
|
71
|
+
with SpaceRouterAdmin("http://localhost:8000") as admin:
|
|
72
|
+
# Create a key (raw value only available here)
|
|
73
|
+
key = admin.create_api_key("my-agent", rate_limit_rpm=120)
|
|
74
|
+
print(key.api_key) # sr_live_...
|
|
75
|
+
|
|
76
|
+
# List keys
|
|
77
|
+
for k in admin.list_api_keys():
|
|
78
|
+
print(k.name, k.key_prefix, k.is_active)
|
|
79
|
+
|
|
80
|
+
# Revoke a key
|
|
81
|
+
admin.revoke_api_key(key.id)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Async variant: `AsyncSpaceRouterAdmin`
|
|
85
|
+
|
|
86
|
+
## Error Handling
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from spacerouter import SpaceRouter
|
|
90
|
+
from spacerouter.exceptions import (
|
|
91
|
+
AuthenticationError, # 407 - invalid API key
|
|
92
|
+
RateLimitError, # 429 - rate limit exceeded
|
|
93
|
+
NoNodesAvailableError, # 503 - no residential nodes online
|
|
94
|
+
UpstreamError, # 502 - target unreachable via node
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
with SpaceRouter("sr_live_xxx") as client:
|
|
98
|
+
try:
|
|
99
|
+
response = client.get("https://example.com")
|
|
100
|
+
except RateLimitError as e:
|
|
101
|
+
print(f"Rate limited, retry after {e.retry_after}s")
|
|
102
|
+
except NoNodesAvailableError:
|
|
103
|
+
print("No nodes available, try again later")
|
|
104
|
+
except UpstreamError as e:
|
|
105
|
+
print(f"Node {e.node_id} could not reach target")
|
|
106
|
+
except AuthenticationError:
|
|
107
|
+
print("Check your API key")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Note: HTTP errors from the target website (e.g. 404, 500) are **not** raised as exceptions. Only proxy-layer errors produce exceptions.
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
| Parameter | Default | Description |
|
|
115
|
+
|-----------|---------|-------------|
|
|
116
|
+
| `api_key` | (required) | API key (`sr_live_...`) |
|
|
117
|
+
| `gateway_url` | `http://localhost:8080` | Proxy gateway URL |
|
|
118
|
+
| `protocol` | `http` | `http` or `socks5` |
|
|
119
|
+
| `ip_type` | `None` | IP type filter |
|
|
120
|
+
| `region` | `None` | Region filter (substring match) |
|
|
121
|
+
| `timeout` | `30.0` | Request timeout in seconds |
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spacerouter"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for the Space Router residential proxy network"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.27,<1.0",
|
|
14
|
+
"pydantic>=2.0,<3.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
socks = ["httpx[socks]>=0.27,<1.0"]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.0",
|
|
21
|
+
"pytest-asyncio>=0.25",
|
|
22
|
+
"respx>=0.22",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/spacerouter"]
|
|
27
|
+
|
|
28
|
+
[tool.pytest.ini_options]
|
|
29
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""SpaceRouter Python SDK — route HTTP requests through residential IPs."""
|
|
2
|
+
|
|
3
|
+
from spacerouter.admin import AsyncSpaceRouterAdmin, SpaceRouterAdmin
|
|
4
|
+
from spacerouter.client import AsyncSpaceRouter, SpaceRouter, fetch_ca_cert
|
|
5
|
+
from spacerouter.exceptions import (
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
NoNodesAvailableError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
SpaceRouterError,
|
|
10
|
+
UpstreamError,
|
|
11
|
+
)
|
|
12
|
+
from spacerouter.models import ApiKey, ApiKeyInfo, ProxyResponse
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"SpaceRouter",
|
|
16
|
+
"AsyncSpaceRouter",
|
|
17
|
+
"SpaceRouterAdmin",
|
|
18
|
+
"AsyncSpaceRouterAdmin",
|
|
19
|
+
"fetch_ca_cert",
|
|
20
|
+
"ApiKey",
|
|
21
|
+
"ApiKeyInfo",
|
|
22
|
+
"ProxyResponse",
|
|
23
|
+
"SpaceRouterError",
|
|
24
|
+
"AuthenticationError",
|
|
25
|
+
"RateLimitError",
|
|
26
|
+
"NoNodesAvailableError",
|
|
27
|
+
"UpstreamError",
|
|
28
|
+
]
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Admin clients for the Space Router Coordination API.
|
|
2
|
+
|
|
3
|
+
Provides :class:`SpaceRouterAdmin` (sync) and :class:`AsyncSpaceRouterAdmin`
|
|
4
|
+
(async) for managing API keys.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from spacerouter.models import ApiKey, ApiKeyInfo
|
|
14
|
+
|
|
15
|
+
_DEFAULT_COORDINATION_URL = "https://coordination.spacerouter.org"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SpaceRouterAdmin:
|
|
19
|
+
"""Synchronous admin client for the Coordination API.
|
|
20
|
+
|
|
21
|
+
Example::
|
|
22
|
+
|
|
23
|
+
with SpaceRouterAdmin() as admin:
|
|
24
|
+
key = admin.create_api_key("my-agent")
|
|
25
|
+
print(key.api_key) # sr_live_...
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
base_url: str = _DEFAULT_COORDINATION_URL,
|
|
31
|
+
*,
|
|
32
|
+
timeout: float = 10.0,
|
|
33
|
+
**httpx_kwargs: Any,
|
|
34
|
+
) -> None:
|
|
35
|
+
self._client = httpx.Client(
|
|
36
|
+
base_url=base_url, timeout=timeout, **httpx_kwargs
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def create_api_key(self, name: str, *, rate_limit_rpm: int = 60) -> ApiKey:
|
|
40
|
+
"""Create a new API key.
|
|
41
|
+
|
|
42
|
+
The raw key value is **only** available in the returned object.
|
|
43
|
+
"""
|
|
44
|
+
response = self._client.post(
|
|
45
|
+
"/api-keys",
|
|
46
|
+
json={"name": name, "rate_limit_rpm": rate_limit_rpm},
|
|
47
|
+
)
|
|
48
|
+
response.raise_for_status()
|
|
49
|
+
return ApiKey.model_validate(response.json())
|
|
50
|
+
|
|
51
|
+
def list_api_keys(self) -> list[ApiKeyInfo]:
|
|
52
|
+
"""List all API keys (raw key values are never returned)."""
|
|
53
|
+
response = self._client.get("/api-keys")
|
|
54
|
+
response.raise_for_status()
|
|
55
|
+
return [ApiKeyInfo.model_validate(item) for item in response.json()]
|
|
56
|
+
|
|
57
|
+
def revoke_api_key(self, key_id: str) -> None:
|
|
58
|
+
"""Revoke an API key (soft-delete)."""
|
|
59
|
+
response = self._client.delete(f"/api-keys/{key_id}")
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
|
|
62
|
+
def fetch_ca_cert(self) -> str | None:
|
|
63
|
+
"""Fetch the proxy network CA certificate.
|
|
64
|
+
|
|
65
|
+
Returns the PEM-encoded certificate, or ``None`` when the proxy
|
|
66
|
+
network does not require a custom CA (HTTP 503).
|
|
67
|
+
"""
|
|
68
|
+
response = self._client.get("/ca-cert")
|
|
69
|
+
if response.status_code == 503:
|
|
70
|
+
return None
|
|
71
|
+
response.raise_for_status()
|
|
72
|
+
return response.text
|
|
73
|
+
|
|
74
|
+
def close(self) -> None:
|
|
75
|
+
self._client.close()
|
|
76
|
+
|
|
77
|
+
def __enter__(self) -> SpaceRouterAdmin:
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __exit__(self, *args: Any) -> None:
|
|
81
|
+
self.close()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AsyncSpaceRouterAdmin:
|
|
85
|
+
"""Asynchronous admin client for the Coordination API.
|
|
86
|
+
|
|
87
|
+
Example::
|
|
88
|
+
|
|
89
|
+
async with AsyncSpaceRouterAdmin() as admin:
|
|
90
|
+
key = await admin.create_api_key("my-agent")
|
|
91
|
+
print(key.api_key)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
base_url: str = _DEFAULT_COORDINATION_URL,
|
|
97
|
+
*,
|
|
98
|
+
timeout: float = 10.0,
|
|
99
|
+
**httpx_kwargs: Any,
|
|
100
|
+
) -> None:
|
|
101
|
+
self._client = httpx.AsyncClient(
|
|
102
|
+
base_url=base_url, timeout=timeout, **httpx_kwargs
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def create_api_key(self, name: str, *, rate_limit_rpm: int = 60) -> ApiKey:
|
|
106
|
+
"""Create a new API key."""
|
|
107
|
+
response = await self._client.post(
|
|
108
|
+
"/api-keys",
|
|
109
|
+
json={"name": name, "rate_limit_rpm": rate_limit_rpm},
|
|
110
|
+
)
|
|
111
|
+
response.raise_for_status()
|
|
112
|
+
return ApiKey.model_validate(response.json())
|
|
113
|
+
|
|
114
|
+
async def list_api_keys(self) -> list[ApiKeyInfo]:
|
|
115
|
+
"""List all API keys."""
|
|
116
|
+
response = await self._client.get("/api-keys")
|
|
117
|
+
response.raise_for_status()
|
|
118
|
+
return [ApiKeyInfo.model_validate(item) for item in response.json()]
|
|
119
|
+
|
|
120
|
+
async def revoke_api_key(self, key_id: str) -> None:
|
|
121
|
+
"""Revoke an API key (soft-delete)."""
|
|
122
|
+
response = await self._client.delete(f"/api-keys/{key_id}")
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
|
|
125
|
+
async def fetch_ca_cert(self) -> str | None:
|
|
126
|
+
"""Fetch the proxy network CA certificate.
|
|
127
|
+
|
|
128
|
+
Returns the PEM-encoded certificate, or ``None`` when the proxy
|
|
129
|
+
network does not require a custom CA (HTTP 503).
|
|
130
|
+
"""
|
|
131
|
+
response = await self._client.get("/ca-cert")
|
|
132
|
+
if response.status_code == 503:
|
|
133
|
+
return None
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
return response.text
|
|
136
|
+
|
|
137
|
+
async def aclose(self) -> None:
|
|
138
|
+
await self._client.aclose()
|
|
139
|
+
|
|
140
|
+
async def __aenter__(self) -> AsyncSpaceRouterAdmin:
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
144
|
+
await self.aclose()
|