checkup 0.2.10__tar.gz → 0.4.0__tar.gz

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 (53) hide show
  1. {checkup-0.2.10 → checkup-0.4.0}/PKG-INFO +3 -1
  2. {checkup-0.2.10 → checkup-0.4.0}/pyproject.toml +9 -1
  3. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/__init__.py +10 -4
  4. checkup-0.4.0/src/checkup/cli/__init__.py +18 -0
  5. checkup-0.4.0/src/checkup/cli/commands/__init__.py +15 -0
  6. checkup-0.4.0/src/checkup/cli/commands/config.py +23 -0
  7. checkup-0.4.0/src/checkup/cli/commands/init.py +23 -0
  8. checkup-0.4.0/src/checkup/cli/commands/run.py +58 -0
  9. checkup-0.4.0/src/checkup/cli/commands/schema.py +29 -0
  10. checkup-0.4.0/src/checkup/cli/config_wizard/__init__.py +8 -0
  11. checkup-0.4.0/src/checkup/cli/config_wizard/_common.py +133 -0
  12. checkup-0.4.0/src/checkup/cli/config_wizard/create.py +134 -0
  13. checkup-0.4.0/src/checkup/cli/config_wizard/edit.py +200 -0
  14. checkup-0.4.0/src/checkup/cli/executor.py +152 -0
  15. checkup-0.4.0/src/checkup/cli/utils.py +75 -0
  16. checkup-0.4.0/src/checkup/configuration/__init__.py +18 -0
  17. checkup-0.4.0/src/checkup/configuration/env.py +149 -0
  18. checkup-0.4.0/src/checkup/configuration/io.py +219 -0
  19. checkup-0.4.0/src/checkup/configuration/models.py +44 -0
  20. checkup-0.4.0/src/checkup/configuration/schema.py +213 -0
  21. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/errors.py +9 -10
  22. checkup-0.4.0/src/checkup/executor/__init__.py +6 -0
  23. checkup-0.4.0/src/checkup/executor/batch_executors.py +169 -0
  24. checkup-0.4.0/src/checkup/executor/metric_calculator.py +213 -0
  25. checkup-0.4.0/src/checkup/executor/provider_executor.py +45 -0
  26. checkup-0.4.0/src/checkup/executor/state.py +101 -0
  27. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/graph.py +1 -1
  28. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/hub.py +38 -60
  29. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/materializers/__init__.py +5 -5
  30. checkup-0.4.0/src/checkup/materializers/base.py +108 -0
  31. checkup-0.4.0/src/checkup/materializers/console.py +92 -0
  32. checkup-0.4.0/src/checkup/materializers/csv_file.py +37 -0
  33. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/materializers/database.py +17 -19
  34. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/materializers/html_report.py +10 -8
  35. checkup-0.4.0/src/checkup/measurement.py +96 -0
  36. checkup-0.4.0/src/checkup/metric.py +118 -0
  37. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/provider.py +0 -13
  38. checkup-0.4.0/src/checkup/providers/tags.py +26 -0
  39. checkup-0.4.0/src/checkup/registry/__init__.py +13 -0
  40. checkup-0.4.0/src/checkup/registry/discovery.py +190 -0
  41. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/templates/metrics_report.html +22 -22
  42. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/validators.py +15 -13
  43. checkup-0.2.10/src/checkup/executor.py +0 -535
  44. checkup-0.2.10/src/checkup/materializers/base.py +0 -104
  45. checkup-0.2.10/src/checkup/materializers/console.py +0 -49
  46. checkup-0.2.10/src/checkup/materializers/csv_file.py +0 -35
  47. checkup-0.2.10/src/checkup/metric.py +0 -74
  48. checkup-0.2.10/src/checkup/providers/tags.py +0 -40
  49. {checkup-0.2.10 → checkup-0.4.0}/README.md +0 -0
  50. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/config.py +0 -0
  51. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/providers/__init__.py +0 -0
  52. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/types.py +0 -0
  53. {checkup-0.2.10 → checkup-0.4.0}/src/checkup/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: checkup
3
- Version: 0.2.10
3
+ Version: 0.4.0
4
4
  Summary: Computational governance framework for measuring data product health
