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.
- {checkup-0.5.0 → checkup-0.5.2}/PKG-INFO +4 -1
- {checkup-0.5.0 → checkup-0.5.2}/pyproject.toml +6 -1
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/__init__.py +2 -1
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/commands/__init__.py +2 -0
- checkup-0.5.2/src/checkup/cli/commands/plugins.py +49 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/commands/run.py +12 -1
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/executor.py +12 -2
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/executor/metric_calculator.py +15 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/hub.py +48 -22
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/registry/__init__.py +2 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/registry/discovery.py +45 -0
- {checkup-0.5.0 → checkup-0.5.2}/README.md +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/__init__.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/commands/config.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/commands/init.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/commands/schema.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/config_wizard/__init__.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/config_wizard/_common.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/config_wizard/create.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/config_wizard/edit.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/cli/utils.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/config.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/configuration/__init__.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/configuration/env.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/configuration/io.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/configuration/models.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/configuration/schema.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/errors.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/executor/__init__.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/executor/batch_executors.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/executor/provider_executor.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/executor/state.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/graph.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/__init__.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/base.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/console.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/csv_file.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/database.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/materializers/html_report.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/measurement.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/metric.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/provider.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/providers/__init__.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/providers/tags.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/templates/metrics_report.html +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/types.py +0 -0
- {checkup-0.5.0 → checkup-0.5.2}/src/checkup/utils.py +0 -0
- {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.
|
|
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.
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
"""
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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(
|
|
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
|
-
|
|
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,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
|
|
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
|