pytrilogy 0.3.142__cp312-cp312-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cp312-win_amd64.pyd +0 -0
  4. pytrilogy-0.3.142.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.142.dist-info/RECORD +200 -0
  6. pytrilogy-0.3.142.dist-info/WHEEL +4 -0
  7. pytrilogy-0.3.142.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.142.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +16 -0
  10. trilogy/ai/README.md +10 -0
  11. trilogy/ai/__init__.py +19 -0
  12. trilogy/ai/constants.py +92 -0
  13. trilogy/ai/conversation.py +107 -0
  14. trilogy/ai/enums.py +7 -0
  15. trilogy/ai/execute.py +50 -0
  16. trilogy/ai/models.py +34 -0
  17. trilogy/ai/prompts.py +100 -0
  18. trilogy/ai/providers/__init__.py +0 -0
  19. trilogy/ai/providers/anthropic.py +106 -0
  20. trilogy/ai/providers/base.py +24 -0
  21. trilogy/ai/providers/google.py +146 -0
  22. trilogy/ai/providers/openai.py +89 -0
  23. trilogy/ai/providers/utils.py +68 -0
  24. trilogy/authoring/README.md +3 -0
  25. trilogy/authoring/__init__.py +148 -0
  26. trilogy/constants.py +113 -0
  27. trilogy/core/README.md +52 -0
  28. trilogy/core/__init__.py +0 -0
  29. trilogy/core/constants.py +6 -0
  30. trilogy/core/enums.py +443 -0
  31. trilogy/core/env_processor.py +120 -0
  32. trilogy/core/environment_helpers.py +320 -0
  33. trilogy/core/ergonomics.py +193 -0
  34. trilogy/core/exceptions.py +123 -0
  35. trilogy/core/functions.py +1227 -0
  36. trilogy/core/graph_models.py +139 -0
  37. trilogy/core/internal.py +85 -0
  38. trilogy/core/models/__init__.py +0 -0
  39. trilogy/core/models/author.py +2669 -0
  40. trilogy/core/models/build.py +2521 -0
  41. trilogy/core/models/build_environment.py +180 -0
  42. trilogy/core/models/core.py +501 -0
  43. trilogy/core/models/datasource.py +322 -0
  44. trilogy/core/models/environment.py +751 -0
  45. trilogy/core/models/execute.py +1177 -0
  46. trilogy/core/optimization.py +251 -0
  47. trilogy/core/optimizations/__init__.py +12 -0
  48. trilogy/core/optimizations/base_optimization.py +17 -0
  49. trilogy/core/optimizations/hide_unused_concept.py +47 -0
  50. trilogy/core/optimizations/inline_datasource.py +102 -0
  51. trilogy/core/optimizations/predicate_pushdown.py +245 -0
  52. trilogy/core/processing/README.md +94 -0
  53. trilogy/core/processing/READMEv2.md +121 -0
  54. trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
  55. trilogy/core/processing/__init__.py +0 -0
  56. trilogy/core/processing/concept_strategies_v3.py +508 -0
  57. trilogy/core/processing/constants.py +15 -0
  58. trilogy/core/processing/discovery_node_factory.py +451 -0
  59. trilogy/core/processing/discovery_utility.py +548 -0
  60. trilogy/core/processing/discovery_validation.py +167 -0
  61. trilogy/core/processing/graph_utils.py +43 -0
  62. trilogy/core/processing/node_generators/README.md +9 -0
  63. trilogy/core/processing/node_generators/__init__.py +31 -0
  64. trilogy/core/processing/node_generators/basic_node.py +160 -0
  65. trilogy/core/processing/node_generators/common.py +268 -0
  66. trilogy/core/processing/node_generators/constant_node.py +38 -0
  67. trilogy/core/processing/node_generators/filter_node.py +315 -0
  68. trilogy/core/processing/node_generators/group_node.py +213 -0
  69. trilogy/core/processing/node_generators/group_to_node.py +117 -0
  70. trilogy/core/processing/node_generators/multiselect_node.py +205 -0
  71. trilogy/core/processing/node_generators/node_merge_node.py +653 -0
  72. trilogy/core/processing/node_generators/recursive_node.py +88 -0
  73. trilogy/core/processing/node_generators/rowset_node.py +165 -0
  74. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  75. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
  76. trilogy/core/processing/node_generators/select_merge_node.py +748 -0
  77. trilogy/core/processing/node_generators/select_node.py +95 -0
  78. trilogy/core/processing/node_generators/synonym_node.py +98 -0
  79. trilogy/core/processing/node_generators/union_node.py +91 -0
  80. trilogy/core/processing/node_generators/unnest_node.py +182 -0
  81. trilogy/core/processing/node_generators/window_node.py +201 -0
  82. trilogy/core/processing/nodes/README.md +28 -0
  83. trilogy/core/processing/nodes/__init__.py +179 -0
  84. trilogy/core/processing/nodes/base_node.py +519 -0
  85. trilogy/core/processing/nodes/filter_node.py +75 -0
  86. trilogy/core/processing/nodes/group_node.py +194 -0
  87. trilogy/core/processing/nodes/merge_node.py +420 -0
  88. trilogy/core/processing/nodes/recursive_node.py +46 -0
  89. trilogy/core/processing/nodes/select_node_v2.py +242 -0
  90. trilogy/core/processing/nodes/union_node.py +53 -0
  91. trilogy/core/processing/nodes/unnest_node.py +62 -0
  92. trilogy/core/processing/nodes/window_node.py +56 -0
  93. trilogy/core/processing/utility.py +823 -0
  94. trilogy/core/query_processor.py +596 -0
  95. trilogy/core/statements/README.md +35 -0
  96. trilogy/core/statements/__init__.py +0 -0
  97. trilogy/core/statements/author.py +536 -0
  98. trilogy/core/statements/build.py +0 -0
  99. trilogy/core/statements/common.py +20 -0
  100. trilogy/core/statements/execute.py +155 -0
  101. trilogy/core/table_processor.py +66 -0
  102. trilogy/core/utility.py +8 -0
  103. trilogy/core/validation/README.md +46 -0
  104. trilogy/core/validation/__init__.py +0 -0
  105. trilogy/core/validation/common.py +161 -0
  106. trilogy/core/validation/concept.py +146 -0
  107. trilogy/core/validation/datasource.py +227 -0
  108. trilogy/core/validation/environment.py +73 -0
  109. trilogy/core/validation/fix.py +256 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1392 -0
  112. trilogy/dialect/bigquery.py +308 -0
  113. trilogy/dialect/common.py +147 -0
  114. trilogy/dialect/config.py +144 -0
  115. trilogy/dialect/dataframe.py +50 -0
  116. trilogy/dialect/duckdb.py +231 -0
  117. trilogy/dialect/enums.py +147 -0
  118. trilogy/dialect/metadata.py +173 -0
  119. trilogy/dialect/mock.py +190 -0
  120. trilogy/dialect/postgres.py +117 -0
  121. trilogy/dialect/presto.py +110 -0
  122. trilogy/dialect/results.py +89 -0
  123. trilogy/dialect/snowflake.py +129 -0
  124. trilogy/dialect/sql_server.py +137 -0
  125. trilogy/engine.py +48 -0
  126. trilogy/execution/config.py +75 -0
  127. trilogy/executor.py +568 -0
  128. trilogy/hooks/__init__.py +4 -0
  129. trilogy/hooks/base_hook.py +40 -0
  130. trilogy/hooks/graph_hook.py +139 -0
  131. trilogy/hooks/query_debugger.py +166 -0
  132. trilogy/metadata/__init__.py +0 -0
  133. trilogy/parser.py +10 -0
  134. trilogy/parsing/README.md +21 -0
  135. trilogy/parsing/__init__.py +0 -0
  136. trilogy/parsing/common.py +1069 -0
  137. trilogy/parsing/config.py +5 -0
  138. trilogy/parsing/exceptions.py +8 -0
  139. trilogy/parsing/helpers.py +1 -0
  140. trilogy/parsing/parse_engine.py +2813 -0
  141. trilogy/parsing/render.py +769 -0
  142. trilogy/parsing/trilogy.lark +540 -0
  143. trilogy/py.typed +0 -0
  144. trilogy/render.py +42 -0
  145. trilogy/scripts/README.md +9 -0
  146. trilogy/scripts/__init__.py +0 -0
  147. trilogy/scripts/agent.py +41 -0
  148. trilogy/scripts/agent_info.py +303 -0
  149. trilogy/scripts/common.py +355 -0
  150. trilogy/scripts/dependency/Cargo.lock +617 -0
  151. trilogy/scripts/dependency/Cargo.toml +39 -0
  152. trilogy/scripts/dependency/README.md +131 -0
  153. trilogy/scripts/dependency/build.sh +25 -0
  154. trilogy/scripts/dependency/src/directory_resolver.rs +177 -0
  155. trilogy/scripts/dependency/src/lib.rs +16 -0
  156. trilogy/scripts/dependency/src/main.rs +770 -0
  157. trilogy/scripts/dependency/src/parser.rs +435 -0
  158. trilogy/scripts/dependency/src/preql.pest +208 -0
  159. trilogy/scripts/dependency/src/python_bindings.rs +303 -0
  160. trilogy/scripts/dependency/src/resolver.rs +716 -0
  161. trilogy/scripts/dependency/tests/base.preql +3 -0
  162. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  163. trilogy/scripts/dependency/tests/customer.preql +6 -0
  164. trilogy/scripts/dependency/tests/main.preql +9 -0
  165. trilogy/scripts/dependency/tests/orders.preql +7 -0
  166. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  167. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  168. trilogy/scripts/dependency.py +323 -0
  169. trilogy/scripts/display.py +512 -0
  170. trilogy/scripts/environment.py +46 -0
  171. trilogy/scripts/fmt.py +32 -0
  172. trilogy/scripts/ingest.py +471 -0
  173. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  174. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  175. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  176. trilogy/scripts/ingest_helpers/typing.py +161 -0
  177. trilogy/scripts/init.py +105 -0
  178. trilogy/scripts/parallel_execution.py +713 -0
  179. trilogy/scripts/plan.py +189 -0
  180. trilogy/scripts/run.py +63 -0
  181. trilogy/scripts/serve.py +140 -0
  182. trilogy/scripts/serve_helpers/__init__.py +41 -0
  183. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  184. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  185. trilogy/scripts/serve_helpers/models.py +38 -0
  186. trilogy/scripts/single_execution.py +131 -0
  187. trilogy/scripts/testing.py +119 -0
  188. trilogy/scripts/trilogy.py +68 -0
  189. trilogy/std/__init__.py +0 -0
  190. trilogy/std/color.preql +3 -0
  191. trilogy/std/date.preql +13 -0
  192. trilogy/std/display.preql +18 -0
  193. trilogy/std/geography.preql +22 -0
  194. trilogy/std/metric.preql +15 -0
  195. trilogy/std/money.preql +67 -0
  196. trilogy/std/net.preql +14 -0
  197. trilogy/std/ranking.preql +7 -0
  198. trilogy/std/report.preql +5 -0
  199. trilogy/std/semantic.preql +6 -0
  200. trilogy/utility.py +34 -0