5
5
  Author: Jan Vanbuel
6
6
  Author-email: Jan Vanbuel <jan.vanbuel@ond.vlaanderen.be>
@@ -10,8 +10,10 @@ Classifier: Programming Language :: Python :: 3.13
10
10
  Requires-Dist: jinja2>=3.1.6
11
11
  Requires-Dist: pydantic>=2.11.7
12
12
  Requires-Dist: pyyaml>=6.0
13
+ Requires-Dist: questionary>=2.0
13
14
  Requires-Dist: rich>=13.0
14
15
  Requires-Dist: sqlalchemy>=2.0
16
+ Requires-Dist: typer>=0.24
15
17
  Requires-Python: >=3.12
16
18
  Description-Content-Type: text/markdown
17
19
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "checkup"
3
- version = "0.2.10"
3
+ version = "0.4.0"
4
4
  description = "Computational governance framework for measuring data product health"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Jan Vanbuel", email = "jan.vanbuel@ond.vlaanderen.be" }]
@@ -14,13 +14,21 @@ dependencies = [
14
14
  "jinja2>=3.1.6",
15
15
  "pydantic>=2.11.7",
16
16
  "pyyaml>=6.0",
17
+ "questionary>=2.0",
17
18
  "rich>=13.0",
18
19
  "sqlalchemy>=2.0",
20
+ "typer>=0.24",
19
21
  ]
20
22
 
21
23
  [project.scripts]
22
24
  checkup = "checkup:main"
23
25
 
26
+ [project.entry-points."checkup.materializers"]
27
+ console = "checkup.materializers:ConsoleMaterializer"
28
+ csv = "checkup.materializers:CSVMaterializer"
29
+ html = "checkup.materializers:HTMLMaterializer"
30
+ sqlalchemy = "checkup.materializers:SQLAlchemyMaterializer"
31
+
24
32
  [build-system]
25
33
  requires = ["uv_build>=0.8.13,<0.9.0"]
26
34
  build-backend = "uv_build"
@@ -1,4 +1,4 @@
1
- """Checkup - Extensible metrics calculation framework."""
1
+ """Checkup - Computational governance framework for measuring data product health."""
2
2
 
