pytrilogy 0.3.148__cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.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 (206) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cpython-312-aarch64-linux-gnu.so +0 -0
  4. pytrilogy-0.3.148.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.148.dist-info/RECORD +206 -0
  6. pytrilogy-0.3.148.dist-info/WHEEL +5 -0
  7. pytrilogy-0.3.148.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.148.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +27 -0
  10. trilogy/ai/README.md +10 -0
  11. trilogy/ai/__init__.py +19 -0
  12. trilogy/ai/constants.py +92 -0
  13. trilogy/ai/conversation.py +107 -0
  14. trilogy/ai/enums.py +7 -0
  15. trilogy/ai/execute.py +50 -0
  16. trilogy/ai/models.py +34 -0
  17. trilogy/ai/prompts.py +100 -0
  18. trilogy/ai/providers/__init__.py +0 -0
  19. trilogy/ai/providers/anthropic.py +106 -0
  20. trilogy/ai/providers/base.py +24 -0
  21. trilogy/ai/providers/google.py +146 -0
  22. trilogy/ai/providers/openai.py +89 -0
  23. trilogy/ai/providers/utils.py +68 -0
  24. trilogy/authoring/README.md +3 -0
  25. trilogy/authoring/__init__.py +148 -0
  26. trilogy/constants.py +119 -0
  27. trilogy/core/README.md +52 -0
  28. trilogy/core/__init__.py +0 -0
  29. trilogy/core/constants.py +6 -0
  30. trilogy/core/enums.py +454 -0
  31. trilogy/core/env_processor.py +239 -0
  32. trilogy/core/environment_helpers.py +320 -0
  33. trilogy/core/ergonomics.py +193 -0
  34. trilogy/core/exceptions.py +123 -0
  35. trilogy/core/functions.py +1240 -0
  36. trilogy/core/graph_models.py +142 -0
  37. trilogy/core/internal.py +85 -0
  38. trilogy/core/models/__init__.py +0 -0
  39. trilogy/core/models/author.py +2662 -0
  40. trilogy/core/models/build.py +2603 -0
  41. trilogy/core/models/build_environment.py +165 -0
  42. trilogy/core/models/core.py +506 -0
  43. trilogy/core/models/datasource.py +434 -0
  44. trilogy/core/models/environment.py +756 -0
  45. trilogy/core/models/execute.py +1213 -0
  46. trilogy/core/optimization.py +251 -0
  47. trilogy/core/optimizations/__init__.py +12 -0
  48. trilogy/core/optimizations/base_optimization.py +17 -0
  49. trilogy/core/optimizations/hide_unused_concept.py +47 -0
  50. trilogy/core/optimizations/inline_datasource.py +102 -0
  51. trilogy/core/optimizations/predicate_pushdown.py +245 -0
  52. trilogy/core/processing/README.md +94 -0
  53. trilogy/core/processing/READMEv2.md +121 -0
  54. trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
  55. trilogy/core/processing/__init__.py +0 -0
  56. trilogy/core/processing/concept_strategies_v3.py +508 -0
  57. trilogy/core/processing/constants.py +15 -0
  58. trilogy/core/processing/discovery_node_factory.py +451 -0
  59. trilogy/core/processing/discovery_utility.py +548 -0
  60. trilogy/core/processing/discovery_validation.py +167 -0
  61. trilogy/core/processing/graph_utils.py +43 -0
  62. trilogy/core/processing/node_generators/README.md +9 -0
  63. trilogy/core/processing/node_generators/__init__.py +31 -0
  64. trilogy/core/processing/node_generators/basic_node.py +160 -0
  65. trilogy/core/processing/node_generators/common.py +270 -0
  66. trilogy/core/processing/node_generators/constant_node.py +38 -0
  67. trilogy/core/processing/node_generators/filter_node.py +315 -0
  68. trilogy/core/processing/node_generators/group_node.py +213 -0
  69. trilogy/core/processing/node_generators/group_to_node.py +117 -0
  70. trilogy/core/processing/node_generators/multiselect_node.py +207 -0
  71. trilogy/core/processing/node_generators/node_merge_node.py +695 -0
  72. trilogy/core/processing/node_generators/recursive_node.py +88 -0
  73. trilogy/core/processing/node_generators/rowset_node.py +165 -0
  74. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  75. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
  76. trilogy/core/processing/node_generators/select_merge_node.py +786 -0
  77. trilogy/core/processing/node_generators/select_node.py +95 -0
  78. trilogy/core/processing/node_generators/synonym_node.py +98 -0
  79. trilogy/core/processing/node_generators/union_node.py +91 -0
  80. trilogy/core/processing/node_generators/unnest_node.py +182 -0
  81. trilogy/core/processing/node_generators/window_node.py +201 -0
  82. trilogy/core/processing/nodes/README.md +28 -0
  83. trilogy/core/processing/nodes/__init__.py +179 -0
  84. trilogy/core/processing/nodes/base_node.py +522 -0
  85. trilogy/core/processing/nodes/filter_node.py +75 -0
  86. trilogy/core/processing/nodes/group_node.py +194 -0
  87. trilogy/core/processing/nodes/merge_node.py +420 -0
  88. trilogy/core/processing/nodes/recursive_node.py +46 -0
  89. trilogy/core/processing/nodes/select_node_v2.py +242 -0
  90. trilogy/core/processing/nodes/union_node.py +53 -0
  91. trilogy/core/processing/nodes/unnest_node.py +62 -0
  92. trilogy/core/processing/nodes/window_node.py +56 -0
  93. trilogy/core/processing/utility.py +823 -0
  94. trilogy/core/query_processor.py +604 -0
  95. trilogy/core/statements/README.md +35 -0
  96. trilogy/core/statements/__init__.py +0 -0
  97. trilogy/core/statements/author.py +536 -0
  98. trilogy/core/statements/build.py +0 -0
  99. trilogy/core/statements/common.py +20 -0
  100. trilogy/core/statements/execute.py +155 -0
  101. trilogy/core/table_processor.py +66 -0
  102. trilogy/core/utility.py +8 -0
  103. trilogy/core/validation/README.md +46 -0
  104. trilogy/core/validation/__init__.py +0 -0
  105. trilogy/core/validation/common.py +161 -0
  106. trilogy/core/validation/concept.py +146 -0
  107. trilogy/core/validation/datasource.py +227 -0
  108. trilogy/core/validation/environment.py +73 -0
  109. trilogy/core/validation/fix.py +256 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1431 -0
  112. trilogy/dialect/bigquery.py +314 -0
  113. trilogy/dialect/common.py +147 -0
  114. trilogy/dialect/config.py +159 -0
  115. trilogy/dialect/dataframe.py +50 -0
  116. trilogy/dialect/duckdb.py +376 -0
  117. trilogy/dialect/enums.py +149 -0
  118. trilogy/dialect/metadata.py +173 -0
  119. trilogy/dialect/mock.py +190 -0
  120. trilogy/dialect/postgres.py +117 -0
  121. trilogy/dialect/presto.py +110 -0
  122. trilogy/dialect/results.py +89 -0
  123. trilogy/dialect/snowflake.py +129 -0
  124. trilogy/dialect/sql_server.py +137 -0
  125. trilogy/engine.py +48 -0
  126. trilogy/execution/__init__.py +17 -0
  127. trilogy/execution/config.py +119 -0
  128. trilogy/execution/state/__init__.py +0 -0
  129. trilogy/execution/state/file_state_store.py +0 -0
  130. trilogy/execution/state/sqllite_state_store.py +0 -0
  131. trilogy/execution/state/state_store.py +301 -0
  132. trilogy/executor.py +656 -0
  133. trilogy/hooks/__init__.py +4 -0
  134. trilogy/hooks/base_hook.py +40 -0
  135. trilogy/hooks/graph_hook.py +135 -0
  136. trilogy/hooks/query_debugger.py +166 -0
  137. trilogy/metadata/__init__.py +0 -0
  138. trilogy/parser.py +10 -0
  139. trilogy/parsing/README.md +21 -0
  140. trilogy/parsing/__init__.py +0 -0
  141. trilogy/parsing/common.py +1069 -0
  142. trilogy/parsing/config.py +5 -0
  143. trilogy/parsing/exceptions.py +8 -0
  144. trilogy/parsing/helpers.py +1 -0
  145. trilogy/parsing/parse_engine.py +2863 -0
  146. trilogy/parsing/render.py +773 -0
  147. trilogy/parsing/trilogy.lark +544 -0
  148. trilogy/py.typed +0 -0
  149. trilogy/render.py +45 -0
  150. trilogy/scripts/README.md +9 -0
  151. trilogy/scripts/__init__.py +0 -0
  152. trilogy/scripts/agent.py +41 -0
  153. trilogy/scripts/agent_info.py +306 -0
  154. trilogy/scripts/common.py +430 -0
  155. trilogy/scripts/dependency/Cargo.lock +617 -0
  156. trilogy/scripts/dependency/Cargo.toml +39 -0
  157. trilogy/scripts/dependency/README.md +131 -0
  158. trilogy/scripts/dependency/build.sh +25 -0
  159. trilogy/scripts/dependency/src/directory_resolver.rs +387 -0
  160. trilogy/scripts/dependency/src/lib.rs +16 -0
  161. trilogy/scripts/dependency/src/main.rs +770 -0
  162. trilogy/scripts/dependency/src/parser.rs +435 -0
  163. trilogy/scripts/dependency/src/preql.pest +208 -0
  164. trilogy/scripts/dependency/src/python_bindings.rs +311 -0
  165. trilogy/scripts/dependency/src/resolver.rs +716 -0
  166. trilogy/scripts/dependency/tests/base.preql +3 -0
  167. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  168. trilogy/scripts/dependency/tests/customer.preql +6 -0
  169. trilogy/scripts/dependency/tests/main.preql +9 -0
  170. trilogy/scripts/dependency/tests/orders.preql +7 -0
  171. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  172. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  173. trilogy/scripts/dependency.py +323 -0
  174. trilogy/scripts/display.py +555 -0
  175. trilogy/scripts/environment.py +59 -0
  176. trilogy/scripts/fmt.py +32 -0
  177. trilogy/scripts/ingest.py +472 -0
  178. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  179. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  180. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  181. trilogy/scripts/ingest_helpers/typing.py +161 -0
  182. trilogy/scripts/init.py +105 -0
  183. trilogy/scripts/parallel_execution.py +748 -0
  184. trilogy/scripts/plan.py +189 -0
  185. trilogy/scripts/refresh.py +106 -0
  186. trilogy/scripts/run.py +79 -0
  187. trilogy/scripts/serve.py +202 -0
  188. trilogy/scripts/serve_helpers/__init__.py +41 -0
  189. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  190. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  191. trilogy/scripts/serve_helpers/models.py +38 -0
  192. trilogy/scripts/single_execution.py +131 -0
  193. trilogy/scripts/testing.py +129 -0
  194. trilogy/scripts/trilogy.py +75 -0
  195. trilogy/std/__init__.py +0 -0
  196. trilogy/std/color.preql +3 -0
  197. trilogy/std/date.preql +13 -0
  198. trilogy/std/display.preql +18 -0
  199. trilogy/std/geography.preql +22 -0
  200. trilogy/std/metric.preql +15 -0
  201. trilogy/std/money.preql +67 -0
  202. trilogy/std/net.preql +14 -0
  203. trilogy/std/ranking.preql +7 -0
  204. trilogy/std/report.preql +5 -0
  205. trilogy/std/semantic.preql +6 -0
  206. trilogy/utility.py +34 -0
