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

1"""DSL 코드 실행 엔진""" 

2 

3import marshal 

4import resource 

5import signal 

6from contextlib import contextmanager 

7from types import CodeType 

8from typing import Any 

9 

10import numpy as np 

11import pandas as pd 

12 

13from mysingle.dsl.errors import DSLExecutionError, DSLMemoryError, DSLTimeoutError 

14from mysingle.dsl.parser import DSLParser 

15 

16 

17class DSLExecutor: 

18 """ 

19 DSL 코드 안전 실행 엔진 

20 

21 리소스 제한과 함께 사용자 코드를 안전하게 실행 

22 """ 

23 

24 # 리소스 제한 설정 

25 MAX_EXECUTION_TIME_SECONDS = 30 # 최대 실행 시간 

26 MAX_MEMORY_MB = 512 # 최대 메모리 

27 MAX_RECURSION_DEPTH = 100 # 최대 재귀 깊이 

28 

29 def __init__(self, parser: DSLParser | None = None): 

30 """ 

31 DSLExecutor 초기화 

32 

33 Args: 

34 parser: DSL 파서 인스턴스 (None이면 새로 생성) 

35 """ 

36 self.parser = parser or DSLParser() 

37 

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 코드 실행 

46 

47 Args: 

48 compiled_code: 컴파일된 바이트코드 (bytes) 또는 code object (CodeType) 

49 data: OHLCV 데이터프레임 

50 params: 파라미터 딕셔너리 

51 

52 Returns: 

53 pd.Series | pd.DataFrame: 계산 결과 

54 

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 

67 

68 # 안전한 글로벌 네임스페이스 구성 

69 namespace = self._build_namespace(data, params) 

70 

71 # 바이트코드 실행 

72 exec(code_object, namespace) # nosec B102 - Controlled execution with RestrictedPython 

73 

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 ) 

79 

80 result = namespace["result"] 

81 

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 ) 

88 

89 return result 

90 

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 

96 

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 

102 

103 except DSLExecutionError: 

104 # 이미 DSL 에러면 그대로 re-raise 

105 raise 

106 

107 except Exception as e: 

108 # 기타 예외를 DSLExecutionError로 래핑 

109 raise DSLExecutionError(f"Execution failed: {e}") from e 

110 

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 코드 컴파일 및 실행 (편의 함수) 

119 

120 Args: 

121 code: DSL 소스 코드 

122 data: OHLCV 데이터프레임 

123 params: 파라미터 딕셔너리 

124 

125 Returns: 

126 pd.Series | pd.DataFrame: 계산 결과 

127 """ 

128 compiled_code = self.parser.parse(code) 

129 return self.execute(compiled_code, data, params) 

130 

131 def _build_namespace( 

132 self, data: pd.DataFrame, params: dict[str, Any] 

133 ) -> dict[str, Any]: 

134 """ 

135 안전한 실행 네임스페이스 구성 

136 

137 Args: 

138 data: OHLCV 데이터 

139 params: 파라미터 

140 

141 Returns: 

142 dict: 실행 네임스페이스 

143 """ 

144 # 기본 안전 글로벌 

145 namespace = self.parser.get_safe_globals() 

146 

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 ) 

158 

159 # 파라미터를 개별 변수로도 주입 (하위 호환성) 

160 namespace.update(params) 

161 

162 # StdLib 함수 추가 

163 from mysingle.dsl.stdlib import get_stdlib_functions 

164 

165 namespace.update(get_stdlib_functions()) 

166 

167 return namespace 

168 

169 @contextmanager 

170 def _resource_limits(self): 

171 """ 

172 리소스 제한 컨텍스트 매니저 

173 

174 CPU 시간 및 메모리 제한 적용 

175 """ 

176 # 기존 제한 값 저장 

177 old_recursion_limit = None 

178 old_alarm_handler = None 

179 old_memory_limit = None 

180 

181 try: 

182 # 재귀 깊이 제한 

183 import sys 

184 

185 old_recursion_limit = sys.getrecursionlimit() 

186 sys.setrecursionlimit(self.MAX_RECURSION_DEPTH) 

187 

188 # CPU 시간 제한 (UNIX 시스템만) 

189 try: 

190 

191 def timeout_handler(signum, frame): 

192 raise TimeoutError("Execution time limit exceeded") 

193 

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 

199 

200 # 메모리 제한 (UNIX 시스템만) 

201 try: 

202 soft, hard = resource.getrlimit(resource.RLIMIT_AS) 

203 old_memory_limit = (soft, hard) 

204 

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 

210 

211 yield 

212 

213 finally: 

214 # 리소스 제한 복원 

215 if old_recursion_limit is not None: 

216 import sys 

217 

218 sys.setrecursionlimit(old_recursion_limit) 

219 

220 if old_alarm_handler is not None: 

221 signal.alarm(0) 

222 signal.signal(signal.SIGALRM, old_alarm_handler) 

223 

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