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.
- LICENSE.md +19 -0
- _preql_import_resolver/__init__.py +5 -0
- _preql_import_resolver/_preql_import_resolver.cp313-win_amd64.pyd +0 -0
- pytrilogy-0.3.149.dist-info/METADATA +555 -0
- pytrilogy-0.3.149.dist-info/RECORD +207 -0
- pytrilogy-0.3.149.dist-info/WHEEL +4 -0
- pytrilogy-0.3.149.dist-info/entry_points.txt +2 -0
- pytrilogy-0.3.149.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 +2670 -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 +436 -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 +846 -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 +1432 -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 +397 -0
- trilogy/dialect/enums.py +151 -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/exceptions.py +26 -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 +406 -0
- trilogy/executor.py +692 -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 +2876 -0
- trilogy/parsing/render.py +775 -0
- trilogy/parsing/trilogy.lark +546 -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 +432 -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 +487 -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 +762 -0
- trilogy/scripts/plan.py +189 -0
- trilogy/scripts/refresh.py +161 -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 +143 -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
trilogy/scripts/plan.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Plan command for Trilogy CLI - shows execution order without running."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path as PathlibPath
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import networkx as nx
|
|
8
|
+
from click import Path, argument, option, pass_context
|
|
9
|
+
from click.exceptions import Exit
|
|
10
|
+
|
|
11
|
+
from trilogy.scripts.common import (
|
|
12
|
+
handle_execution_exception,
|
|
13
|
+
resolve_input_information,
|
|
14
|
+
)
|
|
15
|
+
from trilogy.scripts.dependency import ETLDependencyStrategy, ScriptNode
|
|
16
|
+
from trilogy.scripts.display import print_error, print_info, show_execution_plan
|
|
17
|
+
from trilogy.scripts.parallel_execution import ParallelExecutor
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def safe_relative_path(path: PathlibPath, root: PathlibPath) -> str:
|
|
21
|
+
"""Get path relative to root, falling back to absolute if not a subpath.
|
|
22
|
+
|
|
23
|
+
This handles the case where imports pull in files from outside the root
|
|
24
|
+
directory (e.g., `import ../shared/utils.preql`).
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
return str(path.relative_to(root))
|
|
28
|
+
except ValueError:
|
|
29
|
+
return str(path)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def graph_to_json(graph: nx.DiGraph, root: PathlibPath) -> dict[str, Any]:
|
|
33
|
+
"""Convert dependency graph to JSON-serializable dict."""
|
|
34
|
+
nodes = [
|
|
35
|
+
{"id": safe_relative_path(node.path, root), "path": str(node.path)}
|
|
36
|
+
for node in graph.nodes()
|
|
37
|
+
]
|
|
38
|
+
edges = [
|
|
39
|
+
{
|
|
40
|
+
"from": safe_relative_path(from_node.path, root),
|
|
41
|
+
"to": safe_relative_path(to_node.path, root),
|
|
42
|
+
}
|
|
43
|
+
for from_node, to_node in graph.edges()
|
|
44
|
+
]
|
|
45
|
+
return {"nodes": nodes, "edges": edges}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_execution_levels(graph: nx.DiGraph) -> list[list[ScriptNode]]:
|
|
49
|
+
"""Get execution levels (BFS layers) from dependency graph."""
|
|
50
|
+
if not graph.nodes():
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
levels: list[list[ScriptNode]] = []
|
|
54
|
+
remaining_deps = {node: graph.in_degree(node) for node in graph.nodes()}
|
|
55
|
+
completed: set[ScriptNode] = set()
|
|
56
|
+
|
|
57
|
+
while len(completed) < len(graph.nodes()):
|
|
58
|
+
ready = [
|
|
59
|
+
node
|
|
60
|
+
for node in graph.nodes()
|
|
61
|
+
if remaining_deps[node] == 0 and node not in completed
|
|
62
|
+
]
|
|
63
|
+
if not ready:
|
|
64
|
+
break
|
|
65
|
+
levels.append(ready)
|
|
66
|
+
for node in ready:
|
|
67
|
+
completed.add(node)
|
|
68
|
+
for dependent in graph.successors(node):
|
|
69
|
+
remaining_deps[dependent] -= 1
|
|
70
|
+
|
|
71
|
+
return levels
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def format_plan_text(
|
|
75
|
+
graph: nx.DiGraph, root: PathlibPath
|
|
76
|
+
) -> tuple[list[str], list[tuple[str, str]], list[list[str]]]:
|
|
77
|
+
"""Format plan for text display."""
|
|
78
|
+
nodes = [safe_relative_path(node.path, root) for node in graph.nodes()]
|
|
79
|
+
edges = [
|
|
80
|
+
(
|
|
81
|
+
safe_relative_path(from_node.path, root),
|
|
82
|
+
safe_relative_path(to_node.path, root),
|
|
83
|
+
)
|
|
84
|
+
for from_node, to_node in graph.edges()
|
|
85
|
+
]
|
|
86
|
+
levels = get_execution_levels(graph)
|
|
87
|
+
execution_order = [
|
|
88
|
+
[safe_relative_path(node.path, root) for node in level] for level in levels
|
|
89
|
+
]
|
|
90
|
+
return nodes, edges, execution_order
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@argument("input", type=Path())
|
|
94
|
+
@option(
|
|
95
|
+
"--output",
|
|
96
|
+
"-o",
|
|
97
|
+
type=Path(),
|
|
98
|
+
default=None,
|
|
99
|
+
help="Output file path for the plan",
|
|
100
|
+
)
|
|
101
|
+
@option(
|
|
102
|
+
"--json",
|
|
103
|
+
"json_format",
|
|
104
|
+
is_flag=True,
|
|
105
|
+
default=False,
|
|
106
|
+
help="Output plan as JSON graph with nodes and edges",
|
|
107
|
+
)
|
|
108
|
+
@option(
|
|
109
|
+
"--config",
|
|
110
|
+
type=Path(exists=True),
|
|
111
|
+
help="Path to trilogy.toml configuration file",
|
|
112
|
+
)
|
|
113
|
+
@pass_context
|
|
114
|
+
def plan(ctx, input: str, output: str | None, json_format: bool, config: str | None):
|
|
115
|
+
"""Show execution plan without running scripts."""
|
|
116
|
+
try:
|
|
117
|
+
pathlib_input = PathlibPath(input)
|
|
118
|
+
config_path = PathlibPath(config) if config else None
|
|
119
|
+
|
|
120
|
+
# Resolve input to validate it exists
|
|
121
|
+
_ = list(resolve_input_information(input, config_path)[0])
|
|
122
|
+
|
|
123
|
+
if not pathlib_input.exists():
|
|
124
|
+
print_error(f"Input path '{input}' does not exist.")
|
|
125
|
+
raise Exit(1)
|
|
126
|
+
|
|
127
|
+
# Set up parallel executor with ETL strategy
|
|
128
|
+
parallel_exec = ParallelExecutor(
|
|
129
|
+
max_workers=1,
|
|
130
|
+
dependency_strategy=ETLDependencyStrategy(),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Get execution plan
|
|
134
|
+
if pathlib_input.is_dir():
|
|
135
|
+
root = pathlib_input
|
|
136
|
+
graph = parallel_exec.get_folder_execution_plan(pathlib_input)
|
|
137
|
+
elif pathlib_input.is_file():
|
|
138
|
+
root = pathlib_input.parent
|
|
139
|
+
graph = parallel_exec.get_execution_plan([pathlib_input])
|
|
140
|
+
else:
|
|
141
|
+
print_error(f"Input path '{input}' is not a file or directory.")
|
|
142
|
+
raise Exit(1)
|
|
143
|
+
|
|
144
|
+
if json_format:
|
|
145
|
+
plan_data = graph_to_json(graph, root)
|
|
146
|
+
levels = get_execution_levels(graph)
|
|
147
|
+
plan_data["execution_order"] = [
|
|
148
|
+
[safe_relative_path(node.path, root) for node in level]
|
|
149
|
+
for level in levels
|
|
150
|
+
]
|
|
151
|
+
json_output = json.dumps(plan_data, indent=2)
|
|
152
|
+
|
|
153
|
+
if output:
|
|
154
|
+
output_path = PathlibPath(output)
|
|
155
|
+
output_path.write_text(json_output)
|
|
156
|
+
print_info(f"Plan written to {output_path}")
|
|
157
|
+
else:
|
|
158
|
+
print(json_output)
|
|
159
|
+
else:
|
|
160
|
+
nodes, edges, execution_order = format_plan_text(graph, root)
|
|
161
|
+
|
|
162
|
+
if output:
|
|
163
|
+
output_path = PathlibPath(output)
|
|
164
|
+
lines = []
|
|
165
|
+
lines.append("Execution Plan")
|
|
166
|
+
lines.append(f"Scripts: {len(nodes)}")
|
|
167
|
+
lines.append(f"Dependencies: {len(edges)}")
|
|
168
|
+
lines.append(f"Execution Levels: {len(execution_order)}")
|
|
169
|
+
lines.append("")
|
|
170
|
+
if execution_order:
|
|
171
|
+
lines.append("Execution Order:")
|
|
172
|
+
for level_idx, level_scripts in enumerate(execution_order):
|
|
173
|
+
lines.append(
|
|
174
|
+
f" Level {level_idx + 1}: {', '.join(level_scripts)}"
|
|
175
|
+
)
|
|
176
|
+
lines.append("")
|
|
177
|
+
if edges:
|
|
178
|
+
lines.append("Dependencies:")
|
|
179
|
+
for from_node, to_node in edges:
|
|
180
|
+
lines.append(f" {from_node} -> {to_node}")
|
|
181
|
+
output_path.write_text("\n".join(lines))
|
|
182
|
+
print_info(f"Plan written to {output_path}")
|
|
183
|
+
else:
|
|
184
|
+
show_execution_plan(nodes, edges, execution_order)
|
|
185
|
+
|
|
186
|
+
except Exit:
|
|
187
|
+
raise
|
|
188
|
+
except Exception as e:
|
|
189
|
+
handle_execution_exception(e, debug=ctx.obj.get("DEBUG", False))
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Refresh command for Trilogy CLI - refreshes stale assets."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path as PathlibPath
|
|
4
|
+
|
|
5
|
+
from click import UNPROCESSED, Path, argument, option, pass_context
|
|
6
|
+
from click.exceptions import Exit
|
|
7
|
+
|
|
8
|
+
from trilogy import Executor
|
|
9
|
+
from trilogy.core.statements.execute import ProcessedValidateStatement
|
|
10
|
+
from trilogy.dialect.enums import Dialects
|
|
11
|
+
from trilogy.execution.state.state_store import (
|
|
12
|
+
DatasourceWatermark,
|
|
13
|
+
refresh_stale_assets,
|
|
14
|
+
)
|
|
15
|
+
from trilogy.scripts.common import (
|
|
16
|
+
CLIRuntimeParams,
|
|
17
|
+
ExecutionStats,
|
|
18
|
+
count_statement_stats,
|
|
19
|
+
handle_execution_exception,
|
|
20
|
+
)
|
|
21
|
+
from trilogy.scripts.dependency import ScriptNode
|
|
22
|
+
from trilogy.scripts.parallel_execution import ExecutionMode, run_parallel_execution
|
|
23
|
+
|
|
24
|
+
# Module-level flag for printing watermarks (set by CLI)
|
|
25
|
+
_print_watermarks = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _format_watermarks(watermarks: dict[str, DatasourceWatermark]) -> None:
|
|
29
|
+
"""Print watermark information for all datasources."""
|
|
30
|
+
from trilogy.scripts.display import print_info
|
|
31
|
+
|
|
32
|
+
print_info("Watermarks:")
|
|
33
|
+
for ds_id, watermark in sorted(watermarks.items()):
|
|
34
|
+
if not watermark.keys:
|
|
35
|
+
print_info(f" {ds_id}: (no watermarks)")
|
|
36
|
+
continue
|
|
37
|
+
for key_name, update_key in watermark.keys.items():
|
|
38
|
+
print_info(
|
|
39
|
+
f" {ds_id}.{key_name}: {update_key.value} ({update_key.type.value})"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def execute_script_for_refresh(
|
|
44
|
+
exec: Executor, node: ScriptNode, quiet: bool = False
|
|
45
|
+
) -> ExecutionStats:
|
|
46
|
+
"""Execute a script for the 'refresh' command - parse and refresh stale assets."""
|
|
47
|
+
from trilogy.scripts.display import print_info, print_success, print_warning
|
|
48
|
+
|
|
49
|
+
validation = []
|
|
50
|
+
with open(node.path, "r") as f:
|
|
51
|
+
statements = exec.parse_text(f.read())
|
|
52
|
+
|
|
53
|
+
for x in statements:
|
|
54
|
+
if isinstance(x, ProcessedValidateStatement):
|
|
55
|
+
validation.append(x)
|
|
56
|
+
|
|
57
|
+
stats = count_statement_stats([])
|
|
58
|
+
|
|
59
|
+
def on_stale_found(stale_count: int, root_assets: int, all_assets: int) -> None:
|
|
60
|
+
if stale_count == 0 and not quiet:
|
|
61
|
+
print_info(
|
|
62
|
+
f"No stale assets found in {node.path.name} ({root_assets}/{all_assets} root assets)"
|
|
63
|
+
)
|
|
64
|
+
elif not quiet:
|
|
65
|
+
print_warning(f"Found {stale_count} stale asset(s) in {node.path.name}")
|
|
66
|
+
|
|
67
|
+
def on_refresh(asset_id: str, reason: str) -> None:
|
|
68
|
+
if not quiet:
|
|
69
|
+
print_info(f" Refreshing {asset_id}: {reason}")
|
|
70
|
+
|
|
71
|
+
def on_watermarks(watermarks: dict[str, DatasourceWatermark]) -> None:
|
|
72
|
+
if _print_watermarks:
|
|
73
|
+
_format_watermarks(watermarks)
|
|
74
|
+
|
|
75
|
+
result = refresh_stale_assets(
|
|
76
|
+
exec,
|
|
77
|
+
on_stale_found=on_stale_found,
|
|
78
|
+
on_refresh=on_refresh,
|
|
79
|
+
on_watermarks=on_watermarks,
|
|
80
|
+
)
|
|
81
|
+
stats.update_count = result.refreshed_count
|
|
82
|
+
|
|
83
|
+
for x in validation:
|
|
84
|
+
exec.execute_statement(x)
|
|
85
|
+
stats = count_statement_stats([x])
|
|
86
|
+
|
|
87
|
+
if result.had_stale and not quiet:
|
|
88
|
+
print_success(
|
|
89
|
+
f"Refreshed {result.refreshed_count} asset(s) in {node.path.name}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return stats
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@argument("input", type=Path())
|
|
96
|
+
@argument("dialect", type=str, required=False)
|
|
97
|
+
@option("--param", multiple=True, help="Environment parameters as key=value pairs")
|
|
98
|
+
@option(
|
|
99
|
+
"--parallelism",
|
|
100
|
+
"-p",
|
|
101
|
+
default=None,
|
|
102
|
+
help="Maximum parallel workers for directory execution",
|
|
103
|
+
)
|
|
104
|
+
@option(
|
|
105
|
+
"--config", type=Path(exists=True), help="Path to trilogy.toml configuration file"
|
|
106
|
+
)
|
|
107
|
+
@option(
|
|
108
|
+
"--print-watermarks",
|
|
109
|
+
is_flag=True,
|
|
110
|
+
default=False,
|
|
111
|
+
help="Print watermark values for all datasources before refreshing",
|
|
112
|
+
)
|
|
113
|
+
@option(
|
|
114
|
+
"--env",
|
|
115
|
+
"-e",
|
|
116
|
+
multiple=True,
|
|
117
|
+
help="Set environment variables as KEY=VALUE pairs",
|
|
118
|
+
)
|
|
119
|
+
@argument("conn_args", nargs=-1, type=UNPROCESSED)
|
|
120
|
+
@pass_context
|
|
121
|
+
def refresh(
|
|
122
|
+
ctx,
|
|
123
|
+
input,
|
|
124
|
+
dialect: str | None,
|
|
125
|
+
param,
|
|
126
|
+
parallelism: int | None,
|
|
127
|
+
config,
|
|
128
|
+
print_watermarks,
|
|
129
|
+
env,
|
|
130
|
+
conn_args,
|
|
131
|
+
):
|
|
132
|
+
"""Refresh stale assets in Trilogy scripts.
|
|
133
|
+
|
|
134
|
+
Parses each script, identifies datasources marked as 'root' (source of truth),
|
|
135
|
+
compares watermarks to find stale derived assets, and refreshes them.
|
|
136
|
+
"""
|
|
137
|
+
global _print_watermarks
|
|
138
|
+
_print_watermarks = print_watermarks
|
|
139
|
+
|
|
140
|
+
cli_params = CLIRuntimeParams(
|
|
141
|
+
input=input,
|
|
142
|
+
dialect=Dialects(dialect) if dialect else None,
|
|
143
|
+
parallelism=parallelism,
|
|
144
|
+
param=param,
|
|
145
|
+
conn_args=conn_args,
|
|
146
|
+
debug=ctx.obj["DEBUG"],
|
|
147
|
+
config_path=PathlibPath(config) if config else None,
|
|
148
|
+
execution_strategy="eager_bfs",
|
|
149
|
+
env=env,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
run_parallel_execution(
|
|
154
|
+
cli_params=cli_params,
|
|
155
|
+
execution_fn=execute_script_for_refresh,
|
|
156
|
+
execution_mode=ExecutionMode.REFRESH,
|
|
157
|
+
)
|
|
158
|
+
except Exit:
|
|
159
|
+
raise
|
|
160
|
+
except Exception as e:
|
|
161
|
+
handle_execution_exception(e, debug=cli_params.debug)
|
trilogy/scripts/run.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Run command for Trilogy CLI."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path as PathlibPath
|
|
4
|
+
|
|
5
|
+
from click import UNPROCESSED, Path, argument, option, pass_context
|
|
6
|
+
from click.exceptions import Exit
|
|
7
|
+
|
|
8
|
+
from trilogy import Executor
|
|
9
|
+
from trilogy.dialect.enums import Dialects
|
|
10
|
+
from trilogy.scripts.common import (
|
|
11
|
+
CLIRuntimeParams,
|
|
12
|
+
ExecutionStats,
|
|
13
|
+
execute_script_with_stats,
|
|
14
|
+
handle_execution_exception,
|
|
15
|
+
)
|
|
16
|
+
from trilogy.scripts.dependency import ScriptNode
|
|
17
|
+
from trilogy.scripts.parallel_execution import ExecutionMode, run_parallel_execution
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def execute_script_for_run(
|
|
21
|
+
exec: Executor, node: ScriptNode, quiet: bool = False
|
|
22
|
+
) -> ExecutionStats:
|
|
23
|
+
"""Execute a script for the 'run' command (parallel execution mode)."""
|
|
24
|
+
return execute_script_with_stats(exec, node.path, run_statements=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@argument("input", type=Path())
|
|
28
|
+
@argument("dialect", type=str, required=False)
|
|
29
|
+
@option("--param", multiple=True, help="Environment parameters as key=value pairs")
|
|
30
|
+
@option(
|
|
31
|
+
"--parallelism",
|
|
32
|
+
"-p",
|
|
33
|
+
default=None,
|
|
34
|
+
help="Maximum parallel workers for directory execution",
|
|
35
|
+
)
|
|
36
|
+
@option(
|
|
37
|
+
"--config", type=Path(exists=True), help="Path to trilogy.toml configuration file"
|
|
38
|
+
)
|
|
39
|
+
@option(
|
|
40
|
+
"--env",
|
|
41
|
+
"-e",
|
|
42
|
+
multiple=True,
|
|
43
|
+
help="Set environment variables as KEY=VALUE pairs",
|
|
44
|
+
)
|
|
45
|
+
@argument("conn_args", nargs=-1, type=UNPROCESSED)
|
|
46
|
+
@pass_context
|
|
47
|
+
def run(
|
|
48
|
+
ctx,
|
|
49
|
+
input,
|
|
50
|
+
dialect: str | None,
|
|
51
|
+
param,
|
|
52
|
+
parallelism: int | None,
|
|
53
|
+
config,
|
|
54
|
+
env,
|
|
55
|
+
conn_args,
|
|
56
|
+
):
|
|
57
|
+
"""Execute a Trilogy script or query."""
|
|
58
|
+
cli_params = CLIRuntimeParams(
|
|
59
|
+
input=input,
|
|
60
|
+
dialect=Dialects(dialect) if dialect else None,
|
|
61
|
+
parallelism=parallelism,
|
|
62
|
+
param=param,
|
|
63
|
+
conn_args=conn_args,
|
|
64
|
+
debug=ctx.obj["DEBUG"],
|
|
65
|
+
config_path=PathlibPath(config) if config else None,
|
|
66
|
+
execution_strategy="eager_bfs",
|
|
67
|
+
env=env,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
run_parallel_execution(
|
|
72
|
+
cli_params=cli_params,
|
|
73
|
+
execution_fn=execute_script_for_run,
|
|
74
|
+
execution_mode=ExecutionMode.RUN,
|
|
75
|
+
)
|
|
76
|
+
except Exit:
|
|
77
|
+
raise
|
|
78
|
+
except Exception as e:
|
|
79
|
+
handle_execution_exception(e, debug=cli_params.debug)
|
trilogy/scripts/serve.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Serve command for Trilogy CLI."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path as PathlibPath
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
from click import Path, argument, option, pass_context
|
|
8
|
+
|
|
9
|
+
from trilogy.execution.config import load_config_file
|
|
10
|
+
from trilogy.scripts.common import find_trilogy_config
|
|
11
|
+
from trilogy.scripts.serve_helpers import (
|
|
12
|
+
find_trilogy_files,
|
|
13
|
+
get_relative_model_name,
|
|
14
|
+
get_safe_model_name,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def check_fastapi_available() -> bool:
|
|
19
|
+
"""Check if FastAPI and uvicorn are available."""
|
|
20
|
+
try:
|
|
21
|
+
import fastapi # noqa: F401
|
|
22
|
+
import uvicorn # noqa: F401
|
|
23
|
+
|
|
24
|
+
return True
|
|
25
|
+
except ImportError:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_app(app, engine: str, directory_path: PathlibPath, host: str, port: int):
|
|
30
|
+
from fastapi import HTTPException
|
|
31
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
32
|
+
from fastapi.responses import PlainTextResponse
|
|
33
|
+
|
|
34
|
+
from trilogy.scripts.serve_helpers import (
|
|
35
|
+
ModelImport,
|
|
36
|
+
StoreIndex,
|
|
37
|
+
find_all_model_files,
|
|
38
|
+
find_file_content_by_name,
|
|
39
|
+
find_model_by_name,
|
|
40
|
+
generate_model_index,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
url_host = "localhost" if host == "0.0.0.0" else host
|
|
44
|
+
if port in (80, 443):
|
|
45
|
+
base_url = f"http://{url_host}"
|
|
46
|
+
else:
|
|
47
|
+
base_url = f"http://{url_host}:{port}"
|
|
48
|
+
app.add_middleware(
|
|
49
|
+
CORSMiddleware,
|
|
50
|
+
allow_origins=["*"],
|
|
51
|
+
allow_credentials=True,
|
|
52
|
+
allow_methods=["*"],
|
|
53
|
+
allow_headers=["*"],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@app.get("/")
|
|
57
|
+
async def root():
|
|
58
|
+
"""Root endpoint with server information."""
|
|
59
|
+
file_count = len(find_all_model_files(directory_path))
|
|
60
|
+
return {
|
|
61
|
+
"message": "Trilogy Model Server",
|
|
62
|
+
"description": f"Serving model '{directory_path.name}' with {file_count} files from {directory_path}",
|
|
63
|
+
"endpoints": {
|
|
64
|
+
"/index.json": "Get list of available models",
|
|
65
|
+
"/models/<model-name>.json": "Get specific model details",
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@app.get("/index.json", response_model=StoreIndex)
|
|
70
|
+
async def get_index() -> StoreIndex:
|
|
71
|
+
"""Return the store index with list of available models."""
|
|
72
|
+
return StoreIndex(
|
|
73
|
+
name=f"Trilogy Models - {directory_path.name}",
|
|
74
|
+
models=generate_model_index(directory_path, base_url, engine),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@app.get("/models/{model_name}.json", response_model=ModelImport)
|
|
78
|
+
async def get_model(model_name: str) -> ModelImport:
|
|
79
|
+
"""Return a specific model by name."""
|
|
80
|
+
model = find_model_by_name(model_name, directory_path, base_url, engine)
|
|
81
|
+
if model is None:
|
|
82
|
+
raise HTTPException(status_code=404, detail="Model not found")
|
|
83
|
+
return model
|
|
84
|
+
|
|
85
|
+
@app.get("/files/{file_name}")
|
|
86
|
+
async def get_file(file_name: str):
|
|
87
|
+
"""Return the raw .preql or .sql file content."""
|
|
88
|
+
content = find_file_content_by_name(file_name, directory_path)
|
|
89
|
+
if content is None:
|
|
90
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
91
|
+
return PlainTextResponse(content=content)
|
|
92
|
+
|
|
93
|
+
print(f"Starting Trilogy Model Server on http://{host}:{port}")
|
|
94
|
+
print(f"Serving model '{directory_path.name}' from: {directory_path}")
|
|
95
|
+
print(f"Engine: {engine}")
|
|
96
|
+
print(f"Access the index at: http://{host}:{port}/index.json")
|
|
97
|
+
print(
|
|
98
|
+
f"Found {len(find_all_model_files(directory_path))} model files (.preql, .sql, .csv)"
|
|
99
|
+
)
|
|
100
|
+
return app
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@argument("path", type=Path(exists=True, file_okay=True, dir_okay=True))
|
|
104
|
+
@argument("engine", type=str, required=False, default="generic")
|
|
105
|
+
@option("--port", "-p", default=8100, help="Port to run the server on")
|
|
106
|
+
@option("--host", "-h", default="0.0.0.0", help="Host to bind the server to")
|
|
107
|
+
@option("--timeout", "-t", default=None, type=float, help="Shutdown after N seconds")
|
|
108
|
+
@pass_context
|
|
109
|
+
def serve(ctx, path: str, engine: str, port: int, host: str, timeout: float | None):
|
|
110
|
+
"""Start a FastAPI server to expose Trilogy models from a directory or file."""
|
|
111
|
+
if not check_fastapi_available():
|
|
112
|
+
print(
|
|
113
|
+
"Error: FastAPI and uvicorn are required for the serve command.\n"
|
|
114
|
+
"Please install with: pip install pytrilogy[serve]",
|
|
115
|
+
file=sys.stderr,
|
|
116
|
+
)
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
|
|
119
|
+
import uvicorn
|
|
120
|
+
from fastapi import FastAPI
|
|
121
|
+
|
|
122
|
+
from trilogy import __version__
|
|
123
|
+
|
|
124
|
+
path_obj = PathlibPath(path).resolve()
|
|
125
|
+
|
|
126
|
+
# Determine directory and target file
|
|
127
|
+
if path_obj.is_file():
|
|
128
|
+
directory_path = path_obj.parent
|
|
129
|
+
target_file = path_obj
|
|
130
|
+
else:
|
|
131
|
+
directory_path = path_obj
|
|
132
|
+
target_file = None
|
|
133
|
+
|
|
134
|
+
# Load trilogy.toml to get engine dialect if not explicitly provided
|
|
135
|
+
config_path = find_trilogy_config(directory_path)
|
|
136
|
+
if config_path and engine == "generic":
|
|
137
|
+
try:
|
|
138
|
+
runtime_config = load_config_file(config_path)
|
|
139
|
+
if runtime_config.engine_dialect:
|
|
140
|
+
engine = runtime_config.engine_dialect.value
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# Use localhost instead of 0.0.0.0 in URLs so they resolve properly
|
|
145
|
+
app = FastAPI(title="Trilogy Model Server", version=__version__)
|
|
146
|
+
|
|
147
|
+
create_app(app, engine, directory_path, host, port)
|
|
148
|
+
|
|
149
|
+
# Generate Trilogy Studio URL
|
|
150
|
+
url_host = "localhost" if host == "0.0.0.0" else host
|
|
151
|
+
base_url = (
|
|
152
|
+
f"http://{url_host}:{port}" if port not in (80, 443) else f"http://{url_host}"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Find target file if not specified
|
|
156
|
+
if target_file is None:
|
|
157
|
+
trilogy_files = find_trilogy_files(directory_path)
|
|
158
|
+
if trilogy_files:
|
|
159
|
+
target_file = trilogy_files[0]
|
|
160
|
+
|
|
161
|
+
if target_file:
|
|
162
|
+
# The model URL uses the directory name (not the file name)
|
|
163
|
+
model_safe_name = get_safe_model_name(directory_path.name)
|
|
164
|
+
model_url = f"{base_url}/models/{model_safe_name}.json"
|
|
165
|
+
store_url = base_url
|
|
166
|
+
|
|
167
|
+
# The asset name is the specific file within the model
|
|
168
|
+
asset_name = get_relative_model_name(target_file, directory_path)
|
|
169
|
+
|
|
170
|
+
# URL-encode the parameters
|
|
171
|
+
studio_url = (
|
|
172
|
+
f"https://trilogydata.dev/trilogy-studio-core/#"
|
|
173
|
+
f"import={quote(model_url)}&"
|
|
174
|
+
f"assetType=trilogy&"
|
|
175
|
+
f"assetName={quote(asset_name)}&"
|
|
176
|
+
f"modelName={quote(directory_path.name)}&"
|
|
177
|
+
f"connection={quote(engine)}&"
|
|
178
|
+
f"store={quote(store_url)}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
print("\n" + "=" * 80)
|
|
182
|
+
print("Trilogy Studio Link:")
|
|
183
|
+
print(studio_url)
|
|
184
|
+
print("=" * 80 + "\n")
|
|
185
|
+
|
|
186
|
+
if timeout is not None:
|
|
187
|
+
import threading
|
|
188
|
+
|
|
189
|
+
config = uvicorn.Config(app, host=host, port=port)
|
|
190
|
+
server = uvicorn.Server(config)
|
|
191
|
+
|
|
192
|
+
def shutdown_after_timeout():
|
|
193
|
+
import time
|
|
194
|
+
|
|
195
|
+
time.sleep(timeout)
|
|
196
|
+
server.should_exit = True
|
|
197
|
+
|
|
198
|
+
timer = threading.Thread(target=shutdown_after_timeout, daemon=True)
|
|
199
|
+
timer.start()
|
|
200
|
+
server.run()
|
|
201
|
+
else:
|
|
202
|
+
uvicorn.run(app, host=host, port=port)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Helpers for the serve command."""
|
|
2
|
+
|
|
3
|
+
from trilogy.scripts.serve_helpers.file_discovery import (
|
|
4
|
+
extract_description_from_file,
|
|
5
|
+
find_all_model_files,
|
|
6
|
+
find_csv_files,
|
|
7
|
+
find_preql_files,
|
|
8
|
+
find_sql_files,
|
|
9
|
+
find_trilogy_files,
|
|
10
|
+
get_relative_model_name,
|
|
11
|
+
get_safe_model_name,
|
|
12
|
+
)
|
|
13
|
+
from trilogy.scripts.serve_helpers.index_generation import (
|
|
14
|
+
find_file_content_by_name,
|
|
15
|
+
find_model_by_name,
|
|
16
|
+
generate_model_index,
|
|
17
|
+
)
|
|
18
|
+
from trilogy.scripts.serve_helpers.models import (
|
|
19
|
+
ImportFile,
|
|
20
|
+
ModelImport,
|
|
21
|
+
StoreIndex,
|
|
22
|
+
StoreModelIndex,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ImportFile",
|
|
27
|
+
"ModelImport",
|
|
28
|
+
"StoreIndex",
|
|
29
|
+
"StoreModelIndex",
|
|
30
|
+
"find_preql_files",
|
|
31
|
+
"find_sql_files",
|
|
32
|
+
"find_csv_files",
|
|
33
|
+
"find_trilogy_files",
|
|
34
|
+
"find_all_model_files",
|
|
35
|
+
"extract_description_from_file",
|
|
36
|
+
"get_relative_model_name",
|
|
37
|
+
"get_safe_model_name",
|
|
38
|
+
"generate_model_index",
|
|
39
|
+
"find_model_by_name",
|
|
40
|
+
"find_file_content_by_name",
|
|
41
|
+
]
|