opencode-pyneruntime 6.6.4__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.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. pynecore/utils/stdlib_checker.py +17 -0
@@ -0,0 +1,752 @@
1
+ import ast
2
+ import copy
3
+ import hashlib
4
+
5
+
6
+ # Strategy state accessors are meaningful only in the chart context — the
7
+ # security child process has no strategy state of its own, so referencing
8
+ # these in a request.security() / request.security_lower_tf() expression is
9
+ # always a programmer error. We raise SyntaxError when the offending attribute
10
+ # access is direct (i.e. not bound through a local alias). Transitive flow
11
+ # analysis is intentionally skipped — at runtime the security child returns
12
+ # inert defaults via the `lib._script is None` guard in the strategy module,
13
+ # so escaped cases fail safe rather than crash.
14
+ _FORBIDDEN_STRATEGY_STATE_ATTRS = frozenset({
15
+ "equity", "eventrades", "grossloss", "grossprofit", "initial_capital",
16
+ "losstrades", "max_drawdown", "max_runup", "netprofit", "openprofit",
17
+ "position_avg_price", "position_size", "wintrades",
18
+ })
19
+
20
+ # Raw price series the security child can serve WITHOUT running the script's
21
+ # ``main()``: every one is set straight from the bar by ``_set_lib_properties``
22
+ # (the derived sources hl2/hlc3/ohlc4/hlcc4 are pure functions of that bar's
23
+ # OHLC, also set there), so reading ``lib.<field>`` after the per-bar property
24
+ # set is byte-identical to what ``main()`` would have produced. A
25
+ # ``request.security[_lower_tf]`` whose expression is only these can take the
26
+ # fast path (see ``security_process.security_process_main``).
27
+ _OHLCV_PASSTHROUGH_FIELDS = frozenset({
28
+ "open", "high", "low", "close", "volume",
29
+ "hl2", "hlc3", "ohlc4", "hlcc4",
30
+ })
31
+
32
+
33
+ class SecurityTransformer(ast.NodeTransformer):
34
+ """
35
+ Transform request.security() calls into multiprocessing signal/write/read pattern.
36
+
37
+ Transforms each lib.request.security(symbol, timeframe, expression, ...) call:
38
+
39
+ 1. Function start (chart context only):
40
+ if __active_security__ is None:
41
+ __sec_signal__("sec_id", symbol_expr, timeframe_expr)
42
+
43
+ 2. Original call position:
44
+ if __active_security__ == "sec_id":
45
+ __sec_write__("sec_id", expression)
46
+ var = __sec_read__("sec_id", lib.na)
47
+
48
+ 3. Function end (chart context only):
49
+ if __active_security__ is None:
50
+ __sec_wait__("sec_id")
51
+
52
+ Also creates module-level __security_contexts__ dict with metadata for each context.
53
+ Non-constant symbol/timeframe/lookahead values (e.g., function parameters or
54
+ input-derived expressions) are stored as None in __security_contexts__ and
55
+ resolved at runtime via __sec_signal__ arguments.
56
+
57
+ Must be applied after ImportNormalizerTransformer, before PersistentSeriesTransformer.
58
+ """
59
+
60
+ def __init__(self):
61
+ self._counter = 0
62
+ self._all_contexts: dict[str, dict[str, ast.expr]] = {}
63
+ self._signal_args: dict[
64
+ str, tuple[ast.expr | None, ast.expr | None, ast.expr | None]
65
+ ] = {}
66
+ self._needs_barmerge = False
67
+ self._needs_ltf_unzip = False
68
+ self._ltf_sec_ids: set[str] = set()
69
+ self._module_file: str = '<script>'
70
+
71
+ def _gen_id(self) -> str:
72
+ # The module hash keeps sec ids unique across modules: the main script and
73
+ # any imported library may each have their own security calls, and their
74
+ # contexts are merged into one registry by the runner
75
+ module_hash = hashlib.sha1(self._module_file.encode()).hexdigest()[:8]
76
+ sec_id = f"sec\xb7{module_hash}\xb7{self._counter}"
77
+ self._counter += 1
78
+ return sec_id
79
+
80
+ @staticmethod
81
+ def _is_security_call(node: ast.Call) -> bool:
82
+ """Check if node is lib.request.security(...)."""
83
+ return (isinstance(node.func, ast.Attribute)
84
+ and node.func.attr == 'security'
85
+ and isinstance(node.func.value, ast.Attribute)
86
+ and node.func.value.attr == 'request'
87
+ and isinstance(node.func.value.value, ast.Name)
88
+ and node.func.value.value.id == 'lib')
89
+
90
+ @staticmethod
91
+ def _is_security_lower_tf_call(node: ast.Call) -> bool:
92
+ """Check if node is lib.request.security_lower_tf(...)."""
93
+ return (isinstance(node.func, ast.Attribute)
94
+ and node.func.attr == 'security_lower_tf'
95
+ and isinstance(node.func.value, ast.Attribute)
96
+ and node.func.value.attr == 'request'
97
+ and isinstance(node.func.value.value, ast.Name)
98
+ and node.func.value.value.id == 'lib')
99
+
100
+ @staticmethod
101
+ def _extract_ltf_args(call: ast.Call) -> tuple[
102
+ ast.expr | None, ast.expr | None, ast.expr | None, ast.expr | None
103
+ ]:
104
+ """Extract (symbol, timeframe, expression, ignore_invalid_symbol)
105
+ from request.security_lower_tf() call.
106
+
107
+ Note: no gaps parameter (LTF has no gaps/lookahead).
108
+ """
109
+ args = list(call.args)
110
+ kwargs = {kw.arg: kw.value for kw in call.keywords if kw.arg is not None}
111
+ return (
112
+ kwargs.get('symbol', args[0] if len(args) > 0 else None),
113
+ kwargs.get('timeframe', args[1] if len(args) > 1 else None),
114
+ kwargs.get('expression', args[2] if len(args) > 2 else None),
115
+ kwargs.get('ignore_invalid_symbol', args[3] if len(args) > 3 else None),
116
+ )
117
+
118
+ @staticmethod
119
+ def _extract_args(call: ast.Call) -> tuple[
120
+ ast.expr | None, ast.expr | None, ast.expr | None, ast.expr | None,
121
+ ast.expr | None, ast.expr | None, ast.expr | None
122
+ ]:
123
+ """Extract (symbol, timeframe, expression, gaps, lookahead,
124
+ ignore_invalid_symbol, currency) from request.security() call.
125
+
126
+ Positional order matches Pine v6:
127
+ ``security(symbol, timeframe, expression, gaps, lookahead,
128
+ ignore_invalid_symbol, currency, ...)``.
129
+ """
130
+ args = list(call.args)
131
+ kwargs = {kw.arg: kw.value for kw in call.keywords if kw.arg is not None}
132
+ return (
133
+ kwargs.get('symbol', args[0] if len(args) > 0 else None),
134
+ kwargs.get('timeframe', args[1] if len(args) > 1 else None),
135
+ kwargs.get('expression', args[2] if len(args) > 2 else None),
136
+ kwargs.get('gaps', args[3] if len(args) > 3 else None),
137
+ kwargs.get('lookahead', args[4] if len(args) > 4 else None),
138
+ kwargs.get('ignore_invalid_symbol', args[5] if len(args) > 5 else None),
139
+ kwargs.get('currency', args[6] if len(args) > 6 else None),
140
+ )
141
+
142
+ @staticmethod
143
+ def _walk_skip_funcs(node: ast.AST):
144
+ """Walk AST nodes, skipping nested function definitions."""
145
+ yield node
146
+ for child in ast.iter_child_nodes(node):
147
+ if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
148
+ continue
149
+ yield from SecurityTransformer._walk_skip_funcs(child)
150
+
151
+ @staticmethod
152
+ def _find_forbidden_strategy_state(expr: ast.expr) -> ast.Attribute | None:
153
+ """Return the first `lib.strategy.<state_attr>` Attribute node found in
154
+ ``expr``, or None if there is none.
155
+
156
+ Only direct attribute chains (`lib.strategy.position_size` form,
157
+ produced by ImportNormalizerTransformer for any of `strategy.x`,
158
+ `from pynecore.lib import strategy; strategy.x`, or
159
+ `from pynecore.lib.strategy import x`) are detected. Local aliases
160
+ (`ps = strategy.position_size; security(..., ps)`) are not —
161
+ catching those would need a def-use chain analyzer; instead the
162
+ runtime fallback in the strategy module returns inert defaults.
163
+ """
164
+ for sub in ast.walk(expr):
165
+ if not isinstance(sub, ast.Attribute):
166
+ continue
167
+ if sub.attr not in _FORBIDDEN_STRATEGY_STATE_ATTRS:
168
+ continue
169
+ parent = sub.value
170
+ if (isinstance(parent, ast.Attribute) and parent.attr == 'strategy'
171
+ and isinstance(parent.value, ast.Name)
172
+ and parent.value.id == 'lib'):
173
+ return sub
174
+ return None
175
+
176
+ @staticmethod
177
+ def _is_module_level_expr(node: ast.expr) -> bool:
178
+ """Check if an expression can be evaluated at module level.
179
+
180
+ Constants and lib.* attribute chains are safe. Function parameters
181
+ and other local variables are not.
182
+ """
183
+ if isinstance(node, ast.Constant):
184
+ return True
185
+ if isinstance(node, ast.Attribute):
186
+ return SecurityTransformer._is_module_level_expr(node.value)
187
+ if isinstance(node, ast.Name):
188
+ return node.id == 'lib'
189
+ if isinstance(node, ast.Call):
190
+ return SecurityTransformer._is_module_level_expr(node.func)
191
+ return False
192
+
193
+ @staticmethod
194
+ def _ohlcv_field(node: ast.expr) -> str | None:
195
+ """Return the raw-OHLCV field name if ``node`` is a bare reference to one
196
+ (``close`` or ``lib.close``), else None. Conservative: only a plain
197
+ Name/``lib.<attr>`` matches — any computation falls through to the full
198
+ ``main()`` path."""
199
+ if isinstance(node, ast.Name) and node.id in _OHLCV_PASSTHROUGH_FIELDS:
200
+ return node.id
201
+ if (isinstance(node, ast.Attribute) and node.attr in _OHLCV_PASSTHROUGH_FIELDS
202
+ and isinstance(node.value, ast.Name) and node.value.id == 'lib'):
203
+ return node.attr
204
+ return None
205
+
206
+ @classmethod
207
+ def _ohlcv_passthrough(cls, expression: ast.expr | None) -> tuple[list[str], bool] | None:
208
+ """Detect a plain-OHLCV ``request.security[_lower_tf]`` expression.
209
+
210
+ Returns ``(field_names, is_tuple)`` when every element is a raw price
211
+ series (so the security child can serve it without running ``main()``),
212
+ or ``None`` otherwise. ``is_tuple`` is True for a tuple/list expression
213
+ (column-major arrays) and False for a scalar.
214
+ """
215
+ if expression is None:
216
+ return None
217
+ if isinstance(expression, (ast.Tuple, ast.List)):
218
+ if not expression.elts:
219
+ return None
220
+ fields = [cls._ohlcv_field(e) for e in expression.elts]
221
+ if all(f is not None for f in fields):
222
+ return [f for f in fields if f is not None], True
223
+ return None
224
+ field = cls._ohlcv_field(expression)
225
+ if field is not None:
226
+ return [field], False
227
+ return None
228
+
229
+ # --- AST node builders ---
230
+
231
+ @staticmethod
232
+ def _lib_na() -> ast.Attribute:
233
+ """Build: lib.na"""
234
+ return ast.Attribute(
235
+ value=ast.Name(id='lib', ctx=ast.Load()),
236
+ attr='na', ctx=ast.Load()
237
+ )
238
+
239
+ @staticmethod
240
+ def _default_gaps() -> ast.Attribute:
241
+ """Build: lib.barmerge.gaps_off"""
242
+ return ast.Attribute(
243
+ value=ast.Attribute(
244
+ value=ast.Name(id='lib', ctx=ast.Load()),
245
+ attr='barmerge', ctx=ast.Load()
246
+ ),
247
+ attr='gaps_off', ctx=ast.Load()
248
+ )
249
+
250
+ @staticmethod
251
+ def _func_call(name: str, *args: ast.expr) -> ast.Call:
252
+ return ast.Call(
253
+ func=ast.Name(id=name, ctx=ast.Load()),
254
+ args=list(args), keywords=[]
255
+ )
256
+
257
+ @staticmethod
258
+ def _is_none_check() -> ast.Compare:
259
+ """Build: __active_security__ is None"""
260
+ return ast.Compare(
261
+ left=ast.Name(id='__active_security__', ctx=ast.Load()),
262
+ ops=[ast.Is()], comparators=[ast.Constant(value=None)]
263
+ )
264
+
265
+ @staticmethod
266
+ def _eq_check(sec_id: str) -> ast.Compare:
267
+ """Build: __active_security__ == sec_id"""
268
+ return ast.Compare(
269
+ left=ast.Name(id='__active_security__', ctx=ast.Load()),
270
+ ops=[ast.Eq()], comparators=[ast.Constant(value=sec_id)]
271
+ )
272
+
273
+ def _signal_block(self, sec_ids: list[str]) -> ast.If:
274
+ """Build chart-context signal block for function start.
275
+
276
+ Passes actual symbol and timeframe expressions to __sec_signal__
277
+ so that runtime values (e.g., function parameters) are available.
278
+
279
+ Only includes sec_ids whose symbol/timeframe are module-level
280
+ expressions (constants or lib.* refs). Runtime-dependent signals
281
+ are emitted inline before their write blocks by _transform_body.
282
+ """
283
+ body = []
284
+ for s in sec_ids:
285
+ args: list[ast.expr] = [ast.Constant(value=s)]
286
+ # Lookahead is omitted here: a runtime-dependent lookahead forces the
287
+ # sid onto the inline-signal path (see _process_func classification).
288
+ sym_expr, tf_expr, _ = self._signal_args[s]
289
+ args.append(copy.deepcopy(sym_expr) if sym_expr is not None
290
+ else ast.Constant(value=None))
291
+ args.append(copy.deepcopy(tf_expr) if tf_expr is not None
292
+ else ast.Constant(value=None))
293
+ body.append(ast.Expr(value=self._func_call('__sec_signal__', *args)))
294
+ return ast.If(
295
+ test=self._is_none_check(),
296
+ body=body,
297
+ orelse=[]
298
+ )
299
+
300
+ def _inline_signal(self, sec_id: str) -> ast.If:
301
+ """Build a single inline signal for runtime-dependent
302
+ symbol/timeframe/lookahead."""
303
+ args: list[ast.expr] = [ast.Constant(value=sec_id)]
304
+ sym_expr, tf_expr, la_expr = self._signal_args[sec_id]
305
+ args.append(copy.deepcopy(sym_expr) if sym_expr is not None
306
+ else ast.Constant(value=None))
307
+ args.append(copy.deepcopy(tf_expr) if tf_expr is not None
308
+ else ast.Constant(value=None))
309
+ if la_expr is not None:
310
+ args.append(copy.deepcopy(la_expr))
311
+ return ast.If(
312
+ test=self._is_none_check(),
313
+ body=[ast.Expr(value=self._func_call('__sec_signal__', *args))],
314
+ orelse=[]
315
+ )
316
+
317
+ def _wait_block(self, sec_ids: list[str]) -> ast.If:
318
+ """Build chart-context wait block for function end."""
319
+ return ast.If(
320
+ test=self._is_none_check(),
321
+ body=[
322
+ ast.Expr(value=self._func_call('__sec_wait__', ast.Constant(value=s)))
323
+ for s in sec_ids
324
+ ],
325
+ orelse=[]
326
+ )
327
+
328
+ @staticmethod
329
+ def _in_same_context(sec_id: str) -> ast.Compare:
330
+ """Build: sec_id in __same_context__"""
331
+ return ast.Compare(
332
+ left=ast.Constant(value=sec_id),
333
+ ops=[ast.In()],
334
+ comparators=[ast.Name(id='__same_context__', ctx=ast.Load())]
335
+ )
336
+
337
+ def _write_block(self, sec_id: str, expression: ast.expr) -> ast.If:
338
+ """Build security-context write block.
339
+
340
+ The condition fires in two cases:
341
+ 1. This IS the security process for sec_id (__active_security__ == sec_id)
342
+ 2. This is the chart process and sec_id is same-context (sec_id in __same_context__)
343
+ """
344
+ return ast.If(
345
+ test=ast.BoolOp(
346
+ op=ast.Or(),
347
+ values=[self._eq_check(sec_id), self._in_same_context(sec_id)]
348
+ ),
349
+ body=[
350
+ ast.Expr(value=self._func_call(
351
+ '__sec_write__', ast.Constant(value=sec_id), expression
352
+ ))
353
+ ],
354
+ orelse=[]
355
+ )
356
+
357
+ def _sec_read_call(self, sec_id: str, tuple_len: int | None = None) -> ast.Call:
358
+ """Build: __sec_read__("sec_id", <default>)
359
+
360
+ Default is ``lib.na`` for scalar reads, or an N-tuple of ``lib.na``
361
+ when the LHS unpacks the result. Pine `request.security()` returns a
362
+ tuple of `na` (one per element) on no-data bars — emitting a single
363
+ scalar would crash tuple-unpack with ``TypeError: cannot unpack
364
+ non-iterable NA object`` in `gaps_on` between-period reads or after a
365
+ `write_na` (session gap).
366
+ """
367
+ if tuple_len is None:
368
+ default: ast.expr = self._lib_na()
369
+ else:
370
+ default = ast.Tuple(
371
+ elts=[self._lib_na() for _ in range(tuple_len)],
372
+ ctx=ast.Load()
373
+ )
374
+ return self._func_call(
375
+ '__sec_read__', ast.Constant(value=sec_id), default
376
+ )
377
+
378
+ def _sec_read_call_ltf(self, sec_id: str, arity: int | None = None) -> ast.Call:
379
+ """Build the LTF read expression.
380
+
381
+ Scalar expression: ``__sec_read__("sec_id", [])`` returns the array
382
+ directly. Tuple expression (``arity`` set): wrap in
383
+ ``__ltf_unzip__(__sec_read__("sec_id", []), arity)`` to transpose the
384
+ row-major intrabar buffer into ``arity`` column arrays.
385
+ """
386
+ read = self._func_call(
387
+ '__sec_read__', ast.Constant(value=sec_id), ast.List(elts=[], ctx=ast.Load())
388
+ )
389
+ if arity is None:
390
+ return read
391
+ return self._func_call('__ltf_unzip__', read, ast.Constant(value=arity))
392
+
393
+ @staticmethod
394
+ def _detect_tuple_arity(stmt: ast.stmt, call: ast.Call) -> int | None:
395
+ """If ``stmt`` is a tuple-unpack assignment whose RHS is exactly
396
+ ``call``, return the LHS arity. Otherwise None.
397
+
398
+ Pine statically enforces LHS-arity == RHS-arity for tuple-returning
399
+ ``request.security()`` (compile errors CE10239 / CE10172), so the
400
+ unpack target's arity is the authoritative source.
401
+
402
+ Skipped: star-unpack (``*rest``), multi-target (``a = b = sec(...)``),
403
+ annotated/augmented assigns, calls wrapped in a larger expression.
404
+ """
405
+ if not isinstance(stmt, ast.Assign):
406
+ return None
407
+ if len(stmt.targets) != 1:
408
+ return None
409
+ target = stmt.targets[0]
410
+ if not isinstance(target, (ast.Tuple, ast.List)):
411
+ return None
412
+ if any(isinstance(e, ast.Starred) for e in target.elts):
413
+ return None
414
+ if stmt.value is not call:
415
+ return None
416
+ return len(target.elts)
417
+
418
+ # --- Collection ---
419
+
420
+ def _collect_calls(
421
+ self, body: list[ast.stmt]
422
+ ) -> list[tuple[ast.Call, str, bool]]:
423
+ """
424
+ Find all request.security() and request.security_lower_tf() calls in
425
+ function body, skipping nested functions. Marks each call node with
426
+ _sec_id attribute.
427
+
428
+ :return: List of (call_node, sec_id, is_ltf) tuples
429
+ """
430
+ calls = []
431
+ for stmt in body:
432
+ if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
433
+ continue
434
+ for node in self._walk_skip_funcs(stmt):
435
+ if isinstance(node, ast.Call):
436
+ if self._is_security_call(node):
437
+ sec_id = self._gen_id()
438
+ node._sec_id = sec_id # type: ignore[attr-defined]
439
+ calls.append((node, sec_id, False))
440
+ elif self._is_security_lower_tf_call(node):
441
+ sec_id = self._gen_id()
442
+ node._sec_id = sec_id # type: ignore[attr-defined]
443
+ self._ltf_sec_ids.add(sec_id)
444
+ calls.append((node, sec_id, True))
445
+ return calls
446
+
447
+ # --- Body transformation ---
448
+
449
+ def _transform_body(
450
+ self, body: list[ast.stmt], call_exprs: dict[str, ast.expr],
451
+ runtime_sec_ids: set[str]
452
+ ) -> list[ast.stmt]:
453
+ """
454
+ Recursively transform a body list: insert write blocks before statements
455
+ containing security calls, and replace calls with __sec_read__.
456
+
457
+ For runtime-dependent sec_ids (those in ``runtime_sec_ids``), also
458
+ emits an inline signal just before the write block, so the signal
459
+ runs after the symbol/timeframe variables are defined.
460
+
461
+ Algorithm:
462
+ 1. For each statement, first recurse into compound sub-bodies
463
+ (this replaces calls there and removes their _sec_id markers)
464
+ 2. Then walk the full statement — only expression-level calls remain
465
+ 3. Insert inline signals (if runtime) + write blocks, replace calls
466
+ """
467
+ new_body: list[ast.stmt] = []
468
+ replacer = _CallReplacer(self)
469
+
470
+ for stmt in body:
471
+ if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
472
+ new_body.append(stmt)
473
+ continue
474
+
475
+ self._recurse_subbodies(stmt, call_exprs, runtime_sec_ids)
476
+
477
+ call_nodes_here = [
478
+ n for n in self._walk_skip_funcs(stmt)
479
+ if isinstance(n, ast.Call) and hasattr(n, '_sec_id')
480
+ ]
481
+
482
+ if call_nodes_here:
483
+ for call_node in call_nodes_here:
484
+ arity = self._detect_tuple_arity(stmt, call_node)
485
+ if arity is not None:
486
+ call_node._tuple_len = arity # type: ignore[attr-defined]
487
+ # LTF call with an opaque tuple expression (a function
488
+ # call, not a literal): the expression's own arity is
489
+ # unknowable here, but Pine enforces LHS-arity ==
490
+ # RHS-arity, so the unpack target is just as
491
+ # authoritative — wire the __ltf_unzip__ wrap from it.
492
+ if (getattr(call_node, '_sec_id') in self._ltf_sec_ids
493
+ and not hasattr(call_node, '_ltf_arity')):
494
+ call_node._ltf_arity = arity # type: ignore[attr-defined]
495
+ self._needs_ltf_unzip = True
496
+
497
+ for call_node in call_nodes_here:
498
+ sid = getattr(call_node, '_sec_id')
499
+ if sid in runtime_sec_ids:
500
+ new_body.append(self._inline_signal(sid))
501
+ expr = copy.deepcopy(call_exprs[sid])
502
+ expr = replacer.visit(expr)
503
+ new_body.append(self._write_block(sid, expr))
504
+ new_body.append(replacer.visit(stmt))
505
+ else:
506
+ new_body.append(stmt)
507
+
508
+ return new_body
509
+
510
+ def _recurse_subbodies(
511
+ self, stmt: ast.stmt, call_exprs: dict[str, ast.expr],
512
+ runtime_sec_ids: set[str]
513
+ ):
514
+ """Recurse into sub-bodies of compound statements."""
515
+ if isinstance(stmt, ast.If):
516
+ stmt.body = self._transform_body(stmt.body, call_exprs, runtime_sec_ids)
517
+ stmt.orelse = self._transform_body(stmt.orelse, call_exprs, runtime_sec_ids)
518
+ elif isinstance(stmt, (ast.For, ast.AsyncFor, ast.While)):
519
+ stmt.body = self._transform_body(stmt.body, call_exprs, runtime_sec_ids)
520
+ stmt.orelse = self._transform_body(stmt.orelse, call_exprs, runtime_sec_ids)
521
+ elif isinstance(stmt, (ast.With, ast.AsyncWith)):
522
+ stmt.body = self._transform_body(stmt.body, call_exprs, runtime_sec_ids)
523
+ elif isinstance(stmt, ast.Try):
524
+ stmt.body = self._transform_body(stmt.body, call_exprs, runtime_sec_ids)
525
+ for handler in stmt.handlers:
526
+ handler.body = self._transform_body(handler.body, call_exprs, runtime_sec_ids)
527
+ stmt.orelse = self._transform_body(stmt.orelse, call_exprs, runtime_sec_ids)
528
+ stmt.finalbody = self._transform_body(stmt.finalbody, call_exprs, runtime_sec_ids)
529
+ elif hasattr(ast, 'TryStar') and isinstance(stmt, ast.TryStar):
530
+ stmt.body = self._transform_body(stmt.body, call_exprs, runtime_sec_ids)
531
+ for handler in stmt.handlers:
532
+ handler.body = self._transform_body(handler.body, call_exprs, runtime_sec_ids)
533
+ stmt.orelse = self._transform_body(stmt.orelse, call_exprs, runtime_sec_ids)
534
+ stmt.finalbody = self._transform_body(stmt.finalbody, call_exprs, runtime_sec_ids)
535
+ elif hasattr(ast, 'Match') and isinstance(stmt, ast.Match):
536
+ for case in stmt.cases:
537
+ case.body = self._transform_body(case.body, call_exprs, runtime_sec_ids)
538
+
539
+ # --- Function & module visitors ---
540
+
541
+ def _process_func(self, node: ast.FunctionDef | ast.AsyncFunctionDef):
542
+ """Transform a function containing request.security() / security_lower_tf() calls."""
543
+ calls = self._collect_calls(node.body)
544
+
545
+ if not calls:
546
+ return self.generic_visit(node)
547
+
548
+ call_exprs: dict[str, ast.expr] = {}
549
+ sec_ids: list[str] = []
550
+
551
+ for call, sec_id, is_ltf in calls:
552
+ currency = None
553
+ lookahead = None
554
+ if is_ltf:
555
+ symbol, timeframe, expression, ignore_invalid = (
556
+ self._extract_ltf_args(call)
557
+ )
558
+ gaps = None
559
+ # A tuple/list expression makes security_lower_tf() return one
560
+ # array per element (column-major). The arity is authoritative
561
+ # from the expression itself, independent of how the result is
562
+ # unpacked. Mark the call so the read is wrapped in __ltf_unzip__.
563
+ if isinstance(expression, (ast.Tuple, ast.List)):
564
+ call._ltf_arity = len(expression.elts) # type: ignore[attr-defined]
565
+ self._needs_ltf_unzip = True
566
+ else:
567
+ symbol, timeframe, expression, gaps, lookahead, ignore_invalid, currency = (
568
+ self._extract_args(call)
569
+ )
570
+
571
+ if expression is not None:
572
+ bad = self._find_forbidden_strategy_state(expression)
573
+ if bad is not None:
574
+ fn_name = 'request.security_lower_tf' if is_ltf else 'request.security'
575
+ raise SyntaxError(
576
+ f"'strategy.{bad.attr}' cannot be used as the expression "
577
+ f"argument of {fn_name}() — strategy state is only "
578
+ f"available in the chart context, not in a security "
579
+ f"context.",
580
+ (self._module_file, getattr(bad, 'lineno', 0),
581
+ getattr(bad, 'col_offset', 0) + 1, None)
582
+ )
583
+
584
+ call_exprs[sec_id] = expression if expression is not None else self._lib_na()
585
+
586
+ # Input-derived (Pine "simple") lookahead — e.g. the standard TV
587
+ # non-repaint HTF pattern ``repaint ? lookahead_off : lookahead_on``
588
+ # — cannot be evaluated at module level, so it is resolved at
589
+ # runtime through __sec_signal__ like a deferred symbol/timeframe.
590
+ lookahead_rt: ast.expr | None = None
591
+ if lookahead is not None and not self._is_module_level_expr(lookahead):
592
+ lookahead_rt = copy.deepcopy(lookahead)
593
+
594
+ # Store actual expressions for __sec_signal__ args (always passed at runtime)
595
+ self._signal_args[sec_id] = (
596
+ copy.deepcopy(symbol) if symbol is not None else None,
597
+ copy.deepcopy(timeframe) if timeframe is not None else None,
598
+ lookahead_rt,
599
+ )
600
+
601
+ # For __security_contexts__ at module level: only use values that are
602
+ # evaluable at module scope. Function parameters etc. become None.
603
+ ctx: dict[str, ast.expr] = {}
604
+ if symbol is not None:
605
+ if self._is_module_level_expr(symbol):
606
+ ctx['symbol'] = copy.deepcopy(symbol)
607
+ else:
608
+ ctx['symbol'] = ast.Constant(value=None)
609
+ if timeframe is not None:
610
+ if self._is_module_level_expr(timeframe):
611
+ ctx['timeframe'] = copy.deepcopy(timeframe)
612
+ else:
613
+ ctx['timeframe'] = ast.Constant(value=None)
614
+
615
+ if is_ltf:
616
+ ctx['is_ltf'] = ast.Constant(value=True)
617
+ else:
618
+ gaps_expr = (
619
+ copy.deepcopy(gaps) if gaps is not None else self._default_gaps()
620
+ )
621
+ ctx['gaps'] = gaps_expr
622
+ if lookahead is not None:
623
+ # A module-level lookahead (``barmerge.lookahead_*``) is
624
+ # consumed at module load to wire the HTF transport. An
625
+ # input-derived expression would NameError at import time,
626
+ # so store None as a placeholder — the runtime value passed
627
+ # to ``__sec_signal__`` resolves the mode on the first bar.
628
+ ctx['lookahead'] = (copy.deepcopy(lookahead) if lookahead_rt is None
629
+ else ast.Constant(value=None))
630
+
631
+ if ignore_invalid is not None:
632
+ ctx['ignore_invalid_symbol'] = copy.deepcopy(ignore_invalid)
633
+
634
+ if not is_ltf and currency is not None:
635
+ ctx['currency'] = copy.deepcopy(currency)
636
+
637
+ # Plain-OHLCV fast path: when the expression is only raw price
638
+ # series, the security child can serve it straight from each bar
639
+ # without running main() (see security_process.security_process_main).
640
+ passthrough = self._ohlcv_passthrough(expression)
641
+ if passthrough is not None:
642
+ fields, is_tuple = passthrough
643
+ ctx['ohlcv_fields'] = ast.List(
644
+ elts=[ast.Constant(value=f) for f in fields], ctx=ast.Load()
645
+ )
646
+ ctx['ohlcv_tuple'] = ast.Constant(value=is_tuple)
647
+
648
+ # Track if barmerge is used (only for non-LTF)
649
+ if not is_ltf:
650
+ if gaps is None:
651
+ self._needs_barmerge = True
652
+ elif isinstance(gaps, ast.Attribute) and hasattr(gaps, 'value'):
653
+ v = gaps.value
654
+ if isinstance(v, ast.Attribute) and v.attr == 'barmerge':
655
+ self._needs_barmerge = True
656
+ if (lookahead is not None
657
+ and isinstance(lookahead, ast.Attribute)
658
+ and hasattr(lookahead, 'value')):
659
+ v = lookahead.value
660
+ if isinstance(v, ast.Attribute) and v.attr == 'barmerge':
661
+ self._needs_barmerge = True
662
+
663
+ self._all_contexts[sec_id] = ctx
664
+ sec_ids.append(sec_id)
665
+
666
+ # Separate module-level (constant) signals from runtime-dependent ones.
667
+ # Module-level signals can be emitted at function start for maximum
668
+ # parallelism. Runtime signals must be emitted inline, after the
669
+ # variables they reference have been assigned. A runtime-dependent
670
+ # lookahead forces the inline path too — its expression references
671
+ # locals that do not exist yet at function start.
672
+ top_sec_ids = []
673
+ runtime_sec_ids: set[str] = set()
674
+ for sid in sec_ids:
675
+ sym_expr, tf_expr, la_expr = self._signal_args[sid]
676
+ sym_ok = sym_expr is None or self._is_module_level_expr(sym_expr)
677
+ tf_ok = tf_expr is None or self._is_module_level_expr(tf_expr)
678
+ if sym_ok and tf_ok and la_expr is None:
679
+ top_sec_ids.append(sid)
680
+ else:
681
+ runtime_sec_ids.add(sid)
682
+
683
+ original_body = node.body
684
+ top_block = [self._signal_block(top_sec_ids)] if top_sec_ids else []
685
+ node.body = (
686
+ top_block
687
+ + self._transform_body(original_body, call_exprs, runtime_sec_ids)
688
+ + [self._wait_block(sec_ids)]
689
+ )
690
+
691
+ return self.generic_visit(node)
692
+
693
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
694
+ return self._process_func(node) # type: ignore[return-value]
695
+
696
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AsyncFunctionDef:
697
+ return self._process_func(node) # type: ignore[return-value]
698
+
699
+ def visit_Module(self, node: ast.Module) -> ast.Module:
700
+ self._module_file = getattr(node, '_module_file_path', '<script>')
701
+ node = self.generic_visit(node) # type: ignore[assignment]
702
+
703
+ if self._all_contexts:
704
+ # Add barmerge import if needed (SecurityTransformer runs AFTER ImportNormalizer,
705
+ # so we must add it ourselves)
706
+ if self._needs_barmerge:
707
+ node.body.insert(0, ast.Import(
708
+ names=[ast.alias(name='pynecore.lib.barmerge', asname=None)]
709
+ ))
710
+
711
+ # __ltf_unzip__ transposes tuple-valued security_lower_tf() results.
712
+ if self._needs_ltf_unzip:
713
+ node.body.insert(0, ast.ImportFrom(
714
+ module='pynecore.core.security',
715
+ names=[ast.alias(name='__ltf_unzip__', asname=None)],
716
+ level=0,
717
+ ))
718
+
719
+ node.body.append(ast.Assign(
720
+ targets=[ast.Name(id='__security_contexts__', ctx=ast.Store())],
721
+ value=ast.Dict(
722
+ keys=[ast.Constant(value=sid) for sid in self._all_contexts],
723
+ values=[
724
+ ast.Dict(
725
+ keys=[ast.Constant(value=k) for k in ctx],
726
+ values=list(ctx.values())
727
+ )
728
+ for ctx in self._all_contexts.values()
729
+ ]
730
+ )
731
+ ))
732
+
733
+ return node
734
+
735
+
736
+ class _CallReplacer(ast.NodeTransformer):
737
+ """Replace marked request.security() call nodes with __sec_read__() calls."""
738
+
739
+ def __init__(self, parent: SecurityTransformer):
740
+ self._parent = parent
741
+
742
+ # noinspection PyProtectedMember
743
+ def visit_Call(self, node: ast.Call) -> ast.AST:
744
+ node = self.generic_visit(node) # type: ignore[assignment]
745
+ if hasattr(node, '_sec_id'):
746
+ sec_id = getattr(node, '_sec_id')
747
+ if sec_id in self._parent._ltf_sec_ids:
748
+ ltf_arity = getattr(node, '_ltf_arity', None)
749
+ return self._parent._sec_read_call_ltf(sec_id, ltf_arity)
750
+ tuple_len = getattr(node, '_tuple_len', None)
751
+ return self._parent._sec_read_call(sec_id, tuple_len)
752
+ return node