@@ -0,0 +1,748 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from dataclasses import dataclass
5
+ from datetime import datetime
6
+ from enum import Enum
7
+ from io import StringIO
8
+ from pathlib import Path
9
+ from typing import Any, Callable, Protocol
10
+
11
+ import networkx as nx
12
+ from click.exceptions import Exit
13
+
14
+ from trilogy import Executor
15
+ from trilogy.scripts.common import CLIRuntimeParams, ExecutionStats
16
+ from trilogy.scripts.dependency import (
17
+ DependencyResolver,
18
+ DependencyStrategy,
19
+ ScriptNode,
20
+ create_script_nodes,
21
+ )
22
+
23
+
24
+ class ExecutionMode(Enum):
25
+ """Mode of script execution."""
26
+
27
+ RUN = "run"
28
+ INTEGRATION = "integration"
29
+ UNIT = "unit"
30
+ REFRESH = "refresh"
31
+
32
+
33
+ @dataclass
34
+ class ExecutionResult:
35
+ """Result of executing a single script."""
36
+
37
+ node: ScriptNode
38
+ success: bool
39
+ error: Exception | None = None
40
+ duration: float = 0.0 # seconds
41
+ stats: ExecutionStats | None = None
42
+
43
+
44
+ @dataclass
45
+ class ParallelExecutionSummary:
46
+ """Summary of a parallel execution run."""
47
+
48
+ total_scripts: int
49
+ successful: int
50
+ failed: int
51
+ total_duration: float
52
+ results: list[ExecutionResult]
53
+
54
+ @property
55
+ def all_succeeded(self) -> bool:
56
+ return self.failed == 0
57
+
58
+
59
+ class ExecutionStrategy(Protocol):
60
+ """Protocol for execution traversal strategies."""
61
+
62
+ def execute(
63
+ self,
64
+ graph: nx.DiGraph,
65
+ resolver: DependencyResolver,
66
+ max_workers: int,
67
+ executor_factory: Callable[[ScriptNode], Any],
68
+ execution_fn: Callable[[Any, ScriptNode], Any],
69
+ on_script_start: Callable[[ScriptNode], None] | None = None,
70
+ on_script_complete: Callable[[ExecutionResult], None] | None = None,
71
+ ) -> list[ExecutionResult]:
72
+ """
73
+ Execute scripts according to the strategy.
74
+
75
+ Args:
76
+ graph: The dependency graph (edges point from deps to dependents).
77
+ max_workers: Maximum parallel workers.
78
+ executor_factory: Factory to create executor for each script.
79
+ execution_fn: Function to execute a script.
80
+
81
+ Returns:
82
+ List of ExecutionResult for all scripts.
83
+ """
84
+ ...
85
+
86
+
87
+ # Type aliases for cleaner signatures
88
+ CompletedSet = set[ScriptNode]
89
+ FailedSet = set[ScriptNode]
90
+ InProgressSet = set[ScriptNode]
91
+ ResultsList = list[ExecutionResult]
92
+ RemainingDepsDict = dict[ScriptNode, int]
93
+ ReadyList = list[ScriptNode]
94
+ OnCompleteCallback = Callable[[ExecutionResult], None] | None
95
+
96
+
97
+ def _propagate_failure(
98
+ failed_node: ScriptNode,
99
+ graph: nx.DiGraph,
100
+ completed: CompletedSet,
101
+ in_progress: InProgressSet,
102
+ results: ResultsList,
103
+ failed: FailedSet,
104
+ on_script_complete: OnCompleteCallback,
105
+ ) -> None:
106
+ """
107
+ Recursively mark all *unstarted* dependents of a failed node as failed and skipped.
108
+ """
109
+ for dependent in graph.successors(failed_node):
110
+ if dependent not in completed and dependent not in in_progress:
111
+ skip_result = ExecutionResult(
112
+ node=dependent,
113
+ success=False,
114
+ error=RuntimeError("Skipped due to failed dependency"),
115
+ duration=0.0,
116
+ )
117
+ results.append(skip_result)
118
+ completed.add(dependent)
119
+ failed.add(dependent)
120
+ if on_script_complete:
121
+ on_script_complete(skip_result)
122
+ _propagate_failure(
123
+ dependent,
124
+ graph,
125
+ completed,
126
+ in_progress,
127
+ results,
128
+ failed,
129
+ on_script_complete,
130
+ )
131
+
132
+
133
+ def _get_next_ready(ready: ReadyList) -> ScriptNode | None:
134
+ """Get next ready node from the queue."""
135
+ if ready:
136
+ return ready.pop(0)
137
+ return None
138
+
139
+
140
+ def _mark_node_complete(
141
+ node: ScriptNode,
142
+ success: bool,
143
+ graph: nx.DiGraph,
144
+ completed: CompletedSet,
145
+ failed: FailedSet,
146
+ in_progress: InProgressSet,
147
+ remaining_deps: RemainingDepsDict,
148
+ ready: ReadyList,
149
+ results: ResultsList,
150
+ on_script_complete: OnCompleteCallback,
151
+ ) -> None:
152
+ """
153
+ Mark a node as complete, update dependent counts, and add newly ready/skipped nodes.
154
+ """
155
+ in_progress.discard(node)
156
+ completed.add(node)
157
+ if not success:
158
+ failed.add(node)
159
+
160
+ # Update dependents
161
+ for dependent in graph.successors(node):
162
+ if dependent in completed or dependent in in_progress:
163
+ continue
164
+
165
+ if success:
166
+ remaining_deps[dependent] -= 1
167
+ if remaining_deps[dependent] == 0:
168
+ # Check if any dependency failed before running
169
+ deps = set(graph.predecessors(dependent))
170
+ if deps & failed:
171
+ # Skip this node - dependency failed
172
+ skip_result = ExecutionResult(
173
+ node=dependent,
174
+ success=False,
175
+ error=RuntimeError("Skipped due to failed dependency"),
176
+ duration=0.0,
177
+ )
178
+ results.append(skip_result)
179
+ completed.add(dependent)
180
+ failed.add(dependent)
181
+ if on_script_complete:
182
+ on_script_complete(skip_result)
183
+ # Recursively mark dependents as failed
184
+ _propagate_failure(
185
+ dependent,
186
+ graph,
187
+ completed,
188
+ in_progress,
189
+ results,
190
+ failed,
191
+ on_script_complete,
192
+ )
193
+ else:
194
+ ready.append(dependent)
195
+ else:
196
+ # Current node failed - mark this dependent as skipped
197
+ if dependent not in failed:
198
+ skip_result = ExecutionResult(
199
+ node=dependent,
200
+ success=False,
201
+ error=RuntimeError("Skipped due to failed dependency"),
202
+ duration=0.0,
203
+ )
204
+ results.append(skip_result)
205
+ completed.add(dependent)
206
+ failed.add(dependent)
207
+ if on_script_complete:
208
+ on_script_complete(skip_result)
209
+ # Recursively mark dependents as failed
210
+ _propagate_failure(
211
+ dependent,
212
+ graph,
213
+ completed,
214
+ in_progress,
215
+ results,
216
+ failed,
217
+ on_script_complete,
218
+ )
219
+
220
+
221
+ def _is_execution_done(completed: CompletedSet, total_count: int) -> bool:
222
+ """Check if all nodes have been processed."""
223
+ return len(completed) >= total_count
224
+
225
+
226
+ def _execute_single(
227
+ node: ScriptNode,
228
+ executor_factory: Callable[[ScriptNode], Executor],
229
+ execution_fn: Callable[[Any, ScriptNode], ExecutionStats | None],
230
+ ) -> ExecutionResult:
231
+ """Execute a single script and return the result."""
232
+ start_time = datetime.now()
233
+ executor = None
234
+ try:
235
+ executor = executor_factory(node)
236
+ stats = execution_fn(executor, node)
237
+
238
+ duration = (datetime.now() - start_time).total_seconds()
239
+ if executor:
240
+ executor.close()
241
+ return ExecutionResult(
242
+ node=node,
243
+ success=True,
244
+ error=None,
245
+ duration=duration,
246
+ stats=stats if isinstance(stats, ExecutionStats) else None,
247
+ )
248
+ except Exception as e:
249
+ duration = (datetime.now() - start_time).total_seconds()
250
+ if executor:
251
+ executor.close() # Ensure executor is closed even on failure
252
+ return ExecutionResult(
253
+ node=node,
254
+ success=False,
255
+ error=e,
256
+ duration=duration,
257
+ )
258
+
259
+
260
+ def _create_worker(
261
+ graph: nx.DiGraph,
262
+ lock: threading.Lock,
263
+ work_available: threading.Condition,
264
+ completed: CompletedSet,
265
+ failed: FailedSet,
266
+ in_progress: InProgressSet,
267
+ remaining_deps: RemainingDepsDict,
268
+ ready: ReadyList,
269
+ results: ResultsList,
270
+ total_count: int,
271
+ executor_factory: Callable[[ScriptNode], Any],
272
+ execution_fn: Callable[[Any, ScriptNode], Any],
273
+ on_script_start: Callable[[ScriptNode], None] | None,
274
+ on_script_complete: OnCompleteCallback,
275
+ ) -> Callable[[], None]:
276
+ """
277
+ Create a worker function for thread execution to process the dependency graph.
278
+ """
279
+
280
+ def worker() -> None:
281
+ while True:
282
+ node = None
283
+
284
+ with work_available:
285
+ # Wait for work or global completion
286
+ while not ready and not _is_execution_done(completed, total_count):
287
+ work_available.wait()
288
+
289
+ if _is_execution_done(completed, total_count):
290
+ return
291
+
292
+ node = _get_next_ready(ready)
293
+ if node is None:
294
+ # Should be impossible if total_count check is correct, but handles race condition safety
295
+ continue
296
+
297
+ in_progress.add(node)
298
+
299
+ # Execute outside the lock
300
+ if node is not None:
301
+ if on_script_start:
302
+ on_script_start(node)
303
+ result = _execute_single(node, executor_factory, execution_fn)
304
+
305
+ # Use the lock for state updates and notification
306
+ with lock:
307
+ results.append(result)
308
+
309
+ if on_script_complete:
310
+ on_script_complete(result)
311
+
312
+ _mark_node_complete(
313
+ node,
314
+ result.success,
315
+ graph,
316
+ completed,
317
+ failed,
318
+ in_progress,
319
+ remaining_deps,
320
+ ready,
321
+ results,
322
+ on_script_complete,
323
+ )
324
+ work_available.notify_all() # Notify other workers of new ready/completed state
325
+
326
+ return worker
327
+
328
+
329
+ class EagerBFSStrategy:
330
+ """
331
+ Eager Breadth-First Search (BFS) execution strategy.
332
+
333
+ Scripts execute as soon as all their dependencies complete, maximizing parallelism.
334
+ Uses a thread pool coordinated by locks and condition variables.
335
+ """
336
+
337
+ def execute(
338
+ self,
339
+ graph: nx.DiGraph,
340
+ resolver: DependencyResolver,
341
+ max_workers: int,
342
+ executor_factory: Callable[[ScriptNode], Any],
343
+ execution_fn: Callable[[Any, ScriptNode], Any],
344
+ on_script_start: Callable[[ScriptNode], None] | None = None,
345
+ on_script_complete: Callable[[ExecutionResult], None] | None = None,
346
+ ) -> list[ExecutionResult]:
347
+ """Execute scripts eagerly as dependencies complete."""
348
+ if not graph.nodes():
349
+ return []
350
+
351
+ lock = threading.Lock()
352
+ work_available = threading.Condition(lock)
353
+
354
+ # Track state
355
+ completed: CompletedSet = set()
356
+ failed: FailedSet = set()
357
+ in_progress: InProgressSet = set()
358
+ results: ResultsList = []
359
+
360
+ # Calculate in-degrees (number of incomplete dependencies)
361
+ remaining_deps: RemainingDepsDict = {
362
+ node: graph.in_degree(node) for node in graph.nodes()
363
+ }
364
+
365
+ # Ready queue - nodes with all dependencies satisfied initially (in-degree 0)
366
+ ready: ReadyList = [node for node in graph.nodes() if remaining_deps[node] == 0]
367
+
368
+ total_count = len(graph.nodes())
369
+
370
+ # Create the worker function
371
+ worker = _create_worker(
372
+ graph=graph,
373
+ lock=lock,
374
+ work_available=work_available,
375
+ completed=completed,
376
+ failed=failed,
377
+ in_progress=in_progress,
378
+ remaining_deps=remaining_deps,
379
+ ready=ready,
380
+ results=results,
381
+ total_count=total_count,
382
+ executor_factory=executor_factory,
383
+ execution_fn=execution_fn,
384
+ on_script_start=on_script_start,
385
+ on_script_complete=on_script_complete,
386
+ )
387
+
388
+ # Start worker threads
389
+ workers = min(max_workers, total_count)
390
+ threads: list[threading.Thread] = []
391
+ for _ in range(workers):
392
+ t = threading.Thread(target=worker, daemon=True)
393
+ t.start()
394
+ threads.append(t)
395
+
396
+ # Wake up any waiting workers if we have initial work
397
+ with work_available:
398
+ work_available.notify_all()
399
+
400
+ # Wait for all threads to complete
401
+ for t in threads:
402
+ t.join()
403
+
404
+ return results
405
+
406
+
407
+ class ParallelExecutor:
408
+ """
409
+ Executes scripts in parallel while respecting dependencies.
410
+
411
+ Uses an Eager BFS traversal by default, running scripts as soon as their
412
+ dependencies complete.
413
+ """
414
+
415
+ def __init__(
416
+ self,
417
+ max_workers: int = 5,
418
+ dependency_strategy: DependencyStrategy | None = None,
419
+ execution_strategy: ExecutionStrategy | None = None,
420
+ ):
421
+ """
422
+ Initialize the parallel executor.
423
+
424
+ Args:
425
+ max_workers: Maximum number of parallel workers.
426
+ dependency_strategy: Strategy for resolving dependencies.
427
+ execution_strategy: Strategy for traversing the graph during execution.
428
+ """
429
+ self.max_workers = max_workers
430
+ # Resolver finds dependencies and builds the graph
431
+ self.resolver = DependencyResolver(strategy=dependency_strategy)
432
+ # Execution strategy determines how the graph is traversed and executed
433
+ self.execution_strategy = execution_strategy or EagerBFSStrategy()
434
+
435
+ def execute(
436
+ self,
437
+ root: Path,
438
+ executor_factory: Callable[[ScriptNode], Any],
439
+ execution_fn: Callable[[Any, ScriptNode], Any],
440
+ on_script_start: Callable[[ScriptNode], None] | None = None,
441
+ on_script_complete: Callable[[ExecutionResult], None] | None = None,
442
+ ) -> ParallelExecutionSummary:
443
+ """
444
+ Execute scripts in parallel respecting dependencies.
445
+
446
+ Args:
447
+ root: Root path (folder or single file) to find scripts.
448
+ executor_factory: Factory function to create an executor for a script.
449
+ execution_fn: Function that executes a script given (executor, node).
450
+
451
+ Returns:
452
+ ParallelExecutionSummary with all results.
453
+ """
454
+ start_time = datetime.now()
455
+
456
+ # Build dependency graph
457
+ if root.is_dir():
458
+ graph = self.resolver.build_folder_graph(root)
459
+ nodes = list(graph.nodes())
460
+ else:
461
+ nodes = create_script_nodes([root])
462
+ graph = self.resolver.build_graph(nodes)
463
+
464
+ # Total count of nodes for summary/completion check
465
+ total_scripts = len(nodes)
466
+
467
+ # Execute using the configured strategy
468
+ results = self.execution_strategy.execute(
469
+ graph=graph,
470
+ resolver=self.resolver,
471
+ max_workers=self.max_workers,
472
+ executor_factory=executor_factory,
473
+ execution_fn=execution_fn,
474
+ on_script_start=on_script_start,
475
+ on_script_complete=on_script_complete,
476
+ )
477
+
478
+ total_duration = (datetime.now() - start_time).total_seconds()
479
+ successful = sum(1 for r in results if r.success)
480
+
481
+ return ParallelExecutionSummary(
482
+ total_scripts=total_scripts,
483
+ successful=successful,
484
+ failed=total_scripts - successful,
485
+ total_duration=total_duration,
486
+ results=results,
487
+ )
488
+
489
+ def get_folder_execution_plan(self, folder: Path) -> nx.DiGraph:
490
+ """
491
+ Get the execution plan (dependency graph) for all scripts in a folder.
492
+ """
493
+ return self.resolver.build_folder_graph(folder)
494
+
495
+ def get_execution_plan(self, files: list[Path]) -> nx.DiGraph:
496
+ """
497
+ Get the execution plan (dependency graph) without executing.
498
+ """
499
+ nodes = create_script_nodes(files)
500
+ return self.resolver.build_graph(nodes)
501
+
502
+
503
+ def run_single_script_execution(
504
+ files: list[StringIO | Path],
505
+ directory: Path,
506
+ input_type: str,
507
+ input_name: str,
508
+ edialect,
509
+ param: tuple[str, ...],
510
+ conn_args,
511
+ debug: bool,
512
+ execution_mode: ExecutionMode,
513
+ config,
514
+ ) -> None:
515
+ """Run single script execution with polished multi-statement progress display."""
516
+ from trilogy.scripts.common import (
517
+ create_executor,
518
+ handle_execution_exception,
519
+ validate_datasources,
520
+ )
521
+ from trilogy.scripts.display import (
522
+ RICH_AVAILABLE,
523
+ create_progress_context,
524
+ print_success,
525
+ show_execution_info,
526
+ show_execution_start,
527
+ show_execution_summary,
528
+ )
529
+ from trilogy.scripts.single_execution import (
530
+ execute_queries_simple,
531
+ execute_queries_with_progress,
532
+ )
533
+
534
+ config_path_str = str(config.source_path) if config.source_path else None
535
+ show_execution_info(input_type, input_name, edialect.value, debug, config_path_str)
536
+
537
+ exec = create_executor(param, directory, conn_args, edialect, debug, config)
538
+ base = files[0]
539
+ if isinstance(base, StringIO):
540
+ text = [base.getvalue()]
541
+ else:
542
+ with open(base, "r") as raw:
543
+ text = [raw.read()]
544
+
545
+ if execution_mode == ExecutionMode.RUN:
546
+ # Parse all scripts and collect queries
547
+ queries = []
548
+ try:
549
+ for script in text:
550
+ queries += exec.parse_text(script)
551
+ except Exception as e:
552
+ handle_execution_exception(e, debug=debug)
553
+
554
+ start = datetime.now()
555
+ show_execution_start(len(queries))
556
+
557
+ # Execute with progress tracking for multiple statements
558
+ if len(queries) > 1 and RICH_AVAILABLE:
559
+ progress = create_progress_context()
560
+ else:
561
+ progress = None
562
+
563
+ try:
564
+ if progress:
565
+ exception = execute_queries_with_progress(exec, queries)
566
+ else:
567
+ exception = execute_queries_simple(exec, queries)
568
+
569
+ total_duration = datetime.now() - start
570
+ show_execution_summary(len(queries), total_duration, exception is None)
571
+
572
+ if exception:
573
+ raise Exit(1) from exception
574
+ except Exit:
575
+ raise
576
+ except Exception as e:
577
+ handle_execution_exception(e, debug=debug)
578
+
579
+ elif execution_mode == ExecutionMode.INTEGRATION:
580
+ for script in text:
581
+ exec.parse_text(script)
582
+ validate_datasources(exec, mock=False, quiet=False)
583
+ print_success("Integration tests passed successfully!")
584
+
585
+ elif execution_mode == ExecutionMode.UNIT:
586
+ for script in text:
587
+ exec.parse_text(script)
588
+ validate_datasources(exec, mock=True, quiet=False)
589
+ print_success("Unit tests passed successfully!")
590
+
591
+ elif execution_mode == ExecutionMode.REFRESH:
592
+ from trilogy.execution.state.state_store import BaseStateStore
593
+ from trilogy.scripts.display import print_info, print_warning
594
+
595
+ for script in text:
596
+ exec.parse_text(script)
597
+
598
+ state_store = BaseStateStore()
599
+ stale_assets = state_store.get_stale_assets(exec.environment, exec)
600
+
601
+ if not stale_assets:
602
+ print_info("No stale assets found")
603
+ return
604
+
605
+ print_warning(f"Found {len(stale_assets)} stale asset(s)")
606
+ for asset in stale_assets:
607
+ print_info(f" Refreshing {asset.datasource_id}: {asset.reason}")
608
+ datasource = exec.environment.datasources[asset.datasource_id]
609
+ exec.update_datasource(datasource)
610
+
611
+ print_success(f"Refreshed {len(stale_assets)} asset(s)")
612
+
613
+
614
+ def get_execution_strategy(strategy_name: str):
615
+ """Get execution strategy by name."""
616
+ strategies = {
617
+ "eager_bfs": EagerBFSStrategy,
618
+ }
619
+ if strategy_name not in strategies:
620
+ raise ValueError(
621
+ f"Unknown execution strategy: {strategy_name}. "
622
+ f"Available: {', '.join(strategies.keys())}"
623
+ )
624
+ return strategies[strategy_name]()
625
+
626
+
627
+ def run_parallel_execution(
628
+ cli_params: CLIRuntimeParams,
629
+ execution_fn: Callable[[Executor, ScriptNode, bool], ExecutionStats],
630
+ execution_mode: ExecutionMode = ExecutionMode.RUN,
631
+ ) -> None:
632
+ """
633
+ Run parallel execution for directory inputs, or single-script execution
634
+ with polished progress display for single files/inline queries.
635
+ """
636
+ from trilogy.execution.config import apply_env_vars, load_env_file
637
+ from trilogy.scripts.common import (
638
+ create_executor_for_script,
639
+ merge_runtime_config,
640
+ resolve_input_information,
641
+ )
642
+ from trilogy.scripts.dependency import ETLDependencyStrategy
643
+ from trilogy.scripts.display import (
644
+ print_error,
645
+ print_success,
646
+ show_execution_info,
647
+ show_parallel_execution_start,
648
+ show_parallel_execution_summary,
649
+ show_script_result,
650
+ )
651
+ from trilogy.scripts.environment import parse_env_vars
652
+
653
+ # Check if input is a directory (parallel execution)
654
+ pathlib_input = Path(cli_params.input)
655
+ files_iter, directory, input_type, input_name, config = resolve_input_information(
656
+ cli_params.input, cli_params.config_path
657
+ )
658
+ files = list(files_iter)
659
+
660
+ # Load environment variables from config env_files first
661
+ for env_file in config.env_files:
662
+ env_vars = load_env_file(env_file)
663
+ apply_env_vars(env_vars)
664
+
665
+ # Then apply CLI --env options (these take precedence)
666
+ if cli_params.env:
667
+ cli_env_vars = parse_env_vars(cli_params.env)
668
+ apply_env_vars(cli_env_vars)
669
+
670
+ # Merge CLI params with config file
671
+ edialect, parallelism = merge_runtime_config(cli_params, config)
672
+ if not pathlib_input.exists() or len(files) == 1:
673
+ # Inline query - use polished single-script execution
674
+
675
+ run_single_script_execution(
676
+ files=files,
677
+ directory=directory,
678
+ input_type=input_type,
679
+ input_name=input_name,
680
+ edialect=edialect,
681
+ param=cli_params.param,
682
+ conn_args=cli_params.conn_args,
683
+ debug=cli_params.debug,
684
+ execution_mode=execution_mode,
685
+ config=config,
686
+ )
687
+ return
688
+ # Multiple files - use parallel execution
689
+ config_path_str = str(config.source_path) if config.source_path else None
690
+ show_execution_info(
691
+ input_type, input_name, edialect.value, cli_params.debug, config_path_str
692
+ )
693
+
694
+ # Get execution strategy
695
+ strategy = get_execution_strategy(cli_params.execution_strategy)
696
+
697
+ # Set up parallel executor
698
+ parallel_exec = ParallelExecutor(
699
+ max_workers=parallelism,
700
+ dependency_strategy=ETLDependencyStrategy(),
701
+ execution_strategy=strategy,
702
+ )
703
+
704
+ # Get execution plan for display
705
+ if pathlib_input.is_dir():
706
+ execution_plan = parallel_exec.get_folder_execution_plan(pathlib_input)
707
+ elif pathlib_input.is_file():
708
+ execution_plan = parallel_exec.get_execution_plan([pathlib_input])
709
+ else:
710
+ raise FileNotFoundError(f"Input path '{pathlib_input}' does not exist.")
711
+
712
+ num_edges = execution_plan.number_of_edges()
713
+ num_nodes = execution_plan.number_of_nodes()
714
+
715
+ show_parallel_execution_start(
716
+ num_nodes, num_edges, parallelism, cli_params.execution_strategy
717
+ )
718
+
719
+ # Factory to create executor for each script
720
+ def executor_factory(node: ScriptNode) -> Executor:
721
+ return create_executor_for_script(
722
+ node,
723
+ cli_params.param,
724
+ cli_params.conn_args,
725
+ edialect,
726
+ cli_params.debug,
727
+ config,
728
+ )
729
+
730
+ # Wrap execution_fn to pass quiet=True for parallel execution and return stats
731
+ def quiet_execution_fn(exec: Executor, node: ScriptNode) -> ExecutionStats | None:
732
+ return execution_fn(exec, node, True)
733
+
734
+ # Run parallel execution
735
+ summary = parallel_exec.execute(
736
+ root=pathlib_input,
737
+ executor_factory=executor_factory,
738
+ execution_fn=quiet_execution_fn,
739
+ on_script_complete=show_script_result,
740
+ )
741
+
742
+ show_parallel_execution_summary(summary)
743
+
744
+ if not summary.all_succeeded:
745
+ print_error("Some scripts failed during execution.")
746
+ raise Exit(1)
747
+
748
+ print_success("All scripts executed successfully!")