pytrilogy 0.3.138__cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.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-311-x86_64-linux-gnu.so +0 -0
- pytrilogy-0.3.138.dist-info/METADATA +525 -0
- pytrilogy-0.3.138.dist-info/RECORD +182 -0
- pytrilogy-0.3.138.dist-info/WHEEL +5 -0
- pytrilogy-0.3.138.dist-info/entry_points.txt +2 -0
- pytrilogy-0.3.138.dist-info/licenses/LICENSE.md +19 -0
- trilogy/__init__.py +9 -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 +87 -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 +143 -0
- trilogy/constants.py +113 -0
- trilogy/core/README.md +52 -0
- trilogy/core/__init__.py +0 -0
- trilogy/core/constants.py +6 -0
- trilogy/core/enums.py +443 -0
- trilogy/core/env_processor.py +120 -0
- trilogy/core/environment_helpers.py +320 -0
- trilogy/core/ergonomics.py +193 -0
- trilogy/core/exceptions.py +123 -0
- trilogy/core/functions.py +1227 -0
- trilogy/core/graph_models.py +139 -0
- trilogy/core/internal.py +85 -0
- trilogy/core/models/__init__.py +0 -0
- trilogy/core/models/author.py +2672 -0
- trilogy/core/models/build.py +2521 -0
- trilogy/core/models/build_environment.py +180 -0
- trilogy/core/models/core.py +494 -0
- trilogy/core/models/datasource.py +322 -0
- trilogy/core/models/environment.py +748 -0
- trilogy/core/models/execute.py +1177 -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 +517 -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 +268 -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 +205 -0
- trilogy/core/processing/node_generators/node_merge_node.py +653 -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 +748 -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 +519 -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 +596 -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 +106 -0
- trilogy/dialect/__init__.py +32 -0
- trilogy/dialect/base.py +1359 -0
- trilogy/dialect/bigquery.py +256 -0
- trilogy/dialect/common.py +147 -0
- trilogy/dialect/config.py +144 -0
- trilogy/dialect/dataframe.py +50 -0
- trilogy/dialect/duckdb.py +177 -0
- trilogy/dialect/enums.py +147 -0
- trilogy/dialect/metadata.py +173 -0
- trilogy/dialect/mock.py +190 -0
- trilogy/dialect/postgres.py +91 -0
- trilogy/dialect/presto.py +104 -0
- trilogy/dialect/results.py +89 -0
- trilogy/dialect/snowflake.py +90 -0
- trilogy/dialect/sql_server.py +92 -0
- trilogy/engine.py +48 -0
- trilogy/execution/config.py +75 -0
- trilogy/executor.py +568 -0
- trilogy/hooks/__init__.py +4 -0
- trilogy/hooks/base_hook.py +40 -0
- trilogy/hooks/graph_hook.py +139 -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 +2813 -0
- trilogy/parsing/render.py +750 -0
- trilogy/parsing/trilogy.lark +540 -0
- trilogy/py.typed +0 -0
- trilogy/render.py +42 -0
- trilogy/scripts/README.md +7 -0
- trilogy/scripts/__init__.py +0 -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 +162 -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 +289 -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 +460 -0
- trilogy/scripts/environment.py +46 -0
- trilogy/scripts/parallel_execution.py +483 -0
- trilogy/scripts/single_execution.py +131 -0
- trilogy/scripts/trilogy.py +772 -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,772 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from pathlib import Path as PathlibPath
|
|
7
|
+
from typing import Any, Iterable, Union
|
|
8
|
+
|
|
9
|
+
from click import UNPROCESSED, Path, argument, group, option, pass_context
|
|
10
|
+
from click.exceptions import Exit
|
|
11
|
+
|
|
12
|
+
from trilogy import Executor, parse
|
|
13
|
+
from trilogy.constants import DEFAULT_NAMESPACE
|
|
14
|
+
from trilogy.core.exceptions import (
|
|
15
|
+
ConfigurationException,
|
|
16
|
+
ModelValidationError,
|
|
17
|
+
)
|
|
18
|
+
from trilogy.core.models.environment import Environment
|
|
19
|
+
from trilogy.dialect.enums import Dialects
|
|
20
|
+
from trilogy.execution.config import RuntimeConfig, load_config_file
|
|
21
|
+
from trilogy.hooks.query_debugger import DebuggingHook
|
|
22
|
+
from trilogy.parsing.render import Renderer
|
|
23
|
+
from trilogy.scripts.dependency import (
|
|
24
|
+
ETLDependencyStrategy,
|
|
25
|
+
ScriptNode,
|
|
26
|
+
)
|
|
27
|
+
from trilogy.scripts.display import (
|
|
28
|
+
RICH_AVAILABLE,
|
|
29
|
+
create_progress_context,
|
|
30
|
+
print_error,
|
|
31
|
+
print_info,
|
|
32
|
+
print_success,
|
|
33
|
+
set_rich_mode,
|
|
34
|
+
show_debug_mode,
|
|
35
|
+
show_environment_params,
|
|
36
|
+
show_execution_info,
|
|
37
|
+
show_execution_start,
|
|
38
|
+
show_execution_summary,
|
|
39
|
+
show_formatting_result,
|
|
40
|
+
show_parallel_execution_start,
|
|
41
|
+
show_parallel_execution_summary,
|
|
42
|
+
show_script_result,
|
|
43
|
+
with_status,
|
|
44
|
+
)
|
|
45
|
+
from trilogy.scripts.environment import extra_to_kwargs, parse_env_params
|
|
46
|
+
from trilogy.scripts.parallel_execution import (
|
|
47
|
+
EagerBFSStrategy,
|
|
48
|
+
ParallelExecutor,
|
|
49
|
+
)
|
|
50
|
+
from trilogy.scripts.single_execution import (
|
|
51
|
+
execute_queries_simple,
|
|
52
|
+
execute_queries_with_progress,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
set_rich_mode = set_rich_mode
|
|
56
|
+
|
|
57
|
+
# Default parallelism level
|
|
58
|
+
DEFAULT_PARALLELISM = 2
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CLIRuntimeParams:
|
|
63
|
+
"""Parameters provided via CLI for execution."""
|
|
64
|
+
|
|
65
|
+
input: str
|
|
66
|
+
dialect: Dialects | None = None
|
|
67
|
+
parallelism: int | None = None
|
|
68
|
+
param: tuple[str, ...] = ()
|
|
69
|
+
conn_args: tuple[str, ...] = ()
|
|
70
|
+
debug: bool = False
|
|
71
|
+
config_path: PathlibPath | None = None
|
|
72
|
+
execution_strategy: str = "eager_bfs"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def merge_runtime_config(
|
|
76
|
+
cli_params: CLIRuntimeParams, file_config: RuntimeConfig
|
|
77
|
+
) -> tuple[Dialects, int]:
|
|
78
|
+
"""
|
|
79
|
+
Merge CLI parameters with config file settings.
|
|
80
|
+
CLI parameters take precedence over config file.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
tuple of (dialect, parallelism)
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
Exit: If no dialect is specified in either CLI or config
|
|
87
|
+
"""
|
|
88
|
+
# Resolve dialect: CLI argument takes precedence over config
|
|
89
|
+
if cli_params.dialect:
|
|
90
|
+
dialect = cli_params.dialect
|
|
91
|
+
elif file_config.engine_dialect:
|
|
92
|
+
dialect = file_config.engine_dialect
|
|
93
|
+
else:
|
|
94
|
+
print_error(
|
|
95
|
+
"No dialect specified. Provide dialect as argument or set engine.dialect in config file."
|
|
96
|
+
)
|
|
97
|
+
raise Exit(1)
|
|
98
|
+
|
|
99
|
+
# Resolve parallelism: CLI argument takes precedence over config
|
|
100
|
+
parallelism = (
|
|
101
|
+
cli_params.parallelism
|
|
102
|
+
if cli_params.parallelism is not None
|
|
103
|
+
else file_config.parallelism
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return dialect, parallelism
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class RunMode(Enum):
|
|
110
|
+
RUN = "run"
|
|
111
|
+
INTEGRATION = "integration"
|
|
112
|
+
UNIT = "unit"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def resolve_input(path: PathlibPath) -> list[PathlibPath]:
|
|
116
|
+
|
|
117
|
+
# Directory
|
|
118
|
+
if path.is_dir():
|
|
119
|
+
pattern = "**/*.preql"
|
|
120
|
+
return sorted(path.glob(pattern))
|
|
121
|
+
# Single file
|
|
122
|
+
if path.exists() and path.is_file():
|
|
123
|
+
return [path]
|
|
124
|
+
|
|
125
|
+
raise FileNotFoundError(f"Input path '{path}' does not exist.")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_runtime_config(
|
|
129
|
+
path: PathlibPath, config_override: PathlibPath | None = None
|
|
130
|
+
) -> RuntimeConfig:
|
|
131
|
+
if config_override:
|
|
132
|
+
config_path = config_override
|
|
133
|
+
elif path.is_dir():
|
|
134
|
+
config_path = path / "trilogy.toml"
|
|
135
|
+
else:
|
|
136
|
+
config_path = path.parent / "trilogy.toml"
|
|
137
|
+
if config_path.exists():
|
|
138
|
+
try:
|
|
139
|
+
return load_config_file(config_path)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print_error(f"Failed to load configuration file {config_path}: {e}")
|
|
142
|
+
handle_execution_exception(e)
|
|
143
|
+
return RuntimeConfig(startup_trilogy=[], startup_sql=[])
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def resolve_input_information(
|
|
147
|
+
input: str, config_path_input: PathlibPath | None = None
|
|
148
|
+
) -> tuple[Iterable[PathlibPath | StringIO], PathlibPath, str, str, RuntimeConfig]:
|
|
149
|
+
input_as_path = PathlibPath(input)
|
|
150
|
+
files: Iterable[StringIO | PathlibPath]
|
|
151
|
+
if input_as_path.exists():
|
|
152
|
+
pathlib_path = input_as_path
|
|
153
|
+
files = resolve_input(pathlib_path)
|
|
154
|
+
|
|
155
|
+
if pathlib_path.is_dir():
|
|
156
|
+
directory = pathlib_path
|
|
157
|
+
input_type = "directory"
|
|
158
|
+
config = get_runtime_config(pathlib_path, config_path_input)
|
|
159
|
+
|
|
160
|
+
else:
|
|
161
|
+
directory = pathlib_path.parent
|
|
162
|
+
input_type = "file"
|
|
163
|
+
config = get_runtime_config(pathlib_path, config_path_input)
|
|
164
|
+
|
|
165
|
+
input_name = pathlib_path.name
|
|
166
|
+
else:
|
|
167
|
+
script = input
|
|
168
|
+
files = [StringIO(script)]
|
|
169
|
+
directory = PathlibPath.cwd()
|
|
170
|
+
input_type = "query"
|
|
171
|
+
input_name = "inline"
|
|
172
|
+
config = RuntimeConfig(startup_trilogy=[], startup_sql=[])
|
|
173
|
+
return files, directory, input_type, input_name, config
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def validate_required_connection_params(
|
|
177
|
+
conn_dict: dict[str, Any],
|
|
178
|
+
required_keys: list[str],
|
|
179
|
+
optional_keys: list[str],
|
|
180
|
+
dialect_name: str,
|
|
181
|
+
) -> dict:
|
|
182
|
+
missing = [key for key in required_keys if key not in conn_dict]
|
|
183
|
+
extra = [
|
|
184
|
+
key
|
|
185
|
+
for key in conn_dict
|
|
186
|
+
if key not in required_keys and key not in optional_keys
|
|
187
|
+
]
|
|
188
|
+
if missing:
|
|
189
|
+
raise ConfigurationException(
|
|
190
|
+
f"Missing required {dialect_name} connection parameters: {', '.join(missing)}"
|
|
191
|
+
)
|
|
192
|
+
if extra:
|
|
193
|
+
print(
|
|
194
|
+
f"Warning: Extra {dialect_name} connection parameters provided: {', '.join(extra)}"
|
|
195
|
+
)
|
|
196
|
+
return {
|
|
197
|
+
k: v for k, v in conn_dict.items() if k in required_keys or k in optional_keys
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def get_dialect_config(
|
|
202
|
+
edialect: Dialects, conn_dict: dict[str, Any], runtime_config: RuntimeConfig
|
|
203
|
+
) -> Any:
|
|
204
|
+
"""Get dialect configuration based on dialect type."""
|
|
205
|
+
conf: Union[Any, None] = None
|
|
206
|
+
|
|
207
|
+
if edialect == Dialects.DUCK_DB:
|
|
208
|
+
from trilogy.dialect.config import DuckDBConfig
|
|
209
|
+
|
|
210
|
+
conn_dict = validate_required_connection_params(
|
|
211
|
+
conn_dict, [], ["path"], "DuckDB"
|
|
212
|
+
)
|
|
213
|
+
conf = DuckDBConfig(**conn_dict)
|
|
214
|
+
elif edialect == Dialects.SNOWFLAKE:
|
|
215
|
+
from trilogy.dialect.config import SnowflakeConfig
|
|
216
|
+
|
|
217
|
+
conn_dict = validate_required_connection_params(
|
|
218
|
+
conn_dict, ["username", "password", "account"], [], "Snowflake"
|
|
219
|
+
)
|
|
220
|
+
conf = SnowflakeConfig(**conn_dict)
|
|
221
|
+
elif edialect == Dialects.SQL_SERVER:
|
|
222
|
+
from trilogy.dialect.config import SQLServerConfig
|
|
223
|
+
|
|
224
|
+
conn_dict = validate_required_connection_params(
|
|
225
|
+
conn_dict,
|
|
226
|
+
["host", "port", "username", "password", "database"],
|
|
227
|
+
[],
|
|
228
|
+
"SQL Server",
|
|
229
|
+
)
|
|
230
|
+
conf = SQLServerConfig(**conn_dict)
|
|
231
|
+
elif edialect == Dialects.POSTGRES:
|
|
232
|
+
from trilogy.dialect.config import PostgresConfig
|
|
233
|
+
|
|
234
|
+
conn_dict = validate_required_connection_params(
|
|
235
|
+
conn_dict,
|
|
236
|
+
["host", "port", "username", "password", "database"],
|
|
237
|
+
[],
|
|
238
|
+
"Postgres",
|
|
239
|
+
)
|
|
240
|
+
conf = PostgresConfig(**conn_dict)
|
|
241
|
+
elif edialect == Dialects.BIGQUERY:
|
|
242
|
+
from trilogy.dialect.config import BigQueryConfig
|
|
243
|
+
|
|
244
|
+
conn_dict = validate_required_connection_params(
|
|
245
|
+
conn_dict, [], ["project"], "BigQuery"
|
|
246
|
+
)
|
|
247
|
+
conf = BigQueryConfig(**conn_dict)
|
|
248
|
+
elif edialect == Dialects.PRESTO:
|
|
249
|
+
from trilogy.dialect.config import PrestoConfig
|
|
250
|
+
|
|
251
|
+
conn_dict = validate_required_connection_params(
|
|
252
|
+
conn_dict,
|
|
253
|
+
["host", "port", "username", "password", "catalog"],
|
|
254
|
+
[],
|
|
255
|
+
"Presto",
|
|
256
|
+
)
|
|
257
|
+
conf = PrestoConfig(**conn_dict)
|
|
258
|
+
if conf and runtime_config.engine_config:
|
|
259
|
+
conf = runtime_config.engine_config.merge_config(conf)
|
|
260
|
+
return conf
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def create_executor(
|
|
264
|
+
param: tuple[str, ...],
|
|
265
|
+
directory: PathlibPath,
|
|
266
|
+
conn_args: Iterable[str],
|
|
267
|
+
edialect: Dialects,
|
|
268
|
+
debug: bool,
|
|
269
|
+
config: RuntimeConfig,
|
|
270
|
+
) -> Executor:
|
|
271
|
+
# Parse environment parameters from dedicated flag
|
|
272
|
+
namespace = DEFAULT_NAMESPACE
|
|
273
|
+
try:
|
|
274
|
+
env_params = parse_env_params(param)
|
|
275
|
+
show_environment_params(env_params)
|
|
276
|
+
except ValueError as e:
|
|
277
|
+
print_error(str(e))
|
|
278
|
+
raise Exit(1) from e
|
|
279
|
+
|
|
280
|
+
# Parse connection arguments from remaining args
|
|
281
|
+
conn_dict = extra_to_kwargs(conn_args)
|
|
282
|
+
|
|
283
|
+
# Configure dialect
|
|
284
|
+
try:
|
|
285
|
+
conf = get_dialect_config(edialect, conn_dict, runtime_config=config)
|
|
286
|
+
except Exception as e:
|
|
287
|
+
handle_execution_exception(e)
|
|
288
|
+
|
|
289
|
+
# Create environment and set additional parameters if any exist
|
|
290
|
+
environment = Environment(working_path=str(directory), namespace=namespace)
|
|
291
|
+
if env_params:
|
|
292
|
+
environment.set_parameters(**env_params)
|
|
293
|
+
|
|
294
|
+
exec = Executor(
|
|
295
|
+
dialect=edialect,
|
|
296
|
+
engine=edialect.default_engine(conf=conf),
|
|
297
|
+
environment=environment,
|
|
298
|
+
hooks=[DebuggingHook()] if debug else [],
|
|
299
|
+
)
|
|
300
|
+
if config.startup_sql:
|
|
301
|
+
for script in config.startup_sql:
|
|
302
|
+
print_info(f"Executing startup SQL script: {script}")
|
|
303
|
+
exec.execute_file(script)
|
|
304
|
+
if config.startup_trilogy:
|
|
305
|
+
for script in config.startup_trilogy:
|
|
306
|
+
print_info(f"Executing startup Trilogy script: {script}")
|
|
307
|
+
exec.execute_file(script)
|
|
308
|
+
return exec
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def create_executor_for_script(
|
|
312
|
+
node: ScriptNode,
|
|
313
|
+
param: tuple[str, ...],
|
|
314
|
+
conn_args: Iterable[str],
|
|
315
|
+
edialect: Dialects,
|
|
316
|
+
debug: bool,
|
|
317
|
+
config: RuntimeConfig,
|
|
318
|
+
) -> Executor:
|
|
319
|
+
"""
|
|
320
|
+
Create an executor for a specific script node.
|
|
321
|
+
|
|
322
|
+
Each script gets its own executor with its own environment,
|
|
323
|
+
using the script's parent directory as the working path.
|
|
324
|
+
"""
|
|
325
|
+
directory = node.path.parent
|
|
326
|
+
return create_executor(param, directory, conn_args, edialect, debug, config)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def validate_datasources(
|
|
330
|
+
exec: Executor, mock: bool = False, quiet: bool = False
|
|
331
|
+
) -> None:
|
|
332
|
+
"""Validate datasources with consistent error handling.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
exec: The executor instance
|
|
336
|
+
mock: If True, mock datasources before validation (for unit tests)
|
|
337
|
+
quiet: If True, suppress informational messages (for parallel execution)
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
Exit: If validation fails
|
|
341
|
+
"""
|
|
342
|
+
datasources = exec.environment.datasources.keys()
|
|
343
|
+
if not datasources:
|
|
344
|
+
if not quiet:
|
|
345
|
+
message = "unit" if mock else "integration"
|
|
346
|
+
print_success(f"No datasources found to {message} test.")
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
if mock:
|
|
350
|
+
exec.execute_text("mock datasources {};".format(", ".join(datasources)))
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
exec.execute_text("validate datasources {};".format(", ".join(datasources)))
|
|
354
|
+
except ModelValidationError as e:
|
|
355
|
+
if not e.children:
|
|
356
|
+
print_error(f"Datasource validation failed: {e.message}")
|
|
357
|
+
for idx, child in enumerate(e.children or []):
|
|
358
|
+
print_error(f"Error {idx + 1}: {child.message}")
|
|
359
|
+
raise Exit(1) from e
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def execute_script_for_run(
|
|
363
|
+
exec: Executor, node: ScriptNode, quiet: bool = False
|
|
364
|
+
) -> None:
|
|
365
|
+
"""Execute a script for the 'run' command (parallel execution mode)."""
|
|
366
|
+
with open(node.path, "r") as f:
|
|
367
|
+
queries = exec.parse_text(f.read())
|
|
368
|
+
for query in queries:
|
|
369
|
+
exec.execute_query(query)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def execute_script_for_integration(
|
|
373
|
+
exec: Executor, node: ScriptNode, quiet: bool = False
|
|
374
|
+
) -> None:
|
|
375
|
+
"""Execute a script for the 'integration' command (parse + validate)."""
|
|
376
|
+
with open(node.path, "r") as f:
|
|
377
|
+
exec.parse_text(f.read())
|
|
378
|
+
validate_datasources(exec, mock=False, quiet=quiet)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def execute_script_for_unit(
|
|
382
|
+
exec: Executor, node: ScriptNode, quiet: bool = False
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Execute a script for the 'unit' command (parse + mock validate)."""
|
|
385
|
+
with open(node.path, "r") as f:
|
|
386
|
+
exec.parse_text(f.read())
|
|
387
|
+
validate_datasources(exec, mock=True, quiet=quiet)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def get_execution_strategy(strategy_name: str):
|
|
391
|
+
"""Get execution strategy by name."""
|
|
392
|
+
strategies = {
|
|
393
|
+
"eager_bfs": EagerBFSStrategy,
|
|
394
|
+
}
|
|
395
|
+
if strategy_name not in strategies:
|
|
396
|
+
raise ValueError(
|
|
397
|
+
f"Unknown execution strategy: {strategy_name}. "
|
|
398
|
+
f"Available: {', '.join(strategies.keys())}"
|
|
399
|
+
)
|
|
400
|
+
return strategies[strategy_name]()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def handle_execution_exception(e: Exception, debug: bool = False) -> None:
|
|
404
|
+
print_error(f"Unexpected error: {e}")
|
|
405
|
+
if debug:
|
|
406
|
+
print_error(f"Full traceback:\n{traceback.format_exc()}")
|
|
407
|
+
raise Exit(1) from e
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def run_single_script_execution(
|
|
411
|
+
files: list[StringIO | PathlibPath],
|
|
412
|
+
directory: PathlibPath,
|
|
413
|
+
input_type: str,
|
|
414
|
+
input_name: str,
|
|
415
|
+
edialect: Dialects,
|
|
416
|
+
param: tuple[str, ...],
|
|
417
|
+
conn_args: Iterable[str],
|
|
418
|
+
debug: bool,
|
|
419
|
+
execution_mode: str,
|
|
420
|
+
config: RuntimeConfig,
|
|
421
|
+
) -> None:
|
|
422
|
+
"""
|
|
423
|
+
Run single script execution with polished multi-statement progress display.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
text: List of script contents
|
|
427
|
+
directory: Working directory
|
|
428
|
+
input_type: Type of input (file, query, etc.)
|
|
429
|
+
input_name: Name of the input
|
|
430
|
+
edialect: Dialect to use
|
|
431
|
+
param: Environment parameters
|
|
432
|
+
conn_args: Connection arguments
|
|
433
|
+
debug: Debug mode flag
|
|
434
|
+
execution_mode: One of 'run', 'integration', or 'unit'
|
|
435
|
+
"""
|
|
436
|
+
show_execution_info(input_type, input_name, edialect.value, debug)
|
|
437
|
+
|
|
438
|
+
exec = create_executor(param, directory, conn_args, edialect, debug, config)
|
|
439
|
+
base = files[0]
|
|
440
|
+
if isinstance(base, StringIO):
|
|
441
|
+
text = [base.getvalue()]
|
|
442
|
+
else:
|
|
443
|
+
with open(base, "r") as raw:
|
|
444
|
+
text = [raw.read()]
|
|
445
|
+
|
|
446
|
+
if execution_mode == "run":
|
|
447
|
+
# Parse all scripts and collect queries
|
|
448
|
+
queries = []
|
|
449
|
+
try:
|
|
450
|
+
for script in text:
|
|
451
|
+
queries += exec.parse_text(script)
|
|
452
|
+
except Exception as e:
|
|
453
|
+
handle_execution_exception(e, debug=debug)
|
|
454
|
+
|
|
455
|
+
start = datetime.now()
|
|
456
|
+
show_execution_start(len(queries))
|
|
457
|
+
|
|
458
|
+
# Execute with progress tracking for multiple statements
|
|
459
|
+
if len(queries) > 1 and RICH_AVAILABLE:
|
|
460
|
+
progress = create_progress_context()
|
|
461
|
+
else:
|
|
462
|
+
progress = None
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
if progress:
|
|
466
|
+
exception = execute_queries_with_progress(exec, queries)
|
|
467
|
+
else:
|
|
468
|
+
exception = execute_queries_simple(exec, queries)
|
|
469
|
+
|
|
470
|
+
total_duration = datetime.now() - start
|
|
471
|
+
show_execution_summary(len(queries), total_duration, exception is None)
|
|
472
|
+
|
|
473
|
+
if exception:
|
|
474
|
+
raise Exit(1) from exception
|
|
475
|
+
except Exit:
|
|
476
|
+
raise
|
|
477
|
+
except Exception as e:
|
|
478
|
+
handle_execution_exception(e, debug=debug)
|
|
479
|
+
|
|
480
|
+
elif execution_mode == "integration":
|
|
481
|
+
for script in text:
|
|
482
|
+
exec.parse_text(script)
|
|
483
|
+
validate_datasources(exec, mock=False, quiet=False)
|
|
484
|
+
print_success("Integration tests passed successfully!")
|
|
485
|
+
|
|
486
|
+
elif execution_mode == "unit":
|
|
487
|
+
for script in text:
|
|
488
|
+
exec.parse_text(script)
|
|
489
|
+
validate_datasources(exec, mock=True, quiet=False)
|
|
490
|
+
print_success("Unit tests passed successfully!")
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def run_parallel_execution(
|
|
494
|
+
cli_params: CLIRuntimeParams,
|
|
495
|
+
execution_fn,
|
|
496
|
+
execution_mode: str = "run",
|
|
497
|
+
) -> None:
|
|
498
|
+
"""
|
|
499
|
+
Run parallel execution for directory inputs, or single-script execution
|
|
500
|
+
with polished progress display for single files/inline queries.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
cli_params: CLI runtime parameters containing all execution settings
|
|
504
|
+
execution_fn: Function to execute each script (exec, node, quiet) -> None
|
|
505
|
+
execution_mode: One of 'run', 'integration', or 'unit'
|
|
506
|
+
"""
|
|
507
|
+
# Check if input is a directory (parallel execution)
|
|
508
|
+
pathlib_input = PathlibPath(cli_params.input)
|
|
509
|
+
files_iter, directory, input_type, input_name, config = resolve_input_information(
|
|
510
|
+
cli_params.input, cli_params.config_path
|
|
511
|
+
)
|
|
512
|
+
files = list(files_iter)
|
|
513
|
+
|
|
514
|
+
# Merge CLI params with config file
|
|
515
|
+
edialect, parallelism = merge_runtime_config(cli_params, config)
|
|
516
|
+
if not pathlib_input.exists() or len(files) == 1:
|
|
517
|
+
# Inline query - use polished single-script execution
|
|
518
|
+
|
|
519
|
+
run_single_script_execution(
|
|
520
|
+
files=files,
|
|
521
|
+
directory=directory,
|
|
522
|
+
input_type=input_type,
|
|
523
|
+
input_name=input_name,
|
|
524
|
+
edialect=edialect,
|
|
525
|
+
param=cli_params.param,
|
|
526
|
+
conn_args=cli_params.conn_args,
|
|
527
|
+
debug=cli_params.debug,
|
|
528
|
+
execution_mode=execution_mode,
|
|
529
|
+
config=config,
|
|
530
|
+
)
|
|
531
|
+
return
|
|
532
|
+
# Multiple files - use parallel execution
|
|
533
|
+
show_execution_info(input_type, input_name, edialect.value, cli_params.debug)
|
|
534
|
+
|
|
535
|
+
# Get execution strategy
|
|
536
|
+
strategy = get_execution_strategy(cli_params.execution_strategy)
|
|
537
|
+
|
|
538
|
+
# Set up parallel executor
|
|
539
|
+
parallel_exec = ParallelExecutor(
|
|
540
|
+
max_workers=parallelism,
|
|
541
|
+
dependency_strategy=ETLDependencyStrategy(),
|
|
542
|
+
execution_strategy=strategy,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
# Get execution plan for display
|
|
546
|
+
if pathlib_input.is_dir():
|
|
547
|
+
execution_plan = parallel_exec.get_folder_execution_plan(pathlib_input)
|
|
548
|
+
elif pathlib_input.is_file():
|
|
549
|
+
execution_plan = parallel_exec.get_execution_plan([pathlib_input])
|
|
550
|
+
else:
|
|
551
|
+
raise FileNotFoundError(f"Input path '{pathlib_input}' does not exist.")
|
|
552
|
+
|
|
553
|
+
num_edges = execution_plan.number_of_edges()
|
|
554
|
+
num_nodes = execution_plan.number_of_nodes()
|
|
555
|
+
|
|
556
|
+
show_parallel_execution_start(
|
|
557
|
+
num_nodes, num_edges, parallelism, cli_params.execution_strategy
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Factory to create executor for each script
|
|
561
|
+
def executor_factory(node: ScriptNode) -> Executor:
|
|
562
|
+
return create_executor_for_script(
|
|
563
|
+
node,
|
|
564
|
+
cli_params.param,
|
|
565
|
+
cli_params.conn_args,
|
|
566
|
+
edialect,
|
|
567
|
+
cli_params.debug,
|
|
568
|
+
config,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
# Wrap execution_fn to pass quiet=True for parallel execution
|
|
572
|
+
def quiet_execution_fn(exec: Executor, node: ScriptNode) -> None:
|
|
573
|
+
execution_fn(exec, node, quiet=True)
|
|
574
|
+
|
|
575
|
+
# Run parallel execution
|
|
576
|
+
summary = parallel_exec.execute(
|
|
577
|
+
root=pathlib_input,
|
|
578
|
+
executor_factory=executor_factory,
|
|
579
|
+
execution_fn=quiet_execution_fn,
|
|
580
|
+
on_script_complete=show_script_result,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
show_parallel_execution_summary(summary)
|
|
584
|
+
|
|
585
|
+
if not summary.all_succeeded:
|
|
586
|
+
print_error("Some scripts failed during execution.")
|
|
587
|
+
raise Exit(1)
|
|
588
|
+
|
|
589
|
+
print_success("All scripts executed successfully!")
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@group()
|
|
593
|
+
@option("--debug", default=False, help="Enable debug mode")
|
|
594
|
+
@pass_context
|
|
595
|
+
def cli(ctx, debug: bool):
|
|
596
|
+
"""Trilogy CLI - A beautiful query execution tool."""
|
|
597
|
+
ctx.ensure_object(dict)
|
|
598
|
+
ctx.obj["DEBUG"] = debug
|
|
599
|
+
|
|
600
|
+
if debug:
|
|
601
|
+
show_debug_mode()
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@cli.command("fmt")
|
|
605
|
+
@argument("input", type=Path(exists=True))
|
|
606
|
+
@pass_context
|
|
607
|
+
def fmt(ctx, input):
|
|
608
|
+
"""Format a Trilogy script file."""
|
|
609
|
+
with with_status("Formatting script"):
|
|
610
|
+
start = datetime.now()
|
|
611
|
+
try:
|
|
612
|
+
with open(input, "r") as f:
|
|
613
|
+
script = f.read()
|
|
614
|
+
_, queries = parse(script)
|
|
615
|
+
r = Renderer()
|
|
616
|
+
with open(input, "w") as f:
|
|
617
|
+
f.write("\n".join([r.to_string(x) for x in queries]))
|
|
618
|
+
duration = datetime.now() - start
|
|
619
|
+
|
|
620
|
+
print_success("Script formatted successfully")
|
|
621
|
+
show_formatting_result(input, len(queries), duration)
|
|
622
|
+
|
|
623
|
+
except Exception as e:
|
|
624
|
+
handle_execution_exception(e, debug=ctx.obj["DEBUG"])
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
@cli.command(
|
|
628
|
+
"integration",
|
|
629
|
+
context_settings=dict(
|
|
630
|
+
ignore_unknown_options=True,
|
|
631
|
+
),
|
|
632
|
+
)
|
|
633
|
+
@argument("input", type=Path())
|
|
634
|
+
@argument("dialect", type=str, required=False)
|
|
635
|
+
@option("--param", multiple=True, help="Environment parameters as key=value pairs")
|
|
636
|
+
@option(
|
|
637
|
+
"--parallelism",
|
|
638
|
+
"-p",
|
|
639
|
+
default=None,
|
|
640
|
+
help="Maximum parallel workers for directory execution",
|
|
641
|
+
)
|
|
642
|
+
@option(
|
|
643
|
+
"--config", type=Path(exists=True), help="Path to trilogy.toml configuration file"
|
|
644
|
+
)
|
|
645
|
+
@argument("conn_args", nargs=-1, type=UNPROCESSED)
|
|
646
|
+
@pass_context
|
|
647
|
+
def integration(
|
|
648
|
+
ctx, input, dialect: str | None, param, parallelism: int | None, config, conn_args
|
|
649
|
+
):
|
|
650
|
+
"""Run integration tests on Trilogy scripts."""
|
|
651
|
+
cli_params = CLIRuntimeParams(
|
|
652
|
+
input=input,
|
|
653
|
+
dialect=Dialects(dialect) if dialect else None,
|
|
654
|
+
parallelism=parallelism,
|
|
655
|
+
param=param,
|
|
656
|
+
conn_args=conn_args,
|
|
657
|
+
debug=ctx.obj["DEBUG"],
|
|
658
|
+
config_path=PathlibPath(config) if config else None,
|
|
659
|
+
execution_strategy="eager_bfs",
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
try:
|
|
663
|
+
run_parallel_execution(
|
|
664
|
+
cli_params=cli_params,
|
|
665
|
+
execution_fn=execute_script_for_integration,
|
|
666
|
+
execution_mode="integration",
|
|
667
|
+
)
|
|
668
|
+
except Exit:
|
|
669
|
+
raise
|
|
670
|
+
except Exception as e:
|
|
671
|
+
handle_execution_exception(e, debug=cli_params.debug)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
@cli.command(
|
|
675
|
+
"unit",
|
|
676
|
+
context_settings=dict(
|
|
677
|
+
ignore_unknown_options=True,
|
|
678
|
+
),
|
|
679
|
+
)
|
|
680
|
+
@argument("input", type=Path())
|
|
681
|
+
@option("--param", multiple=True, help="Environment parameters as key=value pairs")
|
|
682
|
+
@option(
|
|
683
|
+
"--parallelism",
|
|
684
|
+
"-p",
|
|
685
|
+
default=None,
|
|
686
|
+
help="Maximum parallel workers for directory execution",
|
|
687
|
+
)
|
|
688
|
+
@option(
|
|
689
|
+
"--config", type=Path(exists=True), help="Path to trilogy.toml configuration file"
|
|
690
|
+
)
|
|
691
|
+
@pass_context
|
|
692
|
+
def unit(
|
|
693
|
+
ctx,
|
|
694
|
+
input,
|
|
695
|
+
param,
|
|
696
|
+
parallelism: int | None,
|
|
697
|
+
config,
|
|
698
|
+
):
|
|
699
|
+
"""Run unit tests on Trilogy scripts with mocked datasources."""
|
|
700
|
+
# Build CLI runtime params (unit tests always use DuckDB)
|
|
701
|
+
cli_params = CLIRuntimeParams(
|
|
702
|
+
input=input,
|
|
703
|
+
dialect=Dialects.DUCK_DB,
|
|
704
|
+
parallelism=parallelism,
|
|
705
|
+
param=param,
|
|
706
|
+
conn_args=(),
|
|
707
|
+
debug=ctx.obj["DEBUG"],
|
|
708
|
+
config_path=PathlibPath(config) if config else None,
|
|
709
|
+
execution_strategy="eager_bfs",
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
try:
|
|
713
|
+
run_parallel_execution(
|
|
714
|
+
cli_params=cli_params,
|
|
715
|
+
execution_fn=execute_script_for_unit,
|
|
716
|
+
execution_mode="unit",
|
|
717
|
+
)
|
|
718
|
+
except Exit:
|
|
719
|
+
raise
|
|
720
|
+
except Exception as e:
|
|
721
|
+
handle_execution_exception(e, debug=cli_params.debug)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
@cli.command(
|
|
725
|
+
"run",
|
|
726
|
+
context_settings=dict(
|
|
727
|
+
ignore_unknown_options=True,
|
|
728
|
+
),
|
|
729
|
+
)
|
|
730
|
+
@argument("input", type=Path())
|
|
731
|
+
@argument("dialect", type=str, required=False)
|
|
732
|
+
@option("--param", multiple=True, help="Environment parameters as key=value pairs")
|
|
733
|
+
@option(
|
|
734
|
+
"--parallelism",
|
|
735
|
+
"-p",
|
|
736
|
+
default=None,
|
|
737
|
+
help="Maximum parallel workers for directory execution",
|
|
738
|
+
)
|
|
739
|
+
@option(
|
|
740
|
+
"--config", type=Path(exists=True), help="Path to trilogy.toml configuration file"
|
|
741
|
+
)
|
|
742
|
+
@argument("conn_args", nargs=-1, type=UNPROCESSED)
|
|
743
|
+
@pass_context
|
|
744
|
+
def run(
|
|
745
|
+
ctx, input, dialect: str | None, param, parallelism: int | None, config, conn_args
|
|
746
|
+
):
|
|
747
|
+
"""Execute a Trilogy script or query."""
|
|
748
|
+
cli_params = CLIRuntimeParams(
|
|
749
|
+
input=input,
|
|
750
|
+
dialect=Dialects(dialect) if dialect else None,
|
|
751
|
+
parallelism=parallelism,
|
|
752
|
+
param=param,
|
|
753
|
+
conn_args=conn_args,
|
|
754
|
+
debug=ctx.obj["DEBUG"],
|
|
755
|
+
config_path=PathlibPath(config) if config else None,
|
|
756
|
+
execution_strategy="eager_bfs",
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
try:
|
|
760
|
+
run_parallel_execution(
|
|
761
|
+
cli_params=cli_params,
|
|
762
|
+
execution_fn=execute_script_for_run,
|
|
763
|
+
execution_mode="run",
|
|
764
|
+
)
|
|
765
|
+
except Exit:
|
|
766
|
+
raise
|
|
767
|
+
except Exception as e:
|
|
768
|
+
handle_execution_exception(e, debug=cli_params.debug)
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
if __name__ == "__main__":
|
|
772
|
+
cli()
|