rainbow-rb-utils 0.0.9.dev5__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.
- rainbow/rb_utils/__init__.py +0 -0
- rainbow/rb_utils/asyncio_helper.py +167 -0
- rainbow/rb_utils/date.py +221 -0
- rainbow/rb_utils/file.py +52 -0
- rainbow/rb_utils/flow_manager.py +400 -0
- rainbow/rb_utils/helper.py +28 -0
- rainbow/rb_utils/manipulate.py +202 -0
- rainbow/rb_utils/pagination.py +38 -0
- rainbow/rb_utils/parser.py +165 -0
- rainbow/rb_utils/py.typed +0 -0
- rainbow/rb_utils/service_exception.py +37 -0
- rainbow_rb_utils-0.0.9.dev5.dist-info/METADATA +139 -0
- rainbow_rb_utils-0.0.9.dev5.dist-info/RECORD +15 -0
- rainbow_rb_utils-0.0.9.dev5.dist-info/WHEEL +5 -0
- rainbow_rb_utils-0.0.9.dev5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import asyncio
|
|
3
|
+
import builtins
|
|
4
|
+
import inspect
|
|
5
|
+
import math
|
|
6
|
+
import operator
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Callable, Coroutine
|
|
9
|
+
from concurrent.futures import Future
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
# --- 간단 테이블 (함수/상수/연산자) -----------------------------------------
|
|
13
|
+
BIN_OPS = {
|
|
14
|
+
ast.Add: operator.add,
|
|
15
|
+
ast.Sub: operator.sub,
|
|
16
|
+
ast.Mult: operator.mul,
|
|
17
|
+
ast.Div: operator.truediv,
|
|
18
|
+
ast.FloorDiv: operator.floordiv,
|
|
19
|
+
ast.Pow: operator.pow,
|
|
20
|
+
ast.Mod: operator.mod,
|
|
21
|
+
}
|
|
22
|
+
UNARY_OPS: dict[type[ast.unaryop], Callable] = {ast.USub: operator.neg, ast.UAdd: operator.pos}
|
|
23
|
+
CMP_OPS: dict[type[ast.cmpop], Callable] = {
|
|
24
|
+
ast.Eq: operator.eq,
|
|
25
|
+
ast.NotEq: operator.ne,
|
|
26
|
+
ast.Lt: operator.lt,
|
|
27
|
+
ast.LtE: operator.le,
|
|
28
|
+
ast.Gt: operator.gt,
|
|
29
|
+
ast.GtE: operator.ge,
|
|
30
|
+
}
|
|
31
|
+
ALLOWED_FUNCS = {
|
|
32
|
+
"sin": math.sin,
|
|
33
|
+
"cos": math.cos,
|
|
34
|
+
"tan": math.tan,
|
|
35
|
+
"sqrt": math.sqrt,
|
|
36
|
+
"log": math.log,
|
|
37
|
+
"log10": math.log10,
|
|
38
|
+
"exp": math.exp,
|
|
39
|
+
"abs": abs,
|
|
40
|
+
"max": max,
|
|
41
|
+
"min": min,
|
|
42
|
+
"pow": pow,
|
|
43
|
+
"sum": sum,
|
|
44
|
+
"round": round,
|
|
45
|
+
"int": int,
|
|
46
|
+
"float": float,
|
|
47
|
+
"str": str,
|
|
48
|
+
"len": len,
|
|
49
|
+
"range": range,
|
|
50
|
+
}
|
|
51
|
+
ALLOWED_CALLABLES = set(ALLOWED_FUNCS.values())
|
|
52
|
+
ALLOWED_CONSTS = {
|
|
53
|
+
"pi": math.pi,
|
|
54
|
+
"e": math.e,
|
|
55
|
+
"tau": getattr(math, "tau", 2 * math.pi),
|
|
56
|
+
"inf": math.inf,
|
|
57
|
+
"nan": math.nan,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
EXPLICIT_EXPR_PATTERN = re.compile(r"\$[A-Za-z_]\w*")
|
|
61
|
+
RB_GLOBAL_PATTERN = re.compile(r"\bRB_[A-Za-z_0-9]*\b")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def safe_eval_expr(
|
|
65
|
+
expr: Any,
|
|
66
|
+
variables: dict[str, dict[str, Any]] | None = None,
|
|
67
|
+
get_global_variable: Callable[[str], Any] | None = None,
|
|
68
|
+
) -> Any:
|
|
69
|
+
if not isinstance(expr, str):
|
|
70
|
+
return expr
|
|
71
|
+
|
|
72
|
+
vars_ = variables or {"local": {}}
|
|
73
|
+
s = expr.strip()
|
|
74
|
+
if not s:
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
is_explicit_expression = bool(EXPLICIT_EXPR_PATTERN.search(s) or RB_GLOBAL_PATTERN.search(s))
|
|
78
|
+
normalized = EXPLICIT_EXPR_PATTERN.sub(lambda match: match.group()[1:], s)
|
|
79
|
+
|
|
80
|
+
# --- 증감(식 전체일 때만 허용) ---------------------------------------------
|
|
81
|
+
inc_post = re.match(r"^([A-Za-z_]\w*)\s*\+\+$", s) # a++
|
|
82
|
+
dec_post = re.match(r"^([A-Za-z_]\w*)\s*--$", s) # a--
|
|
83
|
+
inc_pre = re.match(r"^\+\+\s*([A-Za-z_]\w*)$", s) # ++a
|
|
84
|
+
dec_pre = re.match(r"^--\s*([A-Za-z_]\w*)$", s) # --a
|
|
85
|
+
match = inc_post or dec_post or inc_pre or dec_pre
|
|
86
|
+
if match:
|
|
87
|
+
name = match.group(1)
|
|
88
|
+
exists = name in vars_.get("local", {}) or name in vars_.get("global", {})
|
|
89
|
+
cur: int | None = (
|
|
90
|
+
vars_.get("local", {}).get(name)
|
|
91
|
+
if name in vars_.get("local", {})
|
|
92
|
+
else vars_.get("global", {}).get(name, 0)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if cur is None:
|
|
96
|
+
raise ValueError(f"variable {name} not found")
|
|
97
|
+
|
|
98
|
+
if inc_post:
|
|
99
|
+
vars_.setdefault("local", {})[name] = cur + 1 if exists else 1
|
|
100
|
+
return cur
|
|
101
|
+
if dec_post:
|
|
102
|
+
vars_.setdefault("local", {})[name] = cur - 1 if exists else -1
|
|
103
|
+
return cur
|
|
104
|
+
if inc_pre:
|
|
105
|
+
v = cur + 1 if exists else 1
|
|
106
|
+
vars_.setdefault("local", {})[name] = v
|
|
107
|
+
return v
|
|
108
|
+
if dec_pre:
|
|
109
|
+
v = cur - 1 if exists else -1
|
|
110
|
+
vars_.setdefault("local", {})[name] = v
|
|
111
|
+
return v
|
|
112
|
+
|
|
113
|
+
# --- 내부 평가기 ------------------------------------------------------------
|
|
114
|
+
def _eval(node: ast.AST) -> Any:
|
|
115
|
+
if isinstance(node, ast.Constant):
|
|
116
|
+
return node.value
|
|
117
|
+
|
|
118
|
+
if isinstance(node, ast.Name):
|
|
119
|
+
name = node.id
|
|
120
|
+
if name == "true":
|
|
121
|
+
return True
|
|
122
|
+
if name == "false":
|
|
123
|
+
return False
|
|
124
|
+
if name == "null":
|
|
125
|
+
return None
|
|
126
|
+
if name in ALLOWED_FUNCS: # 함수 이름
|
|
127
|
+
return ALLOWED_FUNCS[name]
|
|
128
|
+
if name in ALLOWED_CONSTS: # 수학 상수
|
|
129
|
+
return ALLOWED_CONSTS[name]
|
|
130
|
+
if name == "math": # math.* 전용
|
|
131
|
+
return "math"
|
|
132
|
+
if name in vars_.get("local", {}):
|
|
133
|
+
return vars_["local"].get(name)
|
|
134
|
+
|
|
135
|
+
if name.startswith("RB_"):
|
|
136
|
+
global_val = get_global_variable(name) if get_global_variable is not None else None
|
|
137
|
+
if global_val is not None:
|
|
138
|
+
return global_val
|
|
139
|
+
raise ValueError(f"global variable {name} not found")
|
|
140
|
+
|
|
141
|
+
return name # 모르는 식별자는 문자열로 취급(원하면 여기서 NameError)
|
|
142
|
+
|
|
143
|
+
if isinstance(node, ast.Attribute):
|
|
144
|
+
base = _eval(node.value)
|
|
145
|
+
attr = node.attr
|
|
146
|
+
if isinstance(base, dict) and attr in base:
|
|
147
|
+
return base[attr]
|
|
148
|
+
if base == "math":
|
|
149
|
+
if attr in ALLOWED_CONSTS:
|
|
150
|
+
return ALLOWED_CONSTS[attr]
|
|
151
|
+
if attr in ALLOWED_FUNCS and hasattr(math, attr):
|
|
152
|
+
return getattr(math, attr)
|
|
153
|
+
raise ValueError(f"unknown math attribute: {attr}")
|
|
154
|
+
raise ValueError(f"attribute access not allowed: {type(base).__name__}.{attr}")
|
|
155
|
+
|
|
156
|
+
if isinstance(node, ast.Subscript):
|
|
157
|
+
seq = _eval(node.value)
|
|
158
|
+
sl = node.slice
|
|
159
|
+
if isinstance(sl, ast.Slice):
|
|
160
|
+
lo = _eval(sl.lower) if sl.lower else None
|
|
161
|
+
up = _eval(sl.upper) if sl.upper else None
|
|
162
|
+
st = _eval(sl.step) if sl.step else None
|
|
163
|
+
return seq[slice(lo, up, st)]
|
|
164
|
+
return seq[_eval(sl)]
|
|
165
|
+
|
|
166
|
+
if isinstance(node, ast.BinOp):
|
|
167
|
+
try:
|
|
168
|
+
bin_fn = BIN_OPS[type(node.op)] # get() 쓰지 말고 [] 사용 → Optional 제거
|
|
169
|
+
except KeyError:
|
|
170
|
+
raise ValueError(f"unsupported operator: {type(node.op)}") from None
|
|
171
|
+
return bin_fn(_eval(node.left), _eval(node.right))
|
|
172
|
+
|
|
173
|
+
if isinstance(node, ast.UnaryOp):
|
|
174
|
+
try:
|
|
175
|
+
un_fn = UNARY_OPS[type(node.op)] # 변수명 분리(= 타입 분리)
|
|
176
|
+
except KeyError:
|
|
177
|
+
raise ValueError(f"unsupported unary operator: {type(node.op)}") from None
|
|
178
|
+
return un_fn(_eval(node.operand))
|
|
179
|
+
|
|
180
|
+
if isinstance(node, ast.Compare):
|
|
181
|
+
left = _eval(node.left)
|
|
182
|
+
for op, comp in zip(node.ops, node.comparators, strict=False): # strict 인자 빼
|
|
183
|
+
try:
|
|
184
|
+
cmp_fn = CMP_OPS[type(op)] # Optional 제거
|
|
185
|
+
except KeyError:
|
|
186
|
+
raise ValueError(f"unsupported comparison: {type(op)}") from None
|
|
187
|
+
right = _eval(comp)
|
|
188
|
+
if not cmp_fn(left, right):
|
|
189
|
+
return False
|
|
190
|
+
left = right
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
if isinstance(node, ast.BoolOp):
|
|
194
|
+
if isinstance(node.op, ast.And):
|
|
195
|
+
return all(_eval(v) for v in node.values)
|
|
196
|
+
if isinstance(node.op, ast.Or):
|
|
197
|
+
return any(_eval(v) for v in node.values)
|
|
198
|
+
|
|
199
|
+
if isinstance(node, ast.Call):
|
|
200
|
+
fn = _eval(node.func)
|
|
201
|
+
if fn not in ALLOWED_CALLABLES:
|
|
202
|
+
raise ValueError(f"function not allowed: {fn}")
|
|
203
|
+
if node.keywords:
|
|
204
|
+
raise ValueError("keyword arguments not allowed")
|
|
205
|
+
args = [_eval(a) for a in node.args]
|
|
206
|
+
return fn(*args)
|
|
207
|
+
|
|
208
|
+
if isinstance(node, ast.List):
|
|
209
|
+
return [_eval(e) for e in node.elts]
|
|
210
|
+
if isinstance(node, ast.Tuple):
|
|
211
|
+
return tuple(_eval(e) for e in node.elts)
|
|
212
|
+
if isinstance(node, ast.Dict):
|
|
213
|
+
return {
|
|
214
|
+
(_eval(k) if k is not None else None): _eval(v)
|
|
215
|
+
for k, v in zip(node.keys, node.values, strict=False)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if isinstance(node, ast.IfExp):
|
|
219
|
+
return _eval(node.body) if _eval(node.test) else _eval(node.orelse)
|
|
220
|
+
|
|
221
|
+
raise ValueError(f"unsupported expression: {ast.dump(node)}")
|
|
222
|
+
|
|
223
|
+
# --- 단순 대입: a = <expr> (한 문장만 허용) ---------------------------------
|
|
224
|
+
try:
|
|
225
|
+
mod = ast.parse(normalized, mode="exec")
|
|
226
|
+
if len(mod.body) == 1:
|
|
227
|
+
stmt = mod.body[0]
|
|
228
|
+
|
|
229
|
+
# 1) 단순 대입: a = <expr>
|
|
230
|
+
if isinstance(stmt, ast.Assign):
|
|
231
|
+
tgt = stmt.targets[0]
|
|
232
|
+
if not isinstance(tgt, ast.Name):
|
|
233
|
+
raise ValueError("only simple assignment like `a = <expr>` is allowed")
|
|
234
|
+
val = _eval(stmt.value)
|
|
235
|
+
vars_.setdefault("local", {})[tgt.id] = val
|
|
236
|
+
return val
|
|
237
|
+
|
|
238
|
+
# 2) 복합 대입: a *= <expr> (+=, -=, /= ... 포함)
|
|
239
|
+
if isinstance(stmt, ast.AugAssign):
|
|
240
|
+
if not isinstance(stmt.target, ast.Name):
|
|
241
|
+
raise ValueError("only simple aug assignment like `a *= <expr>` is allowed")
|
|
242
|
+
|
|
243
|
+
name = stmt.target.id
|
|
244
|
+
|
|
245
|
+
# 현재값 가져오기 (local 우선, 없으면 global, 그래도 없으면 0)
|
|
246
|
+
if name in vars_.get("local", {}):
|
|
247
|
+
cur = vars_["local"][name]
|
|
248
|
+
elif name in vars_.get("global", {}):
|
|
249
|
+
cur = vars_["global"][name]
|
|
250
|
+
else:
|
|
251
|
+
cur = 0
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
bin_fn = BIN_OPS[type(stmt.op)]
|
|
255
|
+
except KeyError:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
f"unsupported operator in aug-assign: {type(stmt.op)}"
|
|
258
|
+
) from None
|
|
259
|
+
|
|
260
|
+
rhs = _eval(stmt.value)
|
|
261
|
+
new_val = bin_fn(cur, rhs)
|
|
262
|
+
|
|
263
|
+
# 로컬에 기록(네 증감 로직이랑 일관되게)
|
|
264
|
+
vars_.setdefault("local", {})[name] = new_val
|
|
265
|
+
return new_val
|
|
266
|
+
except SyntaxError:
|
|
267
|
+
pass # 표현식일 수 있으니 아래로 진행
|
|
268
|
+
except Exception as e:
|
|
269
|
+
raise e
|
|
270
|
+
|
|
271
|
+
# --- 일반 표현식 ------------------------------------------------------------
|
|
272
|
+
try:
|
|
273
|
+
tree = ast.parse(normalized, mode="eval")
|
|
274
|
+
return _eval(tree.body)
|
|
275
|
+
except SyntaxError:
|
|
276
|
+
if is_explicit_expression:
|
|
277
|
+
raise ValueError(f"failed to evaluate expression: {expr}") from None
|
|
278
|
+
return s
|
|
279
|
+
except Exception:
|
|
280
|
+
if is_explicit_expression:
|
|
281
|
+
raise ValueError(f"failed to evaluate expression: {expr}") from None
|
|
282
|
+
return s
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _pick_target_loop(func, provided: dict) -> asyncio.AbstractEventLoop | None:
|
|
286
|
+
bound_self = getattr(func, "__self__", None)
|
|
287
|
+
if bound_self is not None:
|
|
288
|
+
loop = getattr(bound_self, "loop", None)
|
|
289
|
+
if isinstance(loop, asyncio.AbstractEventLoop):
|
|
290
|
+
return loop
|
|
291
|
+
|
|
292
|
+
loop = provided.get("loop")
|
|
293
|
+
if isinstance(loop, asyncio.AbstractEventLoop):
|
|
294
|
+
return loop
|
|
295
|
+
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def call_with_matching_args(func, **provided):
|
|
300
|
+
"""func의 시그니처를 보고 필요한 인자만 추려서 호출 (동기/비동기 모두 지원, 루프 안전)"""
|
|
301
|
+
sig = inspect.signature(func)
|
|
302
|
+
call_kwargs = {}
|
|
303
|
+
|
|
304
|
+
for name, param in sig.parameters.items():
|
|
305
|
+
if name in provided:
|
|
306
|
+
call_kwargs[name] = provided[name]
|
|
307
|
+
elif param.default is inspect.Parameter.empty and param.kind not in (
|
|
308
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
309
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
310
|
+
):
|
|
311
|
+
raise TypeError(f"Missing required parameter: {name}")
|
|
312
|
+
|
|
313
|
+
result: Any = func(**call_kwargs)
|
|
314
|
+
|
|
315
|
+
async def _await_awaitable(a):
|
|
316
|
+
return await a
|
|
317
|
+
|
|
318
|
+
if inspect.isawaitable(result):
|
|
319
|
+
target_loop = _pick_target_loop(func, provided)
|
|
320
|
+
|
|
321
|
+
if target_loop is not None:
|
|
322
|
+
coro: Coroutine[Any, Any, Any] = (
|
|
323
|
+
result if inspect.iscoroutine(result) else _await_awaitable(result)
|
|
324
|
+
)
|
|
325
|
+
future: Future[Any] = asyncio.run_coroutine_threadsafe(coro, target_loop)
|
|
326
|
+
return future.result()
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
running = asyncio.get_running_loop()
|
|
330
|
+
except RuntimeError:
|
|
331
|
+
return asyncio.run(_await_awaitable(result))
|
|
332
|
+
|
|
333
|
+
task: asyncio.Task = running.create_task(_await_awaitable(result))
|
|
334
|
+
return task
|
|
335
|
+
|
|
336
|
+
# 3) 동기 함수면 결과 그대로 반환
|
|
337
|
+
return result
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def eval_value(
|
|
341
|
+
value: Any,
|
|
342
|
+
variables: dict[str, dict[str, Any]] | None = None,
|
|
343
|
+
get_global_variable: Callable[[str], Any] | None = None,
|
|
344
|
+
) -> Any:
|
|
345
|
+
# 1) 문자열이면 → 기존 safe_eval_expr 호출
|
|
346
|
+
if isinstance(value, str):
|
|
347
|
+
return safe_eval_expr(
|
|
348
|
+
value,
|
|
349
|
+
variables=variables,
|
|
350
|
+
get_global_variable=get_global_variable,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# 2) 리스트면 → 각 요소를 재귀적으로 평가
|
|
354
|
+
if isinstance(value, list):
|
|
355
|
+
return [
|
|
356
|
+
eval_value(v, variables=variables, get_global_variable=get_global_variable)
|
|
357
|
+
for v in value
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
# 3) 튜플도 동일
|
|
361
|
+
if isinstance(value, tuple):
|
|
362
|
+
return tuple(
|
|
363
|
+
eval_value(v, variables=variables, get_global_variable=get_global_variable)
|
|
364
|
+
for v in value
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
# 4) 딕셔너리면 → value만 재귀적으로 평가 (key는 그대로)
|
|
368
|
+
if isinstance(value, dict):
|
|
369
|
+
return {
|
|
370
|
+
k: eval_value(v, variables=variables, get_global_variable=get_global_variable)
|
|
371
|
+
for k, v in value.items()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# 5) 나머지(int/float/bool/None 등)는 그대로
|
|
375
|
+
return value
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
DENY_BUILTINS = {
|
|
379
|
+
"__import__", # import 막기 (이거 열리면 끝)
|
|
380
|
+
"open", # 파일 접근
|
|
381
|
+
"input", # 서버에서 대기 위험
|
|
382
|
+
"eval",
|
|
383
|
+
"exec",
|
|
384
|
+
"compile", # 자기 실행 계열
|
|
385
|
+
"globals",
|
|
386
|
+
"locals",
|
|
387
|
+
"vars", # 탐색/우회 보조
|
|
388
|
+
"breakpoint", # 디버거
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def make_builtins_allow_most():
|
|
393
|
+
"""위험하지 않은 모든 내장 함수를 허용하는 딕셔너리를 생성"""
|
|
394
|
+
safe = dict(builtins.__dict__)
|
|
395
|
+
|
|
396
|
+
for k in list(safe.keys()):
|
|
397
|
+
if k in DENY_BUILTINS:
|
|
398
|
+
safe.pop(k, None)
|
|
399
|
+
|
|
400
|
+
return safe
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import socket
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .parser import t_to_dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ManipulateZenohResHelper(obj: Any):
|
|
9
|
+
dict_obj = t_to_dict(obj)
|
|
10
|
+
|
|
11
|
+
if dict_obj.hasAttr("returnValue"):
|
|
12
|
+
dict_obj.set("return_value", dict_obj.get("returnValue"))
|
|
13
|
+
del dict_obj["returnValue"]
|
|
14
|
+
|
|
15
|
+
raise ValueError("obj is not a dict")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def get_current_ip(timeout: float = 0.5) -> str:
|
|
19
|
+
hostname = socket.gethostname()
|
|
20
|
+
loop = asyncio.get_running_loop()
|
|
21
|
+
try:
|
|
22
|
+
return await asyncio.wait_for(
|
|
23
|
+
loop.run_in_executor(None, socket.gethostbyname, hostname),
|
|
24
|
+
timeout=timeout,
|
|
25
|
+
)
|
|
26
|
+
except Exception:
|
|
27
|
+
# DNS가 막히면 이전 IP를 쓰거나, 로컬 IP로 폴백
|
|
28
|
+
return "127.0.0.1"
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
from rainbow.rb_flat_buffers.IPC.State_Core import State_Core
|
|
4
|
+
from rainbow.rb_log.log import rb_log
|
|
5
|
+
from rainbow.rb_schemas.sdk import FlowManagerArgs
|
|
6
|
+
from rainbow.rb_zenoh.client import QueryAllResult, QueryResult, T
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def snake_to_title(s: str) -> str:
|
|
10
|
+
return s.replace("_", " ").title()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_payload(results: list[QueryAllResult], /, fm_args: FlowManagerArgs | None = None):
|
|
14
|
+
"""
|
|
15
|
+
반환된 객체 리스트에서 obj_payload 필드가 None인 경우 예외를 던진다.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
results: 반환된 객체 리스트 (obj_payload 필드가 있는지 확인)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
if fm_args is None:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
call_function = inspect.stack()[1].function
|
|
25
|
+
|
|
26
|
+
for result in results:
|
|
27
|
+
obj_payload = result.get("obj_payload", None)
|
|
28
|
+
key = result.get("key", "unknown_key")
|
|
29
|
+
if obj_payload is None:
|
|
30
|
+
raise RuntimeError(
|
|
31
|
+
f"{snake_to_title(call_function)} failed[{key}]: obj_payload is None"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_multi_return_value(
|
|
36
|
+
results: list[QueryAllResult],
|
|
37
|
+
/,
|
|
38
|
+
title: str | None = None,
|
|
39
|
+
fm_args: FlowManagerArgs | None = None,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
PyFM이 실행할 경우 obj_payload에 returnValue 필드가 있고, 0이 아니면 에러로 간주해서 로그 남기고 예외를 던진다.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
obj_payload: 반환된 객체 (returnValue 필드가 있는지 확인)
|
|
46
|
+
key: 키 값 (로그용)
|
|
47
|
+
"""
|
|
48
|
+
if fm_args is None:
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
call_function = inspect.stack()[1].function
|
|
52
|
+
|
|
53
|
+
for result in results:
|
|
54
|
+
obj_payload = result.get("obj_payload", None)
|
|
55
|
+
key = result.get("key", "unknown_key")
|
|
56
|
+
if obj_payload is None:
|
|
57
|
+
raise RuntimeError(
|
|
58
|
+
f"{title if title is not None else snake_to_title(call_function)} failed[{key}]: obj_payload is None"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if hasattr(obj_payload, "returnValue") and obj_payload.returnValue != 0:
|
|
62
|
+
error_msg = f"[{call_function}] return value is {obj_payload.returnValue}"
|
|
63
|
+
if key is not None:
|
|
64
|
+
error_msg = f"[{call_function}] [{key}] return value is {obj_payload.returnValue}"
|
|
65
|
+
rb_log.error(error_msg)
|
|
66
|
+
raise RuntimeError(error_msg)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_single_return_value(res: QueryResult[T], fm_args: FlowManagerArgs | None = None) -> T:
|
|
70
|
+
"""
|
|
71
|
+
PyFM이 실행할 경우 obj_payload에 returnValue 필드가 있고, 0이 아니면 에러로 간주해서 로그 남기고 예외를 던진다.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
res: query_one 결과 객체
|
|
75
|
+
key: 키 값 (로그용)
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
call_function = inspect.stack()[1].function
|
|
79
|
+
|
|
80
|
+
obj_payload = res.get("obj_payload", None)
|
|
81
|
+
if obj_payload is None:
|
|
82
|
+
raise RuntimeError(f"{snake_to_title(call_function)} failed: obj_payload is None")
|
|
83
|
+
|
|
84
|
+
if hasattr(obj_payload, "returnValue") and obj_payload.returnValue != 0:
|
|
85
|
+
error_msg = f"[{call_function}] return value is {obj_payload.returnValue}"
|
|
86
|
+
raise RuntimeError(error_msg)
|
|
87
|
+
|
|
88
|
+
if fm_args is not None:
|
|
89
|
+
fm_args.done()
|
|
90
|
+
|
|
91
|
+
return obj_payload
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def parse_state_core_mv_to_dict(mv: memoryview) -> dict[str, object]:
|
|
95
|
+
"""
|
|
96
|
+
State_Core 메모리뷰를 딕셔너리로 변환
|
|
97
|
+
Args:
|
|
98
|
+
mv: State_Core 메모리뷰
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
dict[str, object]: State_Core 메모리뷰를 딕셔너리로 변환
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def _numpy_or_list_to_list(values) -> list[float | int]:
|
|
105
|
+
return values.tolist() if hasattr(values, "tolist") else list(values)
|
|
106
|
+
|
|
107
|
+
def _parse_string(value) -> str | None:
|
|
108
|
+
if value is None:
|
|
109
|
+
return None
|
|
110
|
+
if isinstance(value, memoryview):
|
|
111
|
+
value = bytes(value)
|
|
112
|
+
if isinstance(value, bytes | bytearray):
|
|
113
|
+
return bytes(value).decode("utf-8", "ignore")
|
|
114
|
+
return str(value)
|
|
115
|
+
|
|
116
|
+
def _parse_float_array_struct(obj: object) -> dict | None:
|
|
117
|
+
if obj is None:
|
|
118
|
+
return None
|
|
119
|
+
values = obj.FAsNumpy() if hasattr(obj, "FAsNumpy") else obj.F()
|
|
120
|
+
return {"f": _numpy_or_list_to_list(values)}
|
|
121
|
+
|
|
122
|
+
def _parse_uint_array_struct(obj: object) -> dict | None:
|
|
123
|
+
if obj is None:
|
|
124
|
+
return None
|
|
125
|
+
values = obj.UAsNumpy() if hasattr(obj, "UAsNumpy") else obj.U()
|
|
126
|
+
return {"u": _numpy_or_list_to_list(values)}
|
|
127
|
+
|
|
128
|
+
obj = State_Core.GetRootAs(mv, 0)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
"heartBeat": obj.HeartBeat(),
|
|
132
|
+
"timestampUsecMono": obj.TimestampUsecMono(),
|
|
133
|
+
"jointQRef": _parse_float_array_struct(obj.JointQRef()),
|
|
134
|
+
"jointQEnc": _parse_float_array_struct(obj.JointQEnc()),
|
|
135
|
+
"jointTEsti": _parse_float_array_struct(obj.JointTEsti()),
|
|
136
|
+
"jointTMeas": _parse_float_array_struct(obj.JointTMeas()),
|
|
137
|
+
"jointTemper": _parse_float_array_struct(obj.JointTemper()),
|
|
138
|
+
"carteXRef": _parse_float_array_struct(obj.CarteXRef()),
|
|
139
|
+
"carteXEnc": _parse_float_array_struct(obj.CarteXEnc()),
|
|
140
|
+
"carteSpeedRef": _parse_float_array_struct(obj.CarteSpeedRef()),
|
|
141
|
+
"carteSpeedEnc": _parse_float_array_struct(obj.CarteSpeedEnc()),
|
|
142
|
+
"userfSelectionNo": obj.UserfSelectionNo(),
|
|
143
|
+
"userfXRef": _parse_float_array_struct(obj.UserfXRef()),
|
|
144
|
+
"toolSelectionNo": obj.ToolSelectionNo(),
|
|
145
|
+
"toolName": _parse_string(obj.ToolName()),
|
|
146
|
+
"toolTcpX": obj.ToolTcpX(),
|
|
147
|
+
"toolTcpY": obj.ToolTcpY(),
|
|
148
|
+
"toolTcpZ": obj.ToolTcpZ(),
|
|
149
|
+
"toolTcpRx": obj.ToolTcpRx(),
|
|
150
|
+
"toolTcpRy": obj.ToolTcpRy(),
|
|
151
|
+
"toolTcpRz": obj.ToolTcpRz(),
|
|
152
|
+
"toolComM": obj.ToolComM(),
|
|
153
|
+
"toolComX": obj.ToolComX(),
|
|
154
|
+
"toolComY": obj.ToolComY(),
|
|
155
|
+
"toolComZ": obj.ToolComZ(),
|
|
156
|
+
"cboxDigitalInput": _parse_uint_array_struct(obj.CboxDigitalInput()),
|
|
157
|
+
"cboxDigitalOutput": _parse_uint_array_struct(obj.CboxDigitalOutput()),
|
|
158
|
+
"cboxAnalogInput": _parse_float_array_struct(obj.CboxAnalogInput()),
|
|
159
|
+
"cboxAnalogOutput": _parse_float_array_struct(obj.CboxAnalogOutput()),
|
|
160
|
+
"exDigitalInput": _parse_uint_array_struct(obj.ExDigitalInput()),
|
|
161
|
+
"exDigitalOutput": _parse_uint_array_struct(obj.ExDigitalOutput()),
|
|
162
|
+
"exAnalogInput": _parse_float_array_struct(obj.ExAnalogInput()),
|
|
163
|
+
"exAnalogOutput": _parse_float_array_struct(obj.ExAnalogOutput()),
|
|
164
|
+
"toolDigitalInput": _parse_uint_array_struct(obj.ToolDigitalInput()),
|
|
165
|
+
"toolDigitalOutput": _parse_uint_array_struct(obj.ToolDigitalOutput()),
|
|
166
|
+
"toolAnalogInput": _parse_float_array_struct(obj.ToolAnalogInput()),
|
|
167
|
+
"toolAnalogOutput": _parse_float_array_struct(obj.ToolAnalogOutput()),
|
|
168
|
+
"toolVoltageOutput": obj.ToolVoltageOutput(),
|
|
169
|
+
"motionMode": obj.MotionMode(),
|
|
170
|
+
"motionExecutionResult": obj.MotionExecutionResult(),
|
|
171
|
+
"motionSpeedBar": obj.MotionSpeedBar(),
|
|
172
|
+
"motionIsPause": obj.MotionIsPause(),
|
|
173
|
+
"statusLan2can": obj.StatusLan2can(),
|
|
174
|
+
"statusSwitchEmg": obj.StatusSwitchEmg(),
|
|
175
|
+
"statusPowerOut": obj.StatusPowerOut(),
|
|
176
|
+
"statusServoNum": obj.StatusServoNum(),
|
|
177
|
+
"statusIsRefon": obj.StatusIsRefon(),
|
|
178
|
+
"statusOutColl": obj.StatusOutColl(),
|
|
179
|
+
"statusSelfColl": obj.StatusSelfColl(),
|
|
180
|
+
"statusDtMode": obj.StatusDtMode(),
|
|
181
|
+
"statusPyfmState": obj.StatusPyfmState(),
|
|
182
|
+
"forcetorqueExtX": obj.ForcetorqueExtX(),
|
|
183
|
+
"forcetorqueExtY": obj.ForcetorqueExtY(),
|
|
184
|
+
"forcetorqueExtZ": obj.ForcetorqueExtZ(),
|
|
185
|
+
"forcetorqueExtRx": obj.ForcetorqueExtRx(),
|
|
186
|
+
"forcetorqueExtRy": obj.ForcetorqueExtRy(),
|
|
187
|
+
"forcetorqueExtRz": obj.ForcetorqueExtRz(),
|
|
188
|
+
"forcetorqueIntX": obj.ForcetorqueIntX(),
|
|
189
|
+
"forcetorqueIntY": obj.ForcetorqueIntY(),
|
|
190
|
+
"forcetorqueIntZ": obj.ForcetorqueIntZ(),
|
|
191
|
+
"forcetorqueIntRx": obj.ForcetorqueIntRx(),
|
|
192
|
+
"forcetorqueIntRy": obj.ForcetorqueIntRy(),
|
|
193
|
+
"forcetorqueIntRz": obj.ForcetorqueIntRz(),
|
|
194
|
+
"weldInfoTxChunk": obj.WeldInfoTxChunk(),
|
|
195
|
+
"weldInfoTxCurrent": obj.WeldInfoTxCurrent(),
|
|
196
|
+
"weldInfoTxVoltage": obj.WeldInfoTxVoltage(),
|
|
197
|
+
"weldInfoTxFeedrate": obj.WeldInfoTxFeedrate(),
|
|
198
|
+
"weldInfoRxChunk": obj.WeldInfoRxChunk(),
|
|
199
|
+
"weldInfoRxCurrent": obj.WeldInfoRxCurrent(),
|
|
200
|
+
"weldInfoRxVoltage": obj.WeldInfoRxVoltage(),
|
|
201
|
+
"weldInfoRxFeedrate": obj.WeldInfoRxFeedrate(),
|
|
202
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
[페이지네이션 응답 타입]
|
|
3
|
+
"""
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RequestPagination(BaseModel):
|
|
10
|
+
pageNo: int = Field(1, example=1)
|
|
11
|
+
pageSize: int = Field(10, example=10)
|
|
12
|
+
searchText: str = Field("", example="")
|
|
13
|
+
sortOption: str = Field("createdAt", example="createdAt")
|
|
14
|
+
sortDirection: Literal["ASC", "DESC"] | None = Field(None, example="ASC")
|
|
15
|
+
|
|
16
|
+
def get_offset(self) -> int:
|
|
17
|
+
# if self.pageNo is None or self.pageNo < 1:
|
|
18
|
+
# self.pageNo = 1
|
|
19
|
+
# if self.pageSize is None or self.pageSize < 1:
|
|
20
|
+
# self.pageSize = 10
|
|
21
|
+
return (self.pageNo - 1) * self.pageSize
|
|
22
|
+
|
|
23
|
+
def get_limit(self) -> int:
|
|
24
|
+
# if self.pageSize is None or self.pageSize < 1:
|
|
25
|
+
# self.pageSize = 10
|
|
26
|
+
return self.pageSize
|
|
27
|
+
|
|
28
|
+
class ResponsePagination(RequestPagination):
|
|
29
|
+
totalSize: int
|
|
30
|
+
totalPage: int
|
|
31
|
+
|
|
32
|
+
class ResponsePaginationPD(BaseModel):
|
|
33
|
+
"""
|
|
34
|
+
- items: 아이템 목록
|
|
35
|
+
- pageInfo: 페이지 정보
|
|
36
|
+
"""
|
|
37
|
+
items: list[dict]
|
|
38
|
+
pageInfo: ResponsePagination
|