checkup 0.6.0__tar.gz → 0.7.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 (50) hide show
  1. {checkup-0.6.0 → checkup-0.7.0}/PKG-INFO +6 -1
  2. {checkup-0.6.0 → checkup-0.7.0}/README.md +5 -0
  3. {checkup-0.6.0 → checkup-0.7.0}/pyproject.toml +28 -32
  4. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/commands/run.py +17 -0
  5. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/executor.py +25 -0
  6. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/utils.py +2 -0
  7. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/configuration/io.py +2 -0
  8. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/configuration/models.py +2 -6
  9. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/configuration/schema.py +8 -0
  10. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/metric.py +2 -1
  11. checkup-0.7.0/src/checkup/selection.py +103 -0
  12. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/__init__.py +0 -0
  13. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/__init__.py +0 -0
  14. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/commands/__init__.py +0 -0
  15. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/commands/config.py +0 -0
  16. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/commands/init.py +0 -0
  17. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/commands/plugins.py +0 -0
  18. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/commands/schema.py +0 -0
  19. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/config_wizard/__init__.py +0 -0
  20. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/config_wizard/_common.py +0 -0
  21. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/config_wizard/create.py +0 -0
  22. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/cli/config_wizard/edit.py +0 -0
  23. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/config.py +0 -0
  24. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/configuration/__init__.py +0 -0
  25. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/configuration/env.py +0 -0
  26. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/errors.py +0 -0
  27. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/executor/__init__.py +0 -0
  28. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/executor/batch_executors.py +0 -0
  29. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/executor/metric_calculator.py +0 -0
  30. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/executor/provider_executor.py +0 -0
  31. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/executor/state.py +0 -0
  32. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/graph.py +0 -0
  33. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/hub.py +0 -0
  34. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/materializers/__init__.py +0 -0
  35. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/materializers/base.py +0 -0
  36. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/materializers/console.py +0 -0
  37. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/materializers/csv_file.py +0 -0
  38. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/materializers/database.py +0 -0
  39. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/materializers/html_report.py +0 -0
  40. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/materializers/markdown.py +0 -0
  41. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/measurement.py +0 -0
  42. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/provider.py +0 -0
  43. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/providers/__init__.py +0 -0
  44. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/providers/tags.py +0 -0
  45. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/registry/__init__.py +0 -0
  46. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/registry/discovery.py +0 -0
  47. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/templates/metrics_report.html +0 -0
  48. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/types.py +0 -0
  49. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/utils.py +0 -0
  50. {checkup-0.6.0 → checkup-0.7.0}/src/checkup/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: checkup
3
- Version: 0.6.0
3
+ Version: 0.7.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>
@@ -32,6 +32,11 @@ Description-Content-Type: text/markdown
32
32
  <a href="https://pypi.org/project/checkup/"><img src="https://img.shields.io/pypi/pyversions/checkup" alt="Python versions" /></a>
33
33
  </p>
34
34
 
35
+ <p align="center">
36
+ <!-- <img src="https://raw.githubusercontent.com/datamindedbe/checkup/main/images/demo.gif" alt="CheckUp demo" width="100%" /> -->
37
+ <img src="./images/demo.gif" alt="CheckUp demo" width="100%" />
38
+ </p>
39
+
35
40
  ## Plugins
36
41
 
37
42
  Checkup uses plugins to provide metrics for different tools and platforms.
@@ -10,6 +10,11 @@
10
10
  <a href="https://pypi.org/project/checkup/"><img src="https://img.shields.io/pypi/pyversions/checkup" alt="Python versions" /></a>
11
11
  </p>
12
12
 
13
+ <p align="center">
14
+ <!-- <img src="https://raw.githubusercontent.com/datamindedbe/checkup/main/images/demo.gif" alt="CheckUp demo" width="100%" /> -->
15
+ <img src="./images/demo.gif" alt="CheckUp demo" width="100%" />
16
+ </p>
17
+
13
18
  ## Plugins
14
19
 
15
20
  Checkup uses plugins to provide metrics for different tools and platforms.
@@ -1,23 +1,23 @@
1
1
  [project]
2
2
  name = "checkup"
3
- version = "0.6.0"
3
+ version = "0.7.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" }]
7
7
  requires-python = ">=3.12"
