rainbow-rb-utils 0.0.9.dev45__tar.gz → 0.0.73__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 (26) hide show
  1. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/PKG-INFO +2 -2
  2. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/pyproject.toml +2 -2
  3. rainbow_rb_utils-0.0.73/src/rainbow/rb_utils/flatbuffer.py +145 -0
  4. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/flow_manager.py +20 -5
  5. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/helper.py +24 -10
  6. rainbow_rb_utils-0.0.73/src/rainbow/rb_utils/program.py +135 -0
  7. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow_rb_utils.egg-info/PKG-INFO +2 -2
  8. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow_rb_utils.egg-info/SOURCES.txt +1 -0
  9. rainbow_rb_utils-0.0.73/src/rainbow_rb_utils.egg-info/requires.txt +2 -0
  10. rainbow_rb_utils-0.0.9.dev45/src/rainbow/rb_utils/program.py +0 -12
  11. rainbow_rb_utils-0.0.9.dev45/src/rainbow_rb_utils.egg-info/requires.txt +0 -2
  12. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/README.md +0 -0
  13. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/setup.cfg +0 -0
  14. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/__init__.py +0 -0
  15. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/asyncio_helper.py +0 -0
  16. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/date.py +0 -0
  17. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/env.py +0 -0
  18. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/file.py +0 -0
  19. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/manipulate.py +0 -0
  20. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/pagination.py +0 -0
  21. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/parser.py +0 -0
  22. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/path_utils.py +0 -0
  23. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/py.typed +0 -0
  24. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow/rb_utils/service_exception.py +0 -0
  25. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/src/rainbow_rb_utils.egg-info/dependency_links.txt +0 -0
  26. {rainbow_rb_utils-0.0.9.dev45 → rainbow_rb_utils-0.0.73}/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.9.dev45
3
+ Version: 0.0.73
4
4
  Summary: Rainbow Python Utilities
5
5
  Author-email: Derek <dfd1123@rainbow-robotics.com>
6
6
  Requires-Python: >=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.dev45
9
+ Requires-Dist: rainbow-rb-log==0.0.73
10
10
 
11
11
  # rb_utils
12
12
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rainbow-rb-utils"
7
- version = "0.0.9.dev45"
7
+ version = "0.0.73"
8
8
  requires-python = ">=3.11"
9
9
  description = "Rainbow Python Utilities"
10
10
  authors = [
@@ -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.9.dev45",
17
+ "rainbow-rb-log==0.0.73",
18
18
  ]
19
19
 
20
20
  [tool.setuptools]
@@ -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
@@ -88,7 +88,9 @@ def safe_eval_expr(
88
88
  if not s:
89
89
  return ""
90
90
 
91
- is_explicit_expression = bool(EXPLICIT_EXPR_PATTERN.search(s) or RB_GLOBAL_PATTERN.search(s) or GET_VAR_PATTERN.search(s))
91
+ is_explicit_expression = bool(
92
+ EXPLICIT_EXPR_PATTERN.search(s) or RB_GLOBAL_PATTERN.search(s) or GET_VAR_PATTERN.search(s)
93
+ )
92
94
  normalized = EXPLICIT_EXPR_PATTERN.sub(lambda match: match.group()[1:], s)
93
95
 
94
96
  # --- 내부 평가기 ------------------------------------------------------------
@@ -118,8 +120,13 @@ def safe_eval_expr(
118
120
  if name in vars_.get("local", {}):
119
121
  return vars_["local"].get(name)
120
122
 
123
+ if name in vars_.get("global", {}):
124
+ return vars_["global"].get(name)
125
+
121
126
  if name.startswith("RB_"):
127
+ print("eval_value : RB_ started", name)
122
128
  global_val = get_global_variable(name) if get_global_variable is not None else None
129
+ print("eval_value : ", global_val, name)
123
130
  if global_val is not None:
124
131
  return global_val
125
132
  raise ValueError(f"global variable {name} not found")
@@ -187,7 +194,9 @@ def safe_eval_expr(
187
194
  if get_var is None:
188
195
  raise ValueError("get_var is not available in this context")
189
196
  if len(node.args) not in (2, 3):
190
- raise ValueError("get_var requires 2 or 3 arguments: get_var(pid_or_model, var_name[, default])")
197
+ raise ValueError(
198
+ "get_var requires 2 or 3 arguments: get_var(pid_or_model, var_name[, default])"
199
+ )
191
200
  pid_or_model = _eval(node.args[0])
192
201
  var_name = _eval(node.args[1])
193
202
  if len(node.args) == 3:
@@ -421,21 +430,27 @@ def eval_value(
421
430
  # 2) 리스트면 → 각 요소를 재귀적으로 평가
422
431
  if isinstance(value, list):
423
432
  return [
424
- eval_value(v, variables=variables, get_global_variable=get_global_variable, get_var=get_var)
433
+ eval_value(
434
+ v, variables=variables, get_global_variable=get_global_variable, get_var=get_var
435
+ )
425
436
  for v in value
426
437
  ]
427
438
 
428
439
  # 3) 튜플도 동일
429
440
  if isinstance(value, tuple):
430
441
  return tuple(
431
- eval_value(v, variables=variables, get_global_variable=get_global_variable, get_var=get_var)
442
+ eval_value(
443
+ v, variables=variables, get_global_variable=get_global_variable, get_var=get_var
444
+ )
432
445
  for v in value
433
446
  )
434
447
 
435
448
  # 4) 딕셔너리면 → value만 재귀적으로 평가 (key는 그대로)
436
449
  if isinstance(value, dict):
437
450
  return {
438
- k: eval_value(v, variables=variables, get_global_variable=get_global_variable, get_var=get_var)
451
+ k: eval_value(
452
+ v, variables=variables, get_global_variable=get_global_variable, get_var=get_var
453
+ )
439
454
  for k, v in value.items()
440
455
  }
441
456
 
@@ -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
- def resolve_ip() -> str:
23
- try:
24
- with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
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(None, resolve_ip),
46
+ loop.run_in_executor(
47
+ _dns_fallback_pool, lambda: socket.gethostbyname(socket.gethostname())
48
+ ),
34
49
  timeout=timeout,
35
50
  )
36
- except Exception:
37
- # DNS가 막히면 이전 IP를 쓰거나, 로컬 IP로 폴백
51
+ except (TimeoutError, OSError):
38
52
  return "127.0.0.1"
39
53
 
40
54
 
@@ -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
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rainbow-rb-utils
3
- Version: 0.0.9.dev45
3
+ Version: 0.0.73
4
4
  Summary: Rainbow Python Utilities
5
5
  Author-email: Derek <dfd1123@rainbow-robotics.com>
6
6
  Requires-Python: >=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.dev45
9
+ Requires-Dist: rainbow-rb-log==0.0.73
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
@@ -0,0 +1,2 @@
1
+ psutil<8.0.0,>=7.0.0
2
+ rainbow-rb-log==0.0.73
@@ -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"
@@ -1,2 +0,0 @@
1
- psutil<8.0.0,>=7.0.0
2
- rainbow-rb-log==0.0.9.dev45