checkup 0.5.0__tar.gz → 0.5.2__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 (48) hide show
  1. {checkup-0.5.0 → checkup-0.5.2}/PKG-INFO +4 -1
  2. {checkup-0.5.0 → checkup-0.5.2}/pyproject.toml +6 -1
  3. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/__init__.py +2 -1
  4. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/commands/__init__.py +2 -0
  5. checkup-0.5.2/src/checkup/cli/commands/plugins.py +49 -0
  6. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/commands/run.py +12 -1
  7. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/executor.py +12 -2
  8. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/executor/metric_calculator.py +15 -0
  9. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/hub.py +48 -22
  10. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/registry/__init__.py +2 -0
  11. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/registry/discovery.py +45 -0
  12. {checkup-0.5.0 → checkup-0.5.2}/README.md +0 -0
  13. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/__init__.py +0 -0
  14. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/commands/config.py +0 -0
  15. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/commands/init.py +0 -0
  16. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/commands/schema.py +0 -0
  17. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/config_wizard/__init__.py +0 -0
  18. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/config_wizard/_common.py +0 -0
  19. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/config_wizard/create.py +0 -0
  20. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/config_wizard/edit.py +0 -0
  21. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/utils.py +0 -0
  22. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/config.py +0 -0
  23. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/configuration/__init__.py +0 -0
  24. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/configuration/env.py +0 -0
  25. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/configuration/io.py +0 -0
  26. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/configuration/models.py +0 -0
  27. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/configuration/schema.py +0 -0
  28. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/errors.py +0 -0
  29. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/executor/__init__.py +0 -0
  30. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/executor/batch_executors.py +0 -0
  31. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/executor/provider_executor.py +0 -0
  32. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/executor/state.py +0 -0
  33. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/graph.py +0 -0
  34. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/__init__.py +0 -0
  35. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/base.py +0 -0
  36. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/console.py +0 -0
  37. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/csv_file.py +0 -0
  38. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/database.py +0 -0
  39. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/html_report.py +0 -0
  40. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/measurement.py +0 -0
  41. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/metric.py +0 -0
  42. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/provider.py +0 -0
  43. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/providers/__init__.py +0 -0
  44. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/providers/tags.py +0 -0
  45. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/templates/metrics_report.html +0 -0
  46. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/types.py +0 -0
  47. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/utils.py +0 -0
  48. {checkup-0.5.0 → checkup-0.5.2}/src/checkup/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: checkup
3
- Version: 0.5.0
3
+ Version: 0.5.2
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>
@@ -15,6 +15,9 @@ Requires-Dist: rich>=13.0
15
15
  Requires-Dist: sqlalchemy>=2.0
16
16
  Requires-Dist: typer>=0.24
17
17
  Requires-Python: >=3.12
18
+ Project-URL: Homepage, https://github.com/datamindedbe/checkup
19
+ Project-URL: Issues, https://github.com/datamindedbe/checkup/issues
20
+ Project-URL: Source, https://github.com/datamindedbe/checkup
18
21
  Description-Content-Type: text/markdown
19
22
 
20
23
  # CheckUp
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "checkup"
3
- version = "0.5.0"
3
+ version = "0.5.2"
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" }]
@@ -20,6 +20,11 @@ dependencies = [
20
20
  "typer>=0.24",
21
21
  ]
22
22
 
23
+ [project.urls]
24
+ Homepage = "https://github.com/datamindedbe/checkup"
25
+ Source = "https://github.com/datamindedbe/checkup"
26
+ Issues = "https://github.com/datamindedbe/checkup/issues"
27
+
23
28
  [project.scripts]
24
29
  checkup = "checkup:main"
25
30
 
@@ -7,7 +7,7 @@ from typing import Annotated
7
7
 
8
8
  import typer
9
9
 
10
- from checkup.cli.commands import config, init, run, schema
10
+ from checkup.cli.commands import config, init, plugins, run, schema
11
11
 
