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,249 @@
|
|
|
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
|
+
"""Metadata seeding for staged-write mode.
|
|
16
|
+
|
|
17
|
+
When firecube writes through a local temp store before publishing to
|
|
18
|
+
the final target (staged mode), the temp store starts empty and has no
|
|
19
|
+
Zarr metadata. This module copies zarr.json files from the existing
|
|
20
|
+
final target into the temp store so that ``append_time_groups()`` reads
|
|
21
|
+
the correct write cursor and shape — preventing metadata corruption where
|
|
22
|
+
temp-store shape overwrites the cumulative target shape.
|
|
23
|
+
|
|
24
|
+
Best-effort for fresh ingests (no final target yet); strict (raises
|
|
25
|
+
``StagedMetadataError``) when resuming into an existing target.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
from typing import TYPE_CHECKING, Any
|
|
33
|
+
|
|
34
|
+
from firecube.core.storage.uri import StorageUri
|
|
35
|
+
from firecube.ingestor.errors import StagedMetadataError
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from firecube.core.storage.session import StorageSession
|
|
39
|
+
|
|
40
|
+
log = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def seed_staged_store_metadata(
|
|
44
|
+
*,
|
|
45
|
+
temp_store_uri: str,
|
|
46
|
+
final_target_uri: str,
|
|
47
|
+
groups: list[str] | None = None,
|
|
48
|
+
strict: bool = True,
|
|
49
|
+
session: StorageSession,
|
|
50
|
+
coordinate_arrays: list[str] | None = None,
|
|
51
|
+
) -> dict[str, Any]:
|
|
52
|
+
"""Copy zarr.json metadata files (and optionally named coordinate-array chunks) from final_target_uri into temp_store_uri.
|
|
53
|
+
|
|
54
|
+
Seeds the temp store with correct shape/dimension metadata so that
|
|
55
|
+
append_time_groups() starts write_cursor at the right position. By
|
|
56
|
+
default copies only zarr.json files (metadata).
|
|
57
|
+
|
|
58
|
+
When ``coordinate_arrays`` is a non-empty list of coord-array names,
|
|
59
|
+
also copies the chunk payloads of those arrays in every discovered
|
|
60
|
+
group that contains them. Used by the runtime hook to seed the
|
|
61
|
+
workspace timestamp coordinate so that plugin value-based
|
|
62
|
+
timestamp resolvers see real values instead of NaT on staged
|
|
63
|
+
re-ingest. Coordinate arrays missing in a group are tolerated (no-op).
|
|
64
|
+
Never copies data-array chunks.
|
|
65
|
+
|
|
66
|
+
All metadata reads from the final target use the driver-correct
|
|
67
|
+
``session.fs()`` filesystem; ``session`` is REQUIRED.
|
|
68
|
+
|
|
69
|
+
When ``strict=True`` (default), unexpected exceptions are wrapped as
|
|
70
|
+
``StagedMetadataError`` so callers that catch ``StagedMetadataError``
|
|
71
|
+
propagate hard failures while ``except Exception`` debug-suppressors do
|
|
72
|
+
not silently swallow them.
|
|
73
|
+
|
|
74
|
+
``coordinate_arrays=None`` and ``coordinate_arrays=[]`` are
|
|
75
|
+
semantically identical and both preserve exact legacy metadata-only
|
|
76
|
+
behavior.
|
|
77
|
+
|
|
78
|
+
Returns {group: {"seeded": bool, "files": int}} for logging.
|
|
79
|
+
"""
|
|
80
|
+
if groups is None:
|
|
81
|
+
groups = _discover_zarr_groups(session, final_target_uri)
|
|
82
|
+
|
|
83
|
+
results: dict[str, Any] = {}
|
|
84
|
+
|
|
85
|
+
for group in groups:
|
|
86
|
+
results[group] = {"seeded": False, "files": 0}
|
|
87
|
+
try:
|
|
88
|
+
_seed_group_via_session(
|
|
89
|
+
session=session,
|
|
90
|
+
temp_store_uri=temp_store_uri,
|
|
91
|
+
final_target_uri=final_target_uri,
|
|
92
|
+
group=group,
|
|
93
|
+
strict=strict,
|
|
94
|
+
results=results,
|
|
95
|
+
coordinate_arrays=coordinate_arrays,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if results[group]["files"] > 0:
|
|
99
|
+
results[group]["seeded"] = True
|
|
100
|
+
log.debug(
|
|
101
|
+
"Seeded %d %s for group %s",
|
|
102
|
+
results[group]["files"],
|
|
103
|
+
"metadata + coord files" if coordinate_arrays else "zarr.json files",
|
|
104
|
+
group,
|
|
105
|
+
)
|
|
106
|
+
except StagedMetadataError:
|
|
107
|
+
raise
|
|
108
|
+
except Exception as e:
|
|
109
|
+
if strict:
|
|
110
|
+
raise StagedMetadataError(
|
|
111
|
+
f"Staged metadata seeding failed unexpectedly for group {group}: {e}"
|
|
112
|
+
) from e
|
|
113
|
+
log.warning("Staged metadata seeding skipped for group %s: %s", group, e)
|
|
114
|
+
|
|
115
|
+
return results
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _discover_zarr_groups(session: StorageSession, uri: str) -> list[str]:
|
|
119
|
+
"""Discover top-level Zarr groups under ``uri`` from existing metadata."""
|
|
120
|
+
from firecube.core.uris import is_remote_target, local_path_from_target
|
|
121
|
+
|
|
122
|
+
fs = session.fs()
|
|
123
|
+
if is_remote_target(uri):
|
|
124
|
+
final_uri = StorageUri.parse(uri)
|
|
125
|
+
else:
|
|
126
|
+
final_uri = StorageUri.from_local_path(local_path_from_target(uri))
|
|
127
|
+
|
|
128
|
+
if not fs.exists(final_uri):
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
final_root = final_uri.path.rstrip("/")
|
|
132
|
+
groups: set[str] = set()
|
|
133
|
+
for entry in fs.find(final_uri):
|
|
134
|
+
entry_path = entry.path.rstrip("/")
|
|
135
|
+
if not (entry_path.endswith("/zarr.json") or entry_path.endswith("/.zgroup")):
|
|
136
|
+
continue
|
|
137
|
+
rel = (
|
|
138
|
+
entry_path[len(final_root) + 1 :]
|
|
139
|
+
if entry_path.startswith(f"{final_root}/")
|
|
140
|
+
else entry_path.lstrip("/")
|
|
141
|
+
)
|
|
142
|
+
parent = rel.rsplit("/", 1)[0] if "/" in rel else ""
|
|
143
|
+
if not parent:
|
|
144
|
+
continue
|
|
145
|
+
groups.add(parent.split("/", 1)[0])
|
|
146
|
+
|
|
147
|
+
return sorted(groups)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _is_coordinate_chunk(path: str, coords: list[str] | None) -> bool:
|
|
151
|
+
"""Return True if ``path`` is a chunk file under a named coord array.
|
|
152
|
+
|
|
153
|
+
Uses path-segment matching: splits on "/" and looks for the exact
|
|
154
|
+
sequence ``<coord>/c`` where ``<coord>`` is in ``coords``. Returns
|
|
155
|
+
False for the coord's ``zarr.json`` (already covered by the
|
|
156
|
+
metadata filter) and for arrays whose names contain a coord name
|
|
157
|
+
as a substring (e.g. ``timestamp_bounds`` for ``["timestamp"]``).
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
path: The entry path relative to or under the group root.
|
|
161
|
+
Trailing slashes are stripped.
|
|
162
|
+
coords: Coordinate array names to match (exact path segments).
|
|
163
|
+
``None`` and ``[]`` always return False (legacy mode).
|
|
164
|
+
|
|
165
|
+
Examples:
|
|
166
|
+
_is_coordinate_chunk("data/timestamp/c/0", ["timestamp"]) -> True
|
|
167
|
+
_is_coordinate_chunk("data/timestamp/c/0/0", ["timestamp"]) -> True
|
|
168
|
+
_is_coordinate_chunk("data/timestamp/zarr.json", ["timestamp"]) -> False
|
|
169
|
+
_is_coordinate_chunk("data/timestamp_bounds/c/0", ["timestamp"]) -> False
|
|
170
|
+
_is_coordinate_chunk("data/val/c/0", ["timestamp"]) -> False
|
|
171
|
+
_is_coordinate_chunk("data/timestamp/c/0", []) -> False
|
|
172
|
+
_is_coordinate_chunk("data/timestamp/c/0", None) -> False
|
|
173
|
+
"""
|
|
174
|
+
if not coords:
|
|
175
|
+
return False
|
|
176
|
+
parts = path.rstrip("/").split("/")
|
|
177
|
+
return any(parts[i] in coords and parts[i + 1] == "c" for i in range(len(parts) - 2))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _seed_group_via_session(
|
|
181
|
+
*,
|
|
182
|
+
session: StorageSession,
|
|
183
|
+
temp_store_uri: str,
|
|
184
|
+
final_target_uri: str,
|
|
185
|
+
group: str,
|
|
186
|
+
strict: bool,
|
|
187
|
+
results: dict[str, Any],
|
|
188
|
+
coordinate_arrays: list[str] | None = None,
|
|
189
|
+
) -> None:
|
|
190
|
+
from firecube.core.uris import is_remote_target, local_path_from_target
|
|
191
|
+
|
|
192
|
+
fs = session.fs()
|
|
193
|
+
if is_remote_target(final_target_uri):
|
|
194
|
+
final_uri = StorageUri.parse(final_target_uri)
|
|
195
|
+
else:
|
|
196
|
+
final_uri = StorageUri.from_local_path(local_path_from_target(final_target_uri))
|
|
197
|
+
group_uri = final_uri.join(group.strip("/"))
|
|
198
|
+
|
|
199
|
+
if not fs.exists(group_uri):
|
|
200
|
+
return # Fresh ingest — no final target yet
|
|
201
|
+
|
|
202
|
+
all_entries = fs.find(group_uri)
|
|
203
|
+
final_root = final_uri.path.rstrip("/")
|
|
204
|
+
files_to_copy = [
|
|
205
|
+
entry
|
|
206
|
+
for entry in all_entries
|
|
207
|
+
if entry.path.endswith("zarr.json")
|
|
208
|
+
or _is_coordinate_chunk(
|
|
209
|
+
entry.path[len(final_root) + 1 :]
|
|
210
|
+
if entry.path.startswith(f"{final_root}/")
|
|
211
|
+
else entry.path.lstrip("/"),
|
|
212
|
+
coordinate_arrays,
|
|
213
|
+
)
|
|
214
|
+
]
|
|
215
|
+
if not files_to_copy:
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
temp_root = local_path_from_target(temp_store_uri)
|
|
219
|
+
for src_uri in files_to_copy:
|
|
220
|
+
rel = (
|
|
221
|
+
src_uri.path[len(final_root) + 1 :]
|
|
222
|
+
if src_uri.path.startswith(f"{final_root}/")
|
|
223
|
+
else src_uri.path.lstrip("/")
|
|
224
|
+
)
|
|
225
|
+
dst_path = temp_root / rel
|
|
226
|
+
if dst_path.exists():
|
|
227
|
+
results[group]["files"] += 1
|
|
228
|
+
continue
|
|
229
|
+
try:
|
|
230
|
+
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
with fs.open(src_uri, "rb") as fh:
|
|
232
|
+
raw = fh.read()
|
|
233
|
+
if rel.endswith("zarr.json"):
|
|
234
|
+
payload = json.loads(raw)
|
|
235
|
+
if payload.get("node_type") == "array":
|
|
236
|
+
attrs = payload.get("attributes")
|
|
237
|
+
if isinstance(attrs, dict):
|
|
238
|
+
# If a second write-once reserved attr is added later, refactor _reserved_attrs.py into RESERVED_SHAPE_ATTRS (kept on seed) vs RESERVED_RUN_STATE_ATTRS (stripped on seed), and switch this strip from a literal key to a set membership check. YAGNI today (only one such attr exists).
|
|
239
|
+
attrs.pop("firecube_static_written", None)
|
|
240
|
+
dst_path.write_bytes(json.dumps(payload).encode())
|
|
241
|
+
else:
|
|
242
|
+
dst_path.write_bytes(raw)
|
|
243
|
+
results[group]["files"] += 1
|
|
244
|
+
except Exception as e:
|
|
245
|
+
if strict:
|
|
246
|
+
raise StagedMetadataError(
|
|
247
|
+
f"Failed to seed staged metadata {src_uri.to_str()} -> {dst_path}: {e}"
|
|
248
|
+
) from e
|
|
249
|
+
log.warning("Failed to seed staged metadata %s: %s", src_uri.to_str(), e)
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
"""Concrete Zarr write strategy implementations."""
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
"""AppendStrategy — AppendWriteStrategy wrapping ``append_time_groups()``.
|
|
16
|
+
|
|
17
|
+
Delegates entirely to the existing orchestrator in
|
|
18
|
+
``firecube.ingestor.runtime.zarr.append``. No append logic is reimplemented.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
25
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
26
|
+
|
|
27
|
+
import xarray as xr
|
|
28
|
+
|
|
29
|
+
from firecube.core.uris import storage_uri_from_target
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from firecube.core.config import StorageConfig
|
|
33
|
+
from firecube.core.filesystem.store_factory import ZarrStoreHandle
|
|
34
|
+
from firecube.core.storage.session import StorageSession
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _session_for_store(store_uri: str, storage_config: StorageConfig) -> StorageSession:
|
|
38
|
+
from firecube.core.product.identity import ProductIdentity
|
|
39
|
+
from firecube.core.storage.binding import StorageBinding
|
|
40
|
+
from firecube.core.storage.driver_config import StorageDriverConfig
|
|
41
|
+
from firecube.core.storage.session import StorageSession
|
|
42
|
+
|
|
43
|
+
uri = storage_uri_from_target(store_uri)
|
|
44
|
+
return StorageSession(
|
|
45
|
+
StorageBinding(
|
|
46
|
+
identity=ProductIdentity.from_uri(uri, format="zarr", product_name=store_uri),
|
|
47
|
+
driver=StorageDriverConfig.from_storage_config(storage_config),
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AppendStrategy:
|
|
53
|
+
"""Xarray-append write strategy for Zarr stores.
|
|
54
|
+
|
|
55
|
+
Wraps :func:`~firecube.ingestor.runtime.zarr.append.append_time_groups`
|
|
56
|
+
behind the ``AppendWriteStrategy`` Protocol so that
|
|
57
|
+
``GenericZarrIngestor`` can treat append and region writes uniformly.
|
|
58
|
+
|
|
59
|
+
All static configuration is captured at construction time; only
|
|
60
|
+
per-batch dynamic inputs are passed to :meth:`write_groups`.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
store: object,
|
|
67
|
+
store_uri: str | None = None,
|
|
68
|
+
store_handle: ZarrStoreHandle | None = None,
|
|
69
|
+
resume_target_uri: str | None = None,
|
|
70
|
+
arrays_for_group: Callable[[str], list[str]] | None = None,
|
|
71
|
+
chunk_shape: dict[str, int] | None = None,
|
|
72
|
+
shard_shape: dict[str, int] | None = None,
|
|
73
|
+
sharding: bool = False,
|
|
74
|
+
compression: bool | str = False,
|
|
75
|
+
consolidate: bool = False,
|
|
76
|
+
resume_existing: bool = False,
|
|
77
|
+
append_dim: str = "timestamp",
|
|
78
|
+
state_var_name: str = "firecube_timestamp_state",
|
|
79
|
+
state_deleted_value: int = 2,
|
|
80
|
+
logger: logging.Logger | None = None,
|
|
81
|
+
storage_config: StorageConfig | None = None,
|
|
82
|
+
session: StorageSession | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
# NOTE: append_dim is the already-resolved time dim name supplied by
|
|
85
|
+
# the caller (typically GenericZarrIngestor via _resolve_time_dim_name()).
|
|
86
|
+
# The strategy is host-free at runtime — never looks up the dim name
|
|
87
|
+
# from an ingestor object. This keeps the write protocol decoupled and
|
|
88
|
+
# the strategy testable in isolation.
|
|
89
|
+
self._store = store
|
|
90
|
+
self._store_uri = store_uri
|
|
91
|
+
self._store_handle = store_handle
|
|
92
|
+
self._resume_target_uri = resume_target_uri
|
|
93
|
+
self._storage_config = storage_config
|
|
94
|
+
self._arrays_for_group = arrays_for_group
|
|
95
|
+
self._chunk_shape = chunk_shape
|
|
96
|
+
self._shard_shape = shard_shape
|
|
97
|
+
self._sharding = sharding
|
|
98
|
+
self._compression = compression
|
|
99
|
+
self._consolidate = consolidate
|
|
100
|
+
self._resume_existing = resume_existing
|
|
101
|
+
self._append_dim = append_dim
|
|
102
|
+
self._state_var_name = state_var_name
|
|
103
|
+
self._state_deleted_value = state_deleted_value
|
|
104
|
+
self._logger = logger
|
|
105
|
+
self._session = session
|
|
106
|
+
|
|
107
|
+
def write_groups(
|
|
108
|
+
self,
|
|
109
|
+
*,
|
|
110
|
+
group_to_timestamps: Mapping[str, Sequence[Any]],
|
|
111
|
+
dataset_for_batch: Callable[[str, Sequence[Any]], xr.Dataset | None],
|
|
112
|
+
batch_size: int,
|
|
113
|
+
claim_for_group: Callable[[str], Any] | None = None,
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
"""Delegate to ``append_time_groups()`` and return its metrics dict."""
|
|
116
|
+
from firecube.ingestor.runtime.zarr.append import append_time_groups
|
|
117
|
+
|
|
118
|
+
effective_store = self._store
|
|
119
|
+
effective_zarr_store: Any = self._store_handle
|
|
120
|
+
effective_session: StorageSession | None = self._session
|
|
121
|
+
|
|
122
|
+
if effective_zarr_store is None and self._storage_config and self._store_uri:
|
|
123
|
+
if effective_session is None:
|
|
124
|
+
effective_session = _session_for_store(self._store_uri, self._storage_config)
|
|
125
|
+
effective_zarr_store = effective_session.zarr.create_store(
|
|
126
|
+
uri=storage_uri_from_target(self._store_uri),
|
|
127
|
+
mode="a",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if effective_zarr_store is not None:
|
|
131
|
+
typed_handle = cast(Any, effective_zarr_store)
|
|
132
|
+
effective_store = typed_handle.store
|
|
133
|
+
|
|
134
|
+
if effective_zarr_store is None:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
"AppendStrategy.write_groups requires either store_handle or "
|
|
137
|
+
"(storage_config + store_uri) to construct a ZarrStoreHandle."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
resume_zarr_store: Any = None
|
|
141
|
+
resume_session: Any = None
|
|
142
|
+
if self._resume_target_uri and self._storage_config:
|
|
143
|
+
resume_session = _session_for_store(self._resume_target_uri, self._storage_config)
|
|
144
|
+
resume_zarr_store = resume_session.zarr.create_store(
|
|
145
|
+
uri=storage_uri_from_target(self._resume_target_uri),
|
|
146
|
+
mode="r",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
append_time_groups_fn = cast(Any, append_time_groups)
|
|
150
|
+
append_dim = self._append_dim
|
|
151
|
+
return append_time_groups_fn(
|
|
152
|
+
store=effective_store,
|
|
153
|
+
zarr_store=effective_zarr_store,
|
|
154
|
+
group_to_timestamps=group_to_timestamps,
|
|
155
|
+
dataset_for_batch=dataset_for_batch,
|
|
156
|
+
resume_zarr_store=resume_zarr_store,
|
|
157
|
+
resume_session=resume_session,
|
|
158
|
+
arrays_for_group=self._arrays_for_group,
|
|
159
|
+
chunk_shape=self._chunk_shape,
|
|
160
|
+
shard_shape=self._shard_shape,
|
|
161
|
+
sharding=self._sharding,
|
|
162
|
+
compression=self._compression,
|
|
163
|
+
consolidate=self._consolidate,
|
|
164
|
+
resume_existing=self._resume_existing,
|
|
165
|
+
batch_size=batch_size,
|
|
166
|
+
append_dim=append_dim,
|
|
167
|
+
state_var_name=self._state_var_name,
|
|
168
|
+
state_deleted_value=self._state_deleted_value,
|
|
169
|
+
logger=self._logger,
|
|
170
|
+
claim_for_group=claim_for_group,
|
|
171
|
+
session=effective_session,
|
|
172
|
+
)
|