rainbow-rb-utils 0.0.9.dev45__tar.gz → 0.0.73.dev1__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.
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/PKG-INFO +3 -3
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/pyproject.toml +4 -4
- rainbow_rb_utils-0.0.73.dev1/src/rainbow/rb_utils/flatbuffer.py +145 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/flow_manager.py +130 -10
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/helper.py +24 -10
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/manipulate.py +3 -0
- rainbow_rb_utils-0.0.73.dev1/src/rainbow/rb_utils/program.py +135 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow_rb_utils.egg-info/PKG-INFO +3 -3
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow_rb_utils.egg-info/SOURCES.txt +1 -0
- rainbow_rb_utils-0.0.73.dev1/src/rainbow_rb_utils.egg-info/requires.txt +2 -0
- rainbow_rb_utils-0.0.9.dev45/src/rainbow/rb_utils/program.py +0 -12
- rainbow_rb_utils-0.0.9.dev45/src/rainbow_rb_utils.egg-info/requires.txt +0 -2
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/README.md +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/setup.cfg +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/__init__.py +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/asyncio_helper.py +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/date.py +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/env.py +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/file.py +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/pagination.py +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/parser.py +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/path_utils.py +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/py.typed +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/service_exception.py +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow_rb_utils.egg-info/dependency_links.txt +0 -0
- {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow_rb_utils.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rainbow-rb-utils
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.73.dev1
|
|
4
4
|
Summary: Rainbow Python Utilities
|
|
5
5
|
Author-email: Derek <dfd1123@rainbow-robotics.com>
|
|
6
|
-
Requires-Python:
|
|
6
|
+
Requires-Python: <3.14,>=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: psutil<8.0.0,>=7.0.0
|
|
9
|
-
Requires-Dist: rainbow-rb-log==0.0.
|
|
9
|
+
Requires-Dist: rainbow-rb-log==0.0.73.dev1
|
|
10
10
|
|
|
11
11
|
# rb_utils
|
|
12
12
|
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rainbow-rb-utils"
|
|
7
|
-
version = "0.0.
|
|
8
|
-
requires-python = ">=3.11"
|
|
7
|
+
version = "0.0.73.dev1"
|
|
8
|
+
requires-python = ">=3.11,<3.14"
|
|
9
9
|
description = "Rainbow Python Utilities"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name = "Derek", email = "dfd1123@rainbow-robotics.com" },
|
|
@@ -14,7 +14,7 @@ readme = "README.md"
|
|
|
14
14
|
|
|
15
15
|
dependencies = [
|
|
16
16
|
"psutil>=7.0.0,<8.0.0",
|
|
17
|
-
"rainbow-rb-log==0.0.
|
|
17
|
+
"rainbow-rb-log==0.0.73.dev1",
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
[tool.setuptools]
|
|
@@ -26,5 +26,5 @@ where = ["src"]
|
|
|
26
26
|
include = ["rainbow.rb_utils*"]
|
|
27
27
|
|
|
28
28
|
[tool.setuptools.package-data]
|
|
29
|
-
"*" = ["py.typed", "*.pyi", "
|
|
29
|
+
"*" = ["py.typed", "*.pyi", "*.so", "*.pyd"]
|
|
30
30
|
"rainbow.rb_utils" = ["examples/**", "examples/**/*", ".claude/**", ".claude/**/*"]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""서비스 dict를 생성된 FlatBuffer ``*T`` 객체로 변환하는 유틸리티."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import importlib
|
|
7
|
+
import importlib.util
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from rainbow.rb_zenoh.fb_codec import FbTypeError, resolve_fb_type
|
|
14
|
+
|
|
15
|
+
from rainbow.rb_utils.parser import camel_to_snake
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@lru_cache(maxsize=256)
|
|
19
|
+
def _field_annotations(t_cls: type) -> dict[str, str]:
|
|
20
|
+
"""flatc가 생성한 sibling ``.pyi``에서 T 객체의 필드 타입을 읽는다."""
|
|
21
|
+
spec = importlib.util.find_spec(t_cls.__module__)
|
|
22
|
+
if spec is None or not spec.origin or not spec.origin.endswith(".py"):
|
|
23
|
+
return {}
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
with open(spec.origin[:-3] + ".pyi", encoding="utf-8") as file:
|
|
27
|
+
tree = ast.parse(file.read())
|
|
28
|
+
except (OSError, SyntaxError):
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
for node in ast.walk(tree):
|
|
32
|
+
if isinstance(node, ast.ClassDef) and node.name == t_cls.__name__:
|
|
33
|
+
return {
|
|
34
|
+
statement.target.id: ast.unparse(statement.annotation)
|
|
35
|
+
for statement in node.body
|
|
36
|
+
if isinstance(statement, ast.AnnAssign)
|
|
37
|
+
and isinstance(statement.target, ast.Name)
|
|
38
|
+
}
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _strip_optional(annotation: str) -> str:
|
|
43
|
+
annotation = annotation.strip()
|
|
44
|
+
for prefix in ("typing.Optional[", "Optional["):
|
|
45
|
+
if annotation.startswith(prefix) and annotation.endswith("]"):
|
|
46
|
+
return annotation[len(prefix):-1].strip()
|
|
47
|
+
union_parts = [part.strip() for part in annotation.split("|")]
|
|
48
|
+
if len(union_parts) == 2 and "None" in union_parts:
|
|
49
|
+
return next(part for part in union_parts if part != "None")
|
|
50
|
+
return annotation
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _list_element_type(annotation: str) -> str | None:
|
|
54
|
+
annotation = _strip_optional(annotation)
|
|
55
|
+
for prefix in ("typing.List[", "List[", "list["):
|
|
56
|
+
if annotation.startswith(prefix) and annotation.endswith("]"):
|
|
57
|
+
return annotation[len(prefix):-1].strip().strip("'\"")
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _coerce_literal(annotation: str, value: Any, owner_module: str) -> Any:
|
|
62
|
+
if not annotation.startswith(("typing.Literal[", "Literal[")):
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
if isinstance(value, Enum):
|
|
66
|
+
value = value.value
|
|
67
|
+
if not isinstance(value, str):
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
for member in annotation[annotation.index("[") + 1:-1].split(","):
|
|
71
|
+
enum_name, separator, member_name = member.strip().rpartition(".")
|
|
72
|
+
if not separator or member_name != value:
|
|
73
|
+
continue
|
|
74
|
+
enum_module = importlib.import_module(
|
|
75
|
+
f"{owner_module.rpartition('.')[0]}.{enum_name}"
|
|
76
|
+
)
|
|
77
|
+
enum_cls = getattr(enum_module, enum_name)
|
|
78
|
+
return getattr(enum_cls, member_name)
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _coerce_scalar(annotation: str, value: Any, owner_module: str) -> Any:
|
|
83
|
+
annotation = _strip_optional(annotation)
|
|
84
|
+
value = _coerce_literal(annotation, value, owner_module)
|
|
85
|
+
|
|
86
|
+
if annotation == "int":
|
|
87
|
+
if isinstance(value, datetime):
|
|
88
|
+
return int(value.timestamp() * 1000)
|
|
89
|
+
try:
|
|
90
|
+
return int(value)
|
|
91
|
+
except (TypeError, ValueError):
|
|
92
|
+
return int(datetime.fromisoformat(value).timestamp() * 1000)
|
|
93
|
+
if annotation == "float":
|
|
94
|
+
return float(value)
|
|
95
|
+
if annotation == "bool":
|
|
96
|
+
if isinstance(value, str):
|
|
97
|
+
return value.lower() in {"1", "true", "yes"}
|
|
98
|
+
return bool(value)
|
|
99
|
+
if annotation == "str":
|
|
100
|
+
return str(value)
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _build_value(annotation: str | None, value: Any, owner_module: str) -> Any:
|
|
105
|
+
if value is None or annotation is None:
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
element_type = _list_element_type(annotation)
|
|
109
|
+
if element_type is not None:
|
|
110
|
+
values = value if isinstance(value, list | tuple) else [value]
|
|
111
|
+
return [_build_value(element_type, item, owner_module) for item in values]
|
|
112
|
+
|
|
113
|
+
nested_type = _strip_optional(annotation).strip("'\"")
|
|
114
|
+
if isinstance(value, dict):
|
|
115
|
+
try:
|
|
116
|
+
nested_cls = resolve_fb_type(nested_type)
|
|
117
|
+
except FbTypeError:
|
|
118
|
+
return value
|
|
119
|
+
return dict_to_flatbuffer(nested_cls, value)
|
|
120
|
+
|
|
121
|
+
return _coerce_scalar(nested_type, value, owner_module)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def dict_to_flatbuffer(t_cls: type, data: dict[str, Any]) -> Any:
|
|
125
|
+
"""dict를 ``t_cls``와 중첩된 FlatBuffer T 객체로 재귀 변환한다."""
|
|
126
|
+
if not isinstance(data, dict):
|
|
127
|
+
raise TypeError(f"{t_cls.__name__}: dict required, got {type(data).__name__}")
|
|
128
|
+
|
|
129
|
+
obj = t_cls()
|
|
130
|
+
annotations = _field_annotations(t_cls)
|
|
131
|
+
fields = {key: value for key, value in vars(obj).items() if not key.startswith("_")}
|
|
132
|
+
normalized_data = {camel_to_snake(key): value for key, value in data.items()}
|
|
133
|
+
|
|
134
|
+
for field, default in fields.items():
|
|
135
|
+
normalized_field = camel_to_snake(field)
|
|
136
|
+
if normalized_field not in normalized_data:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
value = normalized_data[normalized_field]
|
|
140
|
+
annotation = annotations.get(field)
|
|
141
|
+
if value is None and default is not None and annotation is not None:
|
|
142
|
+
continue
|
|
143
|
+
setattr(obj, field, _build_value(annotation, value, t_cls.__module__))
|
|
144
|
+
|
|
145
|
+
return obj
|
{rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/flow_manager.py
RENAMED
|
@@ -74,7 +74,9 @@ def safe_eval_expr(
|
|
|
74
74
|
variables: dict[str, dict[str, Any]] | None = None,
|
|
75
75
|
get_global_variable: Callable[[str], Any] | None = None,
|
|
76
76
|
get_var: Callable[..., Any] | None = None,
|
|
77
|
+
on_write: Callable[[str, str], None] | None = None,
|
|
77
78
|
) -> Any:
|
|
79
|
+
"""on_write: 대입/증감 시 (변수명, "local"|"global") 통보. 쓰기 없으면 호출 안 됨."""
|
|
78
80
|
if not isinstance(expr, str):
|
|
79
81
|
return expr
|
|
80
82
|
|
|
@@ -88,7 +90,9 @@ def safe_eval_expr(
|
|
|
88
90
|
if not s:
|
|
89
91
|
return ""
|
|
90
92
|
|
|
91
|
-
is_explicit_expression = bool(
|
|
93
|
+
is_explicit_expression = bool(
|
|
94
|
+
EXPLICIT_EXPR_PATTERN.search(s) or RB_GLOBAL_PATTERN.search(s) or GET_VAR_PATTERN.search(s)
|
|
95
|
+
)
|
|
92
96
|
normalized = EXPLICIT_EXPR_PATTERN.sub(lambda match: match.group()[1:], s)
|
|
93
97
|
|
|
94
98
|
# --- 내부 평가기 ------------------------------------------------------------
|
|
@@ -115,15 +119,30 @@ def safe_eval_expr(
|
|
|
115
119
|
return ALLOWED_CONSTS[name]
|
|
116
120
|
if name == "math": # math.* 전용
|
|
117
121
|
return "math"
|
|
118
|
-
if name in vars_.get("local", {}):
|
|
119
|
-
return vars_["local"].get(name)
|
|
120
122
|
|
|
123
|
+
# RB_ 는 로봇 시스템변수 예약 네임스페이스다. 항상 라이브 조회가 먼저다.
|
|
124
|
+
# 캐시를 먼저 보면 한 번 담긴 값(state_monitoring 의 [string] 이거나 이전 조회
|
|
125
|
+
# 결과)에 갇혀 로봇이 갱신해도 반영되지 않는다.
|
|
126
|
+
# 라이브 조회가 실패한 경우(로봇 미응답 등)에만 마지막으로 알려진 캐시로 폴백한다.
|
|
121
127
|
if name.startswith("RB_"):
|
|
122
128
|
global_val = get_global_variable(name) if get_global_variable is not None else None
|
|
123
129
|
if global_val is not None:
|
|
124
130
|
return global_val
|
|
131
|
+
|
|
132
|
+
if name in vars_.get("local", {}):
|
|
133
|
+
return vars_["local"].get(name)
|
|
134
|
+
|
|
135
|
+
if name in vars_.get("global", {}):
|
|
136
|
+
return vars_["global"].get(name)
|
|
137
|
+
|
|
125
138
|
raise ValueError(f"global variable {name} not found")
|
|
126
139
|
|
|
140
|
+
if name in vars_.get("local", {}):
|
|
141
|
+
return vars_["local"].get(name)
|
|
142
|
+
|
|
143
|
+
if name in vars_.get("global", {}):
|
|
144
|
+
return vars_["global"].get(name)
|
|
145
|
+
|
|
127
146
|
return name # 모르는 식별자는 문자열로 취급(원하면 여기서 NameError)
|
|
128
147
|
|
|
129
148
|
if isinstance(node, ast.Attribute):
|
|
@@ -187,7 +206,9 @@ def safe_eval_expr(
|
|
|
187
206
|
if get_var is None:
|
|
188
207
|
raise ValueError("get_var is not available in this context")
|
|
189
208
|
if len(node.args) not in (2, 3):
|
|
190
|
-
raise ValueError(
|
|
209
|
+
raise ValueError(
|
|
210
|
+
"get_var requires 2 or 3 arguments: get_var(pid_or_model, var_name[, default])"
|
|
211
|
+
)
|
|
191
212
|
pid_or_model = _eval(node.args[0])
|
|
192
213
|
var_name = _eval(node.args[1])
|
|
193
214
|
if len(node.args) == 3:
|
|
@@ -225,6 +246,22 @@ def safe_eval_expr(
|
|
|
225
246
|
return slice(lo, up, st)
|
|
226
247
|
return _eval(sl)
|
|
227
248
|
|
|
249
|
+
def _notify_write(target: ast.AST) -> None:
|
|
250
|
+
"""대입 대상의 root 변수명 통보. a[0][1] = 3 → "a" """
|
|
251
|
+
if on_write is None:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
node: ast.AST = target
|
|
255
|
+
while isinstance(node, ast.Subscript | ast.Attribute):
|
|
256
|
+
node = node.value
|
|
257
|
+
if not isinstance(node, ast.Name):
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
if node is target:
|
|
261
|
+
on_write(node.id, "local") # 이름 대입은 항상 local
|
|
262
|
+
else: # a[0] = 3 → a가 들어있는 스코프
|
|
263
|
+
on_write(node.id, "global" if node.id in vars_.get("global", {}) else "local")
|
|
264
|
+
|
|
228
265
|
def _apply_increment(target_expr: str, *, delta: int, is_prefix: bool) -> Any:
|
|
229
266
|
try:
|
|
230
267
|
target = ast.parse(target_expr, mode="eval").body
|
|
@@ -245,6 +282,7 @@ def safe_eval_expr(
|
|
|
245
282
|
|
|
246
283
|
new_val = _increment_value(cur, delta, target_expr) if exists else delta
|
|
247
284
|
vars_.setdefault("local", {})[name] = new_val
|
|
285
|
+
_notify_write(target)
|
|
248
286
|
return new_val if is_prefix else cur
|
|
249
287
|
|
|
250
288
|
if isinstance(target, ast.Subscript):
|
|
@@ -256,6 +294,7 @@ def safe_eval_expr(
|
|
|
256
294
|
|
|
257
295
|
new_val = _increment_value(cur, delta, target_expr)
|
|
258
296
|
container[key] = new_val
|
|
297
|
+
_notify_write(target)
|
|
259
298
|
return new_val if is_prefix else cur
|
|
260
299
|
|
|
261
300
|
raise ValueError("only variable or subscript increment targets are allowed")
|
|
@@ -278,11 +317,13 @@ def safe_eval_expr(
|
|
|
278
317
|
def _set_assign_target_value(target: ast.AST, value: Any) -> Any:
|
|
279
318
|
if isinstance(target, ast.Name):
|
|
280
319
|
vars_.setdefault("local", {})[target.id] = value
|
|
320
|
+
_notify_write(target)
|
|
281
321
|
return value
|
|
282
322
|
|
|
283
323
|
if isinstance(target, ast.Subscript):
|
|
284
324
|
container = _eval(target.value)
|
|
285
325
|
container[_eval_subscript_key(target)] = value
|
|
326
|
+
_notify_write(target)
|
|
286
327
|
return value
|
|
287
328
|
|
|
288
329
|
raise ValueError("only variable or subscript assignment targets are allowed")
|
|
@@ -342,9 +383,9 @@ def safe_eval_expr(
|
|
|
342
383
|
if is_explicit_expression:
|
|
343
384
|
raise ValueError(f"failed to evaluate expression: {expr}") from None
|
|
344
385
|
return s
|
|
345
|
-
except Exception:
|
|
386
|
+
except Exception as e:
|
|
346
387
|
if is_explicit_expression:
|
|
347
|
-
raise ValueError(f"failed to evaluate expression: {expr}") from
|
|
388
|
+
raise ValueError(f"failed to evaluate expression: {expr} ({e})") from e
|
|
348
389
|
return s
|
|
349
390
|
|
|
350
391
|
|
|
@@ -362,9 +403,68 @@ def _pick_target_loop(func, provided: dict) -> asyncio.AbstractEventLoop | None:
|
|
|
362
403
|
return None
|
|
363
404
|
|
|
364
405
|
|
|
406
|
+
_PYBIND_SIG_RE = re.compile(r"^\w+\((?P<params>.*)\)\s*->")
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _split_top_level(src: str) -> list[str]:
|
|
410
|
+
parts: list[str] = []
|
|
411
|
+
depth = 0
|
|
412
|
+
buf: list[str] = []
|
|
413
|
+
for ch in src:
|
|
414
|
+
if ch in "([{":
|
|
415
|
+
depth += 1
|
|
416
|
+
elif ch in ")]}":
|
|
417
|
+
depth -= 1
|
|
418
|
+
if ch == "," and depth == 0:
|
|
419
|
+
parts.append("".join(buf))
|
|
420
|
+
buf = []
|
|
421
|
+
else:
|
|
422
|
+
buf.append(ch)
|
|
423
|
+
parts.append("".join(buf))
|
|
424
|
+
return [p.strip() for p in parts if p.strip()]
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _signature_from_pybind_doc(func) -> inspect.Signature | None:
|
|
428
|
+
"""pybind11 함수는 __text_signature__가 없어 inspect.signature가 실패한다.
|
|
429
|
+
시그니처가 유일하게 남아 있는 __doc__ 첫 줄에서 인자 이름/기본값 유무만 복원한다."""
|
|
430
|
+
doc = (getattr(func, "__doc__", None) or "").lstrip()
|
|
431
|
+
match = _PYBIND_SIG_RE.match(doc.splitlines()[0]) if doc else None
|
|
432
|
+
if match is None:
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
params: list[inspect.Parameter] = []
|
|
436
|
+
for token in _split_top_level(match.group("params")):
|
|
437
|
+
head, _, default = token.partition("=")
|
|
438
|
+
name = head.split(":", 1)[0].strip()
|
|
439
|
+
|
|
440
|
+
if name.startswith("**"):
|
|
441
|
+
kind, name = inspect.Parameter.VAR_KEYWORD, name[2:]
|
|
442
|
+
elif name.startswith("*"):
|
|
443
|
+
kind, name = inspect.Parameter.VAR_POSITIONAL, name[1:]
|
|
444
|
+
else:
|
|
445
|
+
kind = inspect.Parameter.KEYWORD_ONLY
|
|
446
|
+
|
|
447
|
+
if not name or (name == "self" and not params):
|
|
448
|
+
continue
|
|
449
|
+
|
|
450
|
+
has_default = bool(default.strip()) and kind is inspect.Parameter.KEYWORD_ONLY
|
|
451
|
+
params.append(
|
|
452
|
+
inspect.Parameter(
|
|
453
|
+
name, kind, default=None if has_default else inspect.Parameter.empty
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
return inspect.Signature(params)
|
|
458
|
+
|
|
459
|
+
|
|
365
460
|
def call_with_matching_args(func, **provided):
|
|
366
461
|
"""func의 시그니처를 보고 필요한 인자만 추려서 호출 (동기/비동기 모두 지원, 루프 안전)"""
|
|
367
|
-
|
|
462
|
+
try:
|
|
463
|
+
sig = inspect.signature(func)
|
|
464
|
+
except ValueError:
|
|
465
|
+
sig = _signature_from_pybind_doc(func)
|
|
466
|
+
if sig is None:
|
|
467
|
+
raise
|
|
368
468
|
call_kwargs = {}
|
|
369
469
|
|
|
370
470
|
for name, param in sig.parameters.items():
|
|
@@ -408,6 +508,7 @@ def eval_value(
|
|
|
408
508
|
variables: dict[str, dict[str, Any]] | None = None,
|
|
409
509
|
get_global_variable: Callable[[str], Any] | None = None,
|
|
410
510
|
get_var: Callable[..., Any] | None = None,
|
|
511
|
+
on_write: Callable[[str, str], None] | None = None,
|
|
411
512
|
) -> Any:
|
|
412
513
|
# 1) 문자열이면 → 기존 safe_eval_expr 호출
|
|
413
514
|
if isinstance(value, str):
|
|
@@ -416,26 +517,45 @@ def eval_value(
|
|
|
416
517
|
variables=variables,
|
|
417
518
|
get_global_variable=get_global_variable,
|
|
418
519
|
get_var=get_var,
|
|
520
|
+
on_write=on_write,
|
|
419
521
|
)
|
|
420
522
|
|
|
421
523
|
# 2) 리스트면 → 각 요소를 재귀적으로 평가
|
|
422
524
|
if isinstance(value, list):
|
|
423
525
|
return [
|
|
424
|
-
eval_value(
|
|
526
|
+
eval_value(
|
|
527
|
+
v,
|
|
528
|
+
variables=variables,
|
|
529
|
+
get_global_variable=get_global_variable,
|
|
530
|
+
get_var=get_var,
|
|
531
|
+
on_write=on_write,
|
|
532
|
+
)
|
|
425
533
|
for v in value
|
|
426
534
|
]
|
|
427
535
|
|
|
428
536
|
# 3) 튜플도 동일
|
|
429
537
|
if isinstance(value, tuple):
|
|
430
538
|
return tuple(
|
|
431
|
-
eval_value(
|
|
539
|
+
eval_value(
|
|
540
|
+
v,
|
|
541
|
+
variables=variables,
|
|
542
|
+
get_global_variable=get_global_variable,
|
|
543
|
+
get_var=get_var,
|
|
544
|
+
on_write=on_write,
|
|
545
|
+
)
|
|
432
546
|
for v in value
|
|
433
547
|
)
|
|
434
548
|
|
|
435
549
|
# 4) 딕셔너리면 → value만 재귀적으로 평가 (key는 그대로)
|
|
436
550
|
if isinstance(value, dict):
|
|
437
551
|
return {
|
|
438
|
-
k: eval_value(
|
|
552
|
+
k: eval_value(
|
|
553
|
+
v,
|
|
554
|
+
variables=variables,
|
|
555
|
+
get_global_variable=get_global_variable,
|
|
556
|
+
get_var=get_var,
|
|
557
|
+
on_write=on_write,
|
|
558
|
+
)
|
|
439
559
|
for k, v in value.items()
|
|
440
560
|
}
|
|
441
561
|
|
{rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/helper.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import math
|
|
3
3
|
import socket
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
6
7
|
from .parser import t_to_dict
|
|
@@ -18,23 +19,36 @@ def ManipulateZenohResHelper(obj: Any):
|
|
|
18
19
|
raise ValueError("obj is not a dict")
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
# 기본 executor 는 쓰지 않는다. wait_for 는 시작된 스레드를 못 죽여서, 여기가
|
|
23
|
+
# 막히면 이 함수를 넘어 MuscatRouter 핸들러까지 같이 굶는다.
|
|
24
|
+
_dns_fallback_pool = ThreadPoolExecutor(max_workers=1, thread_name_prefix="rb-ip-dns")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _route_lookup_ip() -> str | None:
|
|
28
|
+
"""기본 경로로 나가는 인터페이스 IP. 패킷 안 나가고 라우팅 테이블만 본다 (µs)."""
|
|
29
|
+
try:
|
|
30
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
|
31
|
+
sock.connect(("8.8.8.8", 80))
|
|
32
|
+
return sock.getsockname()[0]
|
|
33
|
+
except OSError:
|
|
34
|
+
return None # 기본 경로 없음
|
|
35
|
+
|
|
36
|
+
|
|
21
37
|
async def get_current_ip(timeout: float = 0.5) -> str:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
sock.connect(("8.8.8.8", 80))
|
|
26
|
-
return sock.getsockname()[0]
|
|
27
|
-
except Exception:
|
|
28
|
-
return socket.gethostbyname(socket.gethostname())
|
|
38
|
+
ip = _route_lookup_ip()
|
|
39
|
+
if ip is not None:
|
|
40
|
+
return ip
|
|
29
41
|
|
|
42
|
+
# 기본 경로가 없을 때만. 호스트명 해석은 DNS 를 탈 수 있다.
|
|
30
43
|
loop = asyncio.get_running_loop()
|
|
31
44
|
try:
|
|
32
45
|
return await asyncio.wait_for(
|
|
33
|
-
loop.run_in_executor(
|
|
46
|
+
loop.run_in_executor(
|
|
47
|
+
_dns_fallback_pool, lambda: socket.gethostbyname(socket.gethostname())
|
|
48
|
+
),
|
|
34
49
|
timeout=timeout,
|
|
35
50
|
)
|
|
36
|
-
except
|
|
37
|
-
# DNS가 막히면 이전 IP를 쓰거나, 로컬 IP로 폴백
|
|
51
|
+
except (TimeoutError, OSError):
|
|
38
52
|
return "127.0.0.1"
|
|
39
53
|
|
|
40
54
|
|
{rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/manipulate.py
RENAMED
|
@@ -79,6 +79,9 @@ def validate_single_return_value(res: QueryResult[T], fm_args: FlowManagerArgs |
|
|
|
79
79
|
|
|
80
80
|
obj_payload = res.get("obj_payload", None)
|
|
81
81
|
if obj_payload is None:
|
|
82
|
+
err = res.get("err")
|
|
83
|
+
if err:
|
|
84
|
+
raise RuntimeError(f"{snake_to_title(call_function)} failed: {err}")
|
|
82
85
|
raise RuntimeError(f"{snake_to_title(call_function)} failed: obj_payload is None")
|
|
83
86
|
|
|
84
87
|
if hasattr(obj_payload, "returnValue") and obj_payload.returnValue != 0:
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
from typing import Any, Literal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_task_id(args: Any | None) -> str:
|
|
6
|
+
if not args:
|
|
7
|
+
return "unknown_module"
|
|
8
|
+
|
|
9
|
+
# FlowManagerArgs 객체와 dict 입력을 모두 허용한다.
|
|
10
|
+
ctx = args.get("ctx") if isinstance(args, dict) else getattr(args, "ctx", None)
|
|
11
|
+
|
|
12
|
+
process_id = getattr(ctx, "process_id", None)
|
|
13
|
+
return str(process_id) if process_id else "unknown_module"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_MISSING = object()
|
|
17
|
+
|
|
18
|
+
_RESOLVE_OPTIONS = ("SKIP", "STOP", "ASK")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InterfaceResult:
|
|
22
|
+
def __init__(self, success: bool, error: str | None = None):
|
|
23
|
+
self.success = success
|
|
24
|
+
self.error = error
|
|
25
|
+
|
|
26
|
+
def __str__(self) -> str:
|
|
27
|
+
if self.error:
|
|
28
|
+
return self.error
|
|
29
|
+
return "success" if self.success else "The request failed."
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def to_int(
|
|
33
|
+
data_or_value,
|
|
34
|
+
field_name: str | None = None,
|
|
35
|
+
default: int | object = _MISSING,
|
|
36
|
+
error_prefix: str | None = None,
|
|
37
|
+
) -> int:
|
|
38
|
+
"""값을 int로 변환하고, default가 있을 때만 None/빈 문자열을 대체한다."""
|
|
39
|
+
|
|
40
|
+
def _raise(msg: str) -> None:
|
|
41
|
+
if error_prefix:
|
|
42
|
+
raise ValueError(f"{error_prefix}: {msg}")
|
|
43
|
+
raise ValueError(msg)
|
|
44
|
+
|
|
45
|
+
if field_name is not None and hasattr(data_or_value, "get"):
|
|
46
|
+
value = data_or_value.get(field_name)
|
|
47
|
+
name = field_name
|
|
48
|
+
else:
|
|
49
|
+
value = data_or_value
|
|
50
|
+
name = field_name or "value"
|
|
51
|
+
|
|
52
|
+
if value is None or value == "":
|
|
53
|
+
if default is _MISSING:
|
|
54
|
+
_raise(f"failed: {name} is required")
|
|
55
|
+
return int(default)
|
|
56
|
+
try:
|
|
57
|
+
return int(value)
|
|
58
|
+
except (TypeError, ValueError) as exc:
|
|
59
|
+
if error_prefix:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"{error_prefix}: failed: int conversion failed {name}={value}"
|
|
62
|
+
) from exc
|
|
63
|
+
raise ValueError(f"failed: int conversion failed {name}={value}") from exc
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def validate_server_ip(data, error_prefix: str) -> str:
|
|
67
|
+
server_ip = data.get("server_ip")
|
|
68
|
+
|
|
69
|
+
if server_ip in (None, "", Ellipsis):
|
|
70
|
+
raise ValueError(f"{error_prefix}: server_ip is required")
|
|
71
|
+
|
|
72
|
+
if not isinstance(server_ip, str):
|
|
73
|
+
raise ValueError(f"{error_prefix}: server_ip must be a string")
|
|
74
|
+
|
|
75
|
+
server_ip = server_ip.strip()
|
|
76
|
+
if not server_ip:
|
|
77
|
+
raise ValueError(f"{error_prefix}: server_ip is required")
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
ipaddress.ip_address(server_ip)
|
|
81
|
+
except ValueError as exc:
|
|
82
|
+
raise ValueError(f"{error_prefix}: invalid server_ip={server_ip}") from exc
|
|
83
|
+
|
|
84
|
+
return server_ip
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_resolve_option(data, error_prefix: str) -> str:
|
|
88
|
+
value = data.get("resolve_option", "SKIP")
|
|
89
|
+
if value not in _RESOLVE_OPTIONS:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"{error_prefix}: invalid resolve_option={value!r} (allowed: {_RESOLVE_OPTIONS})"
|
|
92
|
+
)
|
|
93
|
+
return value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_timeout_sec(data, error_prefix: str, default: float | None = None) -> int:
|
|
97
|
+
value = data.get("timeout_sec")
|
|
98
|
+
if value is None or value == "":
|
|
99
|
+
if default is None:
|
|
100
|
+
raise ValueError(f"{error_prefix}: timeout_sec is required")
|
|
101
|
+
return int(default * 1000)
|
|
102
|
+
try:
|
|
103
|
+
return int(float(value) * 1000)
|
|
104
|
+
except (TypeError, ValueError) as exc:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"{error_prefix}: failed: timeout_sec conversion failed value={value}"
|
|
107
|
+
) from exc
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_required_field(data, field_name: str, error_prefix: str):
|
|
111
|
+
value = data.get(field_name)
|
|
112
|
+
if value in (None, "", Ellipsis):
|
|
113
|
+
raise ValueError(f"{error_prefix}: {field_name} is required")
|
|
114
|
+
return value
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def resolve_interface_flow(
|
|
118
|
+
result,
|
|
119
|
+
resolve_option: Literal["SKIP", "STOP", "ASK"],
|
|
120
|
+
flow_manager_args,
|
|
121
|
+
alarm_fn=None,
|
|
122
|
+
error_prefix: str = "Interface",
|
|
123
|
+
):
|
|
124
|
+
err_msg = getattr(result, "error", None)
|
|
125
|
+
if err_msg in (None, ""):
|
|
126
|
+
err_msg = str(result) if result not in (None, False) else "The request failed."
|
|
127
|
+
if resolve_option == "ASK" and alarm_fn is not None:
|
|
128
|
+
alarm_fn(title="Interface Debug", content=f"{error_prefix}.(Err:{err_msg})")
|
|
129
|
+
flow_manager_args.ctx.pause_all()
|
|
130
|
+
flow_manager_args.ctx.pause()
|
|
131
|
+
flow_manager_args.ctx.check_stop()
|
|
132
|
+
elif resolve_option == "STOP":
|
|
133
|
+
raise RuntimeError(f"{error_prefix}: result={err_msg}")
|
|
134
|
+
else:
|
|
135
|
+
pass # SKIP: do nothing and continue
|
{rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow_rb_utils.egg-info/PKG-INFO
RENAMED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rainbow-rb-utils
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.73.dev1
|
|
4
4
|
Summary: Rainbow Python Utilities
|
|
5
5
|
Author-email: Derek <dfd1123@rainbow-robotics.com>
|
|
6
|
-
Requires-Python:
|
|
6
|
+
Requires-Python: <3.14,>=3.11
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
Requires-Dist: psutil<8.0.0,>=7.0.0
|
|
9
|
-
Requires-Dist: rainbow-rb-log==0.0.
|
|
9
|
+
Requires-Dist: rainbow-rb-log==0.0.73.dev1
|
|
10
10
|
|
|
11
11
|
# rb_utils
|
|
12
12
|
|
|
@@ -5,6 +5,7 @@ src/rainbow/rb_utils/asyncio_helper.py
|
|
|
5
5
|
src/rainbow/rb_utils/date.py
|
|
6
6
|
src/rainbow/rb_utils/env.py
|
|
7
7
|
src/rainbow/rb_utils/file.py
|
|
8
|
+
src/rainbow/rb_utils/flatbuffer.py
|
|
8
9
|
src/rainbow/rb_utils/flow_manager.py
|
|
9
10
|
src/rainbow/rb_utils/helper.py
|
|
10
11
|
src/rainbow/rb_utils/manipulate.py
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def get_task_id(args: Any | None) -> str:
|
|
5
|
-
if not args:
|
|
6
|
-
return "unknown_module"
|
|
7
|
-
|
|
8
|
-
# FlowManagerArgs 객체와 dict 입력을 모두 허용한다.
|
|
9
|
-
ctx = args.get("ctx") if isinstance(args, dict) else getattr(args, "ctx", None)
|
|
10
|
-
|
|
11
|
-
process_id = getattr(ctx, "process_id", None)
|
|
12
|
-
return str(process_id) if process_id else "unknown_module"
|
|
File without changes
|
|
File without changes
|
{rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/__init__.py
RENAMED
|
File without changes
|
{rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/asyncio_helper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/pagination.py
RENAMED
|
File without changes
|
{rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/parser.py
RENAMED
|
File without changes
|
{rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73.dev1}/src/rainbow/rb_utils/path_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|