12
12
  app = typer.Typer(
13
13
  name="checkup",
@@ -36,3 +36,4 @@ app.command()(run)
36
36
  app.command()(init)
37
37
  app.command()(config)
38
38
  app.command()(schema)
39
+ app.command()(plugins)
@@ -4,12 +4,14 @@ CLI commands.
4
4
 
5
5
  from checkup.cli.commands.config import config
6
6
  from checkup.cli.commands.init import init
7
+ from checkup.cli.commands.plugins import plugins
7
8
  from checkup.cli.commands.run import run
8
9
  from checkup.cli.commands.schema import schema
9
10
 
10
11
  __all__ = [
11
12
  "config",
12
13
  "init",
14
+ "plugins",
13
15
  "run",
14
16
  "schema",
15
17
  ]
@@ -0,0 +1,49 @@
1
+ """
2
+ Plugins command. List installed checkup plugins.
3
+ """
4
+
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+
8
+ from checkup.registry import get_registry
9
+
10
+ console = Console()
11
+
12
+
13
+ def plugins() -> None:
14
+ """
15
+ List installed checkup plugins.
16
+ """
17
+
18
+ all_plugins = get_registry().list_plugins()
19
+ all_plugins.pop("checkup", None)
20
+
21
+ if not all_plugins:
22
+ console.print("[yellow]No checkup plugins installed[/yellow]")
23
+ return
24
+
25
+ kinds = [
26
+ ("Providers", "providers"),
27
+ ("Metrics", "metrics"),
28
+ ("Materializers", "materializers"),
29
+ ]
30
+ populated_kinds = [ # Hide columns that are empty
31
+ (header, attr)
32
+ for header, attr in kinds
33
+ if any(getattr(p, attr) for p in all_plugins.values())
34
+ ]
35
+
36
+ table = Table(title="Installed checkup plugins")
37
+ table.add_column("Plugin", style="bold", overflow="fold")
38
+ table.add_column("Version", overflow="fold")
39
+ for header, _ in populated_kinds:
40
+ table.add_column(header, overflow="fold")
41
+
42
+ for name, plugin in all_plugins.items():
43
+ table.add_row(
44
+ name,
45
+ plugin.version or "",
46
+ *("\n".join(getattr(plugin, attr)) for _, attr in populated_kinds),
47
+ )
48
+
49
+ console.print(table)
@@ -40,6 +40,13 @@ def run(
40
40
  bool,
41
41
  typer.Option("--dry-run", help="Don't materialize, just print"),
42
42
  ] = False,
43
+ multiprocessing: Annotated[
44
+ bool,
45
+ typer.Option(
46
+ "--multiprocessing",
47
+ help="When disabled, run sequentially without subprocesses",
48
+ ),
49
+ ] = True,
43
50
  verbose: Annotated[
44
51
  bool,
45
52
  typer.Option("--verbose", "-v", help="Verbose output"),
@@ -55,4 +62,8 @@ def run(
55
62
  cfg = load_config(config_path=config)
56
63
  cfg = apply_cli_overrides(cfg, tag, provider, metric)
57
64
 
58
- execute_checkup(cfg, materializer="console" if dry_run else materializer)
65
+ execute_checkup(
66
+ cfg,
67
+ materializer="console" if dry_run else materializer,
68
+ multiprocessing=multiprocessing,
69
+ )
@@ -23,13 +23,18 @@ logger = logging.getLogger(__name__)
23
23
  console = Console()
24
24
 
25
25
 
26
- def execute_checkup(config: CheckupConfig, materializer: str | None = None) -> None:
26
+ def execute_checkup(
27
+ config: CheckupConfig,
28
+ materializer: str | None = None,
29
+ multiprocessing: bool = True,
30
+ ) -> None:
27
31
  """
28
32
  Execute checkup with the given configuration.
29
33
 
30
34
  Args:
31
35
  config: Loaded checkup configuration
32
36
  materializer: Override materializer type (e.g., "console")
37
+ multiprocessing: If False, run sequentially without subprocesses
33
38
  """
34
39
 
35
40
  registry = get_registry()
@@ -48,7 +53,12 @@ def execute_checkup(config: CheckupConfig, materializer: str | None = None) -> N
48
53
 
49
54
  console.print(f"[blue]Running {len(metrics)} metrics...[/blue]")
50
55
 
51
- result = CheckHub().with_metrics(metrics).with_providers([providers]).measure()
56
+ result = (
57
+ CheckHub()
58
+ .with_metrics(metrics)
59
+ .with_providers([providers])
60
+ .measure(multiprocessing=multiprocessing)
61
+ )
52
62
 
53
63
  if result.errors:
54
64
  for _, error in result.errors:
@@ -37,6 +37,15 @@ class MetricCalculator:
37
37
  Calculates metrics for a given context.
38
38
  """
39
39
 
40
+ def __init__(self, multiprocessing: bool = True) -> None:
41
+ """
42
+ Args:
43
+ multiprocessing: If False, metrics that request the PROCESS executor are run with the THREAD executor instead.
44
+ This avoids spawning subprocesses in environments that do not support it.
45
+ """
46
+
47
+ self._multiprocessing = multiprocessing
48
+
40
49
  def calculate(
41
50
  self,
42
51
  metrics: list[Metric],
@@ -193,6 +202,12 @@ class MetricCalculator:
193
202
  Execute a batch and record results.
194
203
  """
195
204
 
205
+ if not self._multiprocessing and executor_type is ExecutorType.PROCESS:
206
+ logger.debug(
207
+ "No subprocesses requested: running PROCESS batch with THREAD executor"
208
+ )
209
+ executor_type = ExecutorType.THREAD
210
+
196
211
  logger.debug(
197
212
  "Executing batch with %d metrics using %s executor",
198
213
  len(batch),
@@ -57,6 +57,7 @@ def _measure_single_provider_set(
57
57
  provider_set: list[Provider],
58
58
  metrics: list[Metric],
59
59
  execution_order: list[type[Metric]],
60
+ multiprocessing: bool = True,
60
61
  ) -> list[Measurement]:
61
62
  """Calculate all metrics for a single provider set.
62
63
 
@@ -66,6 +67,7 @@ def _measure_single_provider_set(
66
67
  provider_set: List of provider instances
67
68
  metrics: List of metric instances to calculate
68
69
  execution_order: Topologically sorted metric classes
70
+ multiprocessing: If False, run metric batches without subprocesses
69
71
 
70
72
  Returns:
71
73
  List of Measurements with tags merged
@@ -77,7 +79,7 @@ def _measure_single_provider_set(
77
79
  context, errors = ProviderExecutor().execute(provider_set)
78
80
  failed_providers = {type(e.provider): e for e in errors}
79
81
 
80
- return MetricCalculator().calculate(
82
+ return MetricCalculator(multiprocessing=multiprocessing).calculate(
81
83
  metrics,
82
84
  execution_order,
83
85
  context,
@@ -133,11 +135,17 @@ class CheckHub:
133
135
  def measure(
134
136
  self,
135
137
  max_workers: int | None = None,
138
+ multiprocessing: bool = True,
136
139
  ) -> MeasurementResult:
137
- """Execute the measurement pipeline.
140
+ """
141
+ Execute the measurement pipeline.
138
142
 
139
143
  Args:
140
144
  max_workers: Max parallel workers. None = use all CPUs.
145
+ Ignored when multiprocessing=False.
146
+ multiprocessing: If True, provider sets run in parallel via ProcessPoolExecutor.
147
+ If False, provider sets run sequentially and
148
+ metrics requesting the PROCESS executor fall back to threads.
141
149
 
142
150
  Returns:
143
151
  MeasurementResult containing all calculated measurements and errors
@@ -161,28 +169,46 @@ class CheckHub:
161
169
 
162
170
  all_measurements: list[Measurement] = []
163
171
  all_errors: list[tuple[list[Provider], Exception]] = []
164
- workers = max_workers if max_workers is not None else os.cpu_count()
165
-
166
- logger.debug("Executing with %d workers", workers)
167
- with ProcessPoolExecutor(max_workers=workers) as executor:
168
- future_to_provider_set = {
169
- executor.submit(
170
- _measure_single_provider_set,
171
- provider_set=ps,
172
- metrics=self._metrics,
173
- execution_order=execution_order,
174
- ): ps
175
- for ps in provider_sets
176
- }
177
-
178
- for future in as_completed(future_to_provider_set):
179
- ps = future_to_provider_set[future]
172
+
173
+ def _record(ps: list[Provider], e: Exception) -> None:
174
+ logger.error("Provider set failed: %s", e)
175
+ logger.debug("Provider set failure details:", exc_info=True)
176
+ all_errors.append((ps, e))
177
+
178
+ if multiprocessing:
179
+ workers = max_workers if max_workers is not None else os.cpu_count()
180
+ logger.debug("Executing with %d workers", workers)
181
+ with ProcessPoolExecutor(max_workers=workers) as executor:
182
+ future_to_provider_set = {
183
+ executor.submit(
184
+ _measure_single_provider_set,
185
+ provider_set=ps,
186
+ metrics=self._metrics,
187
+ execution_order=execution_order,
188
+ ): ps
189
+ for ps in provider_sets
190
+ }
191
+
192
+ for future in as_completed(future_to_provider_set):
193
+ ps = future_to_provider_set[future]
194
+ try:
195
+ all_measurements.extend(future.result())
196
+ except Exception as e:
197
+ _record(ps, e)
198
+ else:
199
+ logger.debug("Executing sequentially")
200
+ for ps in provider_sets:
180
201
  try:
181
- all_measurements.extend(future.result())
202
+ all_measurements.extend(
203
+ _measure_single_provider_set(
204
+ provider_set=ps,
205
+ metrics=self._metrics,
206
+ execution_order=execution_order,
207
+ multiprocessing=False,
208
+ )
209
+ )
182
210
  except Exception as e:
183
- logger.error("Provider set failed: %s", e)
184
- logger.debug("Provider set failure details:", exc_info=True)
185
- all_errors.append((ps, e))
211
+ _record(ps, e)
186
212
 
187
213
  if all_errors:
188
214
  failed_contexts = []
@@ -3,11 +3,13 @@ Plugin registry for discovering providers, metrics, and materializers.
3
3
  """
4
4
 
5
5
  from checkup.registry.discovery import (
6
+ Plugin,
6
7
  PluginRegistry,
7
8
  get_registry,
8
9
  )
9
10
 
10
11
  __all__ = [
12
+ "Plugin",
11
13
  "PluginRegistry",
12
14
  "get_registry",
13
15
  ]
@@ -3,6 +3,7 @@ Plugin discovery via Python entry points.
3
3
  """
4
4
 
5
5
  import logging
6
+ from dataclasses import dataclass, field
6
7
  from importlib.metadata import entry_points
7
8
  from typing import TYPE_CHECKING
8
9
 
@@ -13,6 +14,19 @@ if TYPE_CHECKING:
13
14
 
14
15
  logger = logging.getLogger(__name__)
15
16
 
17
+
18
+ @dataclass
19
+ class Plugin:
20
+ """
21
+ A distribution registering one or more checkup entry points.
22
+ """
23
+
24
+ version: str | None
25
+ providers: list[str] = field(default_factory=list)
26
+ metrics: list[str] = field(default_factory=list)
27
+ materializers: list[str] = field(default_factory=list)
28
+
29
+
16
30
  # Entry point group names
17
31
  PROVIDERS_GROUP = "checkup.providers"
18
32
  METRICS_GROUP = "checkup.metrics"
@@ -137,6 +151,37 @@ class PluginRegistry:
137
151
 
138
152
  return self._list_entry_point_names(MATERIALIZERS_GROUP)
139
153
 
154
+ def list_plugins(self) -> dict[str, Plugin]:
155
+ """
156
+ List installed plugins grouped by distribution.
157
+
158
+ Walks all checkup entry-point groups and groups them by the
159
+ distribution that registers them. Does not load plugin code.
160
+ """
161
+
162
+ groups = {
163
+ "providers": PROVIDERS_GROUP,
164
+ "metrics": METRICS_GROUP,
165
+ "materializers": MATERIALIZERS_GROUP,
166
+ }
167
+
168
+ plugins: dict[str, Plugin] = {}
169
+
170
+ for kind, group in groups.items():
171
+ for ep in entry_points(group=group):
172
+ dist = ep.dist
173
+ dist_name = dist.name if dist else "unknown"
174
+ dist_version = dist.version if dist else None
175
+
176
+ plugin = plugins.setdefault(dist_name, Plugin(version=dist_version))
177
+ getattr(plugin, kind).append(ep.name)
178
+
179
+ for plugin in plugins.values():
180
+ for kind in groups:
181
+ getattr(plugin, kind).sort()
182
+
183
+ return dict(sorted(plugins.items()))
184
+
140
185
  def list_compatible_metric_names(self, provider_names: list[str]) -> list[str]:
141
186
  """
142
187
  List metric names compatible with the given providers.
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