Coverage for src / mysingle / dsl / executor.py: 0%
83 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-02 00:58 +0900
1"""DSL 코드 실행 엔진"""
3import marshal
4import resource
5import signal
6from contextlib import contextmanager
7from types import CodeType
8from typing import Any
10import numpy as np
11import pandas as pd
13from mysingle.dsl.errors import DSLExecutionError, DSLMemoryError, DSLTimeoutError
14from mysingle.dsl.parser import DSLParser
17class DSLExecutor:
18 """
19 DSL 코드 안전 실행 엔진
21 리소스 제한과 함께 사용자 코드를 안전하게 실행
22 """
24 # 리소스 제한 설정
25 MAX_EXECUTION_TIME_SECONDS = 30 # 최대 실행 시간
26 MAX_MEMORY_MB = 512 # 최대 메모리
27 MAX_RECURSION_DEPTH = 100 # 최대 재귀 깊이
29 def __init__(self, parser: DSLParser | None = None):
30 """
31 DSLExecutor 초기화
33 Args:
34 parser: DSL 파서 인스턴스 (None이면 새로 생성)
35 """
36 self.parser = parser or DSLParser()
38 def execute(
39 self,
40 compiled_code: bytes | CodeType,
41 data: pd.DataFrame,
42 params: dict[str, Any],
43 ) -> pd.Series | pd.DataFrame:
44 """
45 컴파일된 DSL 코드 실행
47 Args:
48 compiled_code: 컴파일된 바이트코드 (bytes) 또는 code object (CodeType)
49 data: OHLCV 데이터프레임
50 params: 파라미터 딕셔너리
52 Returns:
53 pd.Series | pd.DataFrame: 계산 결과
55 Raises:
56 DSLExecutionError: 실행 중 에러 발생
57 DSLTimeoutError: 실행 시간 초과
58 DSLMemoryError: 메모리 제한 초과
59 """
60 with self._resource_limits():
61 try:
62 # 바이트코드인 경우 code object로 변환
63 if isinstance(compiled_code, bytes):
64 code_object = marshal.loads(compiled_code)
65 else:
66 code_object = compiled_code
68 # 안전한 글로벌 네임스페이스 구성
69 namespace = self._build_namespace(data, params)
71 # 바이트코드 실행
72 exec(code_object, namespace) # nosec B102 - Controlled execution with RestrictedPython
74 # 'result' 변수 확인 (DSL 코드가 result = ... 형식으로 작성됨)
75 if "result" not in namespace:
76 raise DSLExecutionError(
77 "Variable 'result' not found. DSL code must assign result to 'result' variable"
78 )
80 result = namespace["result"]
82 # 결과 타입 검증
83 if not isinstance(result, (pd.Series, pd.DataFrame)):
84 raise DSLExecutionError(
85 f"Result must be pd.Series or pd.DataFrame, "
86 f"got {type(result).__name__}"
87 )
89 return result
91 except TimeoutError as e:
92 raise DSLTimeoutError(
93 f"Execution exceeded {self.MAX_EXECUTION_TIME_SECONDS}s timeout. "
94 f"Consider optimizing your code or reducing data size."
95 ) from e
97 except MemoryError as e:
98 raise DSLMemoryError(
99 f"Execution exceeded {self.MAX_MEMORY_MB}MB memory limit. "
100 f"Consider reducing data size or simplifying calculation."
101 ) from e
103 except DSLExecutionError:
104 # 이미 DSL 에러면 그대로 re-raise
105 raise
107 except Exception as e:
108 # 기타 예외를 DSLExecutionError로 래핑
109 raise DSLExecutionError(f"Execution failed: {e}") from e
111 def compile_and_execute(
112 self,
113 code: str,
114 data: pd.DataFrame,
115 params: dict[str, Any],
116 ) -> pd.Series | pd.DataFrame:
117 """
118 DSL 코드 컴파일 및 실행 (편의 함수)
120 Args:
121 code: DSL 소스 코드
122 data: OHLCV 데이터프레임
123 params: 파라미터 딕셔너리
125 Returns:
126 pd.Series | pd.DataFrame: 계산 결과
127 """
128 compiled_code = self.parser.parse(code)
129 return self.execute(compiled_code, data, params)
131 def _build_namespace(
132 self, data: pd.DataFrame, params: dict[str, Any]
133 ) -> dict[str, Any]:
134 """
135 안전한 실행 네임스페이스 구성
137 Args:
138 data: OHLCV 데이터
139 params: 파라미터
141 Returns:
142 dict: 실행 네임스페이스
143 """
144 # 기본 안전 글로벌
145 namespace = self.parser.get_safe_globals()
147 # NumPy, Pandas 추가
148 namespace.update(
149 {
150 "np": np,
151 "pd": pd,
152 # 데이터
153 "data": data,
154 # 파라미터 딕셔너리 (전략에서 params['key'] 또는 params.get('key', default) 형식으로 접근)
155 "params": params,
156 }
157 )
159 # 파라미터를 개별 변수로도 주입 (하위 호환성)
160 namespace.update(params)
162 # StdLib 함수 추가
163 from mysingle.dsl.stdlib import get_stdlib_functions
165 namespace.update(get_stdlib_functions())
167 return namespace
169 @contextmanager
170 def _resource_limits(self):
171 """
172 리소스 제한 컨텍스트 매니저
174 CPU 시간 및 메모리 제한 적용
175 """
176 # 기존 제한 값 저장
177 old_recursion_limit = None
178 old_alarm_handler = None
179 old_memory_limit = None
181 try:
182 # 재귀 깊이 제한
183 import sys
185 old_recursion_limit = sys.getrecursionlimit()
186 sys.setrecursionlimit(self.MAX_RECURSION_DEPTH)
188 # CPU 시간 제한 (UNIX 시스템만)
189 try:
191 def timeout_handler(signum, frame):
192 raise TimeoutError("Execution time limit exceeded")
194 old_alarm_handler = signal.signal(signal.SIGALRM, timeout_handler)
195 signal.alarm(self.MAX_EXECUTION_TIME_SECONDS)
196 except (AttributeError, ValueError):
197 # Windows 등 signal.SIGALRM 미지원 시스템
198 pass
200 # 메모리 제한 (UNIX 시스템만)
201 try:
202 soft, hard = resource.getrlimit(resource.RLIMIT_AS)
203 old_memory_limit = (soft, hard)
205 new_limit = self.MAX_MEMORY_MB * 1024 * 1024
206 resource.setrlimit(resource.RLIMIT_AS, (new_limit, new_limit))
207 except (AttributeError, ValueError):
208 # Windows 등 resource 미지원 시스템
209 pass
211 yield
213 finally:
214 # 리소스 제한 복원
215 if old_recursion_limit is not None:
216 import sys
218 sys.setrecursionlimit(old_recursion_limit)
220 if old_alarm_handler is not None:
221 signal.alarm(0)
222 signal.signal(signal.SIGALRM, old_alarm_handler)
224 if old_memory_limit is not None:
225 try:
226 resource.setrlimit(resource.RLIMIT_AS, old_memory_limit)
227 except (AttributeError, ValueError):
228 pass