spanda-sdk 0.4.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.
- spanda_sdk-0.4.0/PKG-INFO +55 -0
- spanda_sdk-0.4.0/README.md +43 -0
- spanda_sdk-0.4.0/pyproject.toml +22 -0
- spanda_sdk-0.4.0/setup.cfg +4 -0
- spanda_sdk-0.4.0/spanda/__init__.py +5 -0
- spanda_sdk-0.4.0/spanda_sdk/__init__.py +7 -0
- spanda_sdk-0.4.0/spanda_sdk/client.py +176 -0
- spanda_sdk-0.4.0/spanda_sdk/errors.py +35 -0
- spanda_sdk-0.4.0/spanda_sdk/stream.py +43 -0
- spanda_sdk-0.4.0/spanda_sdk.egg-info/PKG-INFO +55 -0
- spanda_sdk-0.4.0/spanda_sdk.egg-info/SOURCES.txt +13 -0
- spanda_sdk-0.4.0/spanda_sdk.egg-info/dependency_links.txt +1 -0
- spanda_sdk-0.4.0/spanda_sdk.egg-info/requires.txt +7 -0
- spanda_sdk-0.4.0/spanda_sdk.egg-info/top_level.txt +2 -0
- spanda_sdk-0.4.0/tests/test_client.py +23 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spanda-sdk
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Official Python SDK for Spanda Control Center API v1
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Provides-Extra: stream
|
|
8
|
+
Requires-Dist: websockets>=12; extra == "stream"
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
11
|
+
Requires-Dist: websockets>=12; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# Spanda Python SDK
|
|
14
|
+
|
|
15
|
+
Official Python client for Spanda Control Center API v1.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e sdk/python
|
|
21
|
+
# WebSocket telemetry
|
|
22
|
+
pip install -e "sdk/python[stream]"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from spanda import SpandaClient
|
|
29
|
+
|
|
30
|
+
client = SpandaClient.local()
|
|
31
|
+
report = client.readiness("rover.sd")
|
|
32
|
+
print(report["report"])
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Control Center running:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
spanda control-center serve --program examples/robotics/rover.sd
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Environment
|
|
42
|
+
|
|
43
|
+
| Variable | Purpose |
|
|
44
|
+
|----------|---------|
|
|
45
|
+
| `SPANDA_CONTROL_CENTER_URL` | API base URL (default `http://127.0.0.1:8080`) |
|
|
46
|
+
| `SPANDA_API_KEY` | Bearer token for authenticated endpoints |
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
- [docs/sdk-python.md](../../docs/sdk-python.md)
|
|
51
|
+
- [docs/control-center-api.md](../../docs/control-center-api.md)
|
|
52
|
+
|
|
53
|
+
## Legacy client
|
|
54
|
+
|
|
55
|
+
Enterprise ops helpers (`ControlCenterClient` for drift, OTA, SRE) remain in `packages/sdk-python`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Spanda Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for Spanda Control Center API v1.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e sdk/python
|
|
9
|
+
# WebSocket telemetry
|
|
10
|
+
pip install -e "sdk/python[stream]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from spanda import SpandaClient
|
|
17
|
+
|
|
18
|
+
client = SpandaClient.local()
|
|
19
|
+
report = client.readiness("rover.sd")
|
|
20
|
+
print(report["report"])
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Control Center running:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
spanda control-center serve --program examples/robotics/rover.sd
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Environment
|
|
30
|
+
|
|
31
|
+
| Variable | Purpose |
|
|
32
|
+
|----------|---------|
|
|
33
|
+
| `SPANDA_CONTROL_CENTER_URL` | API base URL (default `http://127.0.0.1:8080`) |
|
|
34
|
+
| `SPANDA_API_KEY` | Bearer token for authenticated endpoints |
|
|
35
|
+
|
|
36
|
+
## Documentation
|
|
37
|
+
|
|
38
|
+
- [docs/sdk-python.md](../../docs/sdk-python.md)
|
|
39
|
+
- [docs/control-center-api.md](../../docs/control-center-api.md)
|
|
40
|
+
|
|
41
|
+
## Legacy client
|
|
42
|
+
|
|
43
|
+
Enterprise ops helpers (`ControlCenterClient` for drift, OTA, SRE) remain in `packages/sdk-python`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spanda-sdk"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Official Python SDK for Spanda Control Center API v1"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
stream = ["websockets>=12"]
|
|
15
|
+
dev = ["pytest>=7", "websockets>=12"]
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["."]
|
|
19
|
+
include = ["spanda_sdk*", "spanda*"]
|
|
20
|
+
|
|
21
|
+
[tool.pytest.ini_options]
|
|
22
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Official Spanda Python SDK — thin REST client over Control Center API v1."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Any, Mapping, Optional
|
|
11
|
+
|
|
12
|
+
from spanda_sdk.errors import ConnectionError, PermissionError, SpandaError, ValidationError
|
|
13
|
+
from spanda_sdk.stream import TelemetryStream
|
|
14
|
+
|
|
15
|
+
__all__ = ["SpandaClient", "SpandaError", "TelemetryStream"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SpandaClient:
|
|
19
|
+
"""REST v1 client with optional Bearer auth and correlation IDs."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
base_url: Optional[str] = None,
|
|
24
|
+
api_key: Optional[str] = None,
|
|
25
|
+
timeout: float = 30.0,
|
|
26
|
+
) -> None:
|
|
27
|
+
resolved_url = base_url or os.environ.get(
|
|
28
|
+
"SPANDA_CONTROL_CENTER_URL", "http://127.0.0.1:8080"
|
|
29
|
+
)
|
|
30
|
+
self.base_url = resolved_url.rstrip("/")
|
|
31
|
+
self.api_key = api_key if api_key is not None else os.environ.get("SPANDA_API_KEY")
|
|
32
|
+
self.timeout = timeout
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def local(cls) -> "SpandaClient":
|
|
36
|
+
"""Connect to the local Control Center."""
|
|
37
|
+
return cls()
|
|
38
|
+
|
|
39
|
+
def _request(
|
|
40
|
+
self,
|
|
41
|
+
method: str,
|
|
42
|
+
path: str,
|
|
43
|
+
body: Optional[Mapping[str, Any]] = None,
|
|
44
|
+
auth: bool = False,
|
|
45
|
+
) -> Any:
|
|
46
|
+
url = f"{self.base_url}{path}"
|
|
47
|
+
headers = {"Accept": "application/json", "X-Correlation-ID": f"py-sdk-{uuid.uuid4().hex[:12]}"}
|
|
48
|
+
data = None
|
|
49
|
+
if body is not None:
|
|
50
|
+
headers["Content-Type"] = "application/json"
|
|
51
|
+
data = json.dumps(body).encode("utf-8")
|
|
52
|
+
if auth and self.api_key:
|
|
53
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
54
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
55
|
+
try:
|
|
56
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
57
|
+
payload = resp.read().decode("utf-8")
|
|
58
|
+
if not payload:
|
|
59
|
+
return {}
|
|
60
|
+
return json.loads(payload)
|
|
61
|
+
except urllib.error.HTTPError as exc:
|
|
62
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
63
|
+
if exc.code in (401, 403):
|
|
64
|
+
raise PermissionError(detail, status=exc.code) from exc
|
|
65
|
+
if exc.code == 400:
|
|
66
|
+
raise ValidationError(detail, status=exc.code) from exc
|
|
67
|
+
raise SpandaError(detail, status=exc.code) from exc
|
|
68
|
+
except urllib.error.URLError as exc:
|
|
69
|
+
raise ConnectionError(str(exc.reason)) from exc
|
|
70
|
+
|
|
71
|
+
def _program_body(self, file_or_project: str) -> dict[str, str]:
|
|
72
|
+
return {"file": file_or_project}
|
|
73
|
+
|
|
74
|
+
def readiness(self, file_or_project: str) -> Any:
|
|
75
|
+
return self._request(
|
|
76
|
+
"POST", "/v1/programs/readiness", self._program_body(file_or_project)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def assure(self, file_or_project: str) -> Any:
|
|
80
|
+
return self._request(
|
|
81
|
+
"POST", "/v1/programs/assure", self._program_body(file_or_project)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def diagnose(self, trace_or_file: str) -> Any:
|
|
85
|
+
return self._request(
|
|
86
|
+
"POST", "/v1/programs/diagnose", self._program_body(trace_or_file)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def heal(self, target: str) -> Any:
|
|
90
|
+
return self._request("POST", "/v1/programs/recovery/heal", self._program_body(target))
|
|
91
|
+
|
|
92
|
+
def verify_hardware(self, project: str) -> Any:
|
|
93
|
+
return self._request(
|
|
94
|
+
"POST", "/v1/programs/verify/hardware", self._program_body(project)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def verify_capabilities(self, project: str) -> Any:
|
|
98
|
+
return self._request(
|
|
99
|
+
"POST",
|
|
100
|
+
"/v1/programs/verify/capabilities",
|
|
101
|
+
{"file": project, "traceability": True},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def list_entities(self) -> Any:
|
|
105
|
+
return self._request("GET", "/v1/entities")
|
|
106
|
+
|
|
107
|
+
def get_entity(self, entity_id: str) -> Any:
|
|
108
|
+
return self._request("GET", f"/v1/entities/{entity_id}")
|
|
109
|
+
|
|
110
|
+
def list_devices(self) -> Any:
|
|
111
|
+
return self._request("GET", "/v1/devices", auth=True)
|
|
112
|
+
|
|
113
|
+
def provision_device(self, device_id: str, body: Optional[Mapping[str, Any]] = None) -> Any:
|
|
114
|
+
return self._request(
|
|
115
|
+
"POST",
|
|
116
|
+
f"/v1/devices/{device_id}/provision",
|
|
117
|
+
body or {},
|
|
118
|
+
auth=True,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def run_simulation(self, project: str, *, execute: bool = False) -> Any:
|
|
122
|
+
return self._request(
|
|
123
|
+
"POST",
|
|
124
|
+
"/v1/programs/simulation",
|
|
125
|
+
{"file": project, "execute": execute},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def replay(
|
|
129
|
+
self,
|
|
130
|
+
trace: str,
|
|
131
|
+
*,
|
|
132
|
+
deterministic: bool = False,
|
|
133
|
+
playback: bool = False,
|
|
134
|
+
) -> Any:
|
|
135
|
+
return self._request(
|
|
136
|
+
"POST",
|
|
137
|
+
"/v1/programs/replay",
|
|
138
|
+
{
|
|
139
|
+
"file": trace,
|
|
140
|
+
"deterministic": deterministic,
|
|
141
|
+
"playback": playback,
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def get_health(self, entity_id: str) -> Any:
|
|
146
|
+
return self._request("GET", f"/v1/entities/{entity_id}/health")
|
|
147
|
+
|
|
148
|
+
def get_trust(self, entity_id: str) -> Any:
|
|
149
|
+
return self._request("GET", f"/v1/entities/{entity_id}/trust")
|
|
150
|
+
|
|
151
|
+
def get_package_trust(self, package: str, version: Optional[str] = None) -> Any:
|
|
152
|
+
path = f"/v1/trust/package?name={package}"
|
|
153
|
+
if version:
|
|
154
|
+
path += f"&version={version}"
|
|
155
|
+
return self._request("GET", path)
|
|
156
|
+
|
|
157
|
+
def health_check(self) -> Any:
|
|
158
|
+
return self._request("GET", "/v1/health")
|
|
159
|
+
|
|
160
|
+
def rpc(self, method: str, params: Optional[Mapping[str, Any]] = None) -> Any:
|
|
161
|
+
payload = self._request(
|
|
162
|
+
"POST",
|
|
163
|
+
"/v1/rpc",
|
|
164
|
+
{"method": method, "params": params or {}},
|
|
165
|
+
)
|
|
166
|
+
return payload.get("result", payload)
|
|
167
|
+
|
|
168
|
+
# Backward-compatible Control Center helpers
|
|
169
|
+
def dashboard(self) -> Any:
|
|
170
|
+
return self._request("GET", "/v1/dashboard")
|
|
171
|
+
|
|
172
|
+
def readiness_run(self, body: Optional[Mapping[str, Any]] = None) -> Any:
|
|
173
|
+
return self._request("POST", "/v1/readiness/run", body or {})
|
|
174
|
+
|
|
175
|
+
def trust_package(self, name: str, version: Optional[str] = None) -> Any:
|
|
176
|
+
return self.get_package_trust(name, version)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Structured errors for the Spanda Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SpandaError(Exception):
|
|
7
|
+
"""Base SDK error."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, *, status: int | None = None) -> None:
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.status = status
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ValidationError(SpandaError):
|
|
15
|
+
"""Invalid request or response payload."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ReadinessError(SpandaError):
|
|
19
|
+
"""Readiness evaluation failed."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VerificationError(SpandaError):
|
|
23
|
+
"""Hardware or capability verification failed."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SecurityError(SpandaError):
|
|
27
|
+
"""Authentication or authorization failure."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConnectionError(SpandaError):
|
|
31
|
+
"""Network or server connectivity failure."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PermissionError(SpandaError):
|
|
35
|
+
"""Insufficient permissions for the requested operation."""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""WebSocket telemetry stream for Control Center events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, AsyncIterator, Callable, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TelemetryStream:
|
|
12
|
+
"""Async WebSocket client for `WS /v1/stream/telemetry`."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, base_url: Optional[str] = None) -> None:
|
|
15
|
+
http = base_url or os.environ.get(
|
|
16
|
+
"SPANDA_CONTROL_CENTER_URL", "http://127.0.0.1:8080"
|
|
17
|
+
)
|
|
18
|
+
self.ws_url = http.replace("https://", "wss://").replace("http://", "ws://")
|
|
19
|
+
if not self.ws_url.endswith("/v1/stream/telemetry"):
|
|
20
|
+
self.ws_url = f"{self.ws_url.rstrip('/')}/v1/stream/telemetry"
|
|
21
|
+
|
|
22
|
+
async def events(self) -> AsyncIterator[dict[str, Any]]:
|
|
23
|
+
"""Yield parsed JSON events from the telemetry stream."""
|
|
24
|
+
try:
|
|
25
|
+
import websockets
|
|
26
|
+
except ImportError as exc:
|
|
27
|
+
raise RuntimeError(
|
|
28
|
+
"Install stream extras: pip install 'spanda-sdk[stream]'"
|
|
29
|
+
) from exc
|
|
30
|
+
|
|
31
|
+
async with websockets.connect(self.ws_url) as ws:
|
|
32
|
+
async for message in ws:
|
|
33
|
+
yield json.loads(message)
|
|
34
|
+
|
|
35
|
+
async def listen(self, handler: Callable[[dict[str, Any]], None]) -> None:
|
|
36
|
+
"""Invoke handler for each telemetry event."""
|
|
37
|
+
async for event in self.events():
|
|
38
|
+
handler(event)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_stream(handler: Callable[[dict[str, Any]], None], base_url: Optional[str] = None) -> None:
|
|
42
|
+
"""Blocking helper to run an event handler loop."""
|
|
43
|
+
asyncio.run(TelemetryStream(base_url).listen(handler))
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spanda-sdk
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Official Python SDK for Spanda Control Center API v1
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Provides-Extra: stream
|
|
8
|
+
Requires-Dist: websockets>=12; extra == "stream"
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
11
|
+
Requires-Dist: websockets>=12; extra == "dev"
|
|
12
|
+
|
|
13
|
+
# Spanda Python SDK
|
|
14
|
+
|
|
15
|
+
Official Python client for Spanda Control Center API v1.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e sdk/python
|
|
21
|
+
# WebSocket telemetry
|
|
22
|
+
pip install -e "sdk/python[stream]"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from spanda import SpandaClient
|
|
29
|
+
|
|
30
|
+
client = SpandaClient.local()
|
|
31
|
+
report = client.readiness("rover.sd")
|
|
32
|
+
print(report["report"])
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Control Center running:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
spanda control-center serve --program examples/robotics/rover.sd
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Environment
|
|
42
|
+
|
|
43
|
+
| Variable | Purpose |
|
|
44
|
+
|----------|---------|
|
|
45
|
+
| `SPANDA_CONTROL_CENTER_URL` | API base URL (default `http://127.0.0.1:8080`) |
|
|
46
|
+
| `SPANDA_API_KEY` | Bearer token for authenticated endpoints |
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
- [docs/sdk-python.md](../../docs/sdk-python.md)
|
|
51
|
+
- [docs/control-center-api.md](../../docs/control-center-api.md)
|
|
52
|
+
|
|
53
|
+
## Legacy client
|
|
54
|
+
|
|
55
|
+
Enterprise ops helpers (`ControlCenterClient` for drift, OTA, SRE) remain in `packages/sdk-python`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
spanda/__init__.py
|
|
4
|
+
spanda_sdk/__init__.py
|
|
5
|
+
spanda_sdk/client.py
|
|
6
|
+
spanda_sdk/errors.py
|
|
7
|
+
spanda_sdk/stream.py
|
|
8
|
+
spanda_sdk.egg-info/PKG-INFO
|
|
9
|
+
spanda_sdk.egg-info/SOURCES.txt
|
|
10
|
+
spanda_sdk.egg-info/dependency_links.txt
|
|
11
|
+
spanda_sdk.egg-info/requires.txt
|
|
12
|
+
spanda_sdk.egg-info/top_level.txt
|
|
13
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Python SDK smoke tests."""
|
|
2
|
+
|
|
3
|
+
from spanda_sdk import SpandaClient
|
|
4
|
+
from spanda_sdk.errors import SpandaError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_local_client_constructs():
|
|
8
|
+
client = SpandaClient.local()
|
|
9
|
+
assert "127.0.0.1" in client.base_url
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_program_body_shape():
|
|
13
|
+
client = SpandaClient.local()
|
|
14
|
+
body = client._program_body("rover.sd")
|
|
15
|
+
assert body["file"] == "rover.sd"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_health_check_raises_without_server():
|
|
19
|
+
client = SpandaClient(base_url="http://127.0.0.1:1")
|
|
20
|
+
try:
|
|
21
|
+
client.health_check()
|
|
22
|
+
except SpandaError:
|
|
23
|
+
pass
|