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.
@@ -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,2 @@
1
+ # RideScan Python SDK
2
+ 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ requests>=2.25.0
@@ -0,0 +1 @@
1
+ ridescanapi