ridescanapi 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.
- ridescanapi-0.1.0/PKG-INFO +11 -0
- ridescanapi-0.1.0/README.md +2 -0
- ridescanapi-0.1.0/pyproject.toml +16 -0
- ridescanapi-0.1.0/setup.cfg +4 -0
- ridescanapi-0.1.0/src/ridescanapi/__init__.py +19 -0
- ridescanapi-0.1.0/src/ridescanapi/client.py +249 -0
- ridescanapi-0.1.0/src/ridescanapi/exceptions.py +26 -0
- ridescanapi-0.1.0/src/ridescanapi.egg-info/PKG-INFO +11 -0
- ridescanapi-0.1.0/src/ridescanapi.egg-info/SOURCES.txt +10 -0
- ridescanapi-0.1.0/src/ridescanapi.egg-info/dependency_links.txt +1 -0
- ridescanapi-0.1.0/src/ridescanapi.egg-info/requires.txt +1 -0
- ridescanapi-0.1.0/src/ridescanapi.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ridescanapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the RideScan Safety Layer API
|
|
5
|
+
Author-email: RideScan Engineering <support@ridescan.ai>
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests>=2.25.0
|
|
9
|
+
|
|
10
|
+
# RideScan Python SDK
|
|
11
|
+
Official client for the RideScan API.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ridescanapi"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for the RideScan Safety Layer API"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name="RideScan Engineering", email="support@ridescan.ai" },
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.7"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"requests>=2.25.0"
|
|
16
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .client import RideScanClient
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
RideScanError,
|
|
4
|
+
AuthenticationError,
|
|
5
|
+
ValidationError,
|
|
6
|
+
ResourceNotFoundError,
|
|
7
|
+
ConflictError,
|
|
8
|
+
ServerError
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"RideScanClient",
|
|
13
|
+
"RideScanError",
|
|
14
|
+
"AuthenticationError",
|
|
15
|
+
"ValidationError",
|
|
16
|
+
"ResourceNotFoundError",
|
|
17
|
+
"ConflictError",
|
|
18
|
+
"ServerError"
|
|
19
|
+
]
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional, List, Dict, Any, Union
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
RideScanError, AuthenticationError, ValidationError,
|
|
6
|
+
ResourceNotFoundError, ConflictError, ServerError
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
# Set up a library-specific logger
|
|
10
|
+
logger = logging.getLogger("ridescanapi")
|
|
11
|
+
|
|
12
|
+
class RideScanClient:
|
|
13
|
+
"""
|
|
14
|
+
Official Python SDK for the RideScan Safety Layer API.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, api_key: str, base_url: str = "http://localhost:8000/api", timeout: int = 30):
|
|
18
|
+
"""
|
|
19
|
+
Initialize the RideScan client.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
api_key (str): The 'rsk_...' key generated from the dashboard.
|
|
23
|
+
base_url (str): The API endpoint. Defaults to localhost.
|
|
24
|
+
timeout (int): Request timeout in seconds.
|
|
25
|
+
"""
|
|
26
|
+
self.base_url = base_url.rstrip("/")
|
|
27
|
+
self.timeout = timeout
|
|
28
|
+
self.session = requests.Session()
|
|
29
|
+
|
|
30
|
+
# Pre-configure headers (Optimized for reuse)
|
|
31
|
+
self.session.headers.update({
|
|
32
|
+
"X-API-KEY": api_key,
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
"User-Agent": "ridescan-python-sdk/1.0.0"
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
def __enter__(self):
|
|
38
|
+
return self
|
|
39
|
+
|
|
40
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
41
|
+
self.session.close()
|
|
42
|
+
|
|
43
|
+
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
|
|
44
|
+
"""
|
|
45
|
+
Parses the response and raises specific exceptions based on backend error codes.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
# Raise HTTPError for 4xx/5xx first to catch generic connection issues
|
|
49
|
+
response.raise_for_status()
|
|
50
|
+
|
|
51
|
+
# If successful (200-299), return JSON
|
|
52
|
+
if response.status_code != 204: # 204 No Content has no JSON
|
|
53
|
+
return response.json()
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
except requests.exceptions.HTTPError:
|
|
57
|
+
# Attempt to parse the structured backend error
|
|
58
|
+
try:
|
|
59
|
+
payload = response.json()
|
|
60
|
+
error_body = payload.get("error", {})
|
|
61
|
+
|
|
62
|
+
# Handle simplified error strings if they exist
|
|
63
|
+
if isinstance(error_body, str):
|
|
64
|
+
msg = error_body
|
|
65
|
+
code = "UNKNOWN"
|
|
66
|
+
details = None
|
|
67
|
+
else:
|
|
68
|
+
code = error_body.get("code", "UNKNOWN")
|
|
69
|
+
msg = error_body.get("message", str(response.reason))
|
|
70
|
+
details = error_body.get("details")
|
|
71
|
+
|
|
72
|
+
# Map specific Backend Error Codes to Python Exceptions
|
|
73
|
+
if code.startswith("RS-AUTH"):
|
|
74
|
+
raise AuthenticationError(msg, code, details)
|
|
75
|
+
elif code.startswith("RS-VAL"):
|
|
76
|
+
raise ValidationError(msg, code, details)
|
|
77
|
+
elif "002" in code and ("ROBOT" in code or "MSN" in code):
|
|
78
|
+
# RS-ROBOT-002 or equivalent checks
|
|
79
|
+
raise ResourceNotFoundError(msg, code, details)
|
|
80
|
+
elif code in ["RS-ROBOT-001", "RS-MSN-001"]:
|
|
81
|
+
raise ConflictError(msg, code, details)
|
|
82
|
+
elif code.startswith("RS-SYS"):
|
|
83
|
+
raise ServerError(msg, code, details)
|
|
84
|
+
else:
|
|
85
|
+
# Fallback for unmapped errors
|
|
86
|
+
raise RideScanError(msg, code, details)
|
|
87
|
+
|
|
88
|
+
except ValueError:
|
|
89
|
+
# Response wasn't JSON (e.g. 502 Bad Gateway HTML)
|
|
90
|
+
raise RideScanError(f"HTTP {response.status_code}: {response.text[:100]}")
|
|
91
|
+
|
|
92
|
+
def _request(self, method: str, endpoint: str, payload: Optional[Dict] = None) -> Any:
|
|
93
|
+
url = f"{self.base_url}{endpoint}"
|
|
94
|
+
logger.debug(f"Request: {method} {url} | Payload: {payload}")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
# Note: We send 'json' even for GET requests per RideScan backend design
|
|
98
|
+
response = self.session.request(method, url, json=payload, timeout=self.timeout)
|
|
99
|
+
return self._handle_response(response)
|
|
100
|
+
except requests.exceptions.ConnectionError:
|
|
101
|
+
raise RideScanError("Failed to connect to RideScan API. Is the server running?")
|
|
102
|
+
except requests.exceptions.Timeout:
|
|
103
|
+
raise RideScanError(f"Request timed out after {self.timeout}s")
|
|
104
|
+
|
|
105
|
+
# ==========================
|
|
106
|
+
# ROBOT RESOURCES
|
|
107
|
+
# ==========================
|
|
108
|
+
|
|
109
|
+
def create_robot(self, name: str, robot_type: str) -> Dict[str, Any]:
|
|
110
|
+
"""
|
|
111
|
+
Register a new robot in the organization.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
name (str): Unique name for the robot (e.g., 'Spot-Alpha').
|
|
115
|
+
robot_type (str): The model type. Allowed: 'spot', 'ur6'.
|
|
116
|
+
"""
|
|
117
|
+
payload = {
|
|
118
|
+
"params": {
|
|
119
|
+
"robot_name": name,
|
|
120
|
+
"robot_type": robot_type
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return self._request("POST", "/robot/create", payload)
|
|
124
|
+
|
|
125
|
+
def get_robots(self,
|
|
126
|
+
robot_id: Optional[str] = None,
|
|
127
|
+
name: Optional[str] = None,
|
|
128
|
+
robot_type: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
129
|
+
"""
|
|
130
|
+
Search for robots. Returns a list of matches.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
robot_id (str): Filter by the public UUID of the robot.
|
|
134
|
+
name (str): Filter by exact robot name.
|
|
135
|
+
robot_type (str): Filter by type ('spot', 'ur6').
|
|
136
|
+
"""
|
|
137
|
+
criteria = {}
|
|
138
|
+
if robot_id: criteria["robot_id"] = robot_id
|
|
139
|
+
if name: criteria["robot_name"] = name
|
|
140
|
+
if robot_type: criteria["robot_type"] = robot_type
|
|
141
|
+
|
|
142
|
+
# Backend expects 'criteria' object even for GET
|
|
143
|
+
response = self._request("GET", "/getrobot", payload={"criteria": criteria})
|
|
144
|
+
return response.get("robot_list", [])
|
|
145
|
+
|
|
146
|
+
def edit_robot(self, robot_id: str,
|
|
147
|
+
new_name: Optional[str] = None,
|
|
148
|
+
new_type: Optional[str] = None) -> Dict[str, Any]:
|
|
149
|
+
"""
|
|
150
|
+
Update a robot's details.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
robot_id (str): The unique UUID of the robot to edit.
|
|
154
|
+
new_name (str, optional): New name to assign.
|
|
155
|
+
new_type (str, optional): New type to assign.
|
|
156
|
+
"""
|
|
157
|
+
params = {}
|
|
158
|
+
if new_name: params["robot_name"] = new_name
|
|
159
|
+
if new_type: params["robot_type"] = new_type
|
|
160
|
+
|
|
161
|
+
if not params:
|
|
162
|
+
raise ValidationError("Must provide at least new_name or new_type")
|
|
163
|
+
|
|
164
|
+
payload = {
|
|
165
|
+
"criteria": {"robot_id": robot_id},
|
|
166
|
+
"params": params
|
|
167
|
+
}
|
|
168
|
+
return self._request("PATCH", "/editrobot", payload)
|
|
169
|
+
|
|
170
|
+
def delete_robot(self, robot_id: str) -> Dict[str, Any]:
|
|
171
|
+
"""
|
|
172
|
+
Permanently delete a robot.
|
|
173
|
+
"""
|
|
174
|
+
payload = {
|
|
175
|
+
"criteria": {"robot_id": robot_id}
|
|
176
|
+
}
|
|
177
|
+
return self._request("DELETE", "/deleterobot", payload)
|
|
178
|
+
|
|
179
|
+
# ==========================
|
|
180
|
+
# MISSION RESOURCES
|
|
181
|
+
# ==========================
|
|
182
|
+
|
|
183
|
+
def create_mission(self, robot_id: str, mission_name: str) -> Dict[str, Any]:
|
|
184
|
+
"""
|
|
185
|
+
Create a new mission for a specific robot.
|
|
186
|
+
"""
|
|
187
|
+
payload = {
|
|
188
|
+
"params": {
|
|
189
|
+
"robot_id": robot_id,
|
|
190
|
+
"mission_name": mission_name
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return self._request("POST", "/createmission", payload)
|
|
194
|
+
|
|
195
|
+
def get_missions(self,
|
|
196
|
+
robot_id: Optional[str] = None,
|
|
197
|
+
mission_id: Optional[str] = None,
|
|
198
|
+
mission_name: Optional[str] = None,
|
|
199
|
+
start_time: Optional[str] = None,
|
|
200
|
+
end_time: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
201
|
+
"""
|
|
202
|
+
Search for missions.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
robot_id (str, optional): Filter by parent robot UUID.
|
|
206
|
+
mission_id (str, optional): Filter by mission UUID.
|
|
207
|
+
start_time (str, optional): Filter missions created after this ISO timestamp.
|
|
208
|
+
end_time (str, optional): Filter missions created before this ISO timestamp.
|
|
209
|
+
"""
|
|
210
|
+
criteria = {}
|
|
211
|
+
if robot_id: criteria["robot_id"] = robot_id
|
|
212
|
+
if mission_id: criteria["mission_id"] = mission_id
|
|
213
|
+
if mission_name: criteria["mission_name"] = mission_name
|
|
214
|
+
if start_time: criteria["start_time"] = start_time
|
|
215
|
+
if end_time: criteria["end_time"] = end_time
|
|
216
|
+
|
|
217
|
+
response = self._request("GET", "/getmission", payload={"criteria": criteria})
|
|
218
|
+
return response.get("mission_list", [])
|
|
219
|
+
|
|
220
|
+
def edit_mission(self, robot_id: str, mission_id: str, new_name: str) -> Dict[str, Any]:
|
|
221
|
+
"""
|
|
222
|
+
Rename a mission.
|
|
223
|
+
|
|
224
|
+
Note: Requires BOTH robot_id and mission_id for security targeting.
|
|
225
|
+
"""
|
|
226
|
+
payload = {
|
|
227
|
+
"criteria": {
|
|
228
|
+
"robot_id": robot_id,
|
|
229
|
+
"mission_id": mission_id
|
|
230
|
+
},
|
|
231
|
+
"params": {
|
|
232
|
+
"mission_name": new_name
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return self._request("PATCH", "/editmission", payload)
|
|
236
|
+
|
|
237
|
+
def delete_mission(self, robot_id: str, mission_id: str) -> Dict[str, Any]:
|
|
238
|
+
"""
|
|
239
|
+
Delete a mission.
|
|
240
|
+
|
|
241
|
+
Note: Requires BOTH robot_id and mission_id.
|
|
242
|
+
"""
|
|
243
|
+
payload = {
|
|
244
|
+
"criteria": {
|
|
245
|
+
"robot_id": robot_id,
|
|
246
|
+
"mission_id": mission_id
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return self._request("DELETE", "/deletemission", payload)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class RideScanError(Exception):
|
|
2
|
+
"""Base exception for all RideScan API errors."""
|
|
3
|
+
def __init__(self, message: str, code: str = None, details: str = None):
|
|
4
|
+
self.code = code
|
|
5
|
+
self.details = details
|
|
6
|
+
super().__init__(f"[{code}] {message}" if code else message)
|
|
7
|
+
|
|
8
|
+
class AuthenticationError(RideScanError):
|
|
9
|
+
"""Raised when the API key is invalid or missing (RS-AUTH)."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
class ValidationError(RideScanError):
|
|
13
|
+
"""Raised when arguments are invalid or missing (RS-VAL)."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
class ResourceNotFoundError(RideScanError):
|
|
17
|
+
"""Raised when a requested resource (Robot/Mission) is not found (RS-ROBOT-002, etc.)."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
class ConflictError(RideScanError):
|
|
21
|
+
"""Raised when creating a duplicate resource (RS-ROBOT-001, RS-MSN-001)."""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
class ServerError(RideScanError):
|
|
25
|
+
"""Raised when the RideScan server encounters an internal error (RS-SYS)."""
|
|
26
|
+
pass
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ridescanapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the RideScan Safety Layer API
|
|
5
|
+
Author-email: RideScan Engineering <support@ridescan.ai>
|
|
6
|
+
Requires-Python: >=3.7
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests>=2.25.0
|
|
9
|
+
|
|
10
|
+
# RideScan Python SDK
|
|
11
|
+
Official client for the RideScan API.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/ridescanapi/__init__.py
|
|
4
|
+
src/ridescanapi/client.py
|
|
5
|
+
src/ridescanapi/exceptions.py
|
|
6
|
+
src/ridescanapi.egg-info/PKG-INFO
|
|
7
|
+
src/ridescanapi.egg-info/SOURCES.txt
|
|
8
|
+
src/ridescanapi.egg-info/dependency_links.txt
|
|
9
|
+
src/ridescanapi.egg-info/requires.txt
|
|
10
|
+
src/ridescanapi.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.25.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ridescanapi
|