pytrilogy 0.3.149__cp313-cp313-win_amd64.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 (207) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cp313-win_amd64.pyd +0 -0
  4. pytrilogy-0.3.149.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.149.dist-info/RECORD +207 -0
  6. pytrilogy-0.3.149.dist-info/WHEEL +4 -0
  7. pytrilogy-0.3.149.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.149.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 +2670 -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 +436 -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 +846 -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 +1432 -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 +397 -0
  117. trilogy/dialect/enums.py +151 -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/exceptions.py +26 -0
  130. trilogy/execution/state/file_state_store.py +0 -0
  131. trilogy/execution/state/sqllite_state_store.py +0 -0
  132. trilogy/execution/state/state_store.py +406 -0
  133. trilogy/executor.py +692 -0
  134. trilogy/hooks/__init__.py +4 -0
  135. trilogy/hooks/base_hook.py +40 -0
  136. trilogy/hooks/graph_hook.py +135 -0
  137. trilogy/hooks/query_debugger.py +166 -0
  138. trilogy/metadata/__init__.py +0 -0
  139. trilogy/parser.py +10 -0
  140. trilogy/parsing/README.md +21 -0
  141. trilogy/parsing/__init__.py +0 -0
  142. trilogy/parsing/common.py +1069 -0
  143. trilogy/parsing/config.py +5 -0
  144. trilogy/parsing/exceptions.py +8 -0
  145. trilogy/parsing/helpers.py +1 -0
  146. trilogy/parsing/parse_engine.py +2876 -0
  147. trilogy/parsing/render.py +775 -0
  148. trilogy/parsing/trilogy.lark +546 -0
  149. trilogy/py.typed +0 -0
  150. trilogy/render.py +45 -0
  151. trilogy/scripts/README.md +9 -0
  152. trilogy/scripts/__init__.py +0 -0
  153. trilogy/scripts/agent.py +41 -0
  154. trilogy/scripts/agent_info.py +306 -0
  155. trilogy/scripts/common.py +432 -0
  156. trilogy/scripts/dependency/Cargo.lock +617 -0
  157. trilogy/scripts/dependency/Cargo.toml +39 -0
  158. trilogy/scripts/dependency/README.md +131 -0
  159. trilogy/scripts/dependency/build.sh +25 -0
  160. trilogy/scripts/dependency/src/directory_resolver.rs +387 -0
  161. trilogy/scripts/dependency/src/lib.rs +16 -0
  162. trilogy/scripts/dependency/src/main.rs +770 -0
  163. trilogy/scripts/dependency/src/parser.rs +435 -0
  164. trilogy/scripts/dependency/src/preql.pest +208 -0
  165. trilogy/scripts/dependency/src/python_bindings.rs +311 -0
  166. trilogy/scripts/dependency/src/resolver.rs +716 -0
  167. trilogy/scripts/dependency/tests/base.preql +3 -0
  168. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  169. trilogy/scripts/dependency/tests/customer.preql +6 -0
  170. trilogy/scripts/dependency/tests/main.preql +9 -0
  171. trilogy/scripts/dependency/tests/orders.preql +7 -0
  172. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  173. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  174. trilogy/scripts/dependency.py +323 -0
  175. trilogy/scripts/display.py +555 -0
  176. trilogy/scripts/environment.py +59 -0
  177. trilogy/scripts/fmt.py +32 -0
  178. trilogy/scripts/ingest.py +487 -0
  179. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  180. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  181. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  182. trilogy/scripts/ingest_helpers/typing.py +161 -0
  183. trilogy/scripts/init.py +105 -0
  184. trilogy/scripts/parallel_execution.py +762 -0
  185. trilogy/scripts/plan.py +189 -0
  186. trilogy/scripts/refresh.py +161 -0
  187. trilogy/scripts/run.py +79 -0
  188. trilogy/scripts/serve.py +202 -0
  189. trilogy/scripts/serve_helpers/__init__.py +41 -0
  190. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  191. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  192. trilogy/scripts/serve_helpers/models.py +38 -0
  193. trilogy/scripts/single_execution.py +131 -0
  194. trilogy/scripts/testing.py +143 -0
  195. trilogy/scripts/trilogy.py +75 -0
  196. trilogy/std/__init__.py +0 -0
  197. trilogy/std/color.preql +3 -0
  198. trilogy/std/date.preql +13 -0
  199. trilogy/std/display.preql +18 -0
  200. trilogy/std/geography.preql +22 -0
  201. trilogy/std/metric.preql +15 -0
  202. trilogy/std/money.preql +67 -0
  203. trilogy/std/net.preql +14 -0
  204. trilogy/std/ranking.preql +7 -0
  205. trilogy/std/report.preql +5 -0
  206. trilogy/std/semantic.preql +6 -0
  207. trilogy/utility.py +34 -0
