checkup 0.5.3__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.
- {checkup-0.5.3 → checkup-0.7.0}/PKG-INFO +6 -1
- {checkup-0.5.3 → checkup-0.7.0}/README.md +5 -0
- {checkup-0.5.3 → checkup-0.7.0}/pyproject.toml +29 -32
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/run.py +22 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/executor.py +45 -15
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/utils.py +2 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/configuration/io.py +2 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/configuration/models.py +2 -6
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/configuration/schema.py +8 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/__init__.py +2 -0
- checkup-0.7.0/src/checkup/materializers/markdown.py +52 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/metric.py +2 -1
- checkup-0.7.0/src/checkup/selection.py +103 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/__init__.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/__init__.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/__init__.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/config.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/init.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/plugins.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/schema.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/config_wizard/__init__.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/config_wizard/_common.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/config_wizard/create.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/config_wizard/edit.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/config.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/configuration/__init__.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/configuration/env.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/errors.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/executor/__init__.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/executor/batch_executors.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/executor/metric_calculator.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/executor/provider_executor.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/executor/state.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/graph.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/hub.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/base.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/console.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/csv_file.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/database.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/html_report.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/measurement.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/provider.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/providers/__init__.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/providers/tags.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/registry/__init__.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/registry/discovery.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/templates/metrics_report.html +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/types.py +0 -0
- {checkup-0.5.3 → checkup-0.7.0}/src/checkup/utils.py +0 -0
- {checkup-0.5.3 → 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.
|
|
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.
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
"Programming Language :: Python :: 3",
|
|
10
|
+
"Programming Language :: Python :: 3.12",
|
|
11
|
+
"Programming Language :: Python :: 3.13",
|
|
12
12
|
]
|
|
13
13
|
dependencies = [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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]
|
|
@@ -32,6 +32,7 @@ checkup = "checkup:main"
|
|
|
32
32
|
console = "checkup.materializers:ConsoleMaterializer"
|
|
33
33
|
csv = "checkup.materializers:CSVMaterializer"
|
|
34
34
|
html = "checkup.materializers:HTMLMaterializer"
|
|
35
|
+
markdown = "checkup.materializers:MarkdownMaterializer"
|
|
35
36
|
sqlalchemy = "checkup.materializers:SQLAlchemyMaterializer"
|
|
36
37
|
|
|
37
38
|
[build-system]
|
|
@@ -40,23 +41,19 @@ build-backend = "uv_build"
|
|
|
40
41
|
|
|
41
42
|
[tool.uv.workspace]
|
|
42
43
|
members = [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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",
|
|
51
52
|
]
|
|
52
53
|
|
|
53
54
|
[dependency-groups]
|
|
54
55
|
dev = ["pytest>=9.0.2", "pytest-cov>=7.0.0", "ruff>=0.8.6", "prek>=0.3.0"]
|
|
55
|
-
docs = [
|
|
56
|
-
"mkdocs>=1.6.0",
|
|
57
|
-
"mkdocs-material>=9.5.0",
|
|
58
|
-
"pymdown-extensions>=10.0",
|
|
59
|
-
]
|
|
56
|
+
docs = ["mkdocs>=1.6.0", "mkdocs-material>=9.5.0", "pymdown-extensions>=10.0"]
|
|
60
57
|
|
|
61
58
|
[tool.pytest.ini_options]
|
|
62
59
|
pythonpath = [".", "tests"]
|
|
@@ -68,16 +65,16 @@ line-length = 88
|
|
|
68
65
|
|
|
69
66
|
[tool.ruff.lint]
|
|
70
67
|
select = [
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
78
75
|
]
|
|
79
76
|
ignore = [
|
|
80
|
-
|
|
77
|
+
"E501", # line too long (handled by formatter)
|
|
81
78
|
]
|
|
82
79
|
|
|
83
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"),
|
|
@@ -51,6 +66,10 @@ def run(
|
|
|
51
66
|
bool,
|
|
52
67
|
typer.Option("--verbose", "-v", help="Verbose output"),
|
|
53
68
|
] = False,
|
|
69
|
+
quiet: Annotated[
|
|
70
|
+
bool,
|
|
71
|
+
typer.Option("--quiet", "-q", help="Only print materializer output to stdout"),
|
|
72
|
+
] = False,
|
|
54
73
|
) -> None:
|
|
55
74
|
"""
|
|
56
75
|
Run metrics and materialize results.
|
|
@@ -66,4 +85,7 @@ def run(
|
|
|
66
85
|
cfg,
|
|
67
86
|
materializer="console" if dry_run else materializer,
|
|
68
87
|
multiprocessing=multiprocessing,
|
|
88
|
+
quiet=quiet,
|
|
89
|
+
select=select,
|
|
90
|
+
exclude=exclude,
|
|
69
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
|
|
@@ -27,6 +28,9 @@ def execute_checkup(
|
|
|
27
28
|
config: CheckupConfig,
|
|
28
29
|
materializer: str | None = None,
|
|
29
30
|
multiprocessing: bool = True,
|
|
31
|
+
quiet: bool = False,
|
|
32
|
+
select: str | None = None,
|
|
33
|
+
exclude: str | None = None,
|
|
30
34
|
) -> None:
|
|
31
35
|
"""
|
|
32
36
|
Execute checkup with the given configuration.
|
|
@@ -35,23 +39,32 @@ def execute_checkup(
|
|
|
35
39
|
config: Loaded checkup configuration
|
|
36
40
|
materializer: Override materializer type (e.g., "console")
|
|
37
41
|
multiprocessing: If False, run sequentially without subprocesses
|
|
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.
|
|
38
45
|
"""
|
|
39
46
|
|
|
47
|
+
out = Console(stderr=True) if quiet else console
|
|
48
|
+
|
|
40
49
|
registry = get_registry()
|
|
41
50
|
|
|
42
|
-
providers = _resolve_providers(config, registry)
|
|
51
|
+
providers = _resolve_providers(config, registry, out)
|
|
43
52
|
if not providers:
|
|
44
|
-
|
|
53
|
+
out.print("[yellow]No providers configured[/yellow]")
|
|
45
54
|
return
|
|
46
55
|
|
|
47
|
-
metrics = _resolve_metrics(config, registry)
|
|
56
|
+
metrics = _resolve_metrics(config, registry, out)
|
|
48
57
|
if not metrics:
|
|
49
|
-
|
|
58
|
+
out.print("[yellow]No metrics configured[/yellow]")
|
|
50
59
|
return
|
|
51
60
|
|
|
52
|
-
materializer = _resolve_materializer(config, registry, materializer)
|
|
61
|
+
materializer = _resolve_materializer(config, registry, materializer, out)
|
|
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}
|
|
53
66
|
|
|
54
|
-
|
|
67
|
+
out.print(f"[blue]Running {len(metrics)} metrics...[/blue]")
|
|
55
68
|
|
|
56
69
|
result = (
|
|
57
70
|
CheckHub()
|
|
@@ -62,7 +75,23 @@ def execute_checkup(
|
|
|
62
75
|
|
|
63
76
|
if result.errors:
|
|
64
77
|
for _, error in result.errors:
|
|
65
|
-
|
|
78
|
+
out.print(f"[red]Error: {error}[/red]")
|
|
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
|
+
)
|
|
66
95
|
|
|
67
96
|
result.materialize(materializer)
|
|
68
97
|
|
|
@@ -70,6 +99,7 @@ def execute_checkup(
|
|
|
70
99
|
def _resolve_providers(
|
|
71
100
|
config: CheckupConfig,
|
|
72
101
|
registry: "PluginRegistry",
|
|
102
|
+
out: Console = console,
|
|
73
103
|
) -> list["Provider"]:
|
|
74
104
|
"""
|
|
75
105
|
Resolve provider configs to provider instances.
|
|
@@ -83,14 +113,14 @@ def _resolve_providers(
|
|
|
83
113
|
for provider_config in config.providers:
|
|
84
114
|
provider_cls = registry.get_provider(provider_config.name)
|
|
85
115
|
if provider_cls is None:
|
|
86
|
-
|
|
116
|
+
out.print(f"[yellow]Unknown provider: {provider_config.name}[/yellow]")
|
|
87
117
|
continue
|
|
88
118
|
|
|
89
119
|
try:
|
|
90
120
|
provider = provider_cls(**provider_config.config)
|
|
91
121
|
providers.append(provider)
|
|
92
122
|
except Exception as e:
|
|
93
|
-
|
|
123
|
+
out.print(
|
|
94
124
|
f"[red]Failed to instantiate provider {provider_config.name}: {e}[/red]"
|
|
95
125
|
)
|
|
96
126
|
|
|
@@ -100,6 +130,7 @@ def _resolve_providers(
|
|
|
100
130
|
def _resolve_metrics(
|
|
101
131
|
config: CheckupConfig,
|
|
102
132
|
registry: "PluginRegistry",
|
|
133
|
+
out: Console = console,
|
|
103
134
|
) -> list["Metric"]:
|
|
104
135
|
"""
|
|
105
136
|
Resolve metric configs to metric instances.
|
|
@@ -110,7 +141,7 @@ def _resolve_metrics(
|
|
|
110
141
|
for metric_config in config.metrics:
|
|
111
142
|
metric_cls = registry.get_metric(metric_config.type)
|
|
112
143
|
if metric_cls is None:
|
|
113
|
-
|
|
144
|
+
out.print(f"[yellow]Unknown metric: {metric_config.type}[/yellow]")
|
|
114
145
|
continue
|
|
115
146
|
|
|
116
147
|
try:
|
|
@@ -121,7 +152,7 @@ def _resolve_metrics(
|
|
|
121
152
|
)
|
|
122
153
|
)
|
|
123
154
|
except Exception as e:
|
|
124
|
-
|
|
155
|
+
out.print(
|
|
125
156
|
f"[red]Failed to instantiate metric {metric_config.type}: {e}[/red]"
|
|
126
157
|
)
|
|
127
158
|
|
|
@@ -132,6 +163,7 @@ def _resolve_materializer(
|
|
|
132
163
|
config: CheckupConfig,
|
|
133
164
|
registry: "PluginRegistry",
|
|
134
165
|
override: str | None = None,
|
|
166
|
+
out: Console = console,
|
|
135
167
|
) -> "Materializer":
|
|
136
168
|
"""
|
|
137
169
|
Resolve materializer config to materializer instance.
|
|
@@ -150,13 +182,11 @@ def _resolve_materializer(
|
|
|
150
182
|
materializer_cls = registry.get_materializer(mat_type)
|
|
151
183
|
|
|
152
184
|
if materializer_cls is None:
|
|
153
|
-
|
|
154
|
-
f"[yellow]Unknown materializer: {mat_type}, using console[/yellow]"
|
|
155
|
-
)
|
|
185
|
+
out.print(f"[yellow]Unknown materializer: {mat_type}, using console[/yellow]")
|
|
156
186
|
return ConsoleMaterializer()
|
|
157
187
|
|
|
158
188
|
try:
|
|
159
189
|
return materializer_cls(**mat_config)
|
|
160
190
|
except Exception as e:
|
|
161
|
-
|
|
191
|
+
out.print(f"[red]Failed to instantiate materializer {mat_type}: {e}[/red]")
|
|
162
192
|
return ConsoleMaterializer()
|
|
@@ -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",
|
|
@@ -9,11 +9,13 @@ from checkup.materializers.console import ConsoleMaterializer
|
|
|
9
9
|
from checkup.materializers.csv_file import CSVMaterializer
|
|
10
10
|
from checkup.materializers.database import SQLAlchemyMaterializer
|
|
11
11
|
from checkup.materializers.html_report import HTMLMaterializer
|
|
12
|
+
from checkup.materializers.markdown import MarkdownMaterializer
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"ConsoleMaterializer",
|
|
15
16
|
"CSVMaterializer",
|
|
16
17
|
"HTMLMaterializer",
|
|
18
|
+
"MarkdownMaterializer",
|
|
17
19
|
"Materializer",
|
|
18
20
|
"SQLAlchemyMaterializer",
|
|
19
21
|
"group_measurements_by_tags",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Markdown materializer for Markdown table output."""
|
|
2
|
+
|
|
3
|
+
from checkup.materializers.base import Materializer
|
|
4
|
+
from checkup.measurement import Measurement
|
|
5
|
+
|
|
6
|
+
COLUMNS = ("Name", "Description", "Value", "Unit", "Diagnostics")
|
|
7
|
+
ALIGNMENTS = ("---", "---", "---:", "---", "---") # Right-align the Value column.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MarkdownMaterializer(Materializer):
|
|
11
|
+
"""
|
|
12
|
+
Output measurements as a GitHub-flavoured Markdown table.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def materialize(
|
|
16
|
+
self, measurements: list[Measurement], direct_metric_names: set[str]
|
|
17
|
+
) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Print measurements as a Markdown table.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
filtered = self._filter_measurements(measurements, direct_metric_names)
|
|
23
|
+
|
|
24
|
+
rows = [self._row(COLUMNS), self._row(ALIGNMENTS)]
|
|
25
|
+
for measurement in filtered:
|
|
26
|
+
rows.append(
|
|
27
|
+
self._row(
|
|
28
|
+
(
|
|
29
|
+
measurement.metric.name,
|
|
30
|
+
measurement.metric.description,
|
|
31
|
+
str(measurement.value) if measurement.value is not None else "",
|
|
32
|
+
measurement.metric.unit,
|
|
33
|
+
measurement.diagnostic,
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
print("\n".join(rows))
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def _row(cls, cells: tuple[str, ...]) -> str:
|
|
41
|
+
return "| " + " | ".join(cls._cell(cell) for cell in cells) + " |"
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _cell(value: str) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Escape a value for a Markdown table cell.
|
|
47
|
+
|
|
48
|
+
Cells cannot contain a raw pipe (column separator) or newline (row separator),
|
|
49
|
+
so escape pipes and turn newlines into `<br>`.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
return str(value or "").replace("|", "\\|").replace("\n", "<br>").strip()
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|