replx-agent 2.0.0__py3-none-any.whl

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.

Potentially problematic release.


This version of replx-agent might be problematic. Click here for more details.

Files changed (46) hide show
  1. replx_agent/__init__.py +3 -0
  2. replx_agent/__main__.py +5 -0
  3. replx_agent/commands.py +131 -0
  4. replx_agent/helpers/__init__.py +3 -0
  5. replx_agent/helpers/scanner.py +308 -0
  6. replx_agent/pkg/__init__.py +11 -0
  7. replx_agent/pkg/compiler.py +153 -0
  8. replx_agent/pkg/registry.py +387 -0
  9. replx_agent/pkg/service.py +690 -0
  10. replx_agent/pkg/store.py +83 -0
  11. replx_agent/protocol/__init__.py +11 -0
  12. replx_agent/protocol/repl.py +1029 -0
  13. replx_agent/protocol/storage.py +1018 -0
  14. replx_agent/rpc/__init__.py +3 -0
  15. replx_agent/rpc/protocol.py +220 -0
  16. replx_agent/server/__init__.py +3 -0
  17. replx_agent/server/__main__.py +4 -0
  18. replx_agent/server/command_dispatcher.py +49 -0
  19. replx_agent/server/connection_manager.py +654 -0
  20. replx_agent/server/core.py +1221 -0
  21. replx_agent/server/handlers/__init__.py +24 -0
  22. replx_agent/server/handlers/exec.py +553 -0
  23. replx_agent/server/handlers/filesystem.py +383 -0
  24. replx_agent/server/handlers/firmware.py +118 -0
  25. replx_agent/server/handlers/i2c.py +23 -0
  26. replx_agent/server/handlers/package.py +182 -0
  27. replx_agent/server/handlers/repl.py +188 -0
  28. replx_agent/server/handlers/session.py +205 -0
  29. replx_agent/server/handlers/spi.py +23 -0
  30. replx_agent/server/handlers/transfer.py +600 -0
  31. replx_agent/server/handlers/uart.py +32 -0
  32. replx_agent/server/session_manager.py +391 -0
  33. replx_agent/server/tcp.py +207 -0
  34. replx_agent/terminal.py +566 -0
  35. replx_agent/transport/__init__.py +32 -0
  36. replx_agent/transport/serial.py +220 -0
  37. replx_agent/utils/__init__.py +95 -0
  38. replx_agent/utils/constants.py +78 -0
  39. replx_agent/utils/device_info.py +183 -0
  40. replx_agent/utils/exceptions.py +31 -0
  41. replx_agent-2.0.0.dist-info/METADATA +74 -0
  42. replx_agent-2.0.0.dist-info/RECORD +46 -0
  43. replx_agent-2.0.0.dist-info/WHEEL +5 -0
  44. replx_agent-2.0.0.dist-info/entry_points.txt +2 -0
  45. replx_agent-2.0.0.dist-info/licenses/LICENSE +21 -0
  46. replx_agent-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,387 @@
