firecube 0.1.4__py3-none-any.whl
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.
- firecube/__init__.py +23 -0
- firecube/__main__.py +20 -0
- firecube/_version.py +46 -0
- firecube/cli/_audience.py +135 -0
- firecube/cli/_command_schemas.py +112 -0
- firecube/cli/_ctx.py +85 -0
- firecube/cli/_errors.py +115 -0
- firecube/cli/_formatter.py +195 -0
- firecube/cli/_product.py +47 -0
- firecube/cli/_rename_hints.py +123 -0
- firecube/cli/_shared_options.py +247 -0
- firecube/cli/_slot_env.py +102 -0
- firecube/cli/_slot_planning.py +95 -0
- firecube/cli/_typed_options.py +181 -0
- firecube/cli/_uri_policy.py +110 -0
- firecube/cli/advise.py +321 -0
- firecube/cli/archive.py +870 -0
- firecube/cli/catalog.py +196 -0
- firecube/cli/chunks/__init__.py +74 -0
- firecube/cli/chunks/_claims.py +229 -0
- firecube/cli/chunks/_common.py +84 -0
- firecube/cli/chunks/_delete.py +376 -0
- firecube/cli/chunks/_list.py +334 -0
- firecube/cli/chunks/_manager.py +161 -0
- firecube/cli/chunks/_runs.py +294 -0
- firecube/cli/chunks/_snapshots.py +165 -0
- firecube/cli/completion.py +73 -0
- firecube/cli/main.py +866 -0
- firecube/cli/parquet.py +289 -0
- firecube/cli/plugins/__init__.py +28 -0
- firecube/cli/plugins/commands.py +262 -0
- firecube/cli/plugins/introspect.py +304 -0
- firecube/cli/plugins/mgmt.py +198 -0
- firecube/cli/plugins/registry.py +132 -0
- firecube/cli/zarr.py +1043 -0
- firecube/core/__init__.py +14 -0
- firecube/core/api.py +88 -0
- firecube/core/cf/__init__.py +23 -0
- firecube/core/cf/check_ids.py +126 -0
- firecube/core/cf/report.py +57 -0
- firecube/core/cf/validator.py +313 -0
- firecube/core/config.py +307 -0
- firecube/core/controlplane/__init__.py +47 -0
- firecube/core/controlplane/_event_processor.py +136 -0
- firecube/core/controlplane/_helpers.py +114 -0
- firecube/core/controlplane/_projection.py +253 -0
- firecube/core/controlplane/_snapshot.py +215 -0
- firecube/core/controlplane/_wal_reader.py +402 -0
- firecube/core/controlplane/_wal_writer.py +589 -0
- firecube/core/controlplane/claims.py +294 -0
- firecube/core/controlplane/deletion.py +704 -0
- firecube/core/controlplane/events.py +219 -0
- firecube/core/controlplane/manager.py +990 -0
- firecube/core/controlplane/metrics.py +97 -0
- firecube/core/controlplane/repo.py +804 -0
- firecube/core/controlplane/repo_utils.py +73 -0
- firecube/core/controlplane/time_dim.py +111 -0
- firecube/core/controlplane/types.py +445 -0
- firecube/core/credentials.py +36 -0
- firecube/core/duckdb/__init__.py +27 -0
- firecube/core/duckdb/bridge.py +82 -0
- firecube/core/duckdb/io.py +30 -0
- firecube/core/errors.py +74 -0
- firecube/core/filesystem/__init__.py +60 -0
- firecube/core/filesystem/_obstore_compat.py +60 -0
- firecube/core/filesystem/fsspec_backend.py +308 -0
- firecube/core/filesystem/instrumentation.py +260 -0
- firecube/core/filesystem/obstore_backend.py +406 -0
- firecube/core/filesystem/ops.py +412 -0
- firecube/core/filesystem/protocol.py +130 -0
- firecube/core/filesystem/store_factory.py +149 -0
- firecube/core/formats/__init__.py +46 -0
- firecube/core/formats/_iso.py +84 -0
- firecube/core/formats/discovery.py +165 -0
- firecube/core/formats/hdf5.py +137 -0
- firecube/core/formats/netcdf.py +226 -0
- firecube/core/formats/zip.py +192 -0
- firecube/core/intake.py +530 -0
- firecube/core/observability/__init__.py +72 -0
- firecube/core/observability/_state.py +24 -0
- firecube/core/observability/logging.py +147 -0
- firecube/core/observability/metrics.py +230 -0
- firecube/core/observability/telemetry/__init__.py +87 -0
- firecube/core/observability/telemetry/pushgateway.py +339 -0
- firecube/core/observability/telemetry/sinks.py +155 -0
- firecube/core/observability/tracing.py +126 -0
- firecube/core/product/__init__.py +45 -0
- firecube/core/product/identity.py +76 -0
- firecube/core/product/resolver.py +121 -0
- firecube/core/product/target.py +120 -0
- firecube/core/runtime.py +187 -0
- firecube/core/slot_index.py +205 -0
- firecube/core/storage/__init__.py +45 -0
- firecube/core/storage/binding.py +39 -0
- firecube/core/storage/cache_key.py +40 -0
- firecube/core/storage/completion.py +245 -0
- firecube/core/storage/driver_config.py +125 -0
- firecube/core/storage/results.py +30 -0
- firecube/core/storage/session.py +431 -0
- firecube/core/storage/transfer.py +142 -0
- firecube/core/storage/uri.py +173 -0
- firecube/core/tensogram/__init__.py +37 -0
- firecube/core/tensogram/_compat.py +36 -0
- firecube/core/tensogram/controlplane_codec.py +230 -0
- firecube/core/tensogram/converter.py +390 -0
- firecube/core/tensogram/metadata.py +274 -0
- firecube/core/tensogram/restore.py +303 -0
- firecube/core/tensogram/schema.py +93 -0
- firecube/core/uris.py +182 -0
- firecube/core/workspaces.py +68 -0
- firecube/core/zarr/__init__.py +69 -0
- firecube/core/zarr/_reserved_attrs.py +47 -0
- firecube/core/zarr/_reserved_root_attrs.py +74 -0
- firecube/core/zarr/io.py +88 -0
- firecube/core/zarr/layers.py +184 -0
- firecube/core/zarr/multires.py +178 -0
- firecube/core/zarr/region_writer.py +782 -0
- firecube/core/zarr/scrub.py +200 -0
- firecube/core/zarr/state.py +258 -0
- firecube/core/zarr/time_decode.py +71 -0
- firecube/core/zarr/validation.py +622 -0
- firecube/ingestor/__init__.py +27 -0
- firecube/ingestor/api.py +215 -0
- firecube/ingestor/config/__init__.py +19 -0
- firecube/ingestor/config/coercion.py +157 -0
- firecube/ingestor/config/engine.py +188 -0
- firecube/ingestor/contracts/__init__.py +14 -0
- firecube/ingestor/contracts/interfaces.py +235 -0
- firecube/ingestor/devtools/_templates/__init__.py +14 -0
- firecube/ingestor/devtools/_templates/ingestor_base.py.tpl +72 -0
- firecube/ingestor/devtools/_templates/ingestor_direct_zarr.py.tpl +66 -0
- firecube/ingestor/devtools/_templates/ingestor_parquet.py.tpl +59 -0
- firecube/ingestor/devtools/_templates/ingestor_zarr.py.tpl +55 -0
- firecube/ingestor/devtools/_templates/readme.md.tpl +40 -0
- firecube/ingestor/devtools/scaffolding.py +229 -0
- firecube/ingestor/errors.py +79 -0
- firecube/ingestor/extensions/_binning.py +152 -0
- firecube/ingestor/extensions/duck.py +153 -0
- firecube/ingestor/extensions/grid.py +287 -0
- firecube/ingestor/extensions/healpix.py +330 -0
- firecube/ingestor/registry/__init__.py +14 -0
- firecube/ingestor/registry/loader.py +144 -0
- firecube/ingestor/registry/metadata.py +97 -0
- firecube/ingestor/registry/version_compat.py +95 -0
- firecube/ingestor/runtime/__init__.py +14 -0
- firecube/ingestor/runtime/aggregation.py +112 -0
- firecube/ingestor/runtime/base.py +836 -0
- firecube/ingestor/runtime/base_hooks.py +129 -0
- firecube/ingestor/runtime/batching.py +161 -0
- firecube/ingestor/runtime/configure.py +195 -0
- firecube/ingestor/runtime/coverage.py +177 -0
- firecube/ingestor/runtime/engine.py +793 -0
- firecube/ingestor/runtime/parallel_evidence.py +75 -0
- firecube/ingestor/runtime/parallel_execution_state.py +28 -0
- firecube/ingestor/runtime/parallel_gate.py +164 -0
- firecube/ingestor/runtime/parallel_run_id.py +48 -0
- firecube/ingestor/runtime/preflight.py +40 -0
- firecube/ingestor/runtime/recording.py +425 -0
- firecube/ingestor/runtime/resume_guard.py +556 -0
- firecube/ingestor/runtime/resume_types.py +65 -0
- firecube/ingestor/runtime/telemetry.py +171 -0
- firecube/ingestor/runtime/tensogram/__init__.py +14 -0
- firecube/ingestor/runtime/tensogram/strategy.py +164 -0
- firecube/ingestor/runtime/workspace.py +220 -0
- firecube/ingestor/runtime/zarr/__init__.py +34 -0
- firecube/ingestor/runtime/zarr/append.py +466 -0
- firecube/ingestor/runtime/zarr/append_services.py +650 -0
- firecube/ingestor/runtime/zarr/batch_runner.py +190 -0
- firecube/ingestor/runtime/zarr/contracts.py +58 -0
- firecube/ingestor/runtime/zarr/existing_cube_check.py +282 -0
- firecube/ingestor/runtime/zarr/resume_cache.py +92 -0
- firecube/ingestor/runtime/zarr/staged_metadata.py +249 -0
- firecube/ingestor/runtime/zarr/strategies/__init__.py +15 -0
- firecube/ingestor/runtime/zarr/strategies/append.py +172 -0
- firecube/ingestor/runtime/zarr/strategies/indexed_region.py +426 -0
- firecube/ingestor/runtime/zarr/write.py +212 -0
- firecube/ingestor/runtime/zarr/write_context.py +139 -0
- firecube/ingestor/templates/__init__.py +19 -0
- firecube/ingestor/templates/config.py +118 -0
- firecube/ingestor/templates/direct_zarr.py +790 -0
- firecube/ingestor/templates/generic.py +462 -0
- firecube/ingestor/templates/generic_tensogram.py +170 -0
- firecube/ingestor/types/__init__.py +19 -0
- firecube/ingestor/types/config.py +89 -0
- firecube/ingestor/types/context.py +409 -0
- firecube/ingestor/types/manifest.py +53 -0
- firecube/ingestor/types/planned_range.py +152 -0
- firecube/ingestor/types/result_metrics.py +386 -0
- firecube/ingestor/utils/__init__.py +14 -0
- firecube/ingestor/utils/duckdb_utils.py +235 -0
- firecube/ingestor/validation.py +55 -0
- firecube-0.1.4.dist-info/METADATA +242 -0
- firecube-0.1.4.dist-info/RECORD +197 -0
- firecube-0.1.4.dist-info/WHEEL +4 -0
- firecube-0.1.4.dist-info/entry_points.txt +2 -0
- firecube-0.1.4.dist-info/licenses/AUTHORS.md +5 -0
- firecube-0.1.4.dist-info/licenses/LICENSE +201 -0
firecube/cli/advise.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Copyright 2025-2026 EUMETSAT
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Performance advice helpers (read-only)."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json as _json
|
|
20
|
+
import sys
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
import zarr
|
|
25
|
+
|
|
26
|
+
from firecube.cli._ctx import get_storage_config
|
|
27
|
+
from firecube.cli._errors import wrap_user_facing_errors
|
|
28
|
+
from firecube.cli._product import resolve_product_identity
|
|
29
|
+
from firecube.cli._shared_options import (
|
|
30
|
+
product_uri_option,
|
|
31
|
+
storage_driver_option,
|
|
32
|
+
storage_type_option,
|
|
33
|
+
)
|
|
34
|
+
from firecube.cli._uri_policy import apply_smart_default, parse_product_uri
|
|
35
|
+
from firecube.core import observability
|
|
36
|
+
from firecube.core.cf import CFReport
|
|
37
|
+
from firecube.core.cf.validator import validate_cf18
|
|
38
|
+
from firecube.core.storage.binding import StorageBinding
|
|
39
|
+
from firecube.core.storage.driver_config import StorageDriverConfig
|
|
40
|
+
from firecube.core.storage.session import StorageSession
|
|
41
|
+
|
|
42
|
+
_ROOT_GROUP_ALIASES = frozenset({".", "/", ""})
|
|
43
|
+
_COMPLIANCE_PROFILE_VALIDATORS: dict[str, Callable[..., CFReport]] = {
|
|
44
|
+
"cf-18": validate_cf18,
|
|
45
|
+
}
|
|
46
|
+
_COMPLIANCE_PROFILE_LABELS = {
|
|
47
|
+
"cf-18": "CF-1.8",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
52
|
+
@click.pass_context
|
|
53
|
+
def advise(ctx: click.Context) -> None:
|
|
54
|
+
"""performance advice helpers
|
|
55
|
+
|
|
56
|
+
Read-only helpers for configuring ingestion. Use advise batch-size to get
|
|
57
|
+
a recommended pipeline_batch_size before running large ingestion jobs.
|
|
58
|
+
"""
|
|
59
|
+
observability.init_observability("firecube-advise")
|
|
60
|
+
ctx.ensure_object(dict)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@advise.command(
|
|
64
|
+
"batch-size",
|
|
65
|
+
epilog="""\b
|
|
66
|
+
Examples:
|
|
67
|
+
# get recommended batch size for a product group
|
|
68
|
+
firecube advise batch-size -p <product> -g <group>
|
|
69
|
+
|
|
70
|
+
See also: firecube ingest, firecube zarr validate
|
|
71
|
+
""",
|
|
72
|
+
)
|
|
73
|
+
@product_uri_option(tier="inspect")
|
|
74
|
+
@click.option(
|
|
75
|
+
"-g",
|
|
76
|
+
"--group",
|
|
77
|
+
"group_path",
|
|
78
|
+
required=True,
|
|
79
|
+
help="group path inside the product (e.g. F024/FWI)",
|
|
80
|
+
)
|
|
81
|
+
@storage_driver_option(required=False)
|
|
82
|
+
@storage_type_option(required=False)
|
|
83
|
+
@click.pass_context
|
|
84
|
+
@wrap_user_facing_errors
|
|
85
|
+
def batch_size(
|
|
86
|
+
ctx: click.Context,
|
|
87
|
+
product: str,
|
|
88
|
+
group_path: str,
|
|
89
|
+
storage_driver: str | None,
|
|
90
|
+
storage_type: str | None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""recommend optimal batch size
|
|
93
|
+
|
|
94
|
+
Recommends an optimal pipeline_batch_size based on the Zarr store's time
|
|
95
|
+
chunk shape. Reads the store metadata to compute how many time steps fit
|
|
96
|
+
into memory without partial chunks. Run before a long ingestion job to
|
|
97
|
+
set --option pipeline_batch_size=N.
|
|
98
|
+
"""
|
|
99
|
+
parsed_uri = parse_product_uri(product)
|
|
100
|
+
storage_type = apply_smart_default(parsed_uri, storage_type)
|
|
101
|
+
storage_cfg = get_storage_config(
|
|
102
|
+
ctx,
|
|
103
|
+
overrides={"storage_type": storage_type, "storage_driver": storage_driver},
|
|
104
|
+
cache=False,
|
|
105
|
+
)
|
|
106
|
+
driver_config = StorageDriverConfig.from_storage_config(storage_cfg)
|
|
107
|
+
identity = resolve_product_identity(
|
|
108
|
+
parsed_uri.normalized, format="zarr", product_name=parsed_uri.normalized
|
|
109
|
+
)
|
|
110
|
+
session = StorageSession(
|
|
111
|
+
StorageBinding(
|
|
112
|
+
identity=identity,
|
|
113
|
+
driver=driver_config,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
root = session.zarr.open_group(identity.product_uri, mode="r")
|
|
119
|
+
grp = root[group_path]
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
click.echo(f"Error opening store: {exc}", err=True)
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
|
|
124
|
+
if not isinstance(grp, zarr.Group):
|
|
125
|
+
click.echo(f"Path '{group_path}' is not a Zarr group.", err=True)
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
|
|
128
|
+
time_chunk_size = _find_time_chunk_size(grp)
|
|
129
|
+
|
|
130
|
+
if time_chunk_size is None:
|
|
131
|
+
click.echo(f"No time dimension found in group '{group_path}'. Any batch size is valid.")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
if time_chunk_size == 1:
|
|
135
|
+
click.echo(
|
|
136
|
+
"Recommended: --option pipeline_batch_size=10 (any size works; time chunk is 1)\n"
|
|
137
|
+
"Rationale: Time chunk size is 1. Any batch size is safely aligned. Recommend 10 for throughput."
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
click.echo(
|
|
141
|
+
f"Recommended: --option pipeline_batch_size={time_chunk_size}\n"
|
|
142
|
+
f"Rationale: Time chunk size is {time_chunk_size}. "
|
|
143
|
+
"Aligning batch size avoids partial-chunk write overhead."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _find_time_chunk_size(grp: zarr.Group) -> int | None:
|
|
148
|
+
"""Return the time-axis chunk size from the first data array, or *None*."""
|
|
149
|
+
for _name, arr in grp.arrays():
|
|
150
|
+
chunks = arr.chunks
|
|
151
|
+
if not isinstance(chunks, tuple) or len(chunks) == 0:
|
|
152
|
+
continue
|
|
153
|
+
# In this codebase the time dimension is always axis 0 for
|
|
154
|
+
# multi-dimensional data arrays (shape length > 1).
|
|
155
|
+
if len(chunks) > 1:
|
|
156
|
+
return chunks[0]
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@advise.command(
|
|
161
|
+
"compliance",
|
|
162
|
+
epilog="""\b
|
|
163
|
+
Examples:
|
|
164
|
+
# run Tier-1 CF-1.8 checks on a local cube root
|
|
165
|
+
firecube advise compliance --profile cf-18 -p /path/to/cube.zarr -g .
|
|
166
|
+
# check a nested group, emit JSON for downstream tooling
|
|
167
|
+
firecube advise compliance --profile cf-18 -p s3://bucket/x.zarr -g F024/FWI --storage-type s3 --storage-driver fsspec --format json
|
|
168
|
+
# fail (exit 2) on any warning or info finding
|
|
169
|
+
firecube advise compliance --profile cf-18 -p ./cube.zarr -g . --strict
|
|
170
|
+
|
|
171
|
+
See also: firecube zarr validate
|
|
172
|
+
""",
|
|
173
|
+
)
|
|
174
|
+
@click.option(
|
|
175
|
+
"--profile",
|
|
176
|
+
required=True,
|
|
177
|
+
type=click.Choice(sorted(_COMPLIANCE_PROFILE_VALIDATORS)),
|
|
178
|
+
help="compliance profile to run",
|
|
179
|
+
)
|
|
180
|
+
@product_uri_option(tier="inspect")
|
|
181
|
+
@click.option(
|
|
182
|
+
"-g",
|
|
183
|
+
"--group",
|
|
184
|
+
"group_path",
|
|
185
|
+
required=True,
|
|
186
|
+
help="group path inside the cube ('.' or '/' for root, otherwise e.g. F024/FWI)",
|
|
187
|
+
)
|
|
188
|
+
@click.option(
|
|
189
|
+
"--format",
|
|
190
|
+
"output_format",
|
|
191
|
+
type=click.Choice(["text", "json"]),
|
|
192
|
+
default="text",
|
|
193
|
+
show_default=True,
|
|
194
|
+
help="output format for the report",
|
|
195
|
+
)
|
|
196
|
+
@click.option(
|
|
197
|
+
"--strict",
|
|
198
|
+
is_flag=True,
|
|
199
|
+
default=False,
|
|
200
|
+
help="treat any warning or info finding as a non-zero exit (code 2)",
|
|
201
|
+
)
|
|
202
|
+
@storage_driver_option(required=False)
|
|
203
|
+
@storage_type_option(required=False)
|
|
204
|
+
@click.pass_context
|
|
205
|
+
@wrap_user_facing_errors
|
|
206
|
+
def compliance(
|
|
207
|
+
ctx: click.Context,
|
|
208
|
+
profile: str,
|
|
209
|
+
product: str,
|
|
210
|
+
group_path: str,
|
|
211
|
+
output_format: str,
|
|
212
|
+
strict: bool,
|
|
213
|
+
storage_driver: str | None,
|
|
214
|
+
storage_type: str | None,
|
|
215
|
+
) -> None:
|
|
216
|
+
"""run structural compliance checks
|
|
217
|
+
|
|
218
|
+
Opens a Zarr cube and runs the selected compliance profile. Findings are
|
|
219
|
+
emitted as human-readable text (default) or as JSON. Exit codes: 0 on clean
|
|
220
|
+
(or warnings/info without --strict), 1 if any error finding is reported,
|
|
221
|
+
2 if warnings/info appear under --strict.
|
|
222
|
+
"""
|
|
223
|
+
parsed_uri = parse_product_uri(product)
|
|
224
|
+
storage_type = apply_smart_default(parsed_uri, storage_type)
|
|
225
|
+
storage_cfg = get_storage_config(
|
|
226
|
+
ctx,
|
|
227
|
+
overrides={"storage_type": storage_type, "storage_driver": storage_driver},
|
|
228
|
+
cache=False,
|
|
229
|
+
)
|
|
230
|
+
driver_config = StorageDriverConfig.from_storage_config(storage_cfg)
|
|
231
|
+
|
|
232
|
+
identity = resolve_product_identity(
|
|
233
|
+
parsed_uri.normalized, format="zarr", product_name=parsed_uri.normalized
|
|
234
|
+
)
|
|
235
|
+
session = StorageSession(StorageBinding(identity=identity, driver=driver_config))
|
|
236
|
+
|
|
237
|
+
xr_group = "" if group_path in _ROOT_GROUP_ALIASES else group_path
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
root = session.zarr.open_group(identity.product_uri, mode="r")
|
|
241
|
+
if xr_group:
|
|
242
|
+
target = root[xr_group]
|
|
243
|
+
if not isinstance(target, zarr.Group):
|
|
244
|
+
raise click.ClickException(
|
|
245
|
+
f"Path '{group_path}' is a Zarr array, not a group. "
|
|
246
|
+
"Specify a group path (for example '/' or 'F024/FWI')."
|
|
247
|
+
)
|
|
248
|
+
except KeyError as exc:
|
|
249
|
+
raise click.ClickException(f"Group '{group_path}' not found in Zarr cube.") from exc
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
ds = session.zarr.open_dataset(
|
|
253
|
+
identity.product_uri,
|
|
254
|
+
group=xr_group,
|
|
255
|
+
decode_times=False,
|
|
256
|
+
)
|
|
257
|
+
except KeyError as exc:
|
|
258
|
+
raise click.ClickException(f"Group '{group_path}' not found in Zarr cube.") from exc
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
report = _COMPLIANCE_PROFILE_VALIDATORS[profile](ds, product=product, group=group_path)
|
|
262
|
+
finally:
|
|
263
|
+
ds.close()
|
|
264
|
+
|
|
265
|
+
if output_format == "json":
|
|
266
|
+
_render_compliance_json(report, profile=profile)
|
|
267
|
+
else:
|
|
268
|
+
_render_compliance_text(report, profile=profile)
|
|
269
|
+
|
|
270
|
+
summary = report.summary
|
|
271
|
+
if summary.errors > 0:
|
|
272
|
+
sys.exit(1)
|
|
273
|
+
if strict and (summary.warnings > 0 or summary.info > 0):
|
|
274
|
+
sys.exit(2)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _render_compliance_text(report: CFReport, *, profile: str) -> None:
|
|
278
|
+
label = _COMPLIANCE_PROFILE_LABELS[profile]
|
|
279
|
+
click.echo(f"{label} advisor \u2014 product={report.product} group={report.group}")
|
|
280
|
+
|
|
281
|
+
for finding in report.findings:
|
|
282
|
+
sev_tag = f"[{finding.severity.value}]"
|
|
283
|
+
click.echo(f"{sev_tag:<10}{finding.id}: {finding.message}")
|
|
284
|
+
if finding.suggested_fix:
|
|
285
|
+
click.echo(f" \u2192 {finding.suggested_fix}")
|
|
286
|
+
|
|
287
|
+
summary = report.summary
|
|
288
|
+
if not report.findings:
|
|
289
|
+
click.echo(f"No findings \u2014 {label} structural checks passed.")
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
click.echo(
|
|
293
|
+
f"Summary: {summary.errors} error{'s' if summary.errors != 1 else ''}, "
|
|
294
|
+
f"{summary.warnings} warning{'s' if summary.warnings != 1 else ''}, "
|
|
295
|
+
f"{summary.info} info."
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _render_compliance_json(report: CFReport, *, profile: str) -> None:
|
|
300
|
+
summary = report.summary
|
|
301
|
+
payload = {
|
|
302
|
+
"profile": profile,
|
|
303
|
+
"product": report.product,
|
|
304
|
+
"group": report.group,
|
|
305
|
+
"findings": [
|
|
306
|
+
{
|
|
307
|
+
"id": f.id,
|
|
308
|
+
"severity": f.severity.value,
|
|
309
|
+
"target": f.target,
|
|
310
|
+
"message": f.message,
|
|
311
|
+
"suggested_fix": f.suggested_fix,
|
|
312
|
+
}
|
|
313
|
+
for f in report.findings
|
|
314
|
+
],
|
|
315
|
+
"summary": {
|
|
316
|
+
"errors": summary.errors,
|
|
317
|
+
"warnings": summary.warnings,
|
|
318
|
+
"info": summary.info,
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
click.echo(_json.dumps(payload, indent=2))
|