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.
Files changed (50) hide show
  1. {checkup-0.5.3 → checkup-0.7.0}/PKG-INFO +6 -1
  2. {checkup-0.5.3 → checkup-0.7.0}/README.md +5 -0
  3. {checkup-0.5.3 → checkup-0.7.0}/pyproject.toml +29 -32
  4. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/run.py +22 -0
  5. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/executor.py +45 -15
  6. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/utils.py +2 -0
  7. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/configuration/io.py +2 -0
  8. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/configuration/models.py +2 -6
  9. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/configuration/schema.py +8 -0
  10. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/__init__.py +2 -0
  11. checkup-0.7.0/src/checkup/materializers/markdown.py +52 -0
  12. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/metric.py +2 -1
  13. checkup-0.7.0/src/checkup/selection.py +103 -0
  14. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/__init__.py +0 -0
  15. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/__init__.py +0 -0
  16. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/__init__.py +0 -0
  17. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/config.py +0 -0
  18. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/init.py +0 -0
  19. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/plugins.py +0 -0
  20. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/commands/schema.py +0 -0
  21. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/config_wizard/__init__.py +0 -0
  22. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/config_wizard/_common.py +0 -0
  23. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/config_wizard/create.py +0 -0
  24. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/cli/config_wizard/edit.py +0 -0
  25. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/config.py +0 -0
  26. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/configuration/__init__.py +0 -0
  27. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/configuration/env.py +0 -0
  28. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/errors.py +0 -0
  29. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/executor/__init__.py +0 -0
  30. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/executor/batch_executors.py +0 -0
  31. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/executor/metric_calculator.py +0 -0
  32. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/executor/provider_executor.py +0 -0
  33. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/executor/state.py +0 -0
  34. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/graph.py +0 -0
  35. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/hub.py +0 -0
  36. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/base.py +0 -0
  37. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/console.py +0 -0
  38. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/csv_file.py +0 -0
  39. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/database.py +0 -0
  40. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/materializers/html_report.py +0 -0
  41. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/measurement.py +0 -0
  42. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/provider.py +0 -0
  43. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/providers/__init__.py +0 -0
  44. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/providers/tags.py +0 -0
  45. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/registry/__init__.py +0 -0
  46. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/registry/discovery.py +0 -0
  47. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/templates/metrics_report.html +0 -0
  48. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/types.py +0 -0
  49. {checkup-0.5.3 → checkup-0.7.0}/src/checkup/utils.py +0 -0
  50. {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.5.3
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.5.3"
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]
@@ -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
- "plugins/checkup-git",
44
- "plugins/checkup-dbt",
45
- "plugins/checkup-python",
46
- "plugins/checkup-conveyor",
47
- "plugins/checkup-gitlab",
48
- "plugins/checkup-bitbucket",
49
- "plugins/checkup-github",
50
- "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",
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
- "E", # pycodestyle errors
72
- "W", # pycodestyle warnings
73
- "F", # Pyflakes
74
- "I", # isort
75
- "B", # flake8-bugbear
76
- "C4", # flake8-comprehensions
77
- "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
78
75
  ]
79
76
  ignore = [
80
- "E501", # line too long (handled by formatter)
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
- console.print("[yellow]No providers configured[/yellow]")
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
- console.print("[yellow]No metrics configured[/yellow]")
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
- console.print(f"[blue]Running {len(metrics)} metrics...[/blue]")
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
- console.print(f"[red]Error: {error}[/red]")
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
- console.print(f"[yellow]Unknown provider: {provider_config.name}[/yellow]")
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
- console.print(
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
- console.print(f"[yellow]Unknown metric: {metric_config.type}[/yellow]")
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
- console.print(
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
- console.print(
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
- console.print(f"[red]Failed to instantiate materializer {mat_type}: {e}[/red]")
191
+ out.print(f"[red]Failed to instantiate materializer {mat_type}: {e}[/red]")
162
192
  return ConsoleMaterializer()
@@ -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",
@@ -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