1
+ import os
2
+ import urllib.request
3
+ from typing import Optional, Tuple
4
+
5
+ from replx_agent.utils import device_name_to_path
6
+ from replx_agent.utils.constants import HTTP_REQUEST_TIMEOUT
7
+ from replx_agent.utils.exceptions import PkgError
8
+ from .store import StoreManager
9
+
10
+
11
+ class InstallHelper:
12
+
13
+ @staticmethod
14
+ def resolve_spec(spec: str) -> Tuple[str, str]:
15
+ if spec in ("core/", "device/", "core", "device"):
16
+ raise PkgError(
17
+ f"Invalid spec: {spec} (deprecated; use core.all/device.all/core.<file>/device.<file>)"
18
+ )
19
+
20
+ if spec == "core.all":
21
+ return "core", ""
22
+ if spec == "device.all":
23
+ return "device", ""
24
+
25
+ if spec.startswith("core."):
26
+ rest = spec[len("core."):]
27
+ if not rest:
28
+ raise PkgError("Invalid spec: core.<file> requires a filename")
29
+ if rest.count(".") > 1:
30
+ raise PkgError(f"Invalid spec: {spec} (nested dotted filename is not supported)")
31
+ return "core", rest
32
+
33
+ if spec.startswith("device."):
34
+ rest = spec[len("device."):]
35
+ if not rest:
36
+ raise PkgError("Invalid spec: device.<file> requires a filename")
37
+ if rest.count(".") > 1:
38
+ raise PkgError(f"Invalid spec: {spec} (nested dotted filename is not supported)")
39
+ return "device", rest
40
+
41
+ raise PkgError(
42
+ f"Invalid spec: {spec} (expect core.all/device.all/core.<file>/device.<file>)"
43
+ )
44
+
45
+ @staticmethod
46
+ def download_raw_file(owner: str, repo: str, ref_: str, path: str, out_path: str) -> str:
47
+ url = f"https://raw.githubusercontent.com/{owner}/{repo}/{ref_}/{path}"
48
+ req = urllib.request.Request(url, headers=StoreManager.gh_headers())
49
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
50
+ with urllib.request.urlopen(req, timeout=HTTP_REQUEST_TIMEOUT) as r, open(out_path, "wb") as f:
51
+ f.write(r.read())
52
+ return out_path
53
+
54
+ @staticmethod
55
+ def remote_dir_for(scope: str, rel_dir: str, device: str) -> str:
56
+ if scope == "core":
57
+ return "lib/" + (rel_dir + "/" if rel_dir else "")
58
+ return f"lib/{device_name_to_path(device)}/" + (rel_dir + "/" if rel_dir else "")
59
+
60
+ @staticmethod
61
+ def list_local_py_targets(scope: str, rest: str, core: str, device: str) -> Tuple[str, list]:
62
+ if scope == "core":
63
+ base = os.path.join(StoreManager.pkg_root(), "core", core, "src")
64
+ else:
65
+ base = os.path.join(StoreManager.pkg_root(), "device", device_name_to_path(device), "src")
66
+
67
+ target_path = os.path.join(base, rest)
68
+ if os.path.isfile(target_path):
69
+ rel = os.path.relpath(target_path, base).replace("\\", "/")
70
+ return base, [(target_path, rel)]
71
+
72
+ if os.path.isdir(target_path):
73
+ out = []
74
+ for dp, _, fns in os.walk(target_path):
75
+ for fn in fns:
76
+ ap = os.path.join(dp, fn)
77
+ rel = os.path.relpath(ap, base).replace("\\", "/")
78
+ out.append((ap, rel))
79
+ return base, out
80
+ return base, []
81
+
82
+ @staticmethod
83
+ def local_store_ready_for_full_install(core: str, device: str) -> Tuple[bool, str]:
84
+ StoreManager.ensure_home_store()
85
+ meta_path = StoreManager.local_meta_path()
86
+ if not os.path.isfile(meta_path):
87
+ return False, "meta-missing"
88
+
89
+ try:
90
+ _ = StoreManager.load_local_meta()
91
+ except Exception:
92
+ return False, "meta-broken"
93
+
94
+ req_dirs = [
95
+ os.path.join(StoreManager.pkg_root(), "core", core, "src"),
96
+ os.path.join(StoreManager.pkg_root(), "core", core, "typehints"),
97
+ os.path.join(StoreManager.pkg_root(), "device", device_name_to_path(device), "src"),
98
+ os.path.join(StoreManager.pkg_root(), "device", device_name_to_path(device), "typehints"),
99
+ ]
100
+ missing = [p for p in req_dirs if not os.path.isdir(p)]
101
+ if missing:
102
+ return False, "dirs-missing"
103
+
104
+ return True, "ok"
105
+
106
+
107
+ class SearchHelper:
108
+
109
+ @staticmethod
110
+ def fmt_ver_with_star(remote_ver: float, local_ver: float, missing_local: bool) -> str:
111
+ star = "*" if missing_local or (remote_ver > (local_ver or 0.0)) else ""
112
+ return f"{remote_ver:.1f}{star}"
113
+
114
+ @staticmethod
115
+ def key_ci(items: set | dict, name: str) -> Optional[str]:
116
+ if not name:
117
+ return None
118
+
119
+ if isinstance(items, set):
120
+ if name in items:
121
+ return name
122
+ n = name.lower()
123
+ for k in items:
124
+ if isinstance(k, str) and k.lower() == n:
125
+ return k
126
+ return None
127
+
128
+ if isinstance(items, dict):
129
+ if name in items:
130
+ return name
131
+ n = name.lower()
132
+ for k in items.keys():
133
+ if isinstance(k, str) and k.lower() == n:
134
+ return k
135
+ return None
136
+
137
+ return None
138
+
139
+
140
+ class RegistryHelper:
141
+
142
+ @staticmethod
143
+ def root_sections(reg: dict):
144
+ platform_cores = reg.get("platform_cores", {})
145
+ device_configs = reg.get("device_configs", {})
146
+
147
+ cores = set(platform_cores.keys()) if platform_cores else set()
148
+ devices = set(device_configs.keys()) if device_configs else set()
149
+
150
+ return cores, devices
151
+
152
+ @staticmethod
153
+ def get_packages_for_platform(reg: dict, platform: str) -> list[str]:
154
+ platform_cores = reg.get("platform_cores", {})
155
+ core_info = platform_cores.get(platform, {})
156
+ return core_info.get("includes", [])
157
+
158
+ @staticmethod
159
+ def get_packages_for_device(reg: dict, device: str) -> list[str]:
160
+ device_configs = reg.get("device_configs", {})
161
+ device_info = device_configs.get(device, {})
162
+ return device_info.get("includes", [])
163
+
164
+ @staticmethod
165
+ def match_pattern(pkg_name: str, pattern: str) -> bool:
166
+ if "*" in pattern:
167
+ # Wildcard pattern
168
+ prefix = pattern.rstrip("/*")
169
+ return pkg_name.startswith(prefix)
170
+ else:
171
+ pkg_base = pkg_name
172
+ for ext in (".py", ".pyi", ".json"):
173
+ if pkg_base.endswith(ext):
174
+ pkg_base = pkg_base[:-len(ext)]
175
+ break
176
+ return pkg_base == pattern or pkg_name == pattern
177
+
178
+ @staticmethod
179
+ def get_packages_matching(reg: dict, patterns: list[str], package_type: str = None) -> dict:
180
+ packages = reg.get("packages", {})
181
+ result = {}
182
+
183
+ for pkg_name, pkg_meta in packages.items():
184
+ if package_type:
185
+ pkg_type = pkg_meta.get("type", "")
186
+ if package_type == "core" and pkg_type != "core":
187
+ continue
188
+ elif package_type == "device" and not pkg_type.startswith("device-"):
189
+ continue
190
+
191
+ source = pkg_meta.get("source", "")
192
+ for pattern in patterns:
193
+ if RegistryHelper.match_pattern(source, pattern):
194
+ result[pkg_name] = pkg_meta
195
+ break
196
+
197
+ return result
198
+
199
+ @staticmethod
200
+ def get_version(pkg_meta: dict) -> float:
201
+ v = pkg_meta.get("version", "0.0")
202
+ try:
203
+ if isinstance(v, str) and "." in v:
204
+ parts = v.split(".")
205
+ major = int(parts[0]) if len(parts) > 0 and parts[0].isdigit() else 0
206
+ minor = int(parts[1]) if len(parts) > 1 and parts[1].isdigit() else 0
207
+ return float(f"{major}.{minor}")
208
+ return float(v)
209
+ except Exception:
210
+ return 0.0
211
+
212
+ @staticmethod
213
+ def effective_version(reg: dict, scope: str, target: str, part: str, relpath: str) -> float:
214
+ packages = reg.get("packages", {})
215
+
216
+ if scope == "core":
217
+ source_path = f"core/{target}/{part}/{relpath}"
218
+ else:
219
+ source_path = f"device/{target}/{part}/{relpath}"
220
+
221
+ for pkg_name, pkg_meta in packages.items():
222
+ pkg_source = pkg_meta.get("source", "")
223
+ if pkg_source == source_path:
224
+ return RegistryHelper.get_version(pkg_meta)
225
+
226
+ return 0.0
227
+
228
+ @staticmethod
229
+ def walk_files_for_core(reg: dict, core_name: str, part: str = "src"):
230
+ packages = reg.get("packages", {})
231
+ patterns = RegistryHelper.get_packages_for_platform(reg, core_name)
232
+
233
+ for pkg_name, pkg_meta in packages.items():
234
+ if pkg_meta.get("type") != "core":
235
+ continue
236
+
237
+ source = pkg_meta.get("source", "")
238
+
239
+ match_found = False
240
+ for pattern in patterns:
241
+ if "*" in pattern:
242
+ pattern_prefix = pattern.rstrip("/*")
243
+ if f"/{pattern_prefix}/" in source or source.endswith(f"/{pattern_prefix}"):
244
+ match_found = True
245
+ break
246
+ else:
247
+ if RegistryHelper.match_pattern(source, pattern):
248
+ match_found = True
249
+ break
250
+
251
+ if not match_found:
252
+ continue
253
+
254
+ if part == "src":
255
+ file_path = source
256
+ elif part == "typehints":
257
+ file_path = pkg_meta.get("typehint", "")
258
+ if not file_path:
259
+ continue
260
+ else:
261
+ continue
262
+
263
+ path_parts = file_path.split("/")
264
+ if len(path_parts) >= 3 and path_parts[2] == part:
265
+ relpath = "/".join(path_parts[3:])
266
+ yield (relpath, pkg_meta)
267
+
268
+ @staticmethod
269
+ def walk_files_for_device(reg: dict, device_name: str, part: str = "src", include_submodules: bool = False):
270
+ packages = reg.get("packages", {})
271
+ patterns = RegistryHelper.get_packages_for_device(reg, device_name)
272
+
273
+ for pkg_name, pkg_meta in packages.items():
274
+ variants = pkg_meta.get("variants", {})
275
+ for variant_name, variant_meta in variants.items():
276
+ variant_deploy = variant_meta.get("deploy_path", "")
277
+
278
+ match_found = False
279
+ for pattern in patterns:
280
+ module_name = variant_deploy.replace("/__init__.py", "").replace(".py", "")
281
+ variant_full_path = f"{variant_name}/{module_name}"
282
+
283
+ if RegistryHelper.match_pattern(variant_full_path, pattern):
284
+ match_found = True
285
+ break
286
+ if "/" in pattern and variant_full_path.startswith(pattern + "/"):
287
+ match_found = True
288
+ break
289
+
290
+ if not match_found:
291
+ continue
292
+
293
+ if part == "src":
294
+ file_path = variant_meta.get("source", "")
295
+ elif part == "typehints":
296
+ file_path = variant_meta.get("typehint", "")
297
+ if not file_path or not file_path.endswith(".pyi"):
298
+ continue
299
+ else:
300
+ continue
301
+
302
+ if not file_path:
303
+ continue
304
+
305
+ if part == "typehints":
306
+ relpath = variant_deploy.replace(".py", ".pyi")
307
+ else:
308
+ relpath = variant_deploy
309
+ merged_meta = pkg_meta.copy()
310
+ merged_meta.update(variant_meta)
311
+ yield (relpath, merged_meta)
312
+
313
+ pkg_type = pkg_meta.get("type", "")
314
+ if not pkg_type.startswith("device-"):
315
+ continue
316
+
317
+ source = pkg_meta.get("source", "")
318
+ deploy_path = pkg_meta.get("deploy_path", "")
319
+
320
+ match_found = False
321
+ for pattern in patterns:
322
+ if "@" in pkg_name:
323
+ parts = pkg_name.split("@")
324
+ module_name = parts[0]
325
+ device_prefix = parts[1]
326
+ variant_full_path = f"{device_prefix}/{module_name}"
327
+ if RegistryHelper.match_pattern(variant_full_path, pattern):
328
+ match_found = True
329
+ break
330
+
331
+ if source.startswith("device/"):
332
+ source_parts = source.split("/")
333
+ if len(source_parts) >= 4:
334
+ category = source_parts[1]
335
+ file_parts = source_parts[3:]
336
+
337
+ file_path = "/".join(file_parts)
338
+ if file_path.endswith("/__init__.py"):
339
+ file_path = file_path[:-12]
340
+ elif file_path.endswith(".py") or file_path.endswith(".pyi"):
341
+ file_path = file_path[:-3]
342
+
343
+ source_pattern_path = f"{category}/{file_path}"
344
+
345
+ if RegistryHelper.match_pattern(source_pattern_path, pattern):
346
+ match_found = True
347
+ break
348
+
349
+ if RegistryHelper.match_pattern(deploy_path, pattern):
350
+ match_found = True
351
+ break
352
+ if "/" in pattern and deploy_path.startswith(pattern + "/"):
353
+ match_found = True
354
+ break
355
+
356
+ if not match_found:
357
+ continue
358
+
359
+ if part == "src":
360
+ file_path = source
361
+ elif part == "typehints":
362
+ file_path = pkg_meta.get("typehint", "")
363
+ if not file_path or not file_path.endswith(".pyi"):
364
+ continue
365
+ else:
366
+ continue
367
+
368
+ if part == "typehints":
369
+ relpath = deploy_path.replace(".py", ".pyi")
370
+ else:
371
+ relpath = deploy_path
372
+ yield (relpath, pkg_meta)
373
+
374
+ if include_submodules and part == "src":
375
+ deploy_dir = "/".join(deploy_path.split("/")[:-1])
376
+ sub_base = {k: v for k, v in pkg_meta.items() if k not in ("submodules", "submodules_typehints")}
377
+ sub_base["submodules"] = []
378
+ sub_base["submodules_typehints"] = []
379
+ sub_base["_parent_source"] = pkg_meta.get("source", "")
380
+ for sub_src in pkg_meta.get("submodules", []):
381
+ sub_filename = sub_src.split("/")[-1]
382
+ if sub_filename.endswith(".pyi"):
383
+ continue
384
+ sub_relpath = (deploy_dir + "/" + sub_filename) if deploy_dir else sub_filename
385
+ sub_meta = sub_base.copy()
386
+ sub_meta["source"] = sub_src
387
+ yield (sub_relpath, sub_meta)