@@ -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))
trilogy/scripts/run.py ADDED
@@ -0,0 +1,63 @@
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 CLIRuntimeParams, handle_execution_exception
11
+ from trilogy.scripts.dependency import ScriptNode
12
+ from trilogy.scripts.parallel_execution import run_parallel_execution
13
+
14
+
15
+ def execute_script_for_run(
16
+ exec: Executor, node: ScriptNode, quiet: bool = False
17
+ ) -> None:
18
+ """Execute a script for the 'run' command (parallel execution mode)."""
19
+ with open(node.path, "r") as f:
20
+ queries = exec.parse_text(f.read())
21
+ for query in queries:
22
+ exec.execute_query(query)
23
+
24
+
25
+ @argument("input", type=Path())
26
+ @argument("dialect", type=str, required=False)
27
+ @option("--param", multiple=True, help="Environment parameters as key=value pairs")
28
+ @option(
29
+ "--parallelism",
30
+ "-p",
31
+ default=None,
32
+ help="Maximum parallel workers for directory execution",
33
+ )
34
+ @option(
35
+ "--config", type=Path(exists=True), help="Path to trilogy.toml configuration file"
36
+ )
37
+ @argument("conn_args", nargs=-1, type=UNPROCESSED)
38
+ @pass_context
39
+ def run(
40
+ ctx, input, dialect: str | None, param, parallelism: int | None, config, conn_args
41
+ ):
42
+ """Execute a Trilogy script or query."""
43
+ cli_params = CLIRuntimeParams(
44
+ input=input,
45
+ dialect=Dialects(dialect) if dialect else None,
46
+ parallelism=parallelism,
47
+ param=param,
48
+ conn_args=conn_args,
49
+ debug=ctx.obj["DEBUG"],
50
+ config_path=PathlibPath(config) if config else None,
51
+ execution_strategy="eager_bfs",
52
+ )
53
+
54
+ try:
55
+ run_parallel_execution(
56
+ cli_params=cli_params,
57
+ execution_fn=execute_script_for_run,
58
+ execution_mode="run",
59
+ )
60
+ except Exit:
61
+ raise
62
+ except Exception as e:
63
+ handle_execution_exception(e, debug=cli_params.debug)
@@ -0,0 +1,140 @@
1
+ """Serve command for Trilogy CLI."""
2
+
3
+ import sys
4
+ from pathlib import Path as PathlibPath
5
+
6
+ from click import Path, argument, option, pass_context
7
+
8
+
9
+ def check_fastapi_available() -> bool:
10
+ """Check if FastAPI and uvicorn are available."""
11
+ try:
12
+ import fastapi # noqa: F401
13
+ import uvicorn # noqa: F401
14
+
15
+ return True
16
+ except ImportError:
17
+ return False
18
+
19
+
20
+ def create_app(app, engine: str, directory_path: PathlibPath, host: str, port: int):
21
+ from fastapi import HTTPException
22
+ from fastapi.middleware.cors import CORSMiddleware
23
+ from fastapi.responses import PlainTextResponse
24
+
25
+ from trilogy.scripts.serve_helpers import (
26
+ ModelImport,
27
+ StoreIndex,
28
+ find_all_model_files,
29
+ find_file_content_by_name,
30
+ find_model_by_name,
31
+ generate_model_index,
32
+ )
33
+
34
+ url_host = "localhost" if host == "0.0.0.0" else host
35
+ if port in (80, 443):
36
+ base_url = f"http://{url_host}"
37
+ else:
38
+ base_url = f"http://{url_host}:{port}"
39
+ app.add_middleware(
40
+ CORSMiddleware,
41
+ allow_origins=["*"],
42
+ allow_credentials=True,
43
+ allow_methods=["*"],
44
+ allow_headers=["*"],
45
+ )
46
+
47
+ @app.get("/")
48
+ async def root():
49
+ """Root endpoint with server information."""
50
+ file_count = len(find_all_model_files(directory_path))
51
+ return {
52
+ "message": "Trilogy Model Server",
53
+ "description": f"Serving model '{directory_path.name}' with {file_count} files from {directory_path}",
54
+ "endpoints": {
55
+ "/index.json": "Get list of available models",
56
+ "/models/<model-name>.json": "Get specific model details",
57
+ },
58
+ }
59
+
60
+ @app.get("/index.json", response_model=StoreIndex)
61
+ async def get_index() -> StoreIndex:
62
+ """Return the store index with list of available models."""
63
+ return StoreIndex(
64
+ name=f"Trilogy Models - {directory_path.name}",
65
+ models=generate_model_index(directory_path, base_url, engine),
66
+ )
67
+
68
+ @app.get("/models/{model_name}.json", response_model=ModelImport)
69
+ async def get_model(model_name: str) -> ModelImport:
70
+ """Return a specific model by name."""
71
+ model = find_model_by_name(model_name, directory_path, base_url, engine)
72
+ if model is None:
73
+ raise HTTPException(status_code=404, detail="Model not found")
74
+ return model
75
+
76
+ @app.get("/files/{file_name}")
77
+ async def get_file(file_name: str):
78
+ """Return the raw .preql or .sql file content."""
79
+ content = find_file_content_by_name(file_name, directory_path)
80
+ if content is None:
81
+ raise HTTPException(status_code=404, detail="File not found")
82
+ return PlainTextResponse(content=content)
83
+
84
+ print(f"Starting Trilogy Model Server on http://{host}:{port}")
85
+ print(f"Serving model '{directory_path.name}' from: {directory_path}")
86
+ print(f"Engine: {engine}")
87
+ print(f"Access the index at: http://{host}:{port}/index.json")
88
+ print(
89
+ f"Found {len(find_all_model_files(directory_path))} model files (.preql, .sql, .csv)"
90
+ )
91
+ return app
92
+
93
+
94
+ @argument("directory", type=Path(exists=True, file_okay=False, dir_okay=True))
95
+ @argument("engine", type=str, required=False, default="generic")
96
+ @option("--port", "-p", default=8100, help="Port to run the server on")
97
+ @option("--host", "-h", default="0.0.0.0", help="Host to bind the server to")
98
+ @option("--timeout", "-t", default=None, type=float, help="Shutdown after N seconds")
99
+ @pass_context
100
+ def serve(
101
+ ctx, directory: str, engine: str, port: int, host: str, timeout: float | None
102
+ ):
103
+ """Start a FastAPI server to expose Trilogy models from a directory."""
104
+ if not check_fastapi_available():
105
+ print(
106
+ "Error: FastAPI and uvicorn are required for the serve command.\n"
107
+ "Please install with: pip install pytrilogy[serve]",
108
+ file=sys.stderr,
109
+ )
110
+ sys.exit(1)
111
+
112
+ import uvicorn
113
+ from fastapi import FastAPI
114
+
115
+ from trilogy import __version__
116
+
117
+ directory_path = PathlibPath(directory).resolve()
118
+ # Use localhost instead of 0.0.0.0 in URLs so they resolve properly
119
+
120
+ app = FastAPI(title="Trilogy Model Server", version=__version__)
121
+
122
+ create_app(app, engine, directory_path, host, port)
123
+
124
+ if timeout is not None:
125
+ import threading
126
+
127
+ config = uvicorn.Config(app, host=host, port=port)
128
+ server = uvicorn.Server(config)
129
+
130
+ def shutdown_after_timeout():
131
+ import time
132
+
133
+ time.sleep(timeout)
134
+ server.should_exit = True
135
+
136
+ timer = threading.Thread(target=shutdown_after_timeout, daemon=True)
137
+ timer.start()
138
+ server.run()
139
+ else:
140
+ 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
+ ]
@@ -0,0 +1,142 @@
1
+ """File discovery and processing utilities for the serve command."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ def find_preql_files(directory_path: Path) -> list[Path]:
7
+ """Find all .preql files in the directory recursively.
8
+
9
+ Args:
10
+ directory_path: The root directory to search
11
+
12
+ Returns:
13
+ List of Path objects for all .preql files found
14
+ """
15
+ return list(directory_path.rglob("*.preql"))
16
+
17
+
18
+ def find_sql_files(directory_path: Path) -> list[Path]:
19
+ """Find all .sql files in the directory recursively.
20
+
21
+ Args:
22
+ directory_path: The root directory to search
23
+
24
+ Returns:
25
+ List of Path objects for all .sql files found
26
+ """
27
+ return list(directory_path.rglob("*.sql"))
28
+
29
+
30
+ def find_csv_files(directory_path: Path) -> list[Path]:
31
+ """Find all .csv files in the directory recursively.
32
+
33
+ Args:
34
+ directory_path: The root directory to search
35
+
36
+ Returns:
37
+ List of Path objects for all .csv files found
38
+ """
39
+ return list(directory_path.rglob("*.csv"))
40
+
41
+
42
+ def find_trilogy_files(directory_path: Path) -> list[Path]:
43
+ """Find all .preql and .sql files in the directory recursively.
44
+
45
+ Args:
46
+ directory_path: The root directory to search
47
+
48
+ Returns:
49
+ List of Path objects for all .preql and .sql files found, sorted by path
50
+ """
51
+ preql_files = find_preql_files(directory_path)
52
+ sql_files = find_sql_files(directory_path)
53
+ return sorted(preql_files + sql_files)
54
+
55
+
56
+ def find_all_model_files(directory_path: Path) -> list[Path]:
57
+ """Find all model files (.preql, .sql, .csv) in the directory recursively.
58
+
59
+ Args:
60
+ directory_path: The root directory to search
61
+
62
+ Returns:
63
+ List of Path objects for all model files found, sorted by path
64
+ """
65
+ preql_files = find_preql_files(directory_path)
66
+ sql_files = find_sql_files(directory_path)
67
+ csv_files = find_csv_files(directory_path)
68
+ return sorted(preql_files + sql_files + csv_files)
69
+
70
+
71
+ def get_relative_model_name(preql_file: Path, directory_path: Path) -> str:
72
+ """Get the relative model name from a model file path.
73
+
74
+ Args:
75
+ preql_file: Path to the .preql, .sql, or .csv file
76
+ directory_path: Root directory path
77
+
78
+ Returns:
79
+ Relative model name with forward slashes and no extension
80
+ """
81
+ relative_path = preql_file.relative_to(directory_path)
82
+ return (
83
+ str(relative_path)
84
+ .replace("\\", "/")
85
+ .replace(".preql", "")
86
+ .replace(".sql", "")
87
+ .replace(".csv", "")
88
+ )
89
+
90
+
91
+ def get_safe_model_name(model_name: str) -> str:
92
+ """Convert a model name to a URL-safe format.
93
+
94
+ Args:
95
+ model_name: The model name (may contain slashes)
96
+
97
+ Returns:
98
+ URL-safe model name with slashes replaced by hyphens
99
+ """
100
+ return model_name.replace("/", "-")
101
+
102
+
103
+ def extract_description_from_file(file_path: Path) -> str:
104
+ """Extract description from a preql or sql file's comments.
105
+
106
+ Looks for the first comment line (starting with # or --) in the first 5 lines
107
+ of the file and uses it as the description.
108
+
109
+ Args:
110
+ file_path: Path to the .preql or .sql file
111
+
112
+ Returns:
113
+ Description extracted from comments or a default description
114
+ """
115
+ with open(file_path, "r") as f:
116
+ content = f.read()
117
+
118
+ model_name = file_path.stem
119
+ default_description = f"Trilogy model: {model_name}"
120
+
121
+ first_lines = content.split("\n")[:5]
122
+ for line in first_lines:
123
+ stripped = line.strip()
124
+ if stripped.startswith("#"):
125
+ return stripped.lstrip("#").strip()
126
+ if stripped.startswith("--"):
127
+ return stripped.lstrip("-").strip()
128
+
129
+ return default_description
130
+
131
+
132
+ def read_file_content(file_path: Path) -> str:
133
+ """Read and return the content of a file.
134
+
135
+ Args:
136
+ file_path: Path to the file
137
+
138
+ Returns:
139
+ File content as string
140
+ """
141
+ with open(file_path, "r") as f:
142
+ return f.read()