Skip to content

Commit 595e81f

Browse files
authored
Create example project during repo initialization. Various improvements to repo validation (#42)
1 parent df5dca0 commit 595e81f

13 files changed

Lines changed: 422 additions & 70 deletions

File tree

sqlmesh/cli/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import typing as t
2+
from functools import wraps
3+
4+
import click
5+
from sqlglot.errors import SqlglotError
6+
7+
from sqlmesh.utils.concurrency import NodeExecutionFailedError
8+
from sqlmesh.utils.errors import SQLMeshError
9+
10+
11+
def error_handler(func: t.Callable) -> t.Callable:
12+
@wraps(func)
13+
def wrapper(*args, **kwargs):
14+
try:
15+
return func(*args, **kwargs)
16+
except NodeExecutionFailedError as ex:
17+
raise click.ClickException(str(ex.__cause__))
18+
except (SQLMeshError, SqlglotError, ValueError) as ex:
19+
raise click.ClickException(str(ex))
20+
21+
return wrapper

sqlmesh/cli/example_project.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import typing as t
2+
from enum import Enum
3+
from pathlib import Path
4+
5+
import click
6+
7+
DEFAULT_CONFIG = """import duckdb
8+
from sqlmesh.core.config import Config
9+
10+
config = Config(
11+
engine_connection_factory=duckdb.connect,
12+
engine_dialect="duckdb",
13+
)
14+
15+
16+
test_config = config
17+
"""
18+
19+
20+
DEFAULT_AIRFLOW_CONFIG = """import duckdb
21+
from sqlmesh.core.config import AirflowSchedulerBackend, Config
22+
23+
config = Config(
24+
scheduler_backend=AirflowSchedulerBackend(
25+
airflow_url="http://localhost:8080/",
26+
username="airflow",
27+
password="airflow",
28+
),
29+
backfill_concurrent_tasks=4,
30+
ddl_concurrent_tasks=4,
31+
)
32+
33+
34+
test_config = Config(
35+
engine_connection_factory=duckdb.connect,
36+
engine_dialect="duckdb",
37+
)
38+
"""
39+
40+
EXAMPLE_SCHEMA_NAME = "sqlmesh_example"
41+
EXAMPLE_FULL_MODEL_NAME = f"{EXAMPLE_SCHEMA_NAME}.example_full_model"
42+
EXAMPLE_INCREMENTAL_MODEL_NAME = f"{EXAMPLE_SCHEMA_NAME}.example_incremental_model"
43+
44+
45+
EXAMPLE_FULL_MODEL_DEF = f"""MODEL (
46+
name {EXAMPLE_FULL_MODEL_NAME},
47+
kind full,
48+
cron '@daily',
49+
);
50+
51+
SELECT
52+
item_id,
53+
count(distinct id) AS num_orders,
54+
FROM
55+
{EXAMPLE_INCREMENTAL_MODEL_NAME}
56+
GROUP BY item_id
57+
"""
58+
59+
EXAMPLE_INCREMENTAL_MODEL_DEF = f"""MODEL (
60+
name {EXAMPLE_INCREMENTAL_MODEL_NAME},
61+
kind incremental,
62+
time_column ds,
63+
start '2020-01-01',
64+
batch_size 1,
65+
cron '@daily',
66+
);
67+
68+
SELECT
69+
id,
70+
item_id,
71+
ds,
72+
FROM
73+
(VALUES
74+
(1, 1, '2020-01-01'),
75+
(1, 2, '2020-01-01'),
76+
(2, 1, '2020-01-01'),
77+
(3, 3, '2020-01-03'),
78+
(4, 1, '2020-01-04'),
79+
(5, 1, '2020-01-05'),
80+
(6, 1, '2020-01-06'),
81+
(7, 1, '2020-01-07')
82+
) AS t (id, item_id, ds)
83+
WHERE
84+
ds between @start_ds and @end_ds
85+
"""
86+
87+
EXAMPLE_AUDIT = f"""AUDIT (
88+
name asset_positive_order_ids,
89+
model {EXAMPLE_FULL_MODEL_NAME}
90+
);
91+
92+
SELECT *
93+
FROM {EXAMPLE_FULL_MODEL_NAME}
94+
WHERE
95+
item_id < 0
96+
"""
97+
98+
99+
EXAMPLE_TEST = f"""test_example_full_model:
100+
model: {EXAMPLE_FULL_MODEL_NAME}
101+
inputs:
102+
{EXAMPLE_INCREMENTAL_MODEL_NAME}:
103+
rows:
104+
- id: 1
105+
item_id: 1
106+
ds: '2020-01-01'
107+
- id: 2
108+
item_id: 1
109+
ds: '2020-01-02'
110+
- id: 3
111+
item_id: 2
112+
ds: '2020-01-03'
113+
outputs:
114+
query:
115+
rows:
116+
- item_id: 1
117+
num_orders: 2
118+
- item_id: 2
119+
num_orders: 1
120+
"""
121+
122+
123+
class ProjectTemplate(Enum):
124+
AIRFLOW = "airflow"
125+
DEFAULT = "default"
126+
127+
128+
def init_example_project(
129+
path: t.Union[str, Path], template: ProjectTemplate = ProjectTemplate.DEFAULT
130+
) -> None:
131+
root_path = Path(path)
132+
config_path = root_path / "config.py"
133+
audits_path = root_path / "audits"
134+
macros_path = root_path / "macros"
135+
models_path = root_path / "models"
136+
tests_path = root_path / "tests"
137+
138+
if config_path.exists():
139+
raise click.ClickException(f"Found an existing config in '{config_path}'")
140+
141+
_create_folders([audits_path, macros_path, models_path, tests_path])
142+
_create_config(config_path, template)
143+
_create_audits(audits_path)
144+
_create_models(models_path)
145+
_create_tests(tests_path)
146+
147+
148+
def _create_folders(target_folders: t.Sequence[Path]) -> None:
149+
for folder_path in target_folders:
150+
folder_path.mkdir()
151+
(folder_path / ".gitkeep").touch()
152+
153+
154+
def _create_config(config_path: Path, template: ProjectTemplate) -> None:
155+
_write_file(
156+
config_path,
157+
DEFAULT_AIRFLOW_CONFIG
158+
if template == ProjectTemplate.AIRFLOW
159+
else DEFAULT_CONFIG,
160+
)
161+
162+
163+
def _create_audits(audits_path: Path) -> None:
164+
_write_file(audits_path / "example_full_model.sql", EXAMPLE_AUDIT)
165+
166+
167+
def _create_models(models_path: Path) -> None:
168+
for model_name, model_def in [
169+
(EXAMPLE_FULL_MODEL_NAME, EXAMPLE_FULL_MODEL_DEF),
170+
(EXAMPLE_INCREMENTAL_MODEL_NAME, EXAMPLE_INCREMENTAL_MODEL_DEF),
171+
]:
172+
_write_file(models_path / f"{model_name.split('.')[-1]}.sql", model_def)
173+
174+
175+
def _create_tests(tests_path: Path) -> None:
176+
_write_file(tests_path / "test_example_full_model.yaml", EXAMPLE_TEST)
177+
178+
179+
def _write_file(path: Path, payload: str) -> None:
180+
with open(path, "w", encoding="utf-8") as fd:
181+
fd.write(payload)

sqlmesh/cli/main.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
import click
55

6+
from sqlmesh.cli import error_handler
67
from sqlmesh.cli import options as opt
8+
from sqlmesh.cli.example_project import ProjectTemplate, init_example_project
79
from sqlmesh.core.context import Context
810
from sqlmesh.core.test import run_all_model_tests, run_model_tests
911
from sqlmesh.utils.date import TimeLike
@@ -13,6 +15,7 @@
1315
@opt.path
1416
@opt.config
1517
@click.pass_context
18+
@error_handler
1619
def cli(ctx, path, config=None) -> None:
1720
"""SQLMesh command line tool."""
1821
path = os.path.abspath(path)
@@ -33,18 +36,21 @@ def cli(ctx, path, config=None) -> None:
3336

3437

3538
@cli.command("init")
39+
@click.option(
40+
"-t",
41+
"--template",
42+
type=str,
43+
help="Project template. Support values: airflow, default.",
44+
)
3645
@click.pass_context
37-
def init(ctx) -> None:
46+
@error_handler
47+
def init(ctx, template: t.Optional[str] = None) -> None:
3848
"""Create a new SQLMesh repository."""
39-
path = os.path.join(ctx.obj, "config.py")
40-
if os.path.exists(path):
41-
raise click.ClickException(f"Found an existing config in `{path}`.")
42-
with open(path, "w", encoding="utf8") as file:
43-
file.write(
44-
"""from sqlmesh.core.config import Config
45-
46-
config = Config()"""
47-
)
49+
try:
50+
project_template = ProjectTemplate(template.lower() if template else "default")
51+
except ValueError:
52+
raise click.ClickException(f"Invalid project template '{template}'")
53+
init_example_project(ctx.obj, template=project_template)
4854

4955

5056
@cli.command("render")
@@ -54,6 +60,7 @@ def init(ctx) -> None:
5460
@opt.latest_time
5561
@opt.expand
5662
@click.pass_context
63+
@error_handler
5764
def render(
5865
ctx,
5966
model: str,
@@ -91,6 +98,7 @@ def render(
9198
help="The number of rows which the query should be limited to.",
9299
)
93100
@click.pass_context
101+
@error_handler
94102
def evaluate(
95103
ctx,
96104
model: str,
@@ -112,6 +120,7 @@ def evaluate(
112120

113121
@cli.command("format")
114122
@click.pass_context
123+
@error_handler
115124
def format(ctx) -> None:
116125
"""Format all models in a given directory."""
117126
ctx.obj.format()
@@ -120,6 +129,7 @@ def format(ctx) -> None:
120129
@cli.command("diff")
121130
@opt.environment
122131
@click.pass_context
132+
@error_handler
123133
def diff(ctx, environment: t.Optional[str] = None) -> None:
124134
"""Show the diff between the current context and a given environment."""
125135
ctx.obj.diff(environment)
@@ -149,9 +159,11 @@ def diff(ctx, environment: t.Optional[str] = None) -> None:
149159
)
150160
@click.option(
151161
"--no_gaps",
162+
is_flag=True,
152163
help="Ensure that new snapshots have no data gaps when comparing to existing snapshots for matching models in the target environment.",
153164
)
154165
@click.pass_context
166+
@error_handler
155167
def plan(ctx, environment: t.Optional[str] = None, **kwargs) -> None:
156168
"""Plan a migration of the current context's models with the given environment."""
157169
context = ctx.obj
@@ -161,6 +173,7 @@ def plan(ctx, environment: t.Optional[str] = None, **kwargs) -> None:
161173
@cli.command("dag")
162174
@opt.file
163175
@click.pass_context
176+
@error_handler
164177
def dag(ctx, file) -> None:
165178
"""
166179
Renders the dag using graphviz.
@@ -175,6 +188,7 @@ def dag(ctx, file) -> None:
175188
@opt.verbose
176189
@click.argument("tests", nargs=-1)
177190
@click.pass_obj
191+
@error_handler
178192
def test(obj, k, verbose, tests) -> None:
179193
"""Run model unit tests."""
180194
# Set Python unittest verbosity level
@@ -210,6 +224,7 @@ def test(obj, k, verbose, tests) -> None:
210224
@opt.end_time
211225
@opt.latest_time
212226
@click.pass_obj
227+
@error_handler
213228
def audit(
214229
obj,
215230
models: t.Tuple[str],

sqlmesh/cli/options.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
latest_time = click.option(
3131
"-l",
3232
"--latest",
33-
help="The latest time used for non incremental datasets (defaults to yesterday).",
33+
help="The latest time used for non incremental datasets (defaults to now).",
3434
)
3535

3636
expand = click.option(
@@ -41,7 +41,7 @@
4141

4242
environment = click.option(
4343
"--environment",
44-
help="The environment to diff the current context against.",
44+
help="The environment to diff the current context against. Default: prod",
4545
)
4646

4747
file = click.option(

0 commit comments

Comments
 (0)