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,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
[파서 유틸리티]
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import (
|
|
7
|
+
Mapping,
|
|
8
|
+
)
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import orjson
|
|
16
|
+
except ImportError:
|
|
17
|
+
orjson = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def bytes_to_str(b: bytes, encoding: str = "utf-8", errors: str = "strict") -> str:
|
|
21
|
+
return b.decode(encoding, errors)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _orjson_default(obj: Any) -> Any:
|
|
25
|
+
if np is not None and isinstance(obj, np.generic):
|
|
26
|
+
return obj.item()
|
|
27
|
+
if isinstance(obj, bytes | bytearray | memoryview):
|
|
28
|
+
if isinstance(obj, memoryview):
|
|
29
|
+
obj = bytes(obj)
|
|
30
|
+
try:
|
|
31
|
+
return obj.decode("utf-8")
|
|
32
|
+
except UnicodeDecodeError:
|
|
33
|
+
return list(obj)
|
|
34
|
+
if isinstance(obj, set):
|
|
35
|
+
return list(obj)
|
|
36
|
+
|
|
37
|
+
to_list = getattr(obj, "tolist", None)
|
|
38
|
+
if callable(to_list):
|
|
39
|
+
return to_list()
|
|
40
|
+
|
|
41
|
+
obj_dict = getattr(obj, "__dict__", None)
|
|
42
|
+
if obj_dict is not None:
|
|
43
|
+
return obj_dict
|
|
44
|
+
|
|
45
|
+
slots = getattr(type(obj), "__slots__", None)
|
|
46
|
+
if slots:
|
|
47
|
+
return {key: getattr(obj, key, None) for key in slots}
|
|
48
|
+
|
|
49
|
+
raise TypeError(f"Object of type {type(obj)!r} is not JSON serializable")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _t_to_dict_via_orjson(obj: Any) -> Any:
|
|
53
|
+
if orjson is None:
|
|
54
|
+
raise TypeError("orjson unavailable")
|
|
55
|
+
|
|
56
|
+
return orjson.loads(
|
|
57
|
+
orjson.dumps(
|
|
58
|
+
obj,
|
|
59
|
+
default=_orjson_default,
|
|
60
|
+
option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def snake_to_camel(s: str) -> str:
|
|
66
|
+
"""
|
|
67
|
+
- s: 스네이크 케이스 문자열
|
|
68
|
+
- 스네이크 케이스 문자열을 카멜 케이스 문자열로 변환
|
|
69
|
+
"""
|
|
70
|
+
parts = s.split("_")
|
|
71
|
+
return "".join(p[:1].upper() + p[1:] for p in parts if p)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def camel_to_snake(s: str) -> str:
|
|
75
|
+
"""
|
|
76
|
+
- s: 카멜 케이스 문자열
|
|
77
|
+
- 카멜 케이스 문자열을 스네이크 케이스 문자열로 변환
|
|
78
|
+
"""
|
|
79
|
+
return "".join("_" + word.lower() if word.isupper() else word for word in s).lstrip("_")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def t_to_dict(obj: Any) -> Any:
|
|
83
|
+
"""
|
|
84
|
+
- obj: 객체
|
|
85
|
+
- 객체를 딕셔너리로 변환
|
|
86
|
+
"""
|
|
87
|
+
# numpy scalar (np.float32, np.int64 등) -> Python scalar
|
|
88
|
+
if np is not None and isinstance(obj, np.generic):
|
|
89
|
+
return obj.item()
|
|
90
|
+
# 원시형
|
|
91
|
+
if obj is None or isinstance(obj, bool | int | float | str):
|
|
92
|
+
return obj
|
|
93
|
+
# numpy 배열
|
|
94
|
+
if np is not None and isinstance(obj, np.ndarray):
|
|
95
|
+
return obj.tolist()
|
|
96
|
+
# bytes류 → string 디코딩
|
|
97
|
+
if isinstance(obj, bytes | bytearray | memoryview):
|
|
98
|
+
if isinstance(obj, memoryview):
|
|
99
|
+
obj = bytes(obj)
|
|
100
|
+
try:
|
|
101
|
+
return obj.decode("utf-8")
|
|
102
|
+
except UnicodeDecodeError:
|
|
103
|
+
return list(obj)
|
|
104
|
+
# dict류
|
|
105
|
+
if isinstance(obj, Mapping):
|
|
106
|
+
return {k: t_to_dict(v) for k, v in obj.items()}
|
|
107
|
+
# 시퀀스
|
|
108
|
+
if isinstance(obj, list | tuple | set):
|
|
109
|
+
return [t_to_dict(v) for v in obj]
|
|
110
|
+
# 큰 dict-backed/slotted 객체는 C serializer 경로를 우선 시도
|
|
111
|
+
if orjson is not None and (
|
|
112
|
+
getattr(obj, "__dict__", None) is not None or getattr(type(obj), "__slots__", None)
|
|
113
|
+
):
|
|
114
|
+
try:
|
|
115
|
+
return _t_to_dict_via_orjson(obj)
|
|
116
|
+
except (TypeError, orjson.JSONEncodeError, orjson.JSONDecodeError):
|
|
117
|
+
pass
|
|
118
|
+
# 일반/T-object
|
|
119
|
+
if hasattr(obj, "__dict__"):
|
|
120
|
+
return {k: t_to_dict(v) for k, v in vars(obj).items()}
|
|
121
|
+
# 마지막 안전핀
|
|
122
|
+
return str(obj)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _normalize_filter(filter_: Any) -> dict:
|
|
126
|
+
"""
|
|
127
|
+
filter_가 None, dict, JSON string 모두 OK.
|
|
128
|
+
빈 문자열/공백/'null'/'None'이면 {} 반환.
|
|
129
|
+
"""
|
|
130
|
+
if filter_ is None:
|
|
131
|
+
return {}
|
|
132
|
+
if isinstance(filter_, Mapping):
|
|
133
|
+
return dict(filter_)
|
|
134
|
+
if isinstance(filter_, str):
|
|
135
|
+
s = filter_.strip()
|
|
136
|
+
print("s : ", s)
|
|
137
|
+
if s == "" or s.lower() in ("null", "none"):
|
|
138
|
+
return {}
|
|
139
|
+
try:
|
|
140
|
+
data = json.loads(s)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
raise TypeError(f"filter_ JSON 파싱 실패: {e}") from e
|
|
143
|
+
if not isinstance(data, dict):
|
|
144
|
+
raise TypeError("filter_ 문자열은 JSON object여야 합니다.")
|
|
145
|
+
return data
|
|
146
|
+
raise TypeError(f"filter_ 타입이 잘못되었습니다: {type(filter_)}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def to_json(obj: Any) -> str:
|
|
150
|
+
"""
|
|
151
|
+
- obj: 객체
|
|
152
|
+
- 객체를 JSON 문자열로 변환
|
|
153
|
+
"""
|
|
154
|
+
return json.dumps(t_to_dict(obj), ensure_ascii=False)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def to_iso(val: str | datetime):
|
|
158
|
+
"""
|
|
159
|
+
- val: 문자열 또는 datetime
|
|
160
|
+
- 문자열 또는 datetime를 ISO 형식의 문자열로 변환
|
|
161
|
+
"""
|
|
162
|
+
if isinstance(val, datetime):
|
|
163
|
+
return val.isoformat()
|
|
164
|
+
|
|
165
|
+
return val
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
[서비스 예외 클래스]
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import (
|
|
5
|
+
annotations,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from dataclasses import (
|
|
9
|
+
dataclass,
|
|
10
|
+
)
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(slots=True)
|
|
15
|
+
class ServiceException(Exception):
|
|
16
|
+
"""
|
|
17
|
+
- message: 예외 메시지
|
|
18
|
+
- status_code: HTTP 상태 코드 (기본: None)
|
|
19
|
+
"""
|
|
20
|
+
message: str
|
|
21
|
+
status_code: int | None = None # HTTP 등
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
base = self.message
|
|
25
|
+
if self.status_code is not None:
|
|
26
|
+
base += f" (status={self.status_code})"
|
|
27
|
+
return base
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
- message: 예외 메시지
|
|
32
|
+
- status_code: HTTP 상태 코드
|
|
33
|
+
"""
|
|
34
|
+
return {
|
|
35
|
+
"message": self.message,
|
|
36
|
+
"status_code": self.status_code,
|
|
37
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rainbow-rb-utils
|
|
3
|
+
Version: 0.0.9.dev5
|
|
4
|
+
Summary: Rainbow Python Utilities
|
|
5
|
+
Author-email: Derek <dfd1123@rainbow-robotics.com>
|
|
6
|
+
Requires-Python: <3.13,>=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: psutil<8.0.0,>=7.0.0
|
|
9
|
+
Requires-Dist: rainbow-rb-log==0.0.9.dev5
|
|
10
|
+
|
|
11
|
+
# rb_utils
|
|
12
|
+
|
|
13
|
+
`rb_utils`는 백엔드 전반에서 재사용하는 유틸리티 모음 패키지입니다.
|
|
14
|
+
|
|
15
|
+
## 모듈/함수 상세
|
|
16
|
+
|
|
17
|
+
## 1) `rb_utils.parser`
|
|
18
|
+
|
|
19
|
+
- `snake_to_camel(s: str) -> str`: snake_case 문자열을 CamelCase 형태로 변환
|
|
20
|
+
- `camel_to_snake(s: str) -> str`: CamelCase 문자열을 snake_case로 변환
|
|
21
|
+
- `t_to_dict(obj: Any) -> Any`: 중첩 객체(FlatBuffer T, dict/list/tuple/set, bytes, numpy 배열)를 Python 기본 타입으로 재귀 변환
|
|
22
|
+
- `_normalize_filter(filter_: Any) -> dict`: `None/dict/JSON 문자열`을 공통 dict로 정규화
|
|
23
|
+
- `to_json(obj: Any) -> str`: `t_to_dict` 결과를 JSON 문자열로 직렬화
|
|
24
|
+
- `to_iso(val: str | datetime) -> str`: datetime이면 ISO 문자열, 문자열이면 그대로 반환
|
|
25
|
+
|
|
26
|
+
예시:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from rb_utils.parser import t_to_dict, to_json
|
|
30
|
+
|
|
31
|
+
payload_dict = t_to_dict(obj_payload)
|
|
32
|
+
payload_json = to_json(obj_payload)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 2) `rb_utils.flow_manager`
|
|
36
|
+
|
|
37
|
+
- `safe_eval_expr(expr, variables=None, get_global_variable=None) -> Any`: 제한된 AST 규칙으로 안전 평가
|
|
38
|
+
- `_pick_target_loop(func, provided) -> asyncio.AbstractEventLoop | None`: callable 실행 대상 loop 선택
|
|
39
|
+
- `call_with_matching_args(func, **provided) -> Any`: 함수 시그니처에 맞춰 인자만 골라 호출 (sync/async 공통)
|
|
40
|
+
- `eval_value(value, variables=None, get_global_variable=None) -> Any`: 문자열/리스트/튜플/딕셔너리를 재귀 평가
|
|
41
|
+
- `make_builtins_allow_most() -> dict`: 위험 builtins 제외한 builtins dict 생성
|
|
42
|
+
|
|
43
|
+
예시:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from rb_utils.flow_manager import eval_value
|
|
47
|
+
|
|
48
|
+
value = eval_value("a + 10", variables={"local": {"a": 5}, "global": {}})
|
|
49
|
+
# value == 15
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 3) `rb_utils.asyncio_helper`
|
|
53
|
+
|
|
54
|
+
- `spawn(coro, *, name=None, log_ex=True)`: 현재 loop에 task 생성 후 완료/예외 로깅
|
|
55
|
+
- `_ensure_private_loop() -> asyncio.AbstractEventLoop`: 전용 백그라운드 loop 확보
|
|
56
|
+
- `fire_and_log(coro, *, name=None, loop=None)`: 현재 loop/외부 loop/전용 loop에 안전하게 실행
|
|
57
|
+
- `_attach_done_logging(fut_or_task, *, name=None)`: Future/Task 완료 시 예외 로깅 callback 연결
|
|
58
|
+
|
|
59
|
+
예시:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from rb_utils.asyncio_helper import fire_and_log
|
|
63
|
+
|
|
64
|
+
fire_and_log(socket_client.emit("muscat/program/log", {"msg": "hello"}), name="program-log")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 4) `rb_utils.service_exception`
|
|
68
|
+
|
|
69
|
+
- `class ServiceException(Exception)`:
|
|
70
|
+
- 필드: `message: str`, `status_code: int | None`
|
|
71
|
+
- `__str__()`: `message (status=...)` 형태 출력
|
|
72
|
+
- `to_dict()`: `{message, status_code}` 반환
|
|
73
|
+
|
|
74
|
+
예시:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from rb_utils.service_exception import ServiceException
|
|
78
|
+
|
|
79
|
+
raise ServiceException("invalid request", status_code=400)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 5) `rb_utils.date`
|
|
83
|
+
|
|
84
|
+
- `is_valid_date(d)`: `date/str/int(timestamp)` 유효성 검사
|
|
85
|
+
- `timestamp_ms_to_seconds(timestamp, tz=UTC)`: ms/seconds timestamp를 seconds로 정규화
|
|
86
|
+
- `is_has_time(s: str)`: 문자열에 시간 정보 포함 여부 판단
|
|
87
|
+
- `parse_date_with_time(mode, d, tz=UTC)`: 날짜/문자열/타임스탬프를 timezone-aware datetime으로 변환
|
|
88
|
+
- `ensure_datetime(dt, tz)`: `date | datetime`을 지정 tz의 datetime으로 보정
|
|
89
|
+
- `_tz(tz_name)`: timezone 캐시 조회
|
|
90
|
+
- `convert_dt(local, in_tz, out_tz)`: 입력 tz 기준 datetime/date를 출력 tz로 변환
|
|
91
|
+
- `get_current_dt_yyyymmddhhmmss(tz)`: 현재 시각을 `YYYYMMDDHHMMSS` 문자열 반환
|
|
92
|
+
|
|
93
|
+
예시:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from rb_utils.date import parse_date_with_time
|
|
97
|
+
from datetime import UTC
|
|
98
|
+
|
|
99
|
+
dt = parse_date_with_time("start", "2026-02-25", tz=UTC)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 6) `rb_utils.file`
|
|
103
|
+
|
|
104
|
+
- `content_disposition_header(filename) -> str`: 다운로드용 `Content-Disposition` 헤더 생성
|
|
105
|
+
- `sanitize_filename(name, default="export.csv", max_name_len=100) -> str`: OS 금지문자 제거/길이 제한
|
|
106
|
+
- `get_env_path() -> Path | None`: 실행 위치 기준으로 `.env` 파일 탐색
|
|
107
|
+
|
|
108
|
+
예시:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from rb_utils.file import sanitize_filename
|
|
112
|
+
|
|
113
|
+
safe_name = sanitize_filename("../../report?.csv")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 7) `rb_utils.pagination`
|
|
117
|
+
|
|
118
|
+
- `class PageInfo(TypedDict)`: 페이지 응답 메타 타입 (`mode/page/pages/limit/total/sort/order`)
|
|
119
|
+
- `class LogsResponse(TypedDict)`: 로그 목록 응답 타입 (`items`, `pageInfo`)
|
|
120
|
+
|
|
121
|
+
## 8) `rb_utils.helper`
|
|
122
|
+
|
|
123
|
+
- `ManipulateZenohResHelper(obj)`: Zenoh 응답 객체 보정 유틸 의도 함수
|
|
124
|
+
|
|
125
|
+
주의:
|
|
126
|
+
- 현재 구현은 dict 처리가 아닌 `.hasAttr/.set` 접근과 즉시 `ValueError`를 포함하므로, 사용 전 동작 검증이 필요합니다.
|
|
127
|
+
|
|
128
|
+
## 실무 사용 패턴
|
|
129
|
+
|
|
130
|
+
- FlatBuffer/SDK 응답 평탄화: `t_to_dict`
|
|
131
|
+
- Flow step 동적 인자 평가: `eval_value`, `safe_eval_expr`
|
|
132
|
+
- 비동기 fire-and-forget + 예외 로깅: `fire_and_log`
|
|
133
|
+
- 서비스 레이어 공통 예외: `ServiceException`
|
|
134
|
+
- 파일 다운로드/이름 정리: `content_disposition_header`, `sanitize_filename`
|
|
135
|
+
|
|
136
|
+
## 공통 주의사항
|
|
137
|
+
|
|
138
|
+
- `flow_manager` 평가기는 허용된 연산/함수만 실행합니다.
|
|
139
|
+
- timezone 변환 함수는 `Asia/Seoul` 같은 IANA tz 문자열을 명시해야 합니다.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
rainbow/rb_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
rainbow/rb_utils/asyncio_helper.py,sha256=NhKx63RDOf5Q-fjrKuxYa901eLo1jtaaOL1DWYd-Mzw,4866
|
|
3
|
+
rainbow/rb_utils/date.py,sha256=jw9LyTbNmvWerKK5APqhJi7ScElBla_V0Ez7Sk-eh7M,6396
|
|
4
|
+
rainbow/rb_utils/file.py,sha256=rrQbfRFqemC3fNAT1th4bd75_HSJ6CHhQ2BuJYz_ZAU,1369
|
|
5
|
+
rainbow/rb_utils/flow_manager.py,sha256=Gk67dCQ8_4CvO5qdW7JwtuNh4TKV2eERaPHLwB-Ptpc,13834
|
|
6
|
+
rainbow/rb_utils/helper.py,sha256=0ZfbhbrdA44qak5yNckBuYEbi575Gti0O_ZD1GyBloY,751
|
|
7
|
+
rainbow/rb_utils/manipulate.py,sha256=hnYGa7GhFhku1tfCE0EQKtpFNQkMnunmY9t9QY3_bAM,8254
|
|
8
|
+
rainbow/rb_utils/pagination.py,sha256=5x8JVRj48bqrYVernpBlAfDxMpAkDg5GyqfHESIZMK4,1077
|
|
9
|
+
rainbow/rb_utils/parser.py,sha256=CADqHJkeTU_i3UczUt9e5jv6zzPNc_8mPBMc9uezbeg,4664
|
|
10
|
+
rainbow/rb_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
rainbow/rb_utils/service_exception.py,sha256=kXVzY_6eFUm8vEcWALGvOGeekcVK4hCqMFVk4MVrbHM,799
|
|
12
|
+
rainbow_rb_utils-0.0.9.dev5.dist-info/METADATA,sha256=9_WkB1OKKNbSSyc93G4x-bsOUhM9k5KOSh6CAYYzQiY,5228
|
|
13
|
+
rainbow_rb_utils-0.0.9.dev5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
rainbow_rb_utils-0.0.9.dev5.dist-info/top_level.txt,sha256=sFePQxnQ07jm2rW5WMQ-MhaAdxyvnPKeXldV_kQjJ_A,8
|
|
15
|
+
rainbow_rb_utils-0.0.9.dev5.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rainbow
|