checkup 0.3.0__tar.gz → 0.5.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.3.0 → checkup-0.5.0}/PKG-INFO +1 -1
- {checkup-0.3.0 → checkup-0.5.0}/pyproject.toml +1 -1
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/__init__.py +6 -1
- checkup-0.5.0/src/checkup/cli/__init__.py +38 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/cli/config_wizard/create.py +1 -1
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/cli/config_wizard/edit.py +6 -6
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/cli/executor.py +9 -5
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/cli/utils.py +2 -2
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/configuration/io.py +22 -16
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/configuration/models.py +6 -3
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/configuration/schema.py +3 -1
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/errors.py +9 -10
- checkup-0.5.0/src/checkup/executor/__init__.py +6 -0
- checkup-0.5.0/src/checkup/executor/batch_executors.py +169 -0
- checkup-0.5.0/src/checkup/executor/metric_calculator.py +213 -0
- checkup-0.5.0/src/checkup/executor/provider_executor.py +45 -0
- checkup-0.5.0/src/checkup/executor/state.py +101 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/graph.py +1 -1
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/hub.py +5 -13
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/materializers/base.py +1 -1
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/materializers/console.py +1 -1
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/materializers/csv_file.py +1 -1
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/materializers/database.py +1 -1
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/materializers/html_report.py +1 -1
- checkup-0.5.0/src/checkup/measurement.py +96 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/metric.py +29 -29
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/provider.py +0 -13
- checkup-0.5.0/src/checkup/providers/tags.py +26 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/validators.py +15 -13
- checkup-0.3.0/src/checkup/cli/__init__.py +0 -18
- checkup-0.3.0/src/checkup/executor.py +0 -536
- checkup-0.3.0/src/checkup/providers/tags.py +0 -40
- {checkup-0.3.0 → checkup-0.5.0}/README.md +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/cli/commands/__init__.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/cli/commands/config.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/cli/commands/init.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/cli/commands/run.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/cli/commands/schema.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/cli/config_wizard/__init__.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/cli/config_wizard/_common.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/config.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/configuration/__init__.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/configuration/env.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/materializers/__init__.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/providers/__init__.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/registry/__init__.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/registry/discovery.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/templates/metrics_report.html +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/types.py +0 -0
- {checkup-0.3.0 → checkup-0.5.0}/src/checkup/utils.py +0 -0
|
@@ -14,18 +14,23 @@ from checkup.materializers import (
|
|
|
14
14
|
Materializer,
|
|
15
15
|
SQLAlchemyMaterializer,
|
|
16
16
|
)
|
|
17
|
-
from checkup.
|
|
17
|
+
from checkup.measurement import Measurement, Measurements
|
|
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",
|
|
28
32
|
"Measurement",
|
|
33
|
+
"Measurements",
|
|
29
34
|
"ExecutorType",
|
|
30
35
|
"Provider",
|
|
31
36
|
"TagProvider",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Checkup CLI application.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import version as pkg_version
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from checkup.cli.commands import config, init, run, schema
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="checkup",
|
|
14
|
+
help="CheckUp - Computational governance framework for measuring data product health",
|
|
15
|
+
no_args_is_help=True,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def version_callback(value: bool) -> None:
|
|
20
|
+
if value:
|
|
21
|
+
typer.echo(pkg_version("checkup"))
|
|
22
|
+
raise typer.Exit()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.callback()
|
|
26
|
+
def main(
|
|
27
|
+
version: Annotated[
|
|
28
|
+
bool | None,
|
|
29
|
+
typer.Option("--version", "-v", callback=version_callback),
|
|
30
|
+
] = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
app.command()(run)
|
|
36
|
+
app.command()(init)
|
|
37
|
+
app.command()(config)
|
|
38
|
+
app.command()(schema)
|
|
@@ -79,7 +79,7 @@ def _build_config(registry: "PluginRegistry") -> CheckupConfig | None:
|
|
|
79
79
|
return CheckupConfig(
|
|
80
80
|
tags=tags,
|
|
81
81
|
providers=[ProviderConfig(name=p) for p in provider_names],
|
|
82
|
-
metrics=[MetricConfig(
|
|
82
|
+
metrics=[MetricConfig(type=m) for m in metric_names],
|
|
83
83
|
materializer=MaterializerConfig(type=mat) if mat != "console" else None,
|
|
84
84
|
)
|
|
85
85
|
|
|
@@ -58,7 +58,7 @@ def _build_config(
|
|
|
58
58
|
config: CheckupConfig, registry: "PluginRegistry"
|
|
59
59
|
) -> CheckupConfig | None:
|
|
60
60
|
provider_configs = {p.name: p.config for p in config.providers}
|
|
61
|
-
metric_configs = {m.
|
|
61
|
+
metric_configs = {m.type: m.config for m in config.metrics}
|
|
62
62
|
|
|
63
63
|
tags = _prompt_edit_tags(config)
|
|
64
64
|
if tags is None:
|
|
@@ -83,7 +83,7 @@ def _build_config(
|
|
|
83
83
|
for p in provider_names
|
|
84
84
|
],
|
|
85
85
|
metrics=[
|
|
86
|
-
MetricConfig(
|
|
86
|
+
MetricConfig(type=m, config=metric_configs.get(m, {})) for m in metric_names
|
|
87
87
|
],
|
|
88
88
|
materializer=MaterializerConfig(type=mat) if mat else None,
|
|
89
89
|
)
|
|
@@ -121,7 +121,7 @@ def _prompt_edit_metrics(
|
|
|
121
121
|
registry: "PluginRegistry",
|
|
122
122
|
provider_names: list[str],
|
|
123
123
|
) -> list[str] | None:
|
|
124
|
-
|
|
124
|
+
current_types = [m.type for m in config.metrics]
|
|
125
125
|
|
|
126
126
|
with console.status("Loading metrics..."):
|
|
127
127
|
available = registry.list_compatible_metric_names(provider_names)
|
|
@@ -130,8 +130,8 @@ def _prompt_edit_metrics(
|
|
|
130
130
|
if edit is None:
|
|
131
131
|
return None
|
|
132
132
|
elif edit:
|
|
133
|
-
return select_multiple(available,
|
|
134
|
-
return [m.
|
|
133
|
+
return select_multiple(available, current_types, "metrics")
|
|
134
|
+
return [m.type for m in config.metrics if m.type in available]
|
|
135
135
|
|
|
136
136
|
|
|
137
137
|
def _prompt_edit_materializer(
|
|
@@ -194,7 +194,7 @@ def _show_current_config(config: "CheckupConfig") -> None:
|
|
|
194
194
|
console.print("[bold]Current configuration:[/bold]", markup=True)
|
|
195
195
|
console.print(f" Tags: {dict(config.tags) or '(none)'}")
|
|
196
196
|
console.print(f" Providers: {[p.name for p in config.providers] or '(none)'}")
|
|
197
|
-
console.print(f" Metrics: {[m.
|
|
197
|
+
console.print(f" Metrics: {[m.type for m in config.metrics] or '(none)'}")
|
|
198
198
|
mat = config.materializer.type if config.materializer else "(none)"
|
|
199
199
|
console.print(f" Materializer: {mat}")
|
|
200
200
|
console.print()
|
|
@@ -98,17 +98,21 @@ def _resolve_metrics(
|
|
|
98
98
|
metrics: list[Metric] = []
|
|
99
99
|
|
|
100
100
|
for metric_config in config.metrics:
|
|
101
|
-
metric_cls = registry.get_metric(metric_config.
|
|
101
|
+
metric_cls = registry.get_metric(metric_config.type)
|
|
102
102
|
if metric_cls is None:
|
|
103
|
-
console.print(f"[yellow]Unknown metric: {metric_config.
|
|
103
|
+
console.print(f"[yellow]Unknown metric: {metric_config.type}[/yellow]")
|
|
104
104
|
continue
|
|
105
105
|
|
|
106
106
|
try:
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
metrics.append(
|
|
108
|
+
metric_cls(
|
|
109
|
+
**metric_config.config,
|
|
110
|
+
**({"name": metric_config.name} if metric_config.name else {}),
|
|
111
|
+
)
|
|
112
|
+
)
|
|
109
113
|
except Exception as e:
|
|
110
114
|
console.print(
|
|
111
|
-
f"[red]Failed to instantiate metric {metric_config.
|
|
115
|
+
f"[red]Failed to instantiate metric {metric_config.type}: {e}[/red]"
|
|
112
116
|
)
|
|
113
117
|
|
|
114
118
|
return metrics
|
|
@@ -41,8 +41,8 @@ def apply_cli_overrides(
|
|
|
41
41
|
if metrics:
|
|
42
42
|
new_metrics = []
|
|
43
43
|
for m in metrics:
|
|
44
|
-
|
|
45
|
-
new_metrics.append(MetricConfig(
|
|
44
|
+
metric_type, config = parse_cli_item(m)
|
|
45
|
+
new_metrics.append(MetricConfig(type=metric_type, config=config))
|
|
46
46
|
else:
|
|
47
47
|
new_metrics = list(cfg.metrics)
|
|
48
48
|
|
|
@@ -80,13 +80,17 @@ def merge_configs(base: dict[str, Any], override: dict[str, Any]) -> dict[str, A
|
|
|
80
80
|
result[key] = value
|
|
81
81
|
elif key == "metrics":
|
|
82
82
|
if key in result:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
|
|
84
|
+
def _metric_key(m: dict) -> tuple:
|
|
85
|
+
"""
|
|
86
|
+
Key by (type, name) to allow multiple instances of same type
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
return (m.get("type"), m.get("name"))
|
|
90
|
+
|
|
91
|
+
base_metrics = {_metric_key(m): m for m in result.get(key, [])}
|
|
87
92
|
for metric in value:
|
|
88
|
-
|
|
89
|
-
base_metrics[name] = metric
|
|
93
|
+
base_metrics[_metric_key(metric)] = metric
|
|
90
94
|
result[key] = list(base_metrics.values())
|
|
91
95
|
else:
|
|
92
96
|
result[key] = value
|
|
@@ -126,9 +130,10 @@ def parse_metrics(raw: list[Any] | None) -> list[MetricConfig]:
|
|
|
126
130
|
Parse metric configuration from raw YAML.
|
|
127
131
|
|
|
128
132
|
Supports:
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
|
|
133
|
+
- type: git_tracked_file_count
|
|
134
|
+
- type: git_tracked_file_count
|
|
135
|
+
name: cruft_file_exists
|
|
136
|
+
pattern: ".cruft.json"
|
|
132
137
|
"""
|
|
133
138
|
|
|
134
139
|
if not raw:
|
|
@@ -136,13 +141,14 @@ def parse_metrics(raw: list[Any] | None) -> list[MetricConfig]:
|
|
|
136
141
|
|
|
137
142
|
metrics = []
|
|
138
143
|
for item in raw:
|
|
139
|
-
if isinstance(item,
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
if not isinstance(item, dict):
|
|
145
|
+
continue
|
|
146
|
+
metric_type = item.get("type")
|
|
147
|
+
if not metric_type:
|
|
148
|
+
continue
|
|
149
|
+
name = item.get("name")
|
|
150
|
+
config = {k: v for k, v in item.items() if k not in ("type", "name")}
|
|
151
|
+
metrics.append(MetricConfig(type=metric_type, name=name, config=config))
|
|
146
152
|
return metrics
|
|
147
153
|
|
|
148
154
|
|
|
@@ -15,11 +15,14 @@ class ProviderConfig(BaseModel):
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class MetricConfig(BaseModel):
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
name: str
|
|
18
|
+
type: str
|
|
19
|
+
name: str | None = None
|
|
21
20
|
config: dict[str, Any] = Field(default_factory=dict)
|
|
22
21
|
|
|
22
|
+
@property
|
|
23
|
+
def instance_name(self) -> str:
|
|
24
|
+
return self.name or self.type
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
class MaterializerConfig(BaseModel):
|
|
25
28
|
"""Configuration for the materializer."""
|
|
@@ -190,7 +190,9 @@ def generate_schema() -> dict:
|
|
|
190
190
|
"metrics": {
|
|
191
191
|
"type": "array",
|
|
192
192
|
"description": "Metrics to calculate",
|
|
193
|
-
"items": _build_oneof_schema(
|
|
193
|
+
"items": _build_oneof_schema(
|
|
194
|
+
metric_names, metric_schemas, key_field="type"
|
|
195
|
+
),
|
|
194
196
|
},
|
|
195
197
|
"materializer": _build_oneof_schema(
|
|
196
198
|
materializer_names, materializer_schemas, key_field="type"
|
|
@@ -27,7 +27,7 @@ class MetricPicklingError(Exception):
|
|
|
27
27
|
self.metric_cls = metric_cls
|
|
28
28
|
self.original_error = original_error
|
|
29
29
|
super().__init__(
|
|
30
|
-
f"Metric '{metric_cls.
|
|
30
|
+
f"Metric '{metric_cls.__name__}' cannot be pickled for process execution. "
|
|
31
31
|
f"Consider using ExecutorType.THREAD instead. Original error: {original_error}"
|
|
32
32
|
)
|
|
33
33
|
|
|
@@ -39,15 +39,14 @@ class MetricPicklingError(Exception):
|
|
|
39
39
|
class DuplicateMetricNameError(Exception):
|
|
40
40
|
"""Exception raised when multiple metrics have the same name."""
|
|
41
41
|
|
|
42
|
-
def __init__(self,
|
|
43
|
-
self.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
f"
|
|
48
|
-
|
|
49
|
-
)
|
|
42
|
+
def __init__(self, duplicates: "dict[str, list[type[Metric]]]"):
|
|
43
|
+
self.duplicates = duplicates
|
|
44
|
+
lines = []
|
|
45
|
+
for name, classes in duplicates.items():
|
|
46
|
+
class_names = ", ".join(cls.__name__ for cls in classes)
|
|
47
|
+
lines.append(f" '{name}': {class_names}")
|
|
48
|
+
super().__init__("Duplicate metric names found:\n" + "\n".join(lines))
|
|
50
49
|
|
|
51
50
|
def __reduce__(self):
|
|
52
51
|
"""Enable pickling across process boundaries."""
|
|
53
|
-
return (self.__class__, (self.
|
|
52
|
+
return (self.__class__, (self.duplicates,))
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Batch execution strategies for different executor types."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from checkup.measurement import Measurement, Measurements
|
|
10
|
+
from checkup.metric import Metric
|
|
11
|
+
from checkup.types import Context
|
|
12
|
+
from checkup.validators import validate_pickleable
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _calculate_metric_in_process(
|
|
18
|
+
metric: Metric,
|
|
19
|
+
context: Context,
|
|
20
|
+
tags: dict[str, Any],
|
|
21
|
+
calculated: Measurements,
|
|
22
|
+
) -> Measurement:
|
|
23
|
+
"""
|
|
24
|
+
Calculate a single metric in a subprocess.
|
|
25
|
+
|
|
26
|
+
Module-level function for ProcessPoolExecutor compatibility.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
measurement = metric.calculate(context, calculated)
|
|
30
|
+
measurement.tags.update(tags)
|
|
31
|
+
return measurement
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def execute_batch_thread(
|
|
35
|
+
batch: list[Metric],
|
|
36
|
+
context: Context,
|
|
37
|
+
tags: dict[str, Any],
|
|
38
|
+
calculated: Measurements,
|
|
39
|
+
) -> list[tuple[Metric, Measurement]]:
|
|
40
|
+
"""
|
|
41
|
+
Execute metrics using ThreadPoolExecutor.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
results: list[tuple[Metric, Measurement]] = []
|
|
45
|
+
|
|
46
|
+
with ThreadPoolExecutor(max_workers=len(batch)) as executor:
|
|
47
|
+
future_to_metric = {
|
|
48
|
+
executor.submit(
|
|
49
|
+
_calculate_single_metric,
|
|
50
|
+
metric,
|
|
51
|
+
context,
|
|
52
|
+
tags,
|
|
53
|
+
calculated,
|
|
54
|
+
): metric
|
|
55
|
+
for metric in batch
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for future in as_completed(future_to_metric):
|
|
59
|
+
metric = future_to_metric[future]
|
|
60
|
+
measurement = future.result()
|
|
61
|
+
results.append((metric, measurement))
|
|
62
|
+
|
|
63
|
+
return results
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def execute_batch_process(
|
|
67
|
+
batch: list[Metric],
|
|
68
|
+
context: Context,
|
|
69
|
+
tags: dict[str, Any],
|
|
70
|
+
calculated: Measurements,
|
|
71
|
+
) -> list[tuple[Metric, Measurement]]:
|
|
72
|
+
"""
|
|
73
|
+
Execute metrics using ProcessPoolExecutor.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
for metric in batch:
|
|
77
|
+
validate_pickleable(type(metric))
|
|
78
|
+
|
|
79
|
+
results: list[tuple[Metric, Measurement]] = []
|
|
80
|
+
|
|
81
|
+
with ProcessPoolExecutor(max_workers=len(batch)) as executor:
|
|
82
|
+
future_to_metric = {
|
|
83
|
+
executor.submit(
|
|
84
|
+
_calculate_metric_in_process,
|
|
85
|
+
metric,
|
|
86
|
+
context,
|
|
87
|
+
tags,
|
|
88
|
+
calculated,
|
|
89
|
+
): metric
|
|
90
|
+
for metric in batch
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for future in as_completed(future_to_metric):
|
|
94
|
+
metric = future_to_metric[future]
|
|
95
|
+
try:
|
|
96
|
+
measurement = future.result()
|
|
97
|
+
results.append((metric, measurement))
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(
|
|
100
|
+
"Metric %s failed in process executor: %s",
|
|
101
|
+
metric.name,
|
|
102
|
+
e,
|
|
103
|
+
)
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
return results
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def execute_batch_asyncio(
|
|
110
|
+
batch: list[Metric],
|
|
111
|
+
context: Context,
|
|
112
|
+
tags: dict[str, Any],
|
|
113
|
+
calculated: Measurements,
|
|
114
|
+
) -> list[tuple[Metric, Measurement]]:
|
|
115
|
+
"""
|
|
116
|
+
Execute metrics using asyncio.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
return asyncio.run(_execute_batch_asyncio_impl(batch, context, tags, calculated))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async def _execute_batch_asyncio_impl(
|
|
123
|
+
batch: list[Metric],
|
|
124
|
+
context: Context,
|
|
125
|
+
tags: dict[str, Any],
|
|
126
|
+
calculated: Measurements,
|
|
127
|
+
) -> list[tuple[Metric, Measurement]]:
|
|
128
|
+
"""
|
|
129
|
+
Async implementation of batch execution.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
tasks = [
|
|
133
|
+
_calculate_async_metric(metric, context, tags, calculated) for metric in batch
|
|
134
|
+
]
|
|
135
|
+
measurements = await asyncio.gather(*tasks)
|
|
136
|
+
return list(zip(batch, measurements, strict=True))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def _calculate_async_metric(
|
|
140
|
+
metric: Metric,
|
|
141
|
+
context: Context,
|
|
142
|
+
tags: dict[str, Any],
|
|
143
|
+
calculated: Measurements,
|
|
144
|
+
) -> Measurement:
|
|
145
|
+
"""
|
|
146
|
+
Calculate a single metric, handling both sync and async calculate methods.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
if inspect.iscoroutinefunction(metric.calculate):
|
|
150
|
+
measurement = await metric.calculate(context, calculated)
|
|
151
|
+
else:
|
|
152
|
+
measurement = metric.calculate(context, calculated)
|
|
153
|
+
measurement.tags.update(tags)
|
|
154
|
+
return measurement
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _calculate_single_metric(
|
|
158
|
+
metric: Metric,
|
|
159
|
+
context: Context,
|
|
160
|
+
tags: dict[str, Any],
|
|
161
|
+
calculated: Measurements,
|
|
162
|
+
) -> Measurement:
|
|
163
|
+
"""
|
|
164
|
+
Calculate a single metric (for thread executor).
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
measurement = metric.calculate(context, calculated)
|
|
168
|
+
measurement.tags.update(tags)
|
|
169
|
+
return measurement
|