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.
@@ -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()