@@ -0,0 +1,555 @@
1
+ """Display helpers for prettier CLI output with configurable Rich support."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ from click import echo, style
7
+
8
+ # Type checking imports for forward references
9
+ if TYPE_CHECKING:
10
+ from trilogy.scripts.parallel_execution import (
11
+ ExecutionResult,
12
+ ParallelExecutionSummary,
13
+ )
14
+
15
+ # Try to import Rich for enhanced output
16
+ try:
17
+ from rich import box
18
+ from rich.console import Console
19
+ from rich.panel import Panel
20
+ from rich.progress import (
21
+ BarColumn,
22
+ Progress,
23
+ SpinnerColumn,
24
+ TextColumn,
25
+ TimeElapsedColumn,
26
+ )
27
+ from rich.table import Table
28
+
29
+ RICH_AVAILABLE = True
30
+ console: Optional[Console] = Console()
31
+ except ImportError:
32
+ RICH_AVAILABLE = False
33
+ console = None
34
+
35
+ Progress = lambda: None # type: ignore # noqa: E731
36
+ Table = lambda: None # type: ignore # noqa: E731
37
+ Panel = lambda: None # type: ignore # noqa: E731
38
+
39
+ FETCH_LIMIT = 51
40
+
41
+
42
+ @dataclass
43
+ class ResultSet:
44
+ rows: list[tuple]
45
+ columns: list[str]
46
+
47
+
48
+ class SetRichMode:
49
+ """
50
+ Callable class that can be used as both a function and a context manager.
51
+
52
+ Regular usage:
53
+ set_rich_mode(True) # Enable Rich for output formatting for CL
54
+ set_rich_mode(False) # Disables Rich for output formatting
55
+
56
+ Context manager usage:
57
+ with set_rich_mode(True):
58
+ # Rich output mode temporarily disabled
59
+ pass
60
+ # Previous state automatically restored
61
+ """
62
+
63
+ def __call__(self, enabled: bool):
64
+ current = is_rich_available()
65
+ prior = RichModeContext(enabled, current)
66
+ self._set_mode(enabled)
67
+ return prior
68
+
69
+ def _set_mode(self, enabled: bool):
70
+ global RICH_AVAILABLE, console
71
+
72
+ if enabled:
73
+ try:
74
+ from rich.console import Console
75
+
76
+ RICH_AVAILABLE = True
77
+ console = Console()
78
+ except ImportError:
79
+ RICH_AVAILABLE = False
80
+ console = None
81
+ else:
82
+ RICH_AVAILABLE = False
83
+ console = None
84
+
85
+
86
+ class RichModeContext:
87
+ """Context manager returned by SetRichMode for 'with' statement usage."""
88
+
89
+ def __init__(self, enabled: bool, current: bool):
90
+ self.enabled = enabled
91
+ self.old_rich_available = current
92
+ self.old_console = None
93
+
94
+ def __enter__(self):
95
+ global RICH_AVAILABLE, console
96
+
97
+ self.old_console = console
98
+ # The mode was already set by __call__, so we're good
99
+ return self
100
+
101
+ def __exit__(self, exc_type, exc_val, exc_tb):
102
+ global RICH_AVAILABLE, console
103
+
104
+ # Restore previous state
105
+ RICH_AVAILABLE = self.old_rich_available
106
+ console = self.old_console
107
+
108
+
109
+ set_rich_mode = SetRichMode()
110
+
111
+
112
+ def is_rich_available() -> bool:
113
+ """Check if Rich mode is currently available."""
114
+ return RICH_AVAILABLE
115
+
116
+
117
+ def print_success(message: str):
118
+ """Print success message with styling."""
119
+ if RICH_AVAILABLE and console is not None:
120
+ console.print(message, style="bold green")
121
+ else:
122
+ echo(style(message, fg="green", bold=True))
123
+
124
+
125
+ def print_info(message: str):
126
+ """Print info message with styling."""
127
+ if RICH_AVAILABLE and console is not None:
128
+ console.print(message, style="bold blue")
129
+ else:
130
+ echo(style(message, fg="blue", bold=True))
131
+
132
+
133
+ def print_warning(message: str):
134
+ """Print warning message with styling."""
135
+ if RICH_AVAILABLE and console is not None:
136
+ console.print(message, style="bold yellow")
137
+ else:
138
+ echo(style(message, fg="yellow", bold=True))
139
+
140
+
141
+ def print_error(message: str):
142
+ """Print error message with styling."""
143
+ if RICH_AVAILABLE and console is not None:
144
+ console.print(message, style="bold red")
145
+ else:
146
+ echo(style(message, fg="red", bold=True))
147
+
148
+
149
+ def print_header(message: str):
150
+ """Print header message with styling."""
151
+ if RICH_AVAILABLE and console is not None:
152
+ console.print(message, style="bold magenta")
153
+ else:
154
+ echo(style(message, fg="magenta", bold=True))
155
+
156
+
157
+ def format_duration(duration):
158
+ """Format duration nicely."""
159
+ total_seconds = duration.total_seconds()
160
+ if total_seconds < 1:
161
+ return f"{total_seconds*1000:.0f}ms"
162
+ elif total_seconds < 60:
163
+ return f"{total_seconds:.2f}s"
164
+ else:
165
+ minutes = int(total_seconds // 60)
166
+ seconds = total_seconds % 60
167
+ return f"{minutes}m {seconds:.2f}s"
168
+
169
+
170
+ def show_execution_info(
171
+ input_type: str,
172
+ input_name: str,
173
+ dialect: str,
174
+ debug: bool,
175
+ config_path: Optional[str] = None,
176
+ ):
177
+ """Display execution information in a clean format."""
178
+ if RICH_AVAILABLE and console is not None:
179
+ info_text = (
180
+ f"Input: {input_type} ({input_name})\n"
181
+ f"Dialect: [cyan]{dialect}[/cyan]\n"
182
+ f"Debug: {'enabled' if debug else 'disabled'}"
183
+ )
184
+ if config_path:
185
+ info_text += f"\nConfig: [dim]{config_path}[/dim]"
186
+ panel = Panel.fit(info_text, style="blue", title="Execution Info")
187
+ console.print(panel)
188
+ else:
189
+ msg = f"Executing {input_type}: {input_name} | Dialect: {dialect} | Debug: {debug}"
190
+ if config_path:
191
+ msg += f" | Config: {config_path}"
192
+ print_info(msg)
193
+
194
+
195
+ def show_environment_params(env_params: dict):
196
+ """Display environment parameters if any."""
197
+ if env_params:
198
+ if RICH_AVAILABLE and console is not None:
199
+ console.print(f"Environment parameters: {env_params}", style="dim cyan")
200
+ else:
201
+ echo(style(f"Environment parameters: {env_params}", fg="cyan"))
202
+
203
+
204
+ def show_debug_mode():
205
+ """Show debug mode indicator."""
206
+ if RICH_AVAILABLE and console is not None:
207
+ panel = Panel.fit("Debug mode enabled", style="yellow", title="Debug")
208
+ console.print(panel)
209
+
210
+
211
+ def show_statement_type(idx: int, total: int, statement_type: str):
212
+ """Show the type of statement before execution."""
213
+ statement_num = f"Statement {idx+1}"
214
+ if total > 1:
215
+ statement_num += f"/{total}"
216
+
217
+ if RICH_AVAILABLE and console is not None:
218
+ console.print(
219
+ f"[bold cyan]{statement_num}[/bold cyan] [dim]({statement_type})[/dim]"
220
+ )
221
+ else:
222
+ echo(style(f"{statement_num} ({statement_type})", fg="cyan", bold=True))
223
+
224
+
225
+ def print_results_table(results: ResultSet):
226
+ """Print query results using Rich tables or fallback."""
227
+ if RICH_AVAILABLE and console is not None:
228
+ _print_rich_table(results.rows, headers=results.columns)
229
+ else:
230
+ _print_fallback_table(results.rows, results.columns)
231
+
232
+
233
+ def _print_rich_table(result, headers=None):
234
+ """Print query results using Rich tables."""
235
+ if console is None:
236
+ return
237
+
238
+ if not result:
239
+ console.print("No results returned.", style="dim")
240
+ return
241
+
242
+ # Create Rich table
243
+ table = Table(
244
+ box=box.MINIMAL_DOUBLE_HEAD, show_header=True, header_style="bold blue"
245
+ )
246
+
247
+ # Add columns
248
+ column_names = headers
249
+ for col in column_names:
250
+ table.add_column(str(col), style="white", no_wrap=False)
251
+
252
+ # Add rows (limit to reasonable number for display)
253
+ for i, row in enumerate(result):
254
+ if i >= FETCH_LIMIT:
255
+ table.add_row(*["..." for _ in column_names], style="dim")
256
+ console.print(
257
+ f"[dim]Showing first {FETCH_LIMIT-1} rows. Result set was larger.[/dim]"
258
+ )
259
+ break
260
+ # Convert all values to strings and handle None
261
+ row_data = [str(val) if val is not None else "[dim]NULL[/dim]" for val in row]
262
+ table.add_row(*row_data)
263
+
264
+ console.print(table)
265
+
266
+
267
+ def _print_fallback_table(rows, headers: list[str]):
268
+ """Fallback table printing when Rich is not available."""
269
+ print_warning("Install rich for prettier table output")
270
+ print(", ".join(headers))
271
+ for row in rows:
272
+ print(row)
273
+ print("---")
274
+
275
+
276
+ def show_execution_start(num_queries: int):
277
+ """Show execution start message."""
278
+ statement_word = "statement" if num_queries == 1 else "statements"
279
+ if RICH_AVAILABLE and console is not None:
280
+ console.print(f"\n[bold]Executing {num_queries} {statement_word}...[/bold]")
281
+ else:
282
+ print_info(f"Executing {num_queries} {statement_word}...")
283
+
284
+
285
+ def create_progress_context() -> Progress:
286
+ return Progress(
287
+ SpinnerColumn(),
288
+ TextColumn("[progress.description]{task.description}"),
289
+ BarColumn(),
290
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
291
+ TimeElapsedColumn(),
292
+ console=console,
293
+ )
294
+
295
+
296
+ def show_statement_result(
297
+ idx: int, total: int, duration, has_results: bool, error=None, exception_type=None
298
+ ):
299
+ """Show result of individual statement execution."""
300
+ statement_num = f"Statement {idx+1}"
301
+ if total > 1:
302
+ statement_num += f"/{total}"
303
+
304
+ duration_str = f"({format_duration(duration)})"
305
+
306
+ if error is not None:
307
+ # Provide more context for unclear error messages
308
+ error_str = str(error).strip()
309
+ if not error_str or error_str in ["0", "None", "null", ""]:
310
+ if exception_type:
311
+ error_msg = f"{statement_num} failed with {exception_type.__name__}"
312
+ if error_str and error_str not in ["None", "null"]:
313
+ error_msg += f" (code: {error_str})"
314
+ else:
315
+ error_msg = f"{statement_num} failed with unclear error"
316
+ if error_str:
317
+ error_msg += f": '{error_str}'"
318
+ else:
319
+ error_msg = f"{statement_num} failed: {error_str}"
320
+
321
+ print_error(error_msg)
322
+ elif has_results:
323
+ if RICH_AVAILABLE and console is not None:
324
+ console.print(
325
+ f"\n[bold green]{statement_num} Results[/bold green] [dim]{duration_str}[/dim]"
326
+ )
327
+ else:
328
+ print_success(f"{statement_num} completed {duration_str}")
329
+ else:
330
+ if RICH_AVAILABLE and console is not None:
331
+ console.print(
332
+ f"[green]{statement_num} completed[/green] [dim]{duration_str}[/dim]"
333
+ )
334
+ else:
335
+ print_success(f"{statement_num} completed {duration_str}")
336
+
337
+
338
+ def show_execution_summary(num_queries: int, total_duration, all_succeeded: bool):
339
+ """Show final execution summary."""
340
+ if RICH_AVAILABLE and console is not None:
341
+ if all_succeeded:
342
+ style = "green"
343
+ state = "Complete"
344
+ else:
345
+ style = "red"
346
+ state = "Failed"
347
+ summary_text = (
348
+ f"[bold {style}]Execution {state}[/bold {style}]\n"
349
+ f"Total time: [cyan]{format_duration(total_duration)}[/cyan]\n"
350
+ f"Statements: [cyan]{num_queries}[/cyan]"
351
+ )
352
+
353
+ summary = Panel.fit(summary_text, style=style, title="Summary")
354
+ console.print("\n")
355
+ console.print(summary)
356
+ else:
357
+ if not all_succeeded:
358
+ print_error(f"Statements failed in {format_duration(total_duration)}")
359
+ else:
360
+ print_success(
361
+ f"Statements: {num_queries} Completed in: {format_duration(total_duration)}"
362
+ )
363
+
364
+
365
+ def show_formatting_result(filename: str, num_queries: int, duration):
366
+ """Show formatting operation result."""
367
+ if RICH_AVAILABLE and console is not None:
368
+ console.print(f"File: [bold]{filename}[/bold]")
369
+ console.print(
370
+ f"Processed [cyan]{num_queries}[/cyan] queries in [cyan]{format_duration(duration)}[/cyan]"
371
+ )
372
+ else:
373
+ print_success(f"Formatted {num_queries} queries in {format_duration(duration)}")
374
+
375
+
376
+ def with_status(message: str):
377
+ """Context manager for showing status."""
378
+ if RICH_AVAILABLE and console is not None:
379
+ return console.status(f"[bold green]{message}...")
380
+ else:
381
+ print_info(f"{message}...")
382
+ return _DummyContext()
383
+
384
+
385
+ class _DummyContext:
386
+ """Dummy context manager for fallback."""
387
+
388
+ def __enter__(self):
389
+ return self
390
+
391
+ def __exit__(self, *args):
392
+ pass
393
+
394
+
395
+ def show_parallel_execution_start(
396
+ num_files: int, num_edges: int, parallelism: int, strategy: str = "eager_bfs"
397
+ ) -> None:
398
+ """Display parallel execution start information."""
399
+ if RICH_AVAILABLE and console is not None:
400
+ console.print("\n[bold blue]Starting parallel execution:[/bold blue]")
401
+ console.print(f" Files: {num_files}")
402
+ console.print(f" Dependencies: {num_edges}")
403
+ console.print(f" Max parallelism: {parallelism}")
404
+ console.print(f" Strategy: {strategy}")
405
+ else:
406
+ print("\nStarting parallel execution:")
407
+ print(f" Files: {num_files}")
408
+ print(f" Dependencies: {num_edges}")
409
+ print(f" Max parallelism: {parallelism}")
410
+ print(f" Strategy: {strategy}")
411
+
412
+
413
+ def show_parallel_execution_summary(summary: "ParallelExecutionSummary") -> None:
414
+ """Display parallel execution summary."""
415
+ from trilogy.scripts.common import ExecutionStats
416
+
417
+ # Aggregate stats from all results
418
+ total_stats = ExecutionStats()
419
+ for result in summary.results:
420
+ if result.stats:
421
+ total_stats = total_stats + result.stats
422
+
423
+ if RICH_AVAILABLE and console is not None:
424
+ # Summary table
425
+ table = Table(title="Execution Summary", show_header=False)
426
+ table.add_column("Metric", style="cyan")
427
+ table.add_column("Value", style="green")
428
+
429
+ table.add_row("Total Scripts", str(summary.total_scripts))
430
+ table.add_row("Successful", str(summary.successful))
431
+ table.add_row("Failed", str(summary.failed))
432
+ table.add_row("Total Duration", f"{summary.total_duration:.2f}s")
433
+
434
+ # Add aggregated stats
435
+ if total_stats.update_count > 0:
436
+ table.add_row("Datasources Updated", str(total_stats.update_count))
437
+ if total_stats.validate_count > 0:
438
+ table.add_row("Datasources Validated", str(total_stats.validate_count))
439
+ if total_stats.persist_count > 0:
440
+ table.add_row("Tables Persisted", str(total_stats.persist_count))
441
+
442
+ console.print(table)
443
+
444
+ # Failed scripts details
445
+ if summary.failed > 0:
446
+ console.print("\n[bold red]Failed Scripts:[/bold red]")
447
+ for result in summary.results:
448
+ if not result.success:
449
+ console.print(f" [red]✗[/red] {result.node.path}")
450
+ if result.error:
451
+ console.print(f" Error: {result.error}")
452
+ else:
453
+ print("Execution Summary:")
454
+ print(f" Total Scripts: {summary.total_scripts}")
455
+ print(f" Successful: {summary.successful}")
456
+ print(f" Failed: {summary.failed}")
457
+ print(f" Total Duration: {summary.total_duration:.2f}s")
458
+
459
+ # Add aggregated stats
460
+ if total_stats.update_count > 0:
461
+ print(f" Datasources Updated: {total_stats.update_count}")
462
+ if total_stats.validate_count > 0:
463
+ print(f" Datasources Validated: {total_stats.validate_count}")
464
+ if total_stats.persist_count > 0:
465
+ print(f" Tables Persisted: {total_stats.persist_count}")
466
+
467
+ if summary.failed > 0:
468
+ print("\nFailed Scripts:")
469
+ for result in summary.results:
470
+ if not result.success:
471
+ print(f" ✗ {result.node.path}")
472
+ if result.error:
473
+ print(f" Error: {result.error}")
474
+
475
+
476
+ def show_script_result(
477
+ result: "ExecutionResult", stat_types: list[str] | None = None
478
+ ) -> None:
479
+ """Display result of a single script execution."""
480
+ from trilogy.scripts.common import format_stats
481
+
482
+ stats_str = ""
483
+ if result.stats:
484
+ formatted = format_stats(result.stats, stat_types)
485
+ if formatted:
486
+ stats_str = f" [{formatted}]"
487
+
488
+ if RICH_AVAILABLE and console is not None:
489
+ if result.success:
490
+ console.print(
491
+ f" [green]✓[/green] {result.node.path.name} ({result.duration:.2f}s){stats_str}"
492
+ )
493
+ else:
494
+ console.print(
495
+ f" [red]✗[/red] {result.node.path.name} ({result.duration:.2f}s) - {result.error}"
496
+ )
497
+ else:
498
+ if result.success:
499
+ print(f" ✓ {result.node.path.name} ({result.duration:.2f}s){stats_str}")
500
+ else:
501
+ print(
502
+ f" ✗ {result.node.path.name} ({result.duration:.2f}s) - {result.error}"
503
+ )
504
+
505
+
506
+ def show_execution_plan(
507
+ nodes: list[str], edges: list[tuple[str, str]], execution_order: list[list[str]]
508
+ ) -> None:
509
+ """Display execution plan in human-readable format."""
510
+ if RICH_AVAILABLE and console is not None:
511
+ # Summary panel
512
+ info_text = (
513
+ f"Scripts: [cyan]{len(nodes)}[/cyan]\n"
514
+ f"Dependencies: [cyan]{len(edges)}[/cyan]\n"
515
+ f"Execution Levels: [cyan]{len(execution_order)}[/cyan]"
516
+ )
517
+ panel = Panel.fit(info_text, style="blue", title="Execution Plan")
518
+ console.print(panel)
519
+
520
+ # Execution order table
521
+ if execution_order:
522
+ table = Table(
523
+ title="Execution Order",
524
+ show_header=True,
525
+ header_style="bold blue",
526
+ box=box.MINIMAL_DOUBLE_HEAD,
527
+ )
528
+ table.add_column("Level", style="cyan", no_wrap=True)
529
+ table.add_column("Scripts (can run in parallel)", style="white")
530
+
531
+ for level, scripts in enumerate(execution_order):
532
+ table.add_row(str(level + 1), ", ".join(scripts))
533
+
534
+ console.print(table)
535
+
536
+ # Dependency details
537
+ if edges:
538
+ console.print("\n[bold]Dependencies:[/bold]")
539
+ for from_node, to_node in edges:
540
+ console.print(f" [dim]{from_node}[/dim] -> [white]{to_node}[/white]")
541
+ else:
542
+ print("Execution Plan:")
543
+ print(f" Scripts: {len(nodes)}")
544
+ print(f" Dependencies: {len(edges)}")
545
+ print(f" Execution Levels: {len(execution_order)}")
546
+
547
+ if execution_order:
548
+ print("\nExecution Order:")
549
+ for level, scripts in enumerate(execution_order):
550
+ print(f" Level {level + 1}: {', '.join(scripts)}")
551
+
552
+ if edges:
553
+ print("\nDependencies:")
554
+ for from_node, to_node in edges:
555
+ print(f" {from_node} -> {to_node}")
@@ -0,0 +1,59 @@
1
+ from typing import Any, Iterable
2
+
3
+
4
+ def pairwise(t: Iterable[Any]) -> Iterable[tuple[Any, Any]]:
5
+ it = iter(t)
6
+ return zip(it, it)
7
+
8
+
9
+ def smart_convert(value: str):
10
+ """Convert string to appropriate Python type."""
11
+ if not value:
12
+ return value
13
+
14
+ # Handle booleans
15
+ if value.lower() in ("true", "false"):
16
+ return value.lower() == "true"
17
+
18
+ # Try numeric conversion
19
+ try:
20
+ if "." not in value and "e" not in value.lower():
21
+ return int(value)
22
+ return float(value)
23
+ except ValueError:
24
+ return value
25
+
26
+
27
+ def extra_to_kwargs(arg_list: Iterable[str]) -> dict[str, Any]:
28
+ pairs = pairwise(arg_list)
29
+ final = {}
30
+ for k, v in pairs:
31
+ k = k.lstrip("--")
32
+ final[k] = smart_convert(v)
33
+ return final
34
+
35
+
36
+ def parse_env_params(env_param_list: tuple[str, ...]) -> dict[str, Any]:
37
+ """Parse environment parameters from key=value format with type conversion."""
38
+ env_params: dict[str, Any] = {}
39
+ for param in env_param_list:
40
+ if "=" not in param:
41
+ raise ValueError(
42
+ f"Environment parameter must be in key=value format: {param}"
43
+ )
44
+ key, value = param.split("=", 1) # Split on first = only
45
+ env_params[key] = smart_convert(value)
46
+ return env_params
47
+
48
+
49
+ def parse_env_vars(env_var_list: tuple[str, ...]) -> dict[str, str]:
50
+ """Parse environment variables from KEY=VALUE format (keeps values as strings)."""
51
+ env_vars: dict[str, str] = {}
52
+ for param in env_var_list:
53
+ if "=" not in param:
54
+ raise ValueError(
55
+ f"Environment variable must be in KEY=VALUE format: {param}"
56
+ )
57
+ key, value = param.split("=", 1)
58
+ env_vars[key] = value
59
+ return env_vars
trilogy/scripts/fmt.py ADDED
@@ -0,0 +1,32 @@
1
+ """Format command for Trilogy CLI."""
2
+
3
+ from datetime import datetime
4
+
5
+ from click import Path, argument, pass_context
6
+
7
+ from trilogy import parse
8
+ from trilogy.parsing.render import Renderer
9
+ from trilogy.scripts.common import handle_execution_exception
10
+ from trilogy.scripts.display import print_success, show_formatting_result, with_status
11
+
12
+
13
+ @argument("input", type=Path(exists=True))
14
+ @pass_context
15
+ def fmt(ctx, input):
16
+ """Format a Trilogy script file."""
17
+ with with_status("Formatting script"):
18
+ start = datetime.now()
19
+ try:
20
+ with open(input, "r") as f:
21
+ script = f.read()
22
+ _, queries = parse(script)
23
+ r = Renderer()
24
+ with open(input, "w") as f:
25
+ f.write("\n".join([r.to_string(x) for x in queries]))
26
+ duration = datetime.now() - start
27
+
28
+ print_success("Script formatted successfully")
29
+ show_formatting_result(input, len(queries), duration)
30
+
31
+ except Exception as e:
32
+ handle_execution_exception(e, debug=ctx.obj["DEBUG"])