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.
- LICENSE.md +19 -0
- _preql_import_resolver/__init__.py +5 -0
- _preql_import_resolver/_preql_import_resolver.cpython-312-aarch64-linux-gnu.so +0 -0
- pytrilogy-0.3.148.dist-info/METADATA +555 -0
- pytrilogy-0.3.148.dist-info/RECORD +206 -0
- pytrilogy-0.3.148.dist-info/WHEEL +5 -0
- pytrilogy-0.3.148.dist-info/entry_points.txt +2 -0
- pytrilogy-0.3.148.dist-info/licenses/LICENSE.md +19 -0
- trilogy/__init__.py +27 -0
- trilogy/ai/README.md +10 -0
- trilogy/ai/__init__.py +19 -0
- trilogy/ai/constants.py +92 -0
- trilogy/ai/conversation.py +107 -0
- trilogy/ai/enums.py +7 -0
- trilogy/ai/execute.py +50 -0
- trilogy/ai/models.py +34 -0
- trilogy/ai/prompts.py +100 -0
- trilogy/ai/providers/__init__.py +0 -0
- trilogy/ai/providers/anthropic.py +106 -0
- trilogy/ai/providers/base.py +24 -0
- trilogy/ai/providers/google.py +146 -0
- trilogy/ai/providers/openai.py +89 -0
- trilogy/ai/providers/utils.py +68 -0
- trilogy/authoring/README.md +3 -0
- trilogy/authoring/__init__.py +148 -0
- trilogy/constants.py +119 -0
- trilogy/core/README.md +52 -0
- trilogy/core/__init__.py +0 -0
- trilogy/core/constants.py +6 -0
- trilogy/core/enums.py +454 -0
- trilogy/core/env_processor.py +239 -0
- trilogy/core/environment_helpers.py +320 -0
- trilogy/core/ergonomics.py +193 -0
- trilogy/core/exceptions.py +123 -0
- trilogy/core/functions.py +1240 -0
- trilogy/core/graph_models.py +142 -0
- trilogy/core/internal.py +85 -0
- trilogy/core/models/__init__.py +0 -0
- trilogy/core/models/author.py +2662 -0
- trilogy/core/models/build.py +2603 -0
- trilogy/core/models/build_environment.py +165 -0
- trilogy/core/models/core.py +506 -0
- trilogy/core/models/datasource.py +434 -0
- trilogy/core/models/environment.py +756 -0
- trilogy/core/models/execute.py +1213 -0
- trilogy/core/optimization.py +251 -0
- trilogy/core/optimizations/__init__.py +12 -0
- trilogy/core/optimizations/base_optimization.py +17 -0
- trilogy/core/optimizations/hide_unused_concept.py +47 -0
- trilogy/core/optimizations/inline_datasource.py +102 -0
- trilogy/core/optimizations/predicate_pushdown.py +245 -0
- trilogy/core/processing/README.md +94 -0
- trilogy/core/processing/READMEv2.md +121 -0
- trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
- trilogy/core/processing/__init__.py +0 -0
- trilogy/core/processing/concept_strategies_v3.py +508 -0
- trilogy/core/processing/constants.py +15 -0
- trilogy/core/processing/discovery_node_factory.py +451 -0
- trilogy/core/processing/discovery_utility.py +548 -0
- trilogy/core/processing/discovery_validation.py +167 -0
- trilogy/core/processing/graph_utils.py +43 -0
- trilogy/core/processing/node_generators/README.md +9 -0
- trilogy/core/processing/node_generators/__init__.py +31 -0
- trilogy/core/processing/node_generators/basic_node.py +160 -0
- trilogy/core/processing/node_generators/common.py +270 -0
- trilogy/core/processing/node_generators/constant_node.py +38 -0
- trilogy/core/processing/node_generators/filter_node.py +315 -0
- trilogy/core/processing/node_generators/group_node.py +213 -0
- trilogy/core/processing/node_generators/group_to_node.py +117 -0
- trilogy/core/processing/node_generators/multiselect_node.py +207 -0
- trilogy/core/processing/node_generators/node_merge_node.py +695 -0
- trilogy/core/processing/node_generators/recursive_node.py +88 -0
- trilogy/core/processing/node_generators/rowset_node.py +165 -0
- trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
- trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
- trilogy/core/processing/node_generators/select_merge_node.py +786 -0
- trilogy/core/processing/node_generators/select_node.py +95 -0
- trilogy/core/processing/node_generators/synonym_node.py +98 -0
- trilogy/core/processing/node_generators/union_node.py +91 -0
- trilogy/core/processing/node_generators/unnest_node.py +182 -0
- trilogy/core/processing/node_generators/window_node.py +201 -0
- trilogy/core/processing/nodes/README.md +28 -0
- trilogy/core/processing/nodes/__init__.py +179 -0
- trilogy/core/processing/nodes/base_node.py +522 -0
- trilogy/core/processing/nodes/filter_node.py +75 -0
- trilogy/core/processing/nodes/group_node.py +194 -0
- trilogy/core/processing/nodes/merge_node.py +420 -0
- trilogy/core/processing/nodes/recursive_node.py +46 -0
- trilogy/core/processing/nodes/select_node_v2.py +242 -0
- trilogy/core/processing/nodes/union_node.py +53 -0
- trilogy/core/processing/nodes/unnest_node.py +62 -0
- trilogy/core/processing/nodes/window_node.py +56 -0
- trilogy/core/processing/utility.py +823 -0
- trilogy/core/query_processor.py +604 -0
- trilogy/core/statements/README.md +35 -0
- trilogy/core/statements/__init__.py +0 -0
- trilogy/core/statements/author.py +536 -0
- trilogy/core/statements/build.py +0 -0
- trilogy/core/statements/common.py +20 -0
- trilogy/core/statements/execute.py +155 -0
- trilogy/core/table_processor.py +66 -0
- trilogy/core/utility.py +8 -0
- trilogy/core/validation/README.md +46 -0
- trilogy/core/validation/__init__.py +0 -0
- trilogy/core/validation/common.py +161 -0
- trilogy/core/validation/concept.py +146 -0
- trilogy/core/validation/datasource.py +227 -0
- trilogy/core/validation/environment.py +73 -0
- trilogy/core/validation/fix.py +256 -0
- trilogy/dialect/__init__.py +32 -0
- trilogy/dialect/base.py +1431 -0
- trilogy/dialect/bigquery.py +314 -0
- trilogy/dialect/common.py +147 -0
- trilogy/dialect/config.py +159 -0
- trilogy/dialect/dataframe.py +50 -0
- trilogy/dialect/duckdb.py +376 -0
- trilogy/dialect/enums.py +149 -0
- trilogy/dialect/metadata.py +173 -0
- trilogy/dialect/mock.py +190 -0
- trilogy/dialect/postgres.py +117 -0
- trilogy/dialect/presto.py +110 -0
- trilogy/dialect/results.py +89 -0
- trilogy/dialect/snowflake.py +129 -0
- trilogy/dialect/sql_server.py +137 -0
- trilogy/engine.py +48 -0
- trilogy/execution/__init__.py +17 -0
- trilogy/execution/config.py +119 -0
- trilogy/execution/state/__init__.py +0 -0
- trilogy/execution/state/file_state_store.py +0 -0
- trilogy/execution/state/sqllite_state_store.py +0 -0
- trilogy/execution/state/state_store.py +301 -0
- trilogy/executor.py +656 -0
- trilogy/hooks/__init__.py +4 -0
- trilogy/hooks/base_hook.py +40 -0
- trilogy/hooks/graph_hook.py +135 -0
- trilogy/hooks/query_debugger.py +166 -0
- trilogy/metadata/__init__.py +0 -0
- trilogy/parser.py +10 -0
- trilogy/parsing/README.md +21 -0
- trilogy/parsing/__init__.py +0 -0
- trilogy/parsing/common.py +1069 -0
- trilogy/parsing/config.py +5 -0
- trilogy/parsing/exceptions.py +8 -0
- trilogy/parsing/helpers.py +1 -0
- trilogy/parsing/parse_engine.py +2863 -0
- trilogy/parsing/render.py +773 -0
- trilogy/parsing/trilogy.lark +544 -0
- trilogy/py.typed +0 -0
- trilogy/render.py +45 -0
- trilogy/scripts/README.md +9 -0
- trilogy/scripts/__init__.py +0 -0
- trilogy/scripts/agent.py +41 -0
- trilogy/scripts/agent_info.py +306 -0
- trilogy/scripts/common.py +430 -0
- trilogy/scripts/dependency/Cargo.lock +617 -0
- trilogy/scripts/dependency/Cargo.toml +39 -0
- trilogy/scripts/dependency/README.md +131 -0
- trilogy/scripts/dependency/build.sh +25 -0
- trilogy/scripts/dependency/src/directory_resolver.rs +387 -0
- trilogy/scripts/dependency/src/lib.rs +16 -0
- trilogy/scripts/dependency/src/main.rs +770 -0
- trilogy/scripts/dependency/src/parser.rs +435 -0
- trilogy/scripts/dependency/src/preql.pest +208 -0
- trilogy/scripts/dependency/src/python_bindings.rs +311 -0
- trilogy/scripts/dependency/src/resolver.rs +716 -0
- trilogy/scripts/dependency/tests/base.preql +3 -0
- trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
- trilogy/scripts/dependency/tests/customer.preql +6 -0
- trilogy/scripts/dependency/tests/main.preql +9 -0
- trilogy/scripts/dependency/tests/orders.preql +7 -0
- trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
- trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
- trilogy/scripts/dependency.py +323 -0
- trilogy/scripts/display.py +555 -0
- trilogy/scripts/environment.py +59 -0
- trilogy/scripts/fmt.py +32 -0
- trilogy/scripts/ingest.py +472 -0
- trilogy/scripts/ingest_helpers/__init__.py +1 -0
- trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
- trilogy/scripts/ingest_helpers/formatting.py +93 -0
- trilogy/scripts/ingest_helpers/typing.py +161 -0
- trilogy/scripts/init.py +105 -0
- trilogy/scripts/parallel_execution.py +748 -0
- trilogy/scripts/plan.py +189 -0
- trilogy/scripts/refresh.py +106 -0
- trilogy/scripts/run.py +79 -0
- trilogy/scripts/serve.py +202 -0
- trilogy/scripts/serve_helpers/__init__.py +41 -0
- trilogy/scripts/serve_helpers/file_discovery.py +142 -0
- trilogy/scripts/serve_helpers/index_generation.py +206 -0
- trilogy/scripts/serve_helpers/models.py +38 -0
- trilogy/scripts/single_execution.py +131 -0
- trilogy/scripts/testing.py +129 -0
- trilogy/scripts/trilogy.py +75 -0
- trilogy/std/__init__.py +0 -0
- trilogy/std/color.preql +3 -0
- trilogy/std/date.preql +13 -0
- trilogy/std/display.preql +18 -0
- trilogy/std/geography.preql +22 -0
- trilogy/std/metric.preql +15 -0
- trilogy/std/money.preql +67 -0
- trilogy/std/net.preql +14 -0
- trilogy/std/ranking.preql +7 -0
- trilogy/std/report.preql +5 -0
- trilogy/std/semantic.preql +6 -0
- 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"])
|