3
3
  from checkup.errors import (
4
4
  DuplicateMetricNameError,
@@ -14,17 +14,23 @@ from checkup.materializers import (
14
14
  Materializer,
15
15
  SQLAlchemyMaterializer,
16
16
  )
17
+ from checkup.measurement import Measurement, Measurements
17
18
  from checkup.metric import ExecutorType, Metric
18
19
  from checkup.provider import Provider
19
20
  from checkup.providers.tags import TagProvider
20
21
  from checkup.types import Context
21
22
  from checkup.utils import suppress_subprocess_output
22
23
 
24
+ # Rebuild models to resolve forward references after all classes are imported
25
+ Measurement.model_rebuild()
26
+
23
27
  __all__ = [
24
28
  # Core
25
29
  "CheckHub",
26
30
  "MeasurementResult",
27
31
  "Metric",
32
+ "Measurement",
33
+ "Measurements",
28
34
  "ExecutorType",
29
35
  "Provider",
30
36
  "TagProvider",
@@ -48,6 +54,6 @@ __all__ = [
48
54
 
49
55
 
50
56
  def main() -> None:
51
- """CLI entry point."""
52
- print("Checkup metrics framework")
53
- print("Import CheckHub to get started")
57
+ from checkup.cli import app
58
+
59
+ app()
@@ -0,0 +1,18 @@
1
+ """
2
+ Checkup CLI application.
3
+ """
4
+
5
+ import typer
6
+
7
+ from checkup.cli.commands import config, init, run, schema
8
+
9
+ app = typer.Typer(
10
+ name="checkup",
11
+ help="CheckUp - Computational governance framework for measuring data product health",
12
+ no_args_is_help=True,
13
+ )
14
+
15
+ app.command()(run)
16
+ app.command()(init)
17
+ app.command()(config)
18
+ app.command()(schema)
@@ -0,0 +1,15 @@
1
+ """
2
+ CLI commands.
3
+ """
4
+
5
+ from checkup.cli.commands.config import config
6
+ from checkup.cli.commands.init import init
7
+ from checkup.cli.commands.run import run
8
+ from checkup.cli.commands.schema import schema
9
+
10
+ __all__ = [
11
+ "config",
12
+ "init",
13
+ "run",
14
+ "schema",
15
+ ]
@@ -0,0 +1,23 @@
1
+ """
2
+ Config command. Modify an existing config file.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from checkup.cli.config_wizard import edit_config
11
+
12
+
13
+ def config(
14
+ config_path: Annotated[
15
+ Path | None,
16
+ typer.Option("--config", "-c", help="Path to config file"),
17
+ ] = None,
18
+ ) -> None:
19
+ """
20
+ Modify the checkup.yaml config file.
21
+ """
22
+
23
+ edit_config(config_path=config_path)
@@ -0,0 +1,23 @@
1
+ """
2
+ Init command. Create a new config file.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from checkup.cli.config_wizard import create_config
11
+
12
+
13
+ def init(
14
+ output: Annotated[
15
+ Path | None,
16
+ typer.Option("--output", "-o", help="Output path for config file"),
17
+ ] = None,
18
+ ) -> None:
19
+ """
20
+ Create a checkup.yaml config file.
21
+ """
22
+
23
+ create_config(output_path=output)
@@ -0,0 +1,58 @@
1
+ """
2
+ Run command. Run metrics and materialize results.
3
+ """
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+ from checkup.cli.executor import execute_checkup
12
+ from checkup.cli.utils import apply_cli_overrides
13
+ from checkup.configuration import load_config
14
+
15
+
16
+ def run(
17
+ config: Annotated[
18
+ Path | None,
19
+ typer.Option("--config", "-c", help="Path to config file"),
20
+ ] = None,
21
+ tag: Annotated[
22
+ list[str] | None,
23
+ typer.Option("--tag", "-t", help="Set tags (key=value)"),
24
+ ] = None,
25
+ provider: Annotated[
26
+ list[str] | None,
27
+ typer.Option("--provider", "-p", help="Set providers (name or name:key=value)"),
28
+ ] = None,
29
+ metric: Annotated[
30
+ list[str] | None,
31
+ typer.Option("--metric", "-m", help="Set metrics (name or name:key=value)"),
32
+ ] = None,
33
+ materializer: Annotated[
34
+ str | None,
35
+ typer.Option(
36
+ "--materializer", help="Set materializer (type or type:key=value)"
37
+ ),
38
+ ] = None,
39
+ dry_run: Annotated[
40
+ bool,
41
+ typer.Option("--dry-run", help="Don't materialize, just print"),
42
+ ] = False,
43
+ verbose: Annotated[
44
+ bool,
45
+ typer.Option("--verbose", "-v", help="Verbose output"),
46
+ ] = False,
47
+ ) -> None:
48
+ """
49
+ Run metrics and materialize results.
50
+ """
51
+
52
+ if verbose:
53
+ logging.basicConfig(level=logging.DEBUG)
54
+
55
+ cfg = load_config(config_path=config)
56
+ cfg = apply_cli_overrides(cfg, tag, provider, metric)
57
+
58
+ execute_checkup(cfg, materializer="console" if dry_run else materializer)
@@ -0,0 +1,29 @@
1
+ """
2
+ Schema command. Generate JSON schema for checkup.yaml.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ from checkup.configuration.io import SCHEMA_FILENAME
12
+ from checkup.configuration.schema import write_schema
13
+
14
+ console = Console()
15
+
16
+
17
+ def schema(
18
+ output: Annotated[
19
+ Path | None,
20
+ typer.Option("--output", "-o", help="Output path for schema file"),
21
+ ] = None,
22
+ ) -> None:
23
+ """
24
+ Generate JSON schema for checkup.yaml.
25
+ """
26
+
27
+ path = output or Path.cwd() / SCHEMA_FILENAME
28
+ write_schema(path)
29
+ console.print(f"[green]Schema written to {path}[/green]")
@@ -0,0 +1,8 @@
1
+ """
2
+ Interactive config generation and editing.
3
+ """
4
+
5
+ from .create import create_config
6
+ from .edit import edit_config
7
+
8
+ __all__ = ["create_config", "edit_config"]
@@ -0,0 +1,133 @@
1
+ """
2
+ Shared utilities for the config wizard.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ import yaml
9
+ from rich.console import Console
10
+
11
+ from checkup.configuration.io import SCHEMA_FILENAME
12
+ from checkup.configuration.models import CheckupConfig
13
+ from checkup.configuration.schema import write_schema
14
+
15
+ if TYPE_CHECKING:
16
+ import questionary
17
+
18
+ from checkup.registry import PluginRegistry
19
+
20
+ console = Console(markup=False, highlight=False)
21
+
22
+
23
+ def get_questionary() -> "questionary":
24
+ """
25
+ Lazy import questionary to avoid slow startup.
26
+ """
27
+
28
+ import questionary
29
+
30
+ return questionary
31
+
32
+
33
+ def select_multiple(
34
+ available: list[str],
35
+ selected: list[str],
36
+ item_name: str,
37
+ ) -> list[str]:
38
+ """
39
+ Select multiple items with fuzzy search.
40
+ """
41
+
42
+ if not available:
43
+ console.print(
44
+ f"[yellow]No {item_name} found. Install checkup plugins.[/yellow]",
45
+ markup=True,
46
+ )
47
+ return []
48
+
49
+ selected_count = len([s for s in selected if s in available])
50
+ console.print(f"Currently selected: {selected_count}/{len(available)} {item_name}")
51
+
52
+ choices = [
53
+ get_questionary().Choice(name, checked=name in selected)
54
+ for name in sorted(available)
55
+ ]
56
+
57
+ result = (
58
+ get_questionary()
59
+ .checkbox(
60
+ f"Select {item_name}:",
61
+ choices=choices,
62
+ use_search_filter=True,
63
+ use_jk_keys=False,
64
+ instruction="(↑↓ navigate, space toggle, type to filter, enter confirm)",
65
+ )
66
+ .ask()
67
+ )
68
+
69
+ return result or []
70
+
71
+
72
+ def select_materializer(current: str | None, registry: "PluginRegistry") -> str | None:
73
+ """
74
+ Select materializer interactively.
75
+ """
76
+
77
+ available = registry.list_materializer_names()
78
+
79
+ if not available:
80
+ return "console"
81
+
82
+ default = (
83
+ current if current in available else (available[0] if available else "console")
84
+ )
85
+
86
+ return (
87
+ get_questionary()
88
+ .select(
89
+ "Materializer type:",
90
+ choices=available,
91
+ default=default,
92
+ use_search_filter=True,
93
+ use_jk_keys=False,
94
+ )
95
+ .ask()
96
+ )
97
+
98
+
99
+ def write_config(path: Path, config: CheckupConfig) -> None:
100
+ """
101
+ Write config to file with empty lines between sections.
102
+ Also generates the JSON schema file.
103
+ """
104
+
105
+ console.print(f"\n[bold]Writing config to {path}[/bold]", markup=True)
106
+
107
+ data = config.model_dump(exclude_defaults=True)
108
+
109
+ lines = [f"# yaml-language-server: $schema={SCHEMA_FILENAME}"]
110
+
111
+ # Write sections separately with blank lines between them
112
+ for key in CheckupConfig.model_fields:
113
+ if key not in data or not data[key]:
114
+ continue
115
+ lines.append("")
116
+ lines.append(
117
+ yaml.dump(
118
+ {key: data[key]}, default_flow_style=False, sort_keys=False
119
+ ).rstrip()
120
+ )
121
+
122
+ with open(path, "w") as f:
123
+ f.write("\n".join(lines))
124
+ f.write("\n")
125
+
126
+ schema_path = path.parent / SCHEMA_FILENAME
127
+ write_schema(schema_path)
128
+ console.print(f"[green]Schema written to {schema_path}[/green]", markup=True)
129
+
130
+ console.print("[green]Done![/green]", markup=True)
131
+ console.print(
132
+ "Run [bold]checkup run[/bold] to test your configuration.", markup=True
133
+ )
@@ -0,0 +1,134 @@
1
+ """
2
+ Interactive config creation.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING
7
+
8
+ from checkup.configuration.io import CONFIG_FILENAME
9
+ from checkup.configuration.models import (
10
+ CheckupConfig,
11
+ MaterializerConfig,
12
+ MetricConfig,
13
+ ProviderConfig,
14
+ )
15
+ from checkup.registry import get_registry
16
+
17
+ from ._common import (
18
+ console,
19
+ get_questionary,
20
+ select_materializer,
21
+ select_multiple,
22
+ write_config,
23
+ )
24
+
25
+ if TYPE_CHECKING:
26
+ from checkup.registry import PluginRegistry
27
+
28
+
29
+ def create_config(output_path: Path | None = None) -> None:
30
+ """
31
+ Interactively create a new config file.
32
+ """
33
+
34
+ path = output_path or Path.cwd() / CONFIG_FILENAME
35
+
36
+ if not _confirm_overwrite(path):
37
+ return
38
+
39
+ registry = get_registry()
40
+ config = _build_config(registry)
41
+
42
+ if config is None:
43
+ console.print("\n[yellow]Cancelled.[/yellow]", markup=True)
44
+ return
45
+
46
+ write_config(path, config)
47
+
48
+
49
+ def _confirm_overwrite(path: Path) -> bool:
50
+ if not path.exists():
51
+ return True
52
+
53
+ overwrite = (
54
+ get_questionary().confirm(f"{path} exists. Overwrite?", default=False).ask()
55
+ )
56
+ if overwrite is None:
57
+ console.print("\n[yellow]Cancelled.[/yellow]", markup=True)
58
+ return False
59
+ return overwrite
60
+
61
+
62
+ def _build_config(registry: "PluginRegistry") -> CheckupConfig | None:
63
+ tags = _prompt_tags()
64
+ if tags is None:
65
+ return None
66
+
67
+ provider_names = _prompt_providers(registry)
68
+ if provider_names is None:
69
+ return None
70
+
71
+ metric_names = _prompt_metrics(registry, provider_names)
72
+ if metric_names is None:
73
+ return None
74
+
75
+ mat = _prompt_materializer(registry)
76
+ if mat is None:
77
+ return None
78
+
79
+ return CheckupConfig(
80
+ tags=tags,
81
+ providers=[ProviderConfig(name=p) for p in provider_names],
82
+ metrics=[MetricConfig(type=m) for m in metric_names],
83
+ materializer=MaterializerConfig(type=mat) if mat != "console" else None,
84
+ )
85
+
86
+
87
+ def _prompt_tags() -> dict[str, str] | None:
88
+ console.print("\n[bold]Tags[/bold]", markup=True)
89
+ console.print(
90
+ "Tags identify your data product (e.g., product=my-product, team=analytics)"
91
+ )
92
+ return _collect_tags()
93
+
94
+
95
+ def _prompt_providers(registry: "PluginRegistry") -> list[str] | None:
96
+ console.print("\n[bold]Providers[/bold]", markup=True)
97
+ return select_multiple(registry.list_provider_names(), [], "providers")
98
+
99
+
100
+ def _prompt_metrics(
101
+ registry: "PluginRegistry", provider_names: list[str]
102
+ ) -> list[str] | None:
103
+ console.print("\n[bold]Metrics[/bold]", markup=True)
104
+ with console.status("Loading metrics..."):
105
+ available_metrics = registry.list_compatible_metric_names(provider_names)
106
+ return select_multiple(available_metrics, [], "metrics")
107
+
108
+
109
+ def _prompt_materializer(registry: "PluginRegistry") -> str | None:
110
+ console.print("\n[bold]Materializer[/bold]", markup=True)
111
+ return select_materializer(None, registry)
112
+
113
+
114
+ def _collect_tags() -> dict[str, str] | None:
115
+ tags: dict[str, str] = {}
116
+
117
+ while True:
118
+ tag: str = (
119
+ get_questionary()
120
+ .text(
121
+ "Add tag (key=value, or empty to finish):",
122
+ )
123
+ .ask()
124
+ )
125
+
126
+ if tag is None:
127
+ return None
128
+ if not tag:
129
+ break
130
+ if "=" in tag:
131
+ key, value = tag.split("=", 1)
132
+ tags[key.strip()] = value.strip()
133
+
134
+ return tags