8
8
  classifiers = [
9
- "Programming Language :: Python :: 3",
10
- "Programming Language :: Python :: 3.12",
11
- "Programming Language :: Python :: 3.13",
9
+ "Programming Language :: Python :: 3",
10
+ "Programming Language :: Python :: 3.12",
11
+ "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "jinja2>=3.1.6",
15
- "pydantic>=2.11.7",
16
- "pyyaml>=6.0",
17
- "questionary>=2.0",
18
- "rich>=13.0",
19
- "sqlalchemy>=2.0",
20
- "typer>=0.24",
14
+ "jinja2>=3.1.6",
15
+ "pydantic>=2.11.7",
16
+ "pyyaml>=6.0",
17
+ "questionary>=2.0",
18
+ "rich>=13.0",
19
+ "sqlalchemy>=2.0",
20
+ "typer>=0.24",
21
21
  ]
22
22
 
23
23
  [project.urls]
@@ -41,23 +41,19 @@ build-backend = "uv_build"
41
41
 
42
42
  [tool.uv.workspace]
43
43
  members = [
44
- "plugins/checkup-git",
45
- "plugins/checkup-dbt",
46
- "plugins/checkup-python",
47
- "plugins/checkup-conveyor",
48
- "plugins/checkup-gitlab",
49
- "plugins/checkup-bitbucket",
50
- "plugins/checkup-github",
51
- "plugins/checkup-airflow",
44
+ "plugins/checkup-git",
45
+ "plugins/checkup-dbt",
46
+ "plugins/checkup-python",
47
+ "plugins/checkup-conveyor",
48
+ "plugins/checkup-gitlab",
49
+ "plugins/checkup-bitbucket",
50
+ "plugins/checkup-github",
51
+ "plugins/checkup-airflow",
52
52
  ]
53
53
 
54
54
  [dependency-groups]
55
55
  dev = ["pytest>=9.0.2", "pytest-cov>=7.0.0", "ruff>=0.8.6", "prek>=0.3.0"]
56
- docs = [
57
- "mkdocs>=1.6.0",
58
- "mkdocs-material>=9.5.0",
59
- "pymdown-extensions>=10.0",
60
- ]
56
+ docs = ["mkdocs>=1.6.0", "mkdocs-material>=9.5.0", "pymdown-extensions>=10.0"]
61
57
 
62
58
  [tool.pytest.ini_options]
63
59
  pythonpath = [".", "tests"]
@@ -69,16 +65,16 @@ line-length = 88
69
65
 
70
66
  [tool.ruff.lint]
71
67
  select = [
72
- "E", # pycodestyle errors
73
- "W", # pycodestyle warnings
74
- "F", # Pyflakes
75
- "I", # isort
76
- "B", # flake8-bugbear
77
- "C4", # flake8-comprehensions
78
- "UP", # pyupgrade
68
+ "E", # pycodestyle errors
69
+ "W", # pycodestyle warnings
70
+ "F", # Pyflakes
71
+ "I", # isort
72
+ "B", # flake8-bugbear
73
+ "C4", # flake8-comprehensions
74
+ "UP", # pyupgrade
79
75
  ]
80
76
  ignore = [
81
- "E501", # line too long (handled by formatter)
77
+ "E501", # line too long (handled by formatter)
82
78
  ]
83
79
 
84
80
  [tool.ruff.format]
@@ -36,6 +36,21 @@ def run(
36
36
  "--materializer", help="Set materializer (type or type:key=value)"
37
37
  ),
38
38
  ] = None,
