robot-wrapper-sdk 0.2.9__tar.gz → 0.2.11__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.
Files changed (19) hide show
  1. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/PKG-INFO +1 -1
  2. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/pyproject.toml +1 -1
  3. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_sdk/domain/entities.py +12 -2
  4. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_sdk/infrastructure/api_client.py +81 -54
  5. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_sdk/infrastructure/robot_api_repository.py +25 -10
  6. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_wrapper_sdk.egg-info/PKG-INFO +1 -1
  7. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/README.md +0 -0
  8. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/examples/main.py +0 -0
  9. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_sdk/__init__.py +0 -0
  10. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_sdk/application/__init__.py +0 -0
  11. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_sdk/application/usecases.py +0 -0
  12. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_sdk/domain/__init__.py +0 -0
  13. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_sdk/domain/repositories.py +0 -0
  14. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_sdk/infrastructure/__init__.py +0 -0
  15. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_wrapper_sdk.egg-info/SOURCES.txt +0 -0
  16. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_wrapper_sdk.egg-info/dependency_links.txt +0 -0
  17. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_wrapper_sdk.egg-info/requires.txt +0 -0
  18. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/robot_wrapper_sdk.egg-info/top_level.txt +0 -0
  19. {robot_wrapper_sdk-0.2.9 → robot_wrapper_sdk-0.2.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robot-wrapper-sdk
3
- Version: 0.2.9
3
+ Version: 0.2.11
4
4
  Summary: Robot Platform API SDK
5
5
  Author-email: GH Robot Platform Team <team@ghrobot.com>
6
6
  License: MIT
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
8
8
 
9
9
  [project]
10
10
  name = "robot-wrapper-sdk"
11
- version = "0.2.9"
11
+ version = "0.2.11"
12
12
  description = "Robot Platform API SDK"
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.10"
@@ -32,13 +32,15 @@ def normalize_robot_auth_status(status: str | RobotAuthStatus) -> str:
32
32
  def ensure_valid_robot_status(status: str) -> None:
33
33
  allowed = {s.value for s in RobotStatus}
34
34
  if status not in allowed:
35
- raise ValueError(f"invalid status: {status}. allowed={sorted(allowed)}")
35
+ raise ValueError(
36
+ f"invalid status: {status}. allowed={sorted(allowed)}")
36
37
 
37
38
 
38
39
  def ensure_valid_robot_auth_status(status: str) -> None:
39
40
  allowed = {s.value for s in RobotAuthStatus}
40
41
  if status not in allowed:
41
- raise ValueError(f"invalid auth_status: {status}. allowed={sorted(allowed)}")
42
+ raise ValueError(
43
+ f"invalid auth_status: {status}. allowed={sorted(allowed)}")
42
44
 
43
45
 
44
46
  def ensure_valid_worker_robot_auth_status(status: str) -> None:
@@ -70,16 +72,19 @@ class Robot:
70
72
  username: str
71
73
  platform: str
72
74
  status: str
75
+ auth_status: str
73
76
  metadata: Dict[str, Any] = field(default_factory=dict)
74
77
  project_id: Optional[str] = None
75
78
  primary_environment: Optional[str] = None
76
79
  created_at: Optional[str] = None
77
80
 
81
+
78
82
  @dataclass
79
83
  class RobotSecrets:
80
84
  password: str
81
85
  two_fa_secret: str
82
86
 
87
+
83
88
  @dataclass
84
89
  class RobotActivity:
85
90
  id: str
@@ -87,25 +92,30 @@ class RobotActivity:
87
92
  status: str
88
93
  message: str
89
94
 
95
+
90
96
  @dataclass
91
97
  class AcquireNeedLoginRequest:
92
98
  limit: int = 20
93
99
  lock_minutes: int = 30
94
100
 
101
+
95
102
  @dataclass
96
103
  class AcquireUnhardenedRequest:
97
104
  limit: int = 20
98
105
  lock_minutes: int = 30
99
106
  min_age_days: int = 0
100
107
 
108
+
101
109
  @dataclass
102
110
  class UpdateAuthStatusRequest:
103
111
  auth_status: str
104
112
 
113
+
105
114
  @dataclass
106
115
  class UpdateSecurityHardenedRequest:
107
116
  security_hardened: bool
108
117
 
118
+
109
119
  @dataclass
110
120
  class StandardResponse:
111
121
  status: str
@@ -3,14 +3,71 @@ import os
3
3
  from typing import Dict, Any, Optional
4
4
  from httpx import RemoteProtocolError
5
5
 
6
+
7
+ def _build_httpx_client(base_url: str, auth: httpx.Auth, proxy_url: Optional[str]) -> httpx.Client:
8
+ kwargs: Dict[str, Any] = {"base_url": base_url, "auth": auth}
9
+ if proxy_url:
10
+ kwargs["proxy"] = proxy_url
11
+ try:
12
+ return httpx.Client(**kwargs)
13
+ except TypeError:
14
+ if "proxy" in kwargs:
15
+ kwargs["proxies"] = kwargs.pop("proxy")
16
+ return httpx.Client(**kwargs)
17
+
18
+
19
+ def _build_async_httpx_client(base_url: str, auth: httpx.Auth, proxy_url: Optional[str]) -> httpx.AsyncClient:
20
+ kwargs: Dict[str, Any] = {"base_url": base_url, "auth": auth}
21
+ if proxy_url:
22
+ kwargs["proxy"] = proxy_url
23
+ try:
24
+ return httpx.AsyncClient(**kwargs)
25
+ except TypeError:
26
+ if "proxy" in kwargs:
27
+ kwargs["proxies"] = kwargs.pop("proxy")
28
+ return httpx.AsyncClient(**kwargs)
29
+
30
+
31
+ def _build_plain_httpx_client(proxy_url: Optional[str]) -> httpx.Client:
32
+ kwargs: Dict[str, Any] = {}
33
+ if proxy_url:
34
+ kwargs["proxy"] = proxy_url
35
+ try:
36
+ return httpx.Client(**kwargs)
37
+ except TypeError:
38
+ if "proxy" in kwargs:
39
+ kwargs["proxies"] = kwargs.pop("proxy")
40
+ return httpx.Client(**kwargs)
41
+
42
+
43
+ def _build_plain_async_httpx_client(proxy_url: Optional[str]) -> httpx.AsyncClient:
44
+ kwargs: Dict[str, Any] = {}
45
+ if proxy_url:
46
+ kwargs["proxy"] = proxy_url
47
+ try:
48
+ return httpx.AsyncClient(**kwargs)
49
+ except TypeError:
50
+ if "proxy" in kwargs:
51
+ kwargs["proxies"] = kwargs.pop("proxy")
52
+ return httpx.AsyncClient(**kwargs)
53
+
6
54
  def _extract_tokens(payload: Dict[str, Any]) -> tuple[Optional[str], Optional[str]]:
7
- data = payload.get("data") if isinstance(payload, dict) else None
8
- if isinstance(data, dict):
9
- access_token = data.get("access_token")
10
- refresh_token = data.get("refresh_token")
11
- else:
12
- access_token = payload.get("access_token") if isinstance(payload, dict) else None
13
- refresh_token = payload.get("refresh_token") if isinstance(payload, dict) else None
55
+ container = payload.get("data") if isinstance(payload, dict) and isinstance(payload.get("data"), dict) else payload
56
+ if not isinstance(container, dict):
57
+ return None, None
58
+
59
+ access_token = (
60
+ container.get("access_token")
61
+ or container.get("accessToken")
62
+ or container.get("AccessToken")
63
+ or payload.get("access_token") if isinstance(payload, dict) else None
64
+ )
65
+ refresh_token = (
66
+ container.get("refresh_token")
67
+ or container.get("refreshToken")
68
+ or container.get("RefreshToken")
69
+ or payload.get("refresh_token") if isinstance(payload, dict) else None
70
+ )
14
71
 
15
72
  access_token = str(access_token).strip() if access_token else None
16
73
  refresh_token = str(refresh_token).strip() if refresh_token else None
@@ -69,10 +126,10 @@ class HTTPClient:
69
126
  self.access_token: Optional[str] = None
70
127
  self.refresh_token: Optional[str] = None
71
128
 
72
- self.client = httpx.Client(
129
+ self.client = _build_httpx_client(
73
130
  base_url=self.base_url,
74
131
  auth=RobotAuth(self._get_or_login, self._refresh),
75
- proxy=self.proxy_url
132
+ proxy_url=self.proxy_url,
76
133
  )
77
134
 
78
135
  def _get_or_login(self) -> str:
@@ -84,7 +141,7 @@ class HTTPClient:
84
141
  return token
85
142
 
86
143
  def _login(self) -> None:
87
- with httpx.Client(proxy=self.proxy_url) as c:
144
+ with _build_plain_httpx_client(self.proxy_url) as c:
88
145
  resp = c.post(
89
146
  f"{self.base_url}/api/v1/developer/auth/token",
90
147
  json={"app_id": self.app_id, "app_secret": self.app_secret}
@@ -99,28 +156,10 @@ class HTTPClient:
99
156
  raise
100
157
 
101
158
  def _refresh(self) -> str:
102
- # Developer tokens currently don't use refresh flow in the same way, but keeping structure
103
- if not self.refresh_token:
104
- self._login()
105
- token = str(self.access_token).strip() if self.access_token else ""
106
- if not token:
107
- raise RuntimeError("Empty access token after login")
108
- return token
109
-
110
- with httpx.Client(proxy=self.proxy_url) as c:
111
- resp = c.post(
112
- f"{self.base_url}/api/v1/developer/auth/refresh",
113
- json={"refresh_token": self.refresh_token}
114
- )
115
- if resp.status_code >= 400:
116
- self._login()
117
- else:
118
- data = resp.json()
119
- self.access_token, self.refresh_token = _extract_tokens(data)
120
-
159
+ self._login()
121
160
  token = str(self.access_token).strip() if self.access_token else ""
122
161
  if not token:
123
- raise RuntimeError("Token refresh failed: empty access token")
162
+ raise RuntimeError("Empty access token after login")
124
163
  return token
125
164
 
126
165
  def request(self, method: str, path: str, **kwargs: Any) -> Dict[str, Any]:
@@ -132,6 +171,11 @@ class HTTPClient:
132
171
  path = f"/{path}"
133
172
  full_path = f"/api/v1/developer{path}"
134
173
 
174
+ headers = dict(kwargs.pop("headers", {}) or {})
175
+ if "/auth/" not in full_path and "Authorization" not in headers:
176
+ headers["Authorization"] = f"Bearer {self._get_or_login()}"
177
+ kwargs["headers"] = headers
178
+
135
179
  print(f"🚀 Requesting: {self.base_url}{full_path}")
136
180
  last_err: Optional[Exception] = None
137
181
  resp = None
@@ -146,10 +190,10 @@ class HTTPClient:
146
190
  raise
147
191
  last_err = err
148
192
  self.client.close()
149
- self.client = httpx.Client(
193
+ self.client = _build_httpx_client(
150
194
  base_url=self.base_url,
151
195
  auth=RobotAuth(self._get_or_login, self._refresh),
152
- proxy=self.proxy_url,
196
+ proxy_url=self.proxy_url,
153
197
  )
154
198
 
155
199
  if resp is None:
@@ -183,10 +227,10 @@ class AsyncHTTPClient:
183
227
  self.access_token: Optional[str] = None
184
228
  self.refresh_token: Optional[str] = None
185
229
 
186
- self.client = httpx.AsyncClient(
230
+ self.client = _build_async_httpx_client(
187
231
  base_url=self.base_url,
188
232
  auth=AsyncRobotAuth(self._get_or_login, self._refresh),
189
- proxy=self.proxy_url
233
+ proxy_url=self.proxy_url,
190
234
  )
191
235
 
192
236
  async def _get_or_login(self) -> str:
@@ -198,7 +242,7 @@ class AsyncHTTPClient:
198
242
  return token
199
243
 
200
244
  async def _login(self) -> None:
201
- async with httpx.AsyncClient(proxy=self.proxy_url) as c:
245
+ async with _build_plain_async_httpx_client(self.proxy_url) as c:
202
246
  resp = await c.post(
203
247
  f"{self.base_url}/api/v1/developer/auth/token",
204
248
  json={"app_id": self.app_id, "app_secret": self.app_secret}
@@ -213,27 +257,10 @@ class AsyncHTTPClient:
213
257
  raise
214
258
 
215
259
  async def _refresh(self) -> str:
216
- if not self.refresh_token:
217
- await self._login()
218
- token = str(self.access_token).strip() if self.access_token else ""
219
- if not token:
220
- raise RuntimeError("Empty access token after login")
221
- return token
222
-
223
- async with httpx.AsyncClient(proxy=self.proxy_url) as c:
224
- resp = await c.post(
225
- f"{self.base_url}/api/v1/developer/auth/refresh",
226
- json={"refresh_token": self.refresh_token}
227
- )
228
- if resp.status_code >= 400:
229
- await self._login()
230
- else:
231
- data = resp.json()
232
- self.access_token, self.refresh_token = _extract_tokens(data)
233
-
260
+ await self._login()
234
261
  token = str(self.access_token).strip() if self.access_token else ""
235
262
  if not token:
236
- raise RuntimeError("Token refresh failed: empty access token")
263
+ raise RuntimeError("Empty access token after login")
237
264
  return token
238
265
 
239
266
  async def request(self, method: str, path: str, **kwargs: Any) -> Dict[str, Any]:
@@ -39,13 +39,26 @@ def _extract_items(resp: Dict[str, Any]) -> List[Dict[str, Any]]:
39
39
  return [item for item in nested if isinstance(item, dict)]
40
40
  return []
41
41
 
42
+
43
+ def _to_robot_payload(item: Dict[str, Any]) -> Dict[str, Any]:
44
+ payload = dict(item)
45
+ payload.setdefault("auth_status", "")
46
+ payload.setdefault("metadata", {})
47
+ return payload
48
+
49
+
50
+ def _to_robots(items: List[Dict[str, Any]]) -> List[Robot]:
51
+ return [Robot(**_to_robot_payload(item)) for item in items]
52
+
42
53
  class RobotAPIRepository(RobotRepository):
43
54
  def __init__(self, client: HTTPClient):
44
55
  self.client = client
45
56
 
46
57
  def find_by_id(self, robot_id: str) -> Optional[Robot]:
47
58
  resp = self.client.request("GET", f"/robots/{robot_id}")
48
- return Robot(**resp) if resp else None
59
+ payload = resp.get("data") if isinstance(resp, dict) else None
60
+ data = payload if isinstance(payload, dict) else resp
61
+ return Robot(**_to_robot_payload(data)) if isinstance(data, dict) else None
49
62
 
50
63
  def update_status(self, robot_id: str, status: str | RobotStatus) -> None:
51
64
  normalized_status = normalize_robot_status(status)
@@ -67,7 +80,7 @@ class RobotAPIRepository(RobotRepository):
67
80
 
68
81
  resp = self.client.request("GET", "/robots", params=params)
69
82
  data, total = _extract_list_payload(resp)
70
- return [Robot(**r) for r in data], total
83
+ return _to_robots(data), total
71
84
 
72
85
  def acquire_need_login(self, limit: int = 20, lock_minutes: int = 30) -> List[Robot]:
73
86
  resp = self.client.request(
@@ -76,7 +89,7 @@ class RobotAPIRepository(RobotRepository):
76
89
  json={"limit": limit, "lock_minutes": lock_minutes},
77
90
  )
78
91
  data = _extract_items(resp)
79
- return [Robot(**r) for r in data]
92
+ return _to_robots(data)
80
93
 
81
94
  def acquire_unhardened(self, limit: int = 20, lock_minutes: int = 30, min_age_days: int = 0) -> List[Robot]:
82
95
  resp = self.client.request(
@@ -85,7 +98,7 @@ class RobotAPIRepository(RobotRepository):
85
98
  json={"limit": limit, "lock_minutes": lock_minutes, "min_age_days": min_age_days},
86
99
  )
87
100
  data = _extract_items(resp)
88
- return [Robot(**r) for r in data]
101
+ return _to_robots(data)
89
102
 
90
103
  def update_auth_status(self, robot_id: str, auth_status: str | RobotAuthStatus) -> None:
91
104
  normalized_auth_status = normalize_robot_auth_status(auth_status)
@@ -106,7 +119,7 @@ class RobotAPIRepository(RobotRepository):
106
119
  def update_metadata(self, robot_id: str, metadata: Dict[str, Any]) -> None:
107
120
  self.client.request(
108
121
  "PATCH",
109
- f"/robots/{robot_id}",
122
+ f"/robots/{robot_id}/metadata",
110
123
  json={"metadata": metadata},
111
124
  )
112
125
 
@@ -116,7 +129,9 @@ class AsyncRobotAPIRepository(AsyncRobotRepository):
116
129
 
117
130
  async def find_by_id(self, robot_id: str) -> Optional[Robot]:
118
131
  resp = await self.client.request("GET", f"/robots/{robot_id}")
119
- return Robot(**resp) if resp else None
132
+ payload = resp.get("data") if isinstance(resp, dict) else None
133
+ data = payload if isinstance(payload, dict) else resp
134
+ return Robot(**_to_robot_payload(data)) if isinstance(data, dict) else None
120
135
 
121
136
  async def update_status(self, robot_id: str, status: str | RobotStatus) -> None:
122
137
  normalized_status = normalize_robot_status(status)
@@ -138,7 +153,7 @@ class AsyncRobotAPIRepository(AsyncRobotRepository):
138
153
 
139
154
  resp = await self.client.request("GET", "/robots", params=params)
140
155
  data, total = _extract_list_payload(resp)
141
- return [Robot(**r) for r in data], total
156
+ return _to_robots(data), total
142
157
 
143
158
  async def acquire_need_login(self, limit: int = 20, lock_minutes: int = 30) -> List[Robot]:
144
159
  resp = await self.client.request(
@@ -147,7 +162,7 @@ class AsyncRobotAPIRepository(AsyncRobotRepository):
147
162
  json={"limit": limit, "lock_minutes": lock_minutes},
148
163
  )
149
164
  data = _extract_items(resp)
150
- return [Robot(**r) for r in data]
165
+ return _to_robots(data)
151
166
 
152
167
  async def acquire_unhardened(self, limit: int = 20, lock_minutes: int = 30, min_age_days: int = 0) -> List[Robot]:
153
168
  resp = await self.client.request(
@@ -156,7 +171,7 @@ class AsyncRobotAPIRepository(AsyncRobotRepository):
156
171
  json={"limit": limit, "lock_minutes": lock_minutes, "min_age_days": min_age_days},
157
172
  )
158
173
  data = _extract_items(resp)
159
- return [Robot(**r) for r in data]
174
+ return _to_robots(data)
160
175
 
161
176
  async def update_auth_status(self, robot_id: str, auth_status: str | RobotAuthStatus) -> None:
162
177
  normalized_auth_status = normalize_robot_auth_status(auth_status)
@@ -177,6 +192,6 @@ class AsyncRobotAPIRepository(AsyncRobotRepository):
177
192
  async def update_metadata(self, robot_id: str, metadata: Dict[str, Any]) -> None:
178
193
  await self.client.request(
179
194
  "PATCH",
180
- f"/robots/{robot_id}",
195
+ f"/robots/{robot_id}/metadata",
181
196
  json={"metadata": metadata},
182
197
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robot-wrapper-sdk
3
- Version: 0.2.9
3
+ Version: 0.2.11
4
4
  Summary: Robot Platform API SDK
5
5
  Author-email: GH Robot Platform Team <team@ghrobot.com>
6
6
  License: MIT