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
|
@@ -0,0 +1,69 @@
|
|
|
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
|
+
"""Zarr utilities (validation, scrub, multires, state).
|
|
16
|
+
|
|
17
|
+
Public surface for CLI/API and plugins. Keep modules small and layered:
|
|
18
|
+
- `validation`: read-only Zarr inspection.
|
|
19
|
+
- `scrub`: mutating maintenance built on validation + ChunkManager.
|
|
20
|
+
- `multires`: multiresolution builders.
|
|
21
|
+
- `state`: shared Firecube state-array helpers.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import warnings
|
|
25
|
+
|
|
26
|
+
# Zarr v3 warns about non-Zarr files (like our manifest) in the store directory.
|
|
27
|
+
try:
|
|
28
|
+
from zarr.errors import ZarrUserWarning
|
|
29
|
+
|
|
30
|
+
warnings.filterwarnings("ignore", category=ZarrUserWarning)
|
|
31
|
+
except ImportError:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
from firecube.core.zarr.io import ZarrIO
|
|
35
|
+
from firecube.core.zarr.multires import MultiresConfig, ZarrMultiresBuilder
|
|
36
|
+
from firecube.core.zarr.region_writer import RegionZarrWriter, RegionZarrWriterProtocol
|
|
37
|
+
from firecube.core.zarr.scrub import ScrubResult, run_scrub
|
|
38
|
+
from firecube.core.zarr.state import (
|
|
39
|
+
DEFAULT_STATE_MEANING,
|
|
40
|
+
ensure_timestamp_state_array,
|
|
41
|
+
expand_time_index_ranges_to_chunk_boundaries,
|
|
42
|
+
update_timestamp_state,
|
|
43
|
+
)
|
|
44
|
+
from firecube.core.zarr.validation import (
|
|
45
|
+
ZarrValidationReport,
|
|
46
|
+
find_extra_chunks,
|
|
47
|
+
group_exists,
|
|
48
|
+
read_chunk_grid,
|
|
49
|
+
validate_group_with_fs,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"DEFAULT_STATE_MEANING",
|
|
54
|
+
"MultiresConfig",
|
|
55
|
+
"RegionZarrWriter",
|
|
56
|
+
"RegionZarrWriterProtocol",
|
|
57
|
+
"ScrubResult",
|
|
58
|
+
"ZarrIO",
|
|
59
|
+
"ZarrMultiresBuilder",
|
|
60
|
+
"ZarrValidationReport",
|
|
61
|
+
"ensure_timestamp_state_array",
|
|
62
|
+
"expand_time_index_ranges_to_chunk_boundaries",
|
|
63
|
+
"find_extra_chunks",
|
|
64
|
+
"group_exists",
|
|
65
|
+
"read_chunk_grid",
|
|
66
|
+
"run_scrub",
|
|
67
|
+
"update_timestamp_state",
|
|
68
|
+
"validate_group_with_fs",
|
|
69
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
"""Registry of array attribute keys that firecube manages internally.
|
|
16
|
+
|
|
17
|
+
Plugins must not set these keys in ZarrArraySpec.attrs; doing so would
|
|
18
|
+
conflict with firecube's own internal bookkeeping.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Mapping
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
__all__ = ["RESERVED_ARRAY_ATTRS", "assert_attrs_safe"]
|
|
27
|
+
|
|
28
|
+
RESERVED_ARRAY_ATTRS: frozenset[str] = frozenset(
|
|
29
|
+
{
|
|
30
|
+
"_ARRAY_DIMENSIONS",
|
|
31
|
+
"_FillValue",
|
|
32
|
+
"firecube_run_id",
|
|
33
|
+
"firecube_span_id",
|
|
34
|
+
"firecube_internal",
|
|
35
|
+
"firecube_static_written",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def assert_attrs_safe(attrs: Mapping[str, Any]) -> None:
|
|
41
|
+
"""Raise ValueError if *attrs* contains any reserved key."""
|
|
42
|
+
for key in attrs:
|
|
43
|
+
if key in RESERVED_ARRAY_ATTRS:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Reserved attr {key!r} is managed by firecube, not the plugin. "
|
|
46
|
+
"Remove it from ZarrArraySpec.attrs."
|
|
47
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
"""Guard reserved root attrs supplied by external writers.
|
|
16
|
+
|
|
17
|
+
This guard is for code paths that accept root attrs from EXTERNAL sources — user CLI options,
|
|
18
|
+
plugin output, or any other non-firecube author. Use this guard wherever such attrs are about
|
|
19
|
+
to be persisted to a Zarr root.
|
|
20
|
+
|
|
21
|
+
Firecube-internal writers MUST NOT call this guard. The slot-index service
|
|
22
|
+
(ChunkManager.ensure_slot_index_model._mirror_attrs) is the authoritative writer for the
|
|
23
|
+
reserved root attrs and writes them DIRECTLY. Drift detection on subsequent runs is handled by
|
|
24
|
+
the precedence matrix in _apply_slot_model_precedence, not by this guard.
|
|
25
|
+
|
|
26
|
+
In short: "assert no reserved root attrs appear in user/plugin-supplied attrs" — that is the
|
|
27
|
+
entire contract.
|
|
28
|
+
|
|
29
|
+
This module is DISTINCT from _reserved_attrs.py which guards array attributes. Root attributes
|
|
30
|
+
and array attributes are separate protection surfaces.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from collections.abc import Mapping
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
from firecube.core.slot_index import (
|
|
39
|
+
SLOT_INDEX_MODEL_ATTR,
|
|
40
|
+
SLOT_INDEX_MODEL_IDENTITY_HASH_ATTR,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__all__ = ["RESERVED_ROOT_ATTRS", "assert_root_attrs_safe"]
|
|
44
|
+
|
|
45
|
+
RESERVED_ROOT_ATTRS: frozenset[str] = frozenset(
|
|
46
|
+
{
|
|
47
|
+
SLOT_INDEX_MODEL_ATTR,
|
|
48
|
+
SLOT_INDEX_MODEL_IDENTITY_HASH_ATTR,
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def assert_root_attrs_safe(attrs: Mapping[str, Any]) -> None:
|
|
54
|
+
"""Raise ValueError if any key in attrs is a reserved firecube root attribute name.
|
|
55
|
+
|
|
56
|
+
The check is NAME-ONLY: the value content of the attr is NOT inspected.
|
|
57
|
+
Even a "correct-looking" value from user code is refused because this guard
|
|
58
|
+
exists to prevent user/plugin code paths from writing into the reserved namespace,
|
|
59
|
+
not to validate the value.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
attrs: Mapping of root attribute names to values (typically from user/plugin
|
|
63
|
+
code before persisting to a Zarr root).
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If any key in attrs overlaps with RESERVED_ROOT_ATTRS.
|
|
67
|
+
"""
|
|
68
|
+
offending = RESERVED_ROOT_ATTRS.intersection(attrs)
|
|
69
|
+
if offending:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"attrs contain reserved firecube root attribute(s): {sorted(offending)!r}. "
|
|
72
|
+
f"Reserved root attributes are managed internally by firecube "
|
|
73
|
+
f"(see RESERVED_ROOT_ATTRS = {sorted(RESERVED_ROOT_ATTRS)!r})."
|
|
74
|
+
)
|
firecube/core/zarr/io.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
18
|
+
|
|
19
|
+
import xarray as xr
|
|
20
|
+
import zarr
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from firecube.core.filesystem.store_factory import ZarrStoreHandle
|
|
24
|
+
from firecube.core.storage.session import StorageSession
|
|
25
|
+
from firecube.core.storage.uri import StorageUri
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ZarrIO:
|
|
29
|
+
def __init__(self, session: StorageSession) -> None:
|
|
30
|
+
self._session = session
|
|
31
|
+
|
|
32
|
+
def create_store(self, uri: StorageUri, mode: str = "w") -> ZarrStoreHandle:
|
|
33
|
+
"""Create a driver-aware Zarr store handle for a typed storage URI.
|
|
34
|
+
|
|
35
|
+
The returned handle can be splatted into zarr/xarray APIs via
|
|
36
|
+
``handle.zarr_kwargs()`` and is constructed using the parent storage
|
|
37
|
+
session's configured driver.
|
|
38
|
+
"""
|
|
39
|
+
from firecube.core.filesystem import create_session_zarr_store
|
|
40
|
+
from firecube.core.storage.session import storage_config_from_binding
|
|
41
|
+
|
|
42
|
+
return create_session_zarr_store(
|
|
43
|
+
uri=uri,
|
|
44
|
+
storage_config=storage_config_from_binding(self._session._binding),
|
|
45
|
+
mode=mode,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def open_group(self, uri: StorageUri, mode: str = "r") -> Any:
|
|
49
|
+
kwargs: dict[str, Any] = {**self.create_store(uri, mode).zarr_kwargs(), "mode": mode}
|
|
50
|
+
return zarr.open_group(**kwargs)
|
|
51
|
+
|
|
52
|
+
def open_dataset(self, uri: StorageUri, group: str = "", **xr_kwargs: Any) -> xr.Dataset:
|
|
53
|
+
"""Open an xarray dataset from a Zarr group using the session store.
|
|
54
|
+
|
|
55
|
+
``uri`` must be a canonical ``StorageUri``; raw URI strings are not
|
|
56
|
+
accepted by this elevated session API. Additional keyword arguments are
|
|
57
|
+
forwarded to ``xr.open_zarr`` (e.g. ``decode_times=False`` for raw
|
|
58
|
+
attribute inspection by structural validators).
|
|
59
|
+
"""
|
|
60
|
+
handle = self.create_store(uri, mode="r")
|
|
61
|
+
return xr.open_zarr(**handle.zarr_kwargs(), group=group, **xr_kwargs)
|
|
62
|
+
|
|
63
|
+
def write_dataset(
|
|
64
|
+
self,
|
|
65
|
+
ds: xr.Dataset,
|
|
66
|
+
uri: StorageUri,
|
|
67
|
+
group: str,
|
|
68
|
+
mode: str = "w",
|
|
69
|
+
**kwargs: Any,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Write an xarray dataset to a Zarr group using the session store.
|
|
72
|
+
|
|
73
|
+
Store construction is centralized through ``create_store`` so local,
|
|
74
|
+
remote, fsspec, and obstore targets follow the same session-bound driver
|
|
75
|
+
configuration. ``zarr_format`` may be supplied for xarray-style callers;
|
|
76
|
+
only format 3 is supported by the underlying Firecube write helper.
|
|
77
|
+
"""
|
|
78
|
+
handle = self.create_store(uri, mode)
|
|
79
|
+
zarr_format = kwargs.pop("zarr_format", 3)
|
|
80
|
+
if zarr_format != 3:
|
|
81
|
+
raise ValueError("session.zarr.write_dataset only supports zarr_format=3")
|
|
82
|
+
ds.to_zarr(
|
|
83
|
+
**handle.zarr_kwargs(),
|
|
84
|
+
group=group,
|
|
85
|
+
mode=cast(Any, mode),
|
|
86
|
+
zarr_format=zarr_format,
|
|
87
|
+
**kwargs,
|
|
88
|
+
)
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
"""Core logic for building multi-resolution Zarr layers."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from collections.abc import Iterable, MutableMapping
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
23
|
+
|
|
24
|
+
import xarray as xr
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from firecube.core.storage.session import StorageSession
|
|
28
|
+
|
|
29
|
+
from firecube.core.storage.uri import StorageUri
|
|
30
|
+
|
|
31
|
+
DEFAULT_MULTIRES_RESOLUTIONS: tuple[float, ...] = (1.0, 0.5)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_multires_layers(
|
|
35
|
+
zarr_store: str | Path | MutableMapping[str, bytes],
|
|
36
|
+
*,
|
|
37
|
+
session: StorageSession,
|
|
38
|
+
resolutions: Iterable[float] = DEFAULT_MULTIRES_RESOLUTIONS,
|
|
39
|
+
time_dim_name: str = "timestamp",
|
|
40
|
+
silent: bool = True,
|
|
41
|
+
logger: logging.Logger | None = None,
|
|
42
|
+
group: str | None = None,
|
|
43
|
+
strict: bool = False,
|
|
44
|
+
ds_input: xr.Dataset | None = None,
|
|
45
|
+
) -> list[str]:
|
|
46
|
+
"""Build coarsened multi-resolution layers inside an existing Zarr store.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
zarr_store:
|
|
51
|
+
Store path or mapping. When a group is provided, zarr_store is assumed
|
|
52
|
+
to point at the *root* of the product and `group` selects the dataset.
|
|
53
|
+
group:
|
|
54
|
+
Optional group path (e.g. "F048/FWI"). When omitted, the function
|
|
55
|
+
attempts to open the root dataset directly. Pass ``group`` explicitly
|
|
56
|
+
if the store uses a grouped layout.
|
|
57
|
+
strict:
|
|
58
|
+
When True, failures to open the base dataset are raised instead of
|
|
59
|
+
being converted into an empty result. This is useful for CLI/API
|
|
60
|
+
operations where a failure should abort the request.
|
|
61
|
+
ds_input:
|
|
62
|
+
Optional xarray Dataset to coarsen. When provided, the function coarsens
|
|
63
|
+
only this data and appends it to multiresolution layers (instead of
|
|
64
|
+
rebuilding everything from the store).
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
groups_written: list[str] = []
|
|
68
|
+
|
|
69
|
+
# Normalize resolutions
|
|
70
|
+
if resolutions is True:
|
|
71
|
+
resolutions = list(DEFAULT_MULTIRES_RESOLUTIONS)
|
|
72
|
+
elif isinstance(resolutions, str):
|
|
73
|
+
try:
|
|
74
|
+
# Handle "1.0,0.5" or similar
|
|
75
|
+
parsed = [float(r.strip()) for r in resolutions.split(",") if r.strip()]
|
|
76
|
+
except (ValueError, TypeError) as exc:
|
|
77
|
+
raise ValueError(f"Invalid resolutions: {resolutions!r}") from exc
|
|
78
|
+
if not parsed:
|
|
79
|
+
raise ValueError(f"Invalid resolutions: {resolutions!r}")
|
|
80
|
+
resolutions = parsed
|
|
81
|
+
|
|
82
|
+
if not resolutions or not isinstance(resolutions, Iterable):
|
|
83
|
+
return groups_written
|
|
84
|
+
|
|
85
|
+
def _get_base_ds(
|
|
86
|
+
z_store: str,
|
|
87
|
+
grp: str | None,
|
|
88
|
+
) -> xr.Dataset:
|
|
89
|
+
z_uri = StorageUri.parse(z_store)
|
|
90
|
+
open_kwargs = {"chunks": "auto", "consolidated": False}
|
|
91
|
+
if grp:
|
|
92
|
+
return session.zarr.open_dataset(z_uri, **open_kwargs, group=grp)
|
|
93
|
+
try:
|
|
94
|
+
return session.zarr.open_dataset(z_uri, **open_kwargs)
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
raise ValueError(
|
|
97
|
+
f"Cannot open root dataset at {z_store!r}. "
|
|
98
|
+
"If the store uses a grouped layout, pass the group explicitly."
|
|
99
|
+
) from exc
|
|
100
|
+
|
|
101
|
+
ds_base: xr.Dataset
|
|
102
|
+
is_incremental = False
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
if ds_input is not None:
|
|
106
|
+
ds_base = ds_input
|
|
107
|
+
is_incremental = True
|
|
108
|
+
else:
|
|
109
|
+
ds_base = _get_base_ds(str(zarr_store), group)
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
if logger and not silent:
|
|
112
|
+
logger.warning("Failed to open base dataset for multires: %s", exc)
|
|
113
|
+
if strict:
|
|
114
|
+
raise
|
|
115
|
+
return groups_written
|
|
116
|
+
|
|
117
|
+
ds_base = ds_base.sortby(["lat", "lon"])
|
|
118
|
+
|
|
119
|
+
dlat = (
|
|
120
|
+
float(abs(ds_base.lat.diff("lat").mean().values))
|
|
121
|
+
if ds_base.sizes.get("lat", 0) > 1
|
|
122
|
+
else 1.0
|
|
123
|
+
)
|
|
124
|
+
dlon = (
|
|
125
|
+
float(abs(ds_base.lon.diff("lon").mean().values))
|
|
126
|
+
if ds_base.sizes.get("lon", 0) > 1
|
|
127
|
+
else 1.0
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
for res in resolutions:
|
|
131
|
+
lat_factor = max(1, round(res / dlat))
|
|
132
|
+
lon_factor = max(1, round(res / dlon))
|
|
133
|
+
|
|
134
|
+
# When a group is provided, write multiresolution layers under that
|
|
135
|
+
# group (e.g. F048/multires/1.0deg). Otherwise, default to a top-level
|
|
136
|
+
# multires/ path.
|
|
137
|
+
prefix = f"{group.rstrip('/')}/" if group else ""
|
|
138
|
+
group_name = f"{prefix}multires/{res}deg"
|
|
139
|
+
|
|
140
|
+
# Decide mode: if incremental and group already exists, append.
|
|
141
|
+
mode: Literal["w", "a"] = "w"
|
|
142
|
+
if is_incremental:
|
|
143
|
+
try:
|
|
144
|
+
base_s = str(zarr_store).rstrip("/")
|
|
145
|
+
target_uri = f"{base_s}/{group_name}"
|
|
146
|
+
if session.exists(StorageUri.parse(target_uri)):
|
|
147
|
+
mode = "a"
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
if logger and not silent:
|
|
152
|
+
msg = "Building multi-resolution layer"
|
|
153
|
+
if mode == "a":
|
|
154
|
+
msg = "Appending to multi-resolution layer"
|
|
155
|
+
logger.info(
|
|
156
|
+
msg,
|
|
157
|
+
extra={
|
|
158
|
+
"group": group_name,
|
|
159
|
+
"lat_factor": lat_factor,
|
|
160
|
+
"lon_factor": lon_factor,
|
|
161
|
+
"mode": mode,
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
ds_coarse = cast(
|
|
166
|
+
Any, ds_base.coarsen(lat=lat_factor, lon=lon_factor, boundary="trim")
|
|
167
|
+
).mean(skipna=True)
|
|
168
|
+
|
|
169
|
+
session.zarr.write_dataset(
|
|
170
|
+
ds_coarse,
|
|
171
|
+
StorageUri.parse(str(zarr_store)),
|
|
172
|
+
group=group_name,
|
|
173
|
+
mode=mode,
|
|
174
|
+
append_dim=time_dim_name if mode == "a" else None,
|
|
175
|
+
zarr_format=3,
|
|
176
|
+
safe_chunks=False,
|
|
177
|
+
align_chunks=True,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if logger and not silent:
|
|
181
|
+
logger.info("Multi-resolution layer written", extra={"layer": group_name})
|
|
182
|
+
groups_written.append(group_name)
|
|
183
|
+
|
|
184
|
+
return groups_written
|
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
"""Core helpers for building multi-resolution Zarr layers.
|
|
16
|
+
|
|
17
|
+
This module provides a generic, storage-aware multiresolution builder
|
|
18
|
+
that can operate on any existing Zarr product using the shared
|
|
19
|
+
StorageConfig and ChunkManager infrastructure.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Any, cast
|
|
27
|
+
|
|
28
|
+
from firecube.core.config import StorageConfig
|
|
29
|
+
from firecube.core.controlplane import ChunkManager
|
|
30
|
+
from firecube.core.product.identity import ProductIdentity
|
|
31
|
+
from firecube.core.product.target import ProductTarget
|
|
32
|
+
from firecube.core.storage.binding import StorageBinding
|
|
33
|
+
from firecube.core.storage.driver_config import StorageDriverConfig
|
|
34
|
+
from firecube.core.storage.session import StorageSession
|
|
35
|
+
from firecube.core.storage.uri import StorageUri
|
|
36
|
+
from firecube.core.zarr.layers import build_multires_layers
|
|
37
|
+
|
|
38
|
+
log = logging.getLogger("firecube.core.zarr.multires")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class MultiresConfig:
|
|
43
|
+
"""Configuration for building multiresolution Zarr layers."""
|
|
44
|
+
|
|
45
|
+
product: str
|
|
46
|
+
group: str
|
|
47
|
+
resolutions: list[float]
|
|
48
|
+
time_dim_name: str = "timestamp"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ZarrMultiresBuilder:
|
|
52
|
+
"""Generic multiresolution builder for Zarr products.
|
|
53
|
+
|
|
54
|
+
This operates on an existing Zarr product by:
|
|
55
|
+
- resolving the store URI from StorageConfig
|
|
56
|
+
- opening the target group
|
|
57
|
+
- building multiresolution views using shared zarr_utils helpers
|
|
58
|
+
|
|
59
|
+
It is intentionally generic; product-specific helpers (e.g. ExampleProductMultires)
|
|
60
|
+
should build MultiresConfig instances and call .build().
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
storage_config: StorageConfig,
|
|
66
|
+
*,
|
|
67
|
+
product_name: str,
|
|
68
|
+
product_uri: StorageUri | None = None,
|
|
69
|
+
chunk_manager: ChunkManager | None = None,
|
|
70
|
+
session: StorageSession | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
if not product_name:
|
|
73
|
+
raise ValueError("ZarrMultiresBuilder requires a non-empty product_name.")
|
|
74
|
+
self.storage_config = storage_config
|
|
75
|
+
self.product_name = product_name
|
|
76
|
+
self.session = session
|
|
77
|
+
resolved_product_uri = product_uri or (
|
|
78
|
+
session.product.product_uri if session is not None else None
|
|
79
|
+
)
|
|
80
|
+
if resolved_product_uri is None:
|
|
81
|
+
raise ValueError("ZarrMultiresBuilder requires product_uri or session.")
|
|
82
|
+
self.product_uri: StorageUri = resolved_product_uri
|
|
83
|
+
driver_config = StorageDriverConfig.from_storage_config(storage_config)
|
|
84
|
+
# This binding intentionally uses the sentinel identity
|
|
85
|
+
# "__firecube_controlplane__" so multires can address the cube root as a
|
|
86
|
+
# product-agnostic control-plane scope instead of a real product name.
|
|
87
|
+
# The sentinel bypasses the product-name short-circuit in
|
|
88
|
+
# _ControlRootResolver. ChunkManager is currently instantiated but not
|
|
89
|
+
# used in build(); it is reserved here for future control-plane hooks.
|
|
90
|
+
self.chunk_manager = chunk_manager or ChunkManager(
|
|
91
|
+
binding=StorageBinding(
|
|
92
|
+
identity=ProductIdentity.from_uri(
|
|
93
|
+
self.product_uri.parent().join("__firecube_controlplane__"),
|
|
94
|
+
"zarr",
|
|
95
|
+
product_name="__firecube_controlplane__",
|
|
96
|
+
),
|
|
97
|
+
driver=driver_config,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def _store_uri_for(self, product: str) -> str:
|
|
102
|
+
"""Build the underlying store URI for a given product."""
|
|
103
|
+
product_uri = (
|
|
104
|
+
self.product_uri
|
|
105
|
+
if product.strip("/") == self.product_name
|
|
106
|
+
else self.product_uri.parent().join(product)
|
|
107
|
+
)
|
|
108
|
+
return product_uri.to_str()
|
|
109
|
+
|
|
110
|
+
def _session_for(self, cfg: MultiresConfig) -> StorageSession:
|
|
111
|
+
if self.session is not None:
|
|
112
|
+
return self.session
|
|
113
|
+
|
|
114
|
+
driver_config = StorageDriverConfig.from_storage_config(self.storage_config)
|
|
115
|
+
resolved_product = ProductTarget.resolve(
|
|
116
|
+
cfg.product,
|
|
117
|
+
driver_config,
|
|
118
|
+
product_name=cfg.product,
|
|
119
|
+
plugin_default_format="zarr",
|
|
120
|
+
default_base_uri=self.product_uri.parent(),
|
|
121
|
+
)
|
|
122
|
+
return StorageSession(
|
|
123
|
+
StorageBinding(
|
|
124
|
+
identity=ProductIdentity(
|
|
125
|
+
product_name=resolved_product.product_name,
|
|
126
|
+
product_uri=resolved_product.product_uri,
|
|
127
|
+
control_root_uri=resolved_product.control_root_uri,
|
|
128
|
+
format=resolved_product.format,
|
|
129
|
+
),
|
|
130
|
+
driver=driver_config,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def build(self, cfg: MultiresConfig) -> dict:
|
|
135
|
+
"""Build multiresolution layers for the given configuration."""
|
|
136
|
+
if not cfg.resolutions:
|
|
137
|
+
raise ValueError("At least one resolution must be provided for multires.")
|
|
138
|
+
|
|
139
|
+
store_uri = self._store_uri_for(cfg.product)
|
|
140
|
+
log.info(
|
|
141
|
+
"Building Zarr multiresolution layers",
|
|
142
|
+
extra={
|
|
143
|
+
"product": cfg.product,
|
|
144
|
+
"group": cfg.group,
|
|
145
|
+
"store_uri": store_uri,
|
|
146
|
+
"resolutions": cfg.resolutions,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Delegate to shared helper
|
|
151
|
+
build_fn = cast(Any, build_multires_layers)
|
|
152
|
+
layers = build_fn(
|
|
153
|
+
store_uri,
|
|
154
|
+
resolutions=cfg.resolutions,
|
|
155
|
+
silent=False,
|
|
156
|
+
logger=log,
|
|
157
|
+
group=cfg.group,
|
|
158
|
+
strict=True,
|
|
159
|
+
session=self._session_for(cfg),
|
|
160
|
+
time_dim_name=cfg.time_dim_name,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
log.info(
|
|
164
|
+
"Zarr multiresolution build complete",
|
|
165
|
+
extra={
|
|
166
|
+
"product": cfg.product,
|
|
167
|
+
"group": cfg.group,
|
|
168
|
+
"store_uri": store_uri,
|
|
169
|
+
"layers": layers,
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"product": cfg.product,
|
|
175
|
+
"group": cfg.group,
|
|
176
|
+
"store_uri": store_uri,
|
|
177
|
+
"layers": layers,
|
|
178
|
+
}
|