39
+ select: Annotated[
40
+ str | None,
41
+ typer.Option(
42
+ "--select",
43
+ "-s",
44
+ help="Only materialize matching metrics ('method:pattern'). Methods: name (default), tag, type.",
45
+ ),
46
+ ] = None,
47
+ exclude: Annotated[
48
+ str | None,
49
+ typer.Option(
50
+ "--exclude",
51
+ help="Exclude matching metrics from materialization. Same format as --select.",
52
+ ),
53
+ ] = None,
39
54
  dry_run: Annotated[
40
55
  bool,
41
56
  typer.Option("--dry-run", help="Don't materialize, just print"),
@@ -71,4 +86,6 @@ def run(
71
86
  materializer="console" if dry_run else materializer,
72
87
  multiprocessing=multiprocessing,
73
88
  quiet=quiet,
89
+ select=select,
90
+ exclude=exclude,
74
91
  )
@@ -12,6 +12,7 @@ from checkup.hub import CheckHub
12
12
  from checkup.materializers import ConsoleMaterializer
13
13
  from checkup.providers.tags import TagProvider
14
14
  from checkup.registry import get_registry
15
+ from checkup.selection import select_metrics
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from checkup.materializers import Materializer
@@ -28,6 +29,8 @@ def execute_checkup(
28
29
  materializer: str | None = None,
29
30
  multiprocessing: bool = True,
30
31
  quiet: bool = False,
32
+ select: str | None = None,
33
+ exclude: str | None = None,
31
34
  ) -> None:
32
35
  """
33
36
  Execute checkup with the given configuration.
@@ -37,6 +40,8 @@ def execute_checkup(
37
40
  materializer: Override materializer type (e.g., "console")
38
41
  multiprocessing: If False, run sequentially without subprocesses
39
42
  quiet: If True, send status/errors to stderr so stdout holds only the materializer output.
43
+ select: Selector for which metrics to materialize.
44
+ exclude: Selector for metrics to exclude from materialization.
40
45
  """
41
46
 
42
47
  out = Console(stderr=True) if quiet else console
@@ -55,6 +60,10 @@ def execute_checkup(
55
60
 
56
61
  materializer = _resolve_materializer(config, registry, materializer, out)
57
62
 
63
+ select = select if select is not None else config.select
64
+ exclude = exclude if exclude is not None else config.exclude
65
+ type_by_name = {mc.instance_name: mc.type for mc in config.metrics}
66
+
58
67
  out.print(f"[blue]Running {len(metrics)} metrics...[/blue]")
59
68
 
60
69
  result = (
@@ -68,6 +77,22 @@ def execute_checkup(
68
77
  for _, error in result.errors:
69
78
  out.print(f"[red]Error: {error}[/red]")
70
79
 
80
+ if select or exclude:
81
+ selected = select_metrics(
82
+ metrics,
83
+ select=select,
84
+ exclude=exclude,
85
+ type_resolver=lambda metric: type_by_name.get(metric.name),
86
+ )
87
+ result.measurements = [
88
+ m for m in result.measurements if m.metric.name in selected
89
+ ]
90
+ result.direct_metric_names = result.direct_metric_names & selected
91
+
92
+ console.print(
93
+ f"[blue]Materializing {len(selected)} selected of {len(metrics)} total metrics[/blue]"
94
+ )
95
+
71
96
  result.materialize(materializer)
72
97
 
73
98
 
@@ -51,6 +51,8 @@ def apply_cli_overrides(
51
51
  providers=new_providers,
52
52
  metrics=new_metrics,
53
53
  materializer=cfg.materializer,
54
+ select=cfg.select,
55
+ exclude=cfg.exclude,
54
56
  )
55
57
 
56
58
 
@@ -216,4 +216,6 @@ def load_config(
216
216
  providers=parse_providers(raw.get("providers")),
217
217
  metrics=parse_metrics(raw.get("metrics")),
218
218
  materializer=parse_materializer(raw.get("materializer")),
219
+ select=raw.get("select"),
220
+ exclude=raw.get("exclude"),
219
221
  )
@@ -8,8 +8,6 @@ from pydantic import BaseModel, Field
8
8
 
9
9
 
10
10
  class ProviderConfig(BaseModel):
11
- """Configuration for a single provider."""
12
-
13
11
  name: str
14
12
  config: dict[str, Any] = Field(default_factory=dict)
15
13
 
@@ -25,19 +23,17 @@ class MetricConfig(BaseModel):
25
23
 
26
24
 
27
25
  class MaterializerConfig(BaseModel):
28
- """Configuration for the materializer."""
29
-
30
26
  type: str
31
27
  config: dict[str, Any] = Field(default_factory=dict)
32
28
 
33
29
 
34
30
  class CheckupConfig(BaseModel):
35
- """Complete checkup configuration."""
36
-
37
31
  tags: dict[str, Any] = Field(default_factory=dict)
38
32
  providers: list[ProviderConfig] = Field(default_factory=list)
39
33
  metrics: list[MetricConfig] = Field(default_factory=list)
40
34
  materializer: MaterializerConfig | None = None
35
+ select: str | None = None
36
+ exclude: str | None = None
41
37
 
42
38
  @classmethod
43
39
  def empty(cls) -> "CheckupConfig":
@@ -182,6 +182,14 @@ def generate_schema() -> dict:
182
182
  "description": "Tags to identify the data product (e.g., product, team)",
183
183
  "additionalProperties": {"type": "string"},
184
184
  },
185
+ "select": {
186
+ "type": "string",
187
+ "description": "Default selector for materialized output.",
188
+ },
189
+ "exclude": {
190
+ "type": "string",
191
+ "description": "Default selector for materialization to exclude.",
192
+ },
185
193
  "providers": {
186
194
  "type": "array",
187
195
  "description": "Data providers for context enrichment",
@@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
6
6
  from enum import Enum
7
7
  from typing import TYPE_CHECKING, Any
8
8
 
9
- from pydantic import BaseModel
9
+ from pydantic import BaseModel, Field
10
10
 
11
11
  from checkup.types import Context
12
12
 
@@ -44,6 +44,7 @@ class Metric(ABC, BaseModel):
44
44
  name: str
45
45
  description: str = ""
46
46
  unit: str = ""
47
+ tags: list[str] = Field(default_factory=list)
47
48
 
48
49
  model_config = {"frozen": True}
49
50
 
@@ -0,0 +1,103 @@
1
+ """
2
+ Metric selection for filtering materialized output.
3
+
4
+ Selection filters which measurements get materialized, it never changes what
5
+ is calculated, dependencies are always computed.
6
+
7
+ A selector is a set of whitespace-separated atoms (a union).
8
+ Each atom is ``[method:]pattern`` where:
9
+
10
+ - ``method`` is ``name`` (the default), ``tag`` or ``type``
11
+ - ``pattern`` supports ``*`` wildcards
12
+
13
+ Examples::
14
+
15
+ healthcheck_*
16
+ name:healthcheck_*
17
+ tag:healthcheck
18
+ type:git_tracked_file_count tag:healthcheck
19
+ """
20
+
21
+ import fnmatch
22
+ from collections.abc import Callable, Iterable
23
+ from enum import StrEnum
24
+ from typing import assert_never
25
+
26
+ from checkup.metric import Metric
27
+
28
+
29
+ class SelectionMethod(StrEnum):
30
+ NAME = "name"
31
+ TAG = "tag"
32
+ TYPE = "type"
33
+
34
+
35
+ # Resolves a metric instance to its registered type name.
36
+ # Names are needed by the filter, with a resolver being offered as argument
37
+ # because the name-type mapping is defined by the config.
38
+ TypeResolver = Callable[[Metric], str | None]
39
+
40
+
41
+ def select_metrics(
42
+ metrics: Iterable[Metric],
43
+ select: str | None = None,
44
+ exclude: str | None = None,
45
+ type_resolver: TypeResolver | None = None,
46
+ ) -> set[str]:
47
+ """
48
+ Return the names of the metrics whose measurements should be materialized.
49
+
50
+ With no ``select`` every metric is included;
51
+ ``exclude`` (if given) is then removed from the set.
52
+ """
53
+
54
+ metrics = list(metrics)
55
+ type_resolver = type_resolver or (lambda _name: None)
56
+
57
+ if select:
58
+ selected = _match_selector(select, metrics, type_resolver)
59
+ else:
60
+ selected = {metric.name for metric in metrics}
61
+
62
+ if exclude:
63
+ selected -= _match_selector(exclude, metrics, type_resolver)
64
+
65
+ return selected
66
+
67
+
68
+ def _match_selector(
69
+ selector: str,
70
+ metrics: list[Metric],
71
+ type_resolver: TypeResolver,
72
+ ) -> set[str]:
73
+ atoms = selector.split()
74
+ return {
75
+ metric.name
76
+ for metric in metrics
77
+ if any(_match_atom(atom, metric, type_resolver) for atom in atoms)
78
+ }
79
+
80
+
81
+ def _match_atom(
82
+ atom: str,
83
+ metric: Metric,
84
+ type_resolver: TypeResolver,
85
+ *,
86
+ default_method: SelectionMethod = SelectionMethod.NAME,
87
+ ) -> bool:
88
+ method_name, separator, pattern = atom.partition(":")
89
+ if not separator:
90
+ method, pattern = default_method, atom
91
+ else:
92
+ method = SelectionMethod(method_name)
93
+
94
+ match method:
95
+ case SelectionMethod.NAME:
96
+ return fnmatch.fnmatch(metric.name, pattern)
97
+ case SelectionMethod.TAG:
98
+ return any(fnmatch.fnmatch(tag, pattern) for tag in metric.tags)
99
+ case SelectionMethod.TYPE:
100
+ metric_type = type_resolver(metric)
101
+ return metric_type is not None and fnmatch.fnmatch(metric_type, pattern)
102
+ case _:
103
+ assert_never(method)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes