tff-sqlmesh 0.2.0__py3-none-any.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.
@@ -0,0 +1,9 @@
1
+ """SQLMesh adapter for Transformation Fitness Functions (tff)."""
2
+
3
+ from tff.sqlmesh.loader import FitnessLoader
4
+ from tff.sqlmesh.runner import run_all_checks
5
+
6
+ __all__ = [
7
+ "FitnessLoader",
8
+ "run_all_checks",
9
+ ]
tff/sqlmesh/cli.py ADDED
@@ -0,0 +1,90 @@
1
+ """Command-line interface for tff-sqlmesh."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from tff.core.config import load_fitness_config
11
+ from tff.core.context import set_ff_config
12
+ from tff.core.report import render_lint_report
13
+ from tff.sqlmesh.runner import run_all_checks
14
+
15
+
16
+ def _parse_checks(value: str | None) -> list[str] | None:
17
+ if not value:
18
+ return None
19
+ return [part.strip() for part in value.split(",") if part.strip()]
20
+
21
+
22
+ def main(argv: list[str] | None = None) -> int:
23
+ parser = argparse.ArgumentParser(
24
+ prog="tff-sqlmesh",
25
+ description="Run SQLMesh Transformation Fitness Function (tff) checks",
26
+ )
27
+ subparsers = parser.add_subparsers(dest="command", required=True)
28
+
29
+ lint_parser = subparsers.add_parser("lint", help="Run all enabled fitness checks")
30
+ lint_parser.add_argument(
31
+ "--project",
32
+ type=Path,
33
+ default=Path.cwd(),
34
+ help="SQLMesh project root (default: current directory)",
35
+ )
36
+ lint_parser.add_argument(
37
+ "--config",
38
+ default="fitness_functions.yaml",
39
+ help="Path to fitness_functions.yaml (relative to project root)",
40
+ )
41
+ lint_parser.add_argument(
42
+ "--checks",
43
+ default=None,
44
+ help="Comma-separated checks to run (default: all enabled). "
45
+ "Use 'sqlmesh' for SQLMesh linter rules only.",
46
+ )
47
+ lint_parser.add_argument(
48
+ "--fail-level",
49
+ choices=["error", "warning"],
50
+ default="error",
51
+ help="Exit non-zero when findings at or above this severity exist",
52
+ )
53
+ lint_parser.add_argument(
54
+ "--group-by",
55
+ choices=["connascence", "model"],
56
+ default="connascence",
57
+ help="How to group violations in the report (default: connascence)",
58
+ )
59
+
60
+ args = parser.parse_args(argv)
61
+
62
+ if args.command == "lint":
63
+ logging.basicConfig(level=logging.ERROR)
64
+ project_root = args.project.resolve()
65
+ config = load_fitness_config(
66
+ project_root,
67
+ config_path=args.config,
68
+ )
69
+ set_ff_config(config)
70
+ checks = _parse_checks(args.checks)
71
+
72
+ findings, models_checked, executed_checks = run_all_checks(
73
+ project_root=project_root,
74
+ config=config,
75
+ checks=checks,
76
+ )
77
+ passed = render_lint_report(
78
+ findings,
79
+ models_checked=models_checked,
80
+ executed_checks=executed_checks,
81
+ fail_level=args.fail_level, # type: ignore[arg-type]
82
+ group_by=args.group_by, # type: ignore[arg-type]
83
+ )
84
+ return 0 if passed else 1
85
+
86
+ return 1
87
+
88
+
89
+ if __name__ == "__main__":
90
+ sys.exit(main())
tff/sqlmesh/loader.py ADDED
@@ -0,0 +1,107 @@
1
+ """SQLMesh loader that wraps tff-core package rules and registers them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import typing as t
7
+ from pathlib import Path
8
+
9
+ from sqlglot.helper import subclasses
10
+ from sqlmesh.core import constants as c
11
+ from sqlmesh.core.linter.definition import RuleSet
12
+ from sqlmesh.core.linter.rule import Rule as SqlMeshRule, RuleViolation as SqlMeshRuleViolation
13
+ from sqlmesh.core.model import Model as SqlMeshModel
14
+ from sqlmesh.core.loader import SqlMeshLoader
15
+ from sqlmesh.utils import UniqueKeyDict
16
+ from sqlmesh.utils.metaprogramming import import_python_file
17
+
18
+ from tff.core.config import load_fitness_config
19
+ from tff.core.context import set_ff_config
20
+ from tff.core.model import ModelRepresentation
21
+ from tff.core.rules import ALL_RULES as CORE_RULES
22
+
23
+
24
+ def map_sqlmesh_model(model: SqlMeshModel) -> ModelRepresentation:
25
+ columns_to_types = {
26
+ name: str(dtype)
27
+ for name, dtype in (model.columns_to_types or {}).items()
28
+ }
29
+
30
+ audits = []
31
+ if model.audits:
32
+ for audit_name, audit_args in model.audits:
33
+ audits.append((audit_name, audit_args or {}))
34
+
35
+ return ModelRepresentation(
36
+ name=str(model.name),
37
+ path=str(model._path),
38
+ dialect=model.dialect or "bigquery",
39
+ is_symbolic=bool(model.kind.is_symbolic),
40
+ is_external=bool(model.kind.name == "EXTERNAL"),
41
+ columns_to_types=columns_to_types,
42
+ depends_on={str(dep) for dep in model.depends_on},
43
+ description=model.description,
44
+ owner=model.owner,
45
+ grains=[str(g) for g in (model.grains or [])],
46
+ audits=audits,
47
+ )
48
+
49
+
50
+ def wrap_core_rule(core_rule_cls) -> type[SqlMeshRule]:
51
+ def check_model(self, model: SqlMeshModel) -> t.Optional[SqlMeshRuleViolation]:
52
+ rep = map_sqlmesh_model(model)
53
+ rule_instance = core_rule_cls()
54
+ violation = rule_instance.check_model(rep)
55
+ if violation:
56
+ return self.violation(violation.violation_msg)
57
+ return None
58
+
59
+ cls_name = core_rule_cls.__name__
60
+ attrs = {
61
+ "check_model": check_model,
62
+ "__doc__": core_rule_cls.__doc__,
63
+ }
64
+ return type(cls_name, (SqlMeshRule,), attrs)
65
+
66
+
67
+ class FitnessLoader(SqlMeshLoader):
68
+ """Load core fitness rules adapted for SQLMesh, plus optional project-local rules."""
69
+
70
+ def __init__(self, context, path: Path, **loader_kwargs: t.Any) -> None:
71
+ super().__init__(context, path)
72
+ config_path = loader_kwargs.get("fitness_functions_config", "fitness_functions.yaml")
73
+ overrides = {
74
+ key: value
75
+ for key, value in loader_kwargs.items()
76
+ if key != "fitness_functions_config"
77
+ }
78
+ ff_config = load_fitness_config(
79
+ self.config_path,
80
+ config_path=config_path,
81
+ overrides=overrides or None,
82
+ )
83
+ set_ff_config(ff_config)
84
+ self._ff_config = ff_config
85
+
86
+ def _load_linting_rules(self) -> RuleSet:
87
+ user_rules: UniqueKeyDict[str, type[SqlMeshRule]] = UniqueKeyDict("rules")
88
+
89
+ # Dynamically wrap all tff-core rules to be SQLMesh-compatible
90
+ for core_rule_cls in CORE_RULES:
91
+ wrapped = wrap_core_rule(core_rule_cls)
92
+ user_rules[wrapped.name] = wrapped
93
+
94
+ for path in self._glob_paths(
95
+ self.config_path / c.LINTER,
96
+ ignore_patterns=self.config.ignore_patterns,
97
+ extension=".py",
98
+ ):
99
+ if os.path.getsize(path):
100
+ self._track_file(path)
101
+ module = import_python_file(path, self.config_path)
102
+ _rule_exclude: t.Set[t.Type[SqlMeshRule]] = {SqlMeshRule} # type: ignore[type-abstract]
103
+ module_rules = subclasses(module.__name__, SqlMeshRule, exclude=_rule_exclude)
104
+ for user_rule in module_rules:
105
+ user_rules[user_rule.name] = user_rule
106
+
107
+ return RuleSet(user_rules.values())
tff/sqlmesh/runner.py ADDED
@@ -0,0 +1,136 @@
1
+ """Unified lint runner orchestrating SQLMesh rules and architectural checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ from sqlmesh.core.context import Context
9
+ from sqlmesh.core.linter.definition import AnnotatedRuleViolation
10
+
11
+ from tff.core.checks.custom_exclusions import collect_custom_exclusion_findings
12
+ from tff.core.checks.dependency_graph import collect_dependency_graph_findings
13
+ from tff.core.checks.layer_integrity import collect_layer_integrity_findings
14
+ from tff.core.checks.schema_contracts import collect_schema_contract_findings
15
+ from tff.core.config import FitnessFunctionsConfig, load_fitness_config
16
+ from tff.core.context import set_ff_config
17
+ from tff.core.report import LintFinding, format_message, normalize_model_name
18
+ from tff.core.utils.paths import model_path_relative
19
+ from tff.core.model import ModelRepresentation
20
+ from tff.sqlmesh.loader import FitnessLoader, map_sqlmesh_model
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ CHECK_COLLECTORS = {
25
+ "layer_integrity": lambda models, cfg: collect_layer_integrity_findings(models, cfg),
26
+ "custom_exclusions": lambda models, cfg: collect_custom_exclusion_findings(models, cfg),
27
+ "schema_contracts": lambda _models, cfg: collect_schema_contract_findings(cfg),
28
+ "dependency_graph": lambda models, cfg: collect_dependency_graph_findings(models, cfg),
29
+ }
30
+
31
+
32
+ class _SilentLinterConsole:
33
+ def show_linter_violations(self, *args, **kwargs) -> None:
34
+ return None
35
+
36
+
37
+ def collect_sqlmesh_findings(context: Context) -> list[LintFinding]:
38
+ findings: list[LintFinding] = []
39
+ silent_console = _SilentLinterConsole()
40
+
41
+ for model in context.models.values():
42
+ if model.kind.is_symbolic:
43
+ continue
44
+
45
+ linter = context._linters.get(model.project)
46
+ if not linter or not linter.enabled:
47
+ continue
48
+
49
+ _, violations = linter.lint_model(model, context, console=silent_console)
50
+ model_label = normalize_model_name(str(model.name))
51
+ for violation in violations:
52
+ if not isinstance(violation, AnnotatedRuleViolation):
53
+ continue
54
+
55
+ message = format_message(violation.violation_msg)
56
+ if message.startswith(f"{model_label}: "):
57
+ message = message[len(model_label) + 2 :]
58
+
59
+ messages = (
60
+ [part.strip() for part in message.split(";") if part.strip()]
61
+ if violation.rule.name == "sqlcomplexity"
62
+ else [message]
63
+ )
64
+
65
+ for part in messages:
66
+ findings.append(
67
+ LintFinding(
68
+ check=violation.rule.name,
69
+ severity=violation.violation_type,
70
+ model=str(model.name),
71
+ path=model_path_relative(model),
72
+ message=part,
73
+ )
74
+ )
75
+
76
+ return findings
77
+
78
+
79
+ def count_models_checked(context: Context) -> int:
80
+ return sum(
81
+ 1 for model in context.models.values() if not model.kind.is_symbolic
82
+ )
83
+
84
+
85
+ def _check_enabled(config: FitnessFunctionsConfig, check_name: str) -> bool:
86
+ check = getattr(config.checks, check_name, None)
87
+ return bool(getattr(check, "enabled", False))
88
+
89
+
90
+ def map_sqlmesh_context_models(context: Context) -> dict[str, ModelRepresentation]:
91
+ mapped = {}
92
+ for model_name, model in context.models.items():
93
+ mapped[str(model_name)] = map_sqlmesh_model(model)
94
+ return mapped
95
+
96
+
97
+ def run_all_checks(
98
+ project_root: Path | None = None,
99
+ context: Context | None = None,
100
+ config: FitnessFunctionsConfig | None = None,
101
+ checks: list[str] | None = None,
102
+ ) -> tuple[list[LintFinding], int, list[str]]:
103
+ project_root = project_root or Path.cwd()
104
+ if config is None:
105
+ config = load_fitness_config(project_root)
106
+ set_ff_config(config)
107
+
108
+ context = context or Context(
109
+ paths=[str(project_root)],
110
+ loader=FitnessLoader,
111
+ )
112
+
113
+ if checks is None:
114
+ selected = ["sqlmesh"] + [
115
+ name
116
+ for name in CHECK_COLLECTORS
117
+ if _check_enabled(config, name)
118
+ ]
119
+ else:
120
+ selected = checks
121
+
122
+ findings: list[LintFinding] = []
123
+
124
+ if "sqlmesh" in selected:
125
+ findings.extend(collect_sqlmesh_findings(context))
126
+
127
+ mapped_models = map_sqlmesh_context_models(context)
128
+
129
+ for check_name, collector in CHECK_COLLECTORS.items():
130
+ if check_name not in selected:
131
+ continue
132
+ if checks is None and not _check_enabled(config, check_name):
133
+ continue
134
+ findings.extend(collector(mapped_models, config))
135
+
136
+ return findings, count_models_checked(context), selected
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.4
2
+ Name: tff-sqlmesh
3
+ Version: 0.2.0
4
+ Summary: SQLMesh adapter for Transformation Fitness Functions (tff)
5
+ Author-email: Bart Schuijt <schuijt.bart@gmail.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: sqlmesh>=0.234.0
10
+ Requires-Dist: tff-core
11
+ Description-Content-Type: text/markdown
12
+
13
+ # sqlmesh-ff
14
+
15
+ [![PyPI version](https://img.shields.io/pypi/v/sqlmesh-ff.svg)](https://pypi.org/project/sqlmesh-ff/)
16
+ [![Python versions](https://img.shields.io/pypi/pyversions/sqlmesh-ff.svg)](https://pypi.org/project/sqlmesh-ff/)
17
+
18
+ Configurable fitness functions plugin for [SQLMesh](https://sqlmesh.com) projects.
19
+
20
+ Ships SQLMesh linter rules (classification macros, SQL complexity, metadata, naming) and architectural checks (layer integrity, custom exclusions, schema contracts, dependency graph) with a unified Rich lint report.
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ # Install from PyPI:
26
+ uv add sqlmesh-ff
27
+
28
+ # Or using pip:
29
+ pip install sqlmesh-ff
30
+ ```
31
+
32
+ ## Quick start
33
+
34
+ 1. Add `fitness_functions.yaml` to your SQLMesh project root (see [Configuration](#configuration)).
35
+ 2. Add a small `config.py` bootstrap (see [Where configuration lives](#where-configuration-lives)) — SQLMesh requires the loader as a Python class and cannot load `config.py` and `config.yaml` in the same folder.
36
+ 3. Run lint:
37
+
38
+ ```bash
39
+ sqlmesh-ff lint
40
+ ```
41
+
42
+ ## Where configuration lives
43
+
44
+ There are three layers. Only the YAML/JSON files in your project are user-editable settings.
45
+
46
+ | Layer | File | Role | You edit this? |
47
+ |-------|------|------|----------------|
48
+ | Plugin defaults | `sqlmesh_ff/config.py` (installed package) | Pydantic schema and built-in defaults (e.g. `fan_out_warn: 15`) | No — library code, never overwritten |
49
+ | SQLMesh project | `settings.yaml` | Gateways, `linter.rules`, variables, CI/CD bot | Yes — normal SQLMesh config |
50
+ | Fitness functions | `fitness_functions.yaml` | Thresholds, rule toggles, column naming/type rules, paths to JSON data | Yes — main FF config |
51
+ | Loader bootstrap | `config.py` (project root) | Loads `settings.yaml` and registers `FitnessLoader` | Rarely — ~15 lines of wiring |
52
+ | Contract data | `linter_contract_groups.json`, `linter_exclusions.json` | Repo-specific schema parity and dependency exclusions | Yes — project data |
53
+
54
+ **Merge order for fitness settings:** plugin defaults → `fitness_functions.yaml` → optional `loader_kwargs` overrides in `config.py`. Your YAML always wins over plugin defaults. The project `config.py` does not hold fitness thresholds — it only points at `fitness_functions.yaml`.
55
+
56
+ **Why `config.py` exists:** SQLMesh accepts `loader: FitnessLoader` only as a Python class, not as a YAML string. Because SQLMesh rejects having both `config.py` and `config.yaml` in one folder, projects use `settings.yaml` (SQLMesh settings) plus `config.py` (loader registration).
57
+
58
+ Example bootstrap:
59
+
60
+ ```python
61
+ from pathlib import Path
62
+
63
+ from sqlmesh.core.config import Config
64
+ from sqlmesh.utils.yaml import load as yaml_load
65
+ from sqlmesh_ff.loader import FitnessLoader
66
+
67
+ _settings = yaml_load(Path(__file__).parent / "settings.yaml")
68
+ config = Config.parse_obj(_settings).update_with({
69
+ "loader": FitnessLoader,
70
+ "loader_kwargs": {"fitness_functions_config": "fitness_functions.yaml"},
71
+ })
72
+ ```
73
+
74
+ Enable individual SQLMesh rules in `settings.yaml` under `linter.rules` / `linter.warn_rules`.
75
+
76
+ ## Configuration
77
+
78
+ Fitness function settings live in `fitness_functions.yaml` at the project root. Override the file path or individual keys via `loader_kwargs` in `config.py` (advanced — most projects only set `fitness_functions_config`).
79
+
80
+ ### Example `fitness_functions.yaml`
81
+
82
+ ```yaml
83
+ contract_groups_path: linter_contract_groups.json
84
+ exclusions_path: linter_exclusions.json
85
+
86
+ layers:
87
+ order: [sources, derived, core, marts, export]
88
+
89
+ checks:
90
+ layer_integrity: { enabled: true }
91
+ custom_exclusions: { enabled: true }
92
+ schema_contracts: { enabled: true }
93
+ dependency_graph:
94
+ enabled: true
95
+ fan_out_warn: 15
96
+ fan_out_fail: 25
97
+ fan_in_warn: 10
98
+
99
+ rules:
100
+ classification_macros:
101
+ enabled: true
102
+ skip_layers: [sources]
103
+ columns:
104
+ product_type: "@product_type\\b|@PRODUCT_TYPE\\b"
105
+ sql_complexity:
106
+ enabled: true
107
+ thresholds:
108
+ decision_points: [15, 25]
109
+ cte_count: [8, 12]
110
+ join_count: [8, 12]
111
+ line_count: [250, 400]
112
+ mart_naming:
113
+ enabled: true
114
+ layer_name: marts
115
+ rule: prefix_with_subdirectory
116
+ column_names:
117
+ enabled: true
118
+ replacements: {}
119
+ column_types:
120
+ enabled: true
121
+ rules: []
122
+ equivalent_types:
123
+ text: [text, varchar]
124
+ metadata:
125
+ owner: true
126
+ description: true
127
+ grain: true
128
+ filename_equals_modelname:
129
+ enabled: true
130
+ ```
131
+
132
+ ### Project-specific JSON
133
+
134
+ Keep repo-specific contract and exclusion data in your project:
135
+
136
+ - `linter_contract_groups.json` — cross-model schema parity groups
137
+ - `linter_exclusions.json` — blocked dependency patterns and allowed exceptions
138
+
139
+ Reference their paths from `fitness_functions.yaml`. The plugin ships generic engines only; examples live in this README.
140
+
141
+ ### Rule name mapping
142
+
143
+ SQLMesh uses lowercase class names in `linter.rules`:
144
+
145
+ | Config key | SQLMesh rule name |
146
+ |------------|-------------------|
147
+ | `classification_macros` | `classificationmacros` |
148
+ | `sql_complexity` | `sqlcomplexity` |
149
+ | `mart_naming` | `martmodelnamingconvention` |
150
+ | `column_names` | `columnnames` |
151
+ | `column_types` | `columntypes` |
152
+ | `metadata.owner` | `nomissingowner` |
153
+ | `metadata.description` | `nomissingdescription` |
154
+ | `metadata.grain` | `nomissinggrain` |
155
+ | `filename_equals_modelname` | `filenameequalsmodelname` |
156
+
157
+ ## CLI
158
+
159
+ ```
160
+ sqlmesh-ff lint [--project PATH] [--config PATH] [--checks CHECK,...] [--fail-level error|warning] [--group-by connascence|model]
161
+ ```
162
+
163
+ - **Default:** all enabled checks plus SQLMesh linter rules
164
+ - **`--checks layer_integrity,custom_exclusions`:** run subset (for pre-push hooks)
165
+ - **`--fail-level warning`:** treat warnings as failures
166
+ - **`--group-by connascence|model`:** change how violations are grouped in the report (default: `connascence`)
167
+
168
+ ## Integration example
169
+
170
+ Example overrides. `api_request` should always be named `api_call`. `_id` columns should always be of type `text` and `is_` columns should always be of type `boolean`.
171
+
172
+ ```yaml
173
+ column_names:
174
+ replacements:
175
+ api_request: api_call
176
+ column_types:
177
+ rules:
178
+ - name: id_is_text
179
+ pattern: "_id$"
180
+ data_type: text
181
+ - name: boolean
182
+ pattern: "^is_"
183
+ data_type: boolean
184
+ ```
185
+
186
+ ## Examples
187
+
188
+ A complete, runnable example project showcasing the configuration of `sqlmesh-ff` rules, exclusions, contracts, and a continuous integration workflow is located in the [examples/](file:///Users/bartschuijt/git/sqlmesh-ff/examples/) directory.
189
+
190
+ To run the linter against the example project locally, run:
191
+ ```bash
192
+ sqlmesh-ff lint --project examples/minimal-sqlmesh-project
193
+ ```
194
+
195
+ See [examples/minimal-sqlmesh-project/fitness_functions.yaml](file:///Users/bartschuijt/git/sqlmesh-ff/examples/minimal-sqlmesh-project/fitness_functions.yaml) to inspect the configured rules.
196
+
197
+ ## Development
198
+
199
+ Initialize your local environment and configure the Git pre-push hook:
200
+ ```bash
201
+ make init
202
+ ```
203
+
204
+ Run linter, tests, or check diff coverage:
205
+ ```bash
206
+ make lint
207
+ make test
208
+ make coverage
209
+ ```
210
+
211
+ ### Releases and PR titles
212
+
213
+ Releases are automated with [release-please](https://github.com/googleapis/release-please) on merges to `main`. Use [Conventional Commits](https://www.conventionalcommits.org/) in PR titles so changelog entries and semver bumps are correct.
214
+
215
+ PR titles must start with a type prefix, for example:
216
+
217
+ - `feat: add dependency graph fan-in check`
218
+ - `fix: remove unused import in loader tests`
219
+ - `docs: document fitness_functions.yaml merge order`
220
+ - `ci: add release-please workflow`
221
+
222
+ Supported types include `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, and `chore`. The PR title check in CI enforces this format.
@@ -0,0 +1,9 @@
1
+ tff/sqlmesh/__init__.py,sha256=AYeEn8DxjOf6tyuvdst2vqh8htimcFIrmsqOelp4O9U,216
2
+ tff/sqlmesh/cli.py,sha256=4_7QuKiYTHhpOX-_bAnLox9PQScArreg6aerv4X34t4,2720
3
+ tff/sqlmesh/loader.py,sha256=Ohg-SAACYChphe6hvdRTvlDzMu7vCfF03Tf3OID9sRg,3921
4
+ tff/sqlmesh/runner.py,sha256=w5M_9WCLBVrDFJWPR9wcQcHW9ECv0IqiN2QTXljVas4,4681
5
+ tff_sqlmesh-0.2.0.dist-info/METADATA,sha256=8mNHWXy4v0uUmT-PCcOXJnnTyUMhSAoOrk4L9eEnc38,7944
6
+ tff_sqlmesh-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ tff_sqlmesh-0.2.0.dist-info/entry_points.txt,sha256=lGH_81NMPZjHJnPOex_2Vu74wIKi1Wq8aFKHmOLN4Wk,53
8
+ tff_sqlmesh-0.2.0.dist-info/licenses/LICENSE,sha256=qJDdrCs67OvVJ8jlstmR3HCqsECKLxOJWsjys5K8tNg,1069
9
+ tff_sqlmesh-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tff-sqlmesh = tff.sqlmesh.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bart Schuijt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.