robot-wrapper-sdk 0.2.5__tar.gz → 0.2.7__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 (20) hide show
  1. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/PKG-INFO +1 -1
  2. robot_wrapper_sdk-0.2.7/examples/main.py +80 -0
  3. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/pyproject.toml +1 -1
  4. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_sdk/infrastructure/api_client.py +80 -21
  5. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_sdk/infrastructure/robot_api_repository.py +33 -8
  6. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_wrapper_sdk.egg-info/PKG-INFO +1 -1
  7. robot_wrapper_sdk-0.2.5/examples/main.py +0 -42
  8. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/README.md +0 -0
  9. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_sdk/__init__.py +0 -0
  10. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_sdk/application/__init__.py +0 -0
  11. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_sdk/application/usecases.py +0 -0
  12. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_sdk/domain/__init__.py +0 -0
  13. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_sdk/domain/entities.py +0 -0
  14. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_sdk/domain/repositories.py +0 -0
  15. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_sdk/infrastructure/__init__.py +0 -0
  16. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_wrapper_sdk.egg-info/SOURCES.txt +0 -0
  17. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_wrapper_sdk.egg-info/dependency_links.txt +0 -0
  18. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_wrapper_sdk.egg-info/requires.txt +0 -0
  19. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/robot_wrapper_sdk.egg-info/top_level.txt +0 -0
  20. {robot_wrapper_sdk-0.2.5 → robot_wrapper_sdk-0.2.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robot-wrapper-sdk
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Robot Platform API SDK
5
5
  Author-email: GH Robot Platform Team <team@ghrobot.com>
6
6
  License: MIT
@@ -0,0 +1,80 @@
1
+ import asyncio
2
+ import os
3
+ import sys
4
+
5
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
6
+ from robot_sdk import RobotPlatformModule, AsyncRobotPlatformModule
7
+
8
+
9
+ def setup_environ():
10
+ # Update theo develop-app auth flow
11
+ os.environ["ROBOT_PLATFORM_BASE_URL"] = "http://localhost:8085"
12
+ os.environ["ROBOT_PLATFORM_APP_ID"] = "your_app_id"
13
+ os.environ["ROBOT_PLATFORM_APP_SECRET"] = "your_app_secret"
14
+ # os.environ["ROBOT_PLATFORM_PROXY_URL"] = "socks5h://192.168.3.100:1080"
15
+
16
+
17
+ def test_sync():
18
+ print("--- SYNC EXECUTION ---")
19
+ module = RobotPlatformModule()
20
+ try:
21
+ robot_id = "2047631542552334336"
22
+
23
+ robot = module.get_robot(robot_id)
24
+ if robot:
25
+ print("Robot:", robot.username, robot.platform)
26
+ else:
27
+ print("Robot not found")
28
+
29
+ login_tasks = module.acquire_need_login(limit=20, lock_minutes=30)
30
+ print("Acquire login tasks:", len(login_tasks))
31
+
32
+ module.update_auth_status(robot_id, "authorized")
33
+ print("Updated auth_status -> authorized")
34
+
35
+ hardening_tasks = module.acquire_unhardened(limit=20, lock_minutes=30, min_age_days=0)
36
+ print("Acquire hardening tasks:", len(hardening_tasks))
37
+
38
+ module.update_security_hardened(robot_id, True)
39
+ print("Updated security_hardened -> true")
40
+
41
+ except Exception as e:
42
+ print("Sync demo error:", e)
43
+ finally:
44
+ module.close()
45
+
46
+
47
+ async def test_async():
48
+ print("--- ASYNC EXECUTION ---")
49
+ module = AsyncRobotPlatformModule()
50
+ try:
51
+ robot_id = "2047631542552334336"
52
+
53
+ robot = await module.get_robot(robot_id)
54
+ if robot:
55
+ print("Robot:", robot.username, robot.platform)
56
+ else:
57
+ print("Robot not found")
58
+
59
+ login_tasks = await module.acquire_need_login(limit=20, lock_minutes=30)
60
+ print("Acquire login tasks:", len(login_tasks))
61
+
62
+ await module.update_auth_status(robot_id, "authorized")
63
+ print("Updated auth_status -> authorized")
64
+
65
+ hardening_tasks = await module.acquire_unhardened(limit=20, lock_minutes=30, min_age_days=0)
66
+ print("Acquire hardening tasks:", len(hardening_tasks))
67
+
68
+ await module.update_security_hardened(robot_id, True)
69
+ print("Updated security_hardened -> true")
70
+
71
+ except Exception as e:
72
+ print("Async demo error:", e)
73
+ finally:
74
+ await module.close()
75
+
76
+
77
+ if __name__ == "__main__":
78
+ setup_environ()
79
+ test_sync()
80
+ asyncio.run(test_async())
@@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"
8
8
 
9
9
  [project]
10
10
  name = "robot-wrapper-sdk"
11
- version = "0.2.5"
11
+ version = "0.2.7"
12
12
  description = "Robot Platform API SDK"
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.10"
@@ -1,6 +1,20 @@
1
1
  import httpx
2
2
  import os
3
3
  from typing import Dict, Any, Optional
4
+ from httpx import RemoteProtocolError
5
+
6
+ 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
14
+
15
+ access_token = str(access_token).strip() if access_token else None
16
+ refresh_token = str(refresh_token).strip() if refresh_token else None
17
+ return access_token, refresh_token
4
18
 
5
19
  class RobotAuth(httpx.Auth):
6
20
  def __init__(self, login_func, refresh_func):
@@ -9,11 +23,15 @@ class RobotAuth(httpx.Auth):
9
23
 
10
24
  def auth_flow(self, request):
11
25
  token = self.login_func()
26
+ if not token:
27
+ raise RuntimeError("Missing access token. Authenticate first.")
12
28
  request.headers["Authorization"] = f"Bearer {token}"
13
29
  response = yield request
14
30
 
15
31
  if response.status_code == 401:
16
32
  token = self.refresh_func()
33
+ if not token:
34
+ raise RuntimeError("Token refresh failed: empty access token")
17
35
  request.headers["Authorization"] = f"Bearer {token}"
18
36
  yield request
19
37
 
@@ -24,11 +42,15 @@ class AsyncRobotAuth(httpx.Auth):
24
42
 
25
43
  async def async_auth_flow(self, request):
26
44
  token = await self.login_func()
45
+ if not token:
46
+ raise RuntimeError("Missing access token. Authenticate first.")
27
47
  request.headers["Authorization"] = f"Bearer {token}"
28
48
  response = yield request
29
49
 
30
50
  if response.status_code == 401:
31
51
  token = await self.refresh_func()
52
+ if not token:
53
+ raise RuntimeError("Token refresh failed: empty access token")
32
54
  request.headers["Authorization"] = f"Bearer {token}"
33
55
  yield request
34
56
 
@@ -56,7 +78,10 @@ class HTTPClient:
56
78
  def _get_or_login(self) -> str:
57
79
  if not self.access_token:
58
80
  self._login()
59
- return str(self.access_token) if self.access_token else ""
81
+ token = str(self.access_token).strip() if self.access_token else ""
82
+ if not token:
83
+ raise RuntimeError("Empty access token after login")
84
+ return token
60
85
 
61
86
  def _login(self) -> None:
62
87
  with httpx.Client(proxy=self.proxy_url) as c:
@@ -67,8 +92,7 @@ class HTTPClient:
67
92
  resp.raise_for_status()
68
93
  try:
69
94
  data = resp.json()
70
- self.access_token = data.get("access_token")
71
- self.refresh_token = data.get("refresh_token")
95
+ self.access_token, self.refresh_token = _extract_tokens(data)
72
96
  except Exception as e:
73
97
  print(f"❌ Failed to parse JSON from _login: {e}")
74
98
  print(f"📄 Login Response Content: {resp.text[:500]}")
@@ -78,7 +102,10 @@ class HTTPClient:
78
102
  # Developer tokens currently don't use refresh flow in the same way, but keeping structure
79
103
  if not self.refresh_token:
80
104
  self._login()
81
- return str(self.access_token) if self.access_token else ""
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
82
109
 
83
110
  with httpx.Client(proxy=self.proxy_url) as c:
84
111
  resp = c.post(
@@ -89,22 +116,47 @@ class HTTPClient:
89
116
  self._login()
90
117
  else:
91
118
  data = resp.json()
92
- self.access_token = data.get("access_token")
93
- self.refresh_token = data.get("refresh_token")
94
-
95
- return str(self.access_token) if self.access_token else ""
119
+ self.access_token, self.refresh_token = _extract_tokens(data)
120
+
121
+ token = str(self.access_token).strip() if self.access_token else ""
122
+ if not token:
123
+ raise RuntimeError("Token refresh failed: empty access token")
124
+ return token
96
125
 
97
126
  def request(self, method: str, path: str, **kwargs: Any) -> Dict[str, Any]:
98
- # Intelligent path prefixing
99
127
  full_path = path
100
- if not path.startswith("/api/v1/developer"):
101
- # If path doesn't start with /api, add it
128
+ if path.startswith("/api/"):
129
+ full_path = path
130
+ else:
102
131
  if not path.startswith("/"):
103
132
  path = f"/{path}"
104
133
  full_path = f"/api/v1/developer{path}"
105
-
134
+
106
135
  print(f"🚀 Requesting: {self.base_url}{full_path}")
107
- resp = self.client.request(method, full_path, **kwargs)
136
+ last_err: Optional[Exception] = None
137
+ resp = None
138
+ for attempt in range(2):
139
+ try:
140
+ resp = self.client.request(method, full_path, **kwargs)
141
+ break
142
+ except Exception as err:
143
+ msg = str(err)
144
+ is_disconnect = isinstance(err, RemoteProtocolError) or "Server disconnected without sending a response" in msg
145
+ if not is_disconnect or attempt == 1:
146
+ raise
147
+ last_err = err
148
+ self.client.close()
149
+ self.client = httpx.Client(
150
+ base_url=self.base_url,
151
+ auth=RobotAuth(self._get_or_login, self._refresh),
152
+ proxy=self.proxy_url,
153
+ )
154
+
155
+ if resp is None:
156
+ if last_err is not None:
157
+ raise last_err
158
+ raise RuntimeError("Request failed without response")
159
+
108
160
  resp.raise_for_status()
109
161
  try:
110
162
  return dict(resp.json())
@@ -140,7 +192,10 @@ class AsyncHTTPClient:
140
192
  async def _get_or_login(self) -> str:
141
193
  if not self.access_token:
142
194
  await self._login()
143
- return str(self.access_token) if self.access_token else ""
195
+ token = str(self.access_token).strip() if self.access_token else ""
196
+ if not token:
197
+ raise RuntimeError("Empty access token after login")
198
+ return token
144
199
 
145
200
  async def _login(self) -> None:
146
201
  async with httpx.AsyncClient(proxy=self.proxy_url) as c:
@@ -151,8 +206,7 @@ class AsyncHTTPClient:
151
206
  resp.raise_for_status()
152
207
  try:
153
208
  data = resp.json()
154
- self.access_token = data.get("access_token")
155
- self.refresh_token = data.get("refresh_token")
209
+ self.access_token, self.refresh_token = _extract_tokens(data)
156
210
  except Exception as e:
157
211
  print(f"❌ Failed to parse JSON from async _login: {e}")
158
212
  print(f"📄 Login Response Content: {resp.text[:500]}")
@@ -161,7 +215,10 @@ class AsyncHTTPClient:
161
215
  async def _refresh(self) -> str:
162
216
  if not self.refresh_token:
163
217
  await self._login()
164
- return str(self.access_token) if self.access_token else ""
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
165
222
 
166
223
  async with httpx.AsyncClient(proxy=self.proxy_url) as c:
167
224
  resp = await c.post(
@@ -172,10 +229,12 @@ class AsyncHTTPClient:
172
229
  await self._login()
173
230
  else:
174
231
  data = resp.json()
175
- self.access_token = data.get("access_token")
176
- self.refresh_token = data.get("refresh_token")
177
-
178
- return str(self.access_token) if self.access_token else ""
232
+ self.access_token, self.refresh_token = _extract_tokens(data)
233
+
234
+ token = str(self.access_token).strip() if self.access_token else ""
235
+ if not token:
236
+ raise RuntimeError("Token refresh failed: empty access token")
237
+ return token
179
238
 
180
239
  async def request(self, method: str, path: str, **kwargs: Any) -> Dict[str, Any]:
181
240
  # Intelligent path prefixing
@@ -3,6 +3,33 @@ from .api_client import HTTPClient, AsyncHTTPClient
3
3
  from ..domain.repositories import RobotRepository, AsyncRobotRepository
4
4
  from ..domain.entities import Robot, RobotSecrets
5
5
 
6
+
7
+ def _extract_list_payload(resp: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], int]:
8
+ container = resp.get("data") if isinstance(resp, dict) else None
9
+ if isinstance(container, dict):
10
+ items = container.get("data")
11
+ total = container.get("total", 0)
12
+ else:
13
+ items = container
14
+ total = resp.get("total", 0) if isinstance(resp, dict) else 0
15
+
16
+ if not isinstance(items, list):
17
+ return [], int(total) if isinstance(total, int) else 0
18
+
19
+ mapped = [item for item in items if isinstance(item, dict)]
20
+ return mapped, int(total) if isinstance(total, int) else 0
21
+
22
+
23
+ def _extract_items(resp: Dict[str, Any]) -> List[Dict[str, Any]]:
24
+ container = resp.get("data") if isinstance(resp, dict) else None
25
+ if isinstance(container, list):
26
+ return [item for item in container if isinstance(item, dict)]
27
+ if isinstance(container, dict):
28
+ nested = container.get("data")
29
+ if isinstance(nested, list):
30
+ return [item for item in nested if isinstance(item, dict)]
31
+ return []
32
+
6
33
  class RobotAPIRepository(RobotRepository):
7
34
  def __init__(self, client: HTTPClient):
8
35
  self.client = client
@@ -28,8 +55,7 @@ class RobotAPIRepository(RobotRepository):
28
55
  if project_id: params["project_id"] = project_id
29
56
 
30
57
  resp = self.client.request("GET", "/robots", params=params)
31
- data = resp.get("data", [])
32
- total = resp.get("total", 0)
58
+ data, total = _extract_list_payload(resp)
33
59
  return [Robot(**r) for r in data], total
34
60
 
35
61
  def acquire_need_login(self, limit: int = 20, lock_minutes: int = 30) -> List[Robot]:
@@ -38,7 +64,7 @@ class RobotAPIRepository(RobotRepository):
38
64
  "/robots/login/acquire",
39
65
  json={"limit": limit, "lock_minutes": lock_minutes},
40
66
  )
41
- data = resp.get("data", [])
67
+ data = _extract_items(resp)
42
68
  return [Robot(**r) for r in data]
43
69
 
44
70
  def acquire_unhardened(self, limit: int = 20, lock_minutes: int = 30, min_age_days: int = 0) -> List[Robot]:
@@ -47,7 +73,7 @@ class RobotAPIRepository(RobotRepository):
47
73
  "/robots/security-hardened/acquire",
48
74
  json={"limit": limit, "lock_minutes": lock_minutes, "min_age_days": min_age_days},
49
75
  )
50
- data = resp.get("data", [])
76
+ data = _extract_items(resp)
51
77
  return [Robot(**r) for r in data]
52
78
 
53
79
  def update_auth_status(self, robot_id: str, auth_status: str) -> None:
@@ -89,8 +115,7 @@ class AsyncRobotAPIRepository(AsyncRobotRepository):
89
115
  if project_id: params["project_id"] = project_id
90
116
 
91
117
  resp = await self.client.request("GET", "/robots", params=params)
92
- data = resp.get("data", [])
93
- total = resp.get("total", 0)
118
+ data, total = _extract_list_payload(resp)
94
119
  return [Robot(**r) for r in data], total
95
120
 
96
121
  async def acquire_need_login(self, limit: int = 20, lock_minutes: int = 30) -> List[Robot]:
@@ -99,7 +124,7 @@ class AsyncRobotAPIRepository(AsyncRobotRepository):
99
124
  "/robots/login/acquire",
100
125
  json={"limit": limit, "lock_minutes": lock_minutes},
101
126
  )
102
- data = resp.get("data", [])
127
+ data = _extract_items(resp)
103
128
  return [Robot(**r) for r in data]
104
129
 
105
130
  async def acquire_unhardened(self, limit: int = 20, lock_minutes: int = 30, min_age_days: int = 0) -> List[Robot]:
@@ -108,7 +133,7 @@ class AsyncRobotAPIRepository(AsyncRobotRepository):
108
133
  "/robots/security-hardened/acquire",
109
134
  json={"limit": limit, "lock_minutes": lock_minutes, "min_age_days": min_age_days},
110
135
  )
111
- data = resp.get("data", [])
136
+ data = _extract_items(resp)
112
137
  return [Robot(**r) for r in data]
113
138
 
114
139
  async def update_auth_status(self, robot_id: str, auth_status: str) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: robot-wrapper-sdk
3
- Version: 0.2.5
3
+ Version: 0.2.7
4
4
  Summary: Robot Platform API SDK
5
5
  Author-email: GH Robot Platform Team <team@ghrobot.com>
6
6
  License: MIT
@@ -1,42 +0,0 @@
1
- import asyncio
2
- import os
3
- import sys
4
-
5
- # Mở rộng đường dẫn tránh lỗi module khi chạy local
6
- sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
7
- from robot_sdk import RobotPlatformModule, AsyncRobotPlatformModule
8
-
9
- def setup_environ():
10
- os.environ["ROBOT_PLATFORM_BASE_URL"] = "http://localhost:8080"
11
- os.environ["ROBOT_PLATFORM_USERNAME"] = "testuser"
12
- os.environ["ROBOT_PLATFORM_PASSWORD"] = "testpass"
13
-
14
- def test_sync():
15
- """Ví dụ sử dụng thư viện đồng bộ."""
16
- print("--- SYNC EXECUTION ---")
17
- module = RobotPlatformModule()
18
- try:
19
- robot = module.robot_usecase.get_robot_profile("1234")
20
- print("Tải thông tin thành công:", robot.username)
21
- except Exception as e:
22
- print("Lỗi Demo:", e)
23
- finally:
24
- module.close()
25
-
26
- async def test_async():
27
- """Ví dụ sử dụng thư viện bất đồng bộ."""
28
- print("--- ASYNC EXECUTION ---")
29
- module = AsyncRobotPlatformModule()
30
- try:
31
- robot = await module.robot_usecase.get_robot_profile("1234")
32
- print("Tải thông tin thành công:", robot.username)
33
- except Exception as e:
34
- print("Lỗi Demo:", e)
35
- finally:
36
- await module.close()
37
-
38
- if __name__ == "__main__":
39
- setup_environ()
40
-
41
- test_sync()
42
- asyncio.run(test_async())