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,200 @@
|
|
|
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 scrub orchestration (mutating) used by CLI and API.
|
|
16
|
+
|
|
17
|
+
This module *acts* on validation results: it maps debris chunk keys back to
|
|
18
|
+
manifest entries and deletes them via ChunkManager.
|
|
19
|
+
|
|
20
|
+
Keep this separate from `firecube.core.zarr.validation`, which is read-only.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
|
|
27
|
+
from firecube.core.controlplane import ChunkInfo, ChunkManager
|
|
28
|
+
from firecube.core.filesystem import create_filesystem
|
|
29
|
+
from firecube.core.storage.binding import StorageBinding
|
|
30
|
+
from firecube.core.storage.session import StorageSession
|
|
31
|
+
from firecube.core.zarr.validation import validate_group_with_fs
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ScrubResult:
|
|
36
|
+
"""Result of a scrub operation for a single group."""
|
|
37
|
+
|
|
38
|
+
product: str
|
|
39
|
+
group: str
|
|
40
|
+
extra_chunks: list[str]
|
|
41
|
+
deleted_chunks: int
|
|
42
|
+
deleted_size_bytes: int
|
|
43
|
+
storage_errors: list[str]
|
|
44
|
+
manifest_errors: list[str]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_scrub(
|
|
48
|
+
session: StorageSession,
|
|
49
|
+
group: str,
|
|
50
|
+
*,
|
|
51
|
+
max_chunks: int | None = None,
|
|
52
|
+
include_orphans: bool = False,
|
|
53
|
+
) -> ScrubResult:
|
|
54
|
+
"""Scrub debris chunks for a given array group using ChunkManager.
|
|
55
|
+
|
|
56
|
+
The ``session`` carries the product binding (URI, driver, credentials);
|
|
57
|
+
no string-concat URI building or raw credential dicts live here.
|
|
58
|
+
|
|
59
|
+
This helper:
|
|
60
|
+
1) Validates the group to find out-of-range chunks.
|
|
61
|
+
2) Maps those chunk paths back to manifest keys.
|
|
62
|
+
3) Uses ChunkManager to delete only matching chunks from storage
|
|
63
|
+
and manifests.
|
|
64
|
+
"""
|
|
65
|
+
product_uri = session.product.product_uri
|
|
66
|
+
product_name = session.product.product_name
|
|
67
|
+
fs = session.fs()
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
report = validate_group_with_fs(
|
|
71
|
+
fs,
|
|
72
|
+
product_uri,
|
|
73
|
+
group,
|
|
74
|
+
)
|
|
75
|
+
except ValueError as exc:
|
|
76
|
+
if "is a Zarr group (container)" not in str(exc):
|
|
77
|
+
raise
|
|
78
|
+
return ScrubResult(
|
|
79
|
+
product=product_name,
|
|
80
|
+
group=group,
|
|
81
|
+
extra_chunks=[],
|
|
82
|
+
deleted_chunks=0,
|
|
83
|
+
deleted_size_bytes=0,
|
|
84
|
+
storage_errors=[],
|
|
85
|
+
manifest_errors=[],
|
|
86
|
+
)
|
|
87
|
+
extra_paths = list(report.extra_chunks)
|
|
88
|
+
|
|
89
|
+
if max_chunks is not None and max_chunks >= 0:
|
|
90
|
+
extra_paths = extra_paths[:max_chunks]
|
|
91
|
+
|
|
92
|
+
prefix = f"{product_name}/"
|
|
93
|
+
manifest_keys: list[str] = []
|
|
94
|
+
for path in extra_paths:
|
|
95
|
+
key = path
|
|
96
|
+
if prefix in path:
|
|
97
|
+
key = path.split(prefix, 1)[1]
|
|
98
|
+
manifest_keys.append(key)
|
|
99
|
+
|
|
100
|
+
if not manifest_keys:
|
|
101
|
+
return ScrubResult(
|
|
102
|
+
product=product_name,
|
|
103
|
+
group=group,
|
|
104
|
+
extra_chunks=[],
|
|
105
|
+
deleted_chunks=0,
|
|
106
|
+
deleted_size_bytes=0,
|
|
107
|
+
storage_errors=[],
|
|
108
|
+
manifest_errors=[],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
binding = StorageBinding(identity=session.product, driver=session.driver)
|
|
112
|
+
manager = ChunkManager(binding=binding, filesystem=create_filesystem(binding))
|
|
113
|
+
try:
|
|
114
|
+
all_chunks: list[ChunkInfo] = []
|
|
115
|
+
for key in manifest_keys:
|
|
116
|
+
matches = manager.list_chunks(pattern=key, product=product_name)
|
|
117
|
+
all_chunks.extend(matches)
|
|
118
|
+
|
|
119
|
+
seen = set()
|
|
120
|
+
unique_chunks: list[ChunkInfo] = []
|
|
121
|
+
for chunk in all_chunks:
|
|
122
|
+
k = (chunk.product, chunk.key, chunk.manifest_path)
|
|
123
|
+
if k not in seen:
|
|
124
|
+
seen.add(k)
|
|
125
|
+
unique_chunks.append(chunk)
|
|
126
|
+
|
|
127
|
+
total_deleted_chunks = 0
|
|
128
|
+
total_deleted_size = 0
|
|
129
|
+
storage_errors: list[str] = []
|
|
130
|
+
manifest_errors: list[str] = []
|
|
131
|
+
|
|
132
|
+
if unique_chunks:
|
|
133
|
+
plan = manager.create_deletion_plan()
|
|
134
|
+
plan.chunks = unique_chunks
|
|
135
|
+
plan.total_size = sum(c.size for c in unique_chunks)
|
|
136
|
+
plan.products_affected = {product_name}
|
|
137
|
+
plan.manifest_files = {c.manifest_path for c in unique_chunks}
|
|
138
|
+
|
|
139
|
+
result = manager.execute_deletion(
|
|
140
|
+
plan,
|
|
141
|
+
delete_storage=True,
|
|
142
|
+
delete_manifest=True,
|
|
143
|
+
storage_config=None,
|
|
144
|
+
dry_run=False,
|
|
145
|
+
)
|
|
146
|
+
total_deleted_chunks += result.get("deleted_chunks", 0)
|
|
147
|
+
total_deleted_size += result.get("deleted_size_bytes", 0)
|
|
148
|
+
storage_errors.extend(result.get("storage_errors", []))
|
|
149
|
+
manifest_errors.extend(result.get("manifest_errors", []))
|
|
150
|
+
|
|
151
|
+
if include_orphans:
|
|
152
|
+
manifest_key_set = {c.key for c in unique_chunks}
|
|
153
|
+
orphan_keys = [k for k in manifest_keys if k not in manifest_key_set]
|
|
154
|
+
|
|
155
|
+
if orphan_keys:
|
|
156
|
+
from time import time as _time
|
|
157
|
+
|
|
158
|
+
now_ts = _time()
|
|
159
|
+
orphan_chunks: list[ChunkInfo] = [
|
|
160
|
+
ChunkInfo(
|
|
161
|
+
key=key,
|
|
162
|
+
product=product_name,
|
|
163
|
+
chunk_type="chunk",
|
|
164
|
+
size=0,
|
|
165
|
+
timestamp=now_ts,
|
|
166
|
+
manifest_path="",
|
|
167
|
+
)
|
|
168
|
+
for key in orphan_keys
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
orphan_plan = manager.create_deletion_plan()
|
|
172
|
+
orphan_plan.chunks = orphan_chunks
|
|
173
|
+
orphan_plan.total_size = 0
|
|
174
|
+
orphan_plan.products_affected = {product_name}
|
|
175
|
+
orphan_plan.manifest_files = set()
|
|
176
|
+
|
|
177
|
+
orphan_result = manager.execute_deletion(
|
|
178
|
+
orphan_plan,
|
|
179
|
+
delete_storage=True,
|
|
180
|
+
delete_manifest=False,
|
|
181
|
+
storage_config=None,
|
|
182
|
+
dry_run=False,
|
|
183
|
+
)
|
|
184
|
+
if not orphan_result.get("storage_errors"):
|
|
185
|
+
total_deleted_chunks += len(orphan_keys)
|
|
186
|
+
total_deleted_size += orphan_result.get("deleted_size_bytes", 0)
|
|
187
|
+
storage_errors.extend(orphan_result.get("storage_errors", []))
|
|
188
|
+
manifest_errors.extend(orphan_result.get("manifest_errors", []))
|
|
189
|
+
|
|
190
|
+
return ScrubResult(
|
|
191
|
+
product=product_name,
|
|
192
|
+
group=group,
|
|
193
|
+
extra_chunks=manifest_keys,
|
|
194
|
+
deleted_chunks=total_deleted_chunks,
|
|
195
|
+
deleted_size_bytes=total_deleted_size,
|
|
196
|
+
storage_errors=storage_errors,
|
|
197
|
+
manifest_errors=manifest_errors,
|
|
198
|
+
)
|
|
199
|
+
finally:
|
|
200
|
+
manager.close()
|
|
@@ -0,0 +1,258 @@
|
|
|
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
|
+
"""Shared helpers for Firecube-managed Zarr state arrays.
|
|
16
|
+
|
|
17
|
+
This module centralizes the semantics around the per-timestamp state array
|
|
18
|
+
(`firecube_timestamp_state`) used by span-based deletion/resume logic.
|
|
19
|
+
|
|
20
|
+
It is intentionally small and "dumb": callers decide *when* to create/update
|
|
21
|
+
the state array; this module only provides safe primitives.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from collections.abc import Sequence
|
|
27
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
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
|
+
|
|
35
|
+
DEFAULT_STATE_MEANING: dict[str, str] = {
|
|
36
|
+
"0": "unknown",
|
|
37
|
+
"1": "present",
|
|
38
|
+
"2": "deleted_by_firecube",
|
|
39
|
+
"3": "failed_batch",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _session_zarr_store(
|
|
44
|
+
*,
|
|
45
|
+
store_uri: str,
|
|
46
|
+
storage_config: StorageConfig,
|
|
47
|
+
mode: str,
|
|
48
|
+
) -> ZarrStoreHandle:
|
|
49
|
+
from firecube.core.product.identity import ProductIdentity
|
|
50
|
+
from firecube.core.storage.binding import StorageBinding
|
|
51
|
+
from firecube.core.storage.driver_config import StorageDriverConfig
|
|
52
|
+
from firecube.core.storage.session import StorageSession
|
|
53
|
+
|
|
54
|
+
uri = storage_uri_from_target(store_uri)
|
|
55
|
+
session = StorageSession(
|
|
56
|
+
StorageBinding(
|
|
57
|
+
identity=ProductIdentity.from_uri(uri, format="zarr", product_name=store_uri),
|
|
58
|
+
driver=StorageDriverConfig.from_storage_config(storage_config),
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
return session.zarr.create_store(uri=uri, mode=mode)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def attach_timestamp_state_dataset(
|
|
65
|
+
dataset: Any,
|
|
66
|
+
*,
|
|
67
|
+
dim: str = "timestamp",
|
|
68
|
+
var_name: str = "firecube_timestamp_state",
|
|
69
|
+
present_value: int = 1,
|
|
70
|
+
meaning: dict[str, str] | None = None,
|
|
71
|
+
) -> Any:
|
|
72
|
+
"""Attach a per-timestamp state variable to an xarray Dataset (best effort).
|
|
73
|
+
|
|
74
|
+
This is used by ingestors so span-based tooling (delete spans / scrub) can
|
|
75
|
+
update state without relying on plugin-specific semantics.
|
|
76
|
+
"""
|
|
77
|
+
meaning = meaning or DEFAULT_STATE_MEANING
|
|
78
|
+
try:
|
|
79
|
+
import numpy as np
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
raise RuntimeError("numpy required for timestamp state dataset helper") from exc
|
|
82
|
+
|
|
83
|
+
if dataset is None:
|
|
84
|
+
return dataset
|
|
85
|
+
|
|
86
|
+
length = 0
|
|
87
|
+
try:
|
|
88
|
+
length = int(getattr(dataset, "sizes", {}).get(dim, 0))
|
|
89
|
+
except Exception:
|
|
90
|
+
length = 0
|
|
91
|
+
if length <= 0:
|
|
92
|
+
return dataset
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
state_vals = np.full((length,), np.uint8(int(present_value)), dtype=np.uint8)
|
|
96
|
+
dataset[var_name] = ((dim,), state_vals)
|
|
97
|
+
attrs = getattr(dataset[var_name], "attrs", None)
|
|
98
|
+
if isinstance(attrs, dict):
|
|
99
|
+
attrs.update({"firecube_meaning": dict(meaning)})
|
|
100
|
+
else:
|
|
101
|
+
dataset[var_name].attrs.update({"firecube_meaning": dict(meaning)})
|
|
102
|
+
except Exception:
|
|
103
|
+
return dataset
|
|
104
|
+
|
|
105
|
+
return dataset
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _normalize_time_index_ranges(ranges: Sequence[Any]) -> list[tuple[int, int]]:
|
|
109
|
+
normalized: list[tuple[int, int]] = []
|
|
110
|
+
for pair in ranges:
|
|
111
|
+
if not (isinstance(pair, (list, tuple)) and len(pair) == 2):
|
|
112
|
+
continue
|
|
113
|
+
try:
|
|
114
|
+
start = int(pair[0])
|
|
115
|
+
end = int(pair[1])
|
|
116
|
+
except Exception:
|
|
117
|
+
continue
|
|
118
|
+
if end < start:
|
|
119
|
+
continue
|
|
120
|
+
normalized.append((start, end))
|
|
121
|
+
return normalized
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def expand_time_index_ranges_to_chunk_boundaries(
|
|
125
|
+
ranges: Sequence[Any],
|
|
126
|
+
*,
|
|
127
|
+
chunk_len: int,
|
|
128
|
+
length: int,
|
|
129
|
+
) -> list[list[int]]:
|
|
130
|
+
"""Expand time index ranges to full chunk coverage boundaries.
|
|
131
|
+
|
|
132
|
+
This is used when deleting whole chunks for spans that are not aligned:
|
|
133
|
+
the state array should be updated for the full removed chunk coverage.
|
|
134
|
+
"""
|
|
135
|
+
chunk_len = int(chunk_len or 0)
|
|
136
|
+
length = int(length or 0)
|
|
137
|
+
if chunk_len <= 0 or length <= 0:
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
normalized = _normalize_time_index_ranges(ranges)
|
|
141
|
+
if not normalized:
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
chunk_indices = set()
|
|
145
|
+
for start, end in normalized:
|
|
146
|
+
start_idx = start // chunk_len
|
|
147
|
+
end_idx = end // chunk_len
|
|
148
|
+
for idx in range(start_idx, end_idx + 1):
|
|
149
|
+
chunk_indices.add(idx)
|
|
150
|
+
|
|
151
|
+
expanded: list[list[int]] = []
|
|
152
|
+
for idx in sorted(i for i in chunk_indices if i >= 0):
|
|
153
|
+
start = idx * chunk_len
|
|
154
|
+
end = min(length - 1, (idx + 1) * chunk_len - 1)
|
|
155
|
+
if end >= start:
|
|
156
|
+
expanded.append([start, end])
|
|
157
|
+
return expanded
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def ensure_timestamp_state_array(
|
|
161
|
+
*,
|
|
162
|
+
store_uri: str,
|
|
163
|
+
array_path: str,
|
|
164
|
+
length: int,
|
|
165
|
+
chunk_len: int,
|
|
166
|
+
dim: str = "timestamp",
|
|
167
|
+
storage_config: StorageConfig | None = None,
|
|
168
|
+
storage_options: dict[str, Any] | None = None,
|
|
169
|
+
present_value: int = 1,
|
|
170
|
+
fill_value: int = 0,
|
|
171
|
+
meaning: dict[str, str] | None = None,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Ensure a `uint8` timestamp state array exists (best effort)."""
|
|
174
|
+
length = int(length or 0)
|
|
175
|
+
if length <= 0:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
import numpy as np
|
|
180
|
+
import zarr
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
raise RuntimeError("zarr+numpy required for timestamp state updates") from exc
|
|
183
|
+
|
|
184
|
+
meaning = meaning or DEFAULT_STATE_MEANING
|
|
185
|
+
chunk_len = int(chunk_len or length)
|
|
186
|
+
chunk_len = max(1, min(chunk_len, length))
|
|
187
|
+
|
|
188
|
+
if storage_config is not None:
|
|
189
|
+
handle = _session_zarr_store(store_uri=store_uri, storage_config=storage_config, mode="a")
|
|
190
|
+
root = zarr.open_group(**handle.zarr_kwargs(), mode="a", zarr_format=3)
|
|
191
|
+
else:
|
|
192
|
+
root = zarr.open_group(store=store_uri, mode="a", storage_options=storage_options)
|
|
193
|
+
parts = str(array_path).strip("/").split("/")
|
|
194
|
+
grp = root
|
|
195
|
+
for part in parts[:-1]:
|
|
196
|
+
grp = grp.require_group(part)
|
|
197
|
+
|
|
198
|
+
name = parts[-1]
|
|
199
|
+
if name in grp:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
arr = grp.create_array(
|
|
204
|
+
name,
|
|
205
|
+
shape=(length,),
|
|
206
|
+
dtype="u1",
|
|
207
|
+
chunks=(chunk_len,),
|
|
208
|
+
fill_value=int(fill_value),
|
|
209
|
+
dimension_names=(dim,),
|
|
210
|
+
overwrite=False,
|
|
211
|
+
)
|
|
212
|
+
arr[:] = np.uint8(int(present_value))
|
|
213
|
+
arr.attrs.update({"firecube_meaning": dict(meaning)})
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
# Zarr v3 raises ContainsArrayError if overwrite=False.
|
|
216
|
+
# Fallback to existing if someone else created it in the meantime.
|
|
217
|
+
if "ContainsArrayError" in type(exc).__name__ or "exists" in str(exc).lower():
|
|
218
|
+
return
|
|
219
|
+
raise
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def update_timestamp_state(
|
|
223
|
+
*,
|
|
224
|
+
store_uri: str,
|
|
225
|
+
array_path: str,
|
|
226
|
+
time_index_ranges: Sequence[Any],
|
|
227
|
+
value: int,
|
|
228
|
+
storage_config: StorageConfig | None = None,
|
|
229
|
+
storage_options: dict[str, Any] | None = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Best-effort update of a per-timestamp state array (uint8)."""
|
|
232
|
+
normalized = _normalize_time_index_ranges(time_index_ranges)
|
|
233
|
+
if not normalized:
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
import numpy as np
|
|
238
|
+
import zarr
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
raise RuntimeError("zarr+numpy required for timestamp state updates") from exc
|
|
241
|
+
|
|
242
|
+
if storage_config is not None:
|
|
243
|
+
handle = _session_zarr_store(store_uri=store_uri, storage_config=storage_config, mode="a")
|
|
244
|
+
root = zarr.open_group(**handle.zarr_kwargs(), mode="a", zarr_format=3)
|
|
245
|
+
else:
|
|
246
|
+
root = zarr.open_group(store=store_uri, mode="a", storage_options=storage_options)
|
|
247
|
+
parts = str(array_path).strip("/").split("/")
|
|
248
|
+
grp = root
|
|
249
|
+
for part in parts[:-1]:
|
|
250
|
+
grp = grp.require_group(part)
|
|
251
|
+
name = parts[-1]
|
|
252
|
+
if name not in grp:
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
arr = grp[name]
|
|
256
|
+
state_val = np.uint8(int(value))
|
|
257
|
+
for start, end in normalized:
|
|
258
|
+
cast(Any, arr)[start : end + 1] = state_val
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
"""Self-describing time-array decode helper.
|
|
16
|
+
|
|
17
|
+
Dispatches on (dtype, attrs) — firecube's vocabulary for encoded time arrays.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from collections.abc import Mapping
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
__all__ = ["decode_time_array"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def decode_time_array(values: np.ndarray, attrs: Mapping[str, Any]) -> np.ndarray:
|
|
31
|
+
"""Return a ``datetime64`` array decoded from *values* using *attrs*.
|
|
32
|
+
|
|
33
|
+
The decoded array preserves its native resolution rather than being forced
|
|
34
|
+
to a fixed granularity. Coverage bounds and dedup keys are derived from this
|
|
35
|
+
output, so coarsening to seconds here would silently collapse distinct
|
|
36
|
+
sub-second timestamps into one (corrupting dedup/coverage); coarsening is
|
|
37
|
+
therefore left to the storage layer, which owns the on-disk precision
|
|
38
|
+
contract. The resolution that ``decode_cf_datetime`` selects is range-aware,
|
|
39
|
+
so this also avoids forcing a finer unit that could overflow for
|
|
40
|
+
out-of-range epochs.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
values = np.asarray(values)
|
|
44
|
+
|
|
45
|
+
if values.dtype.kind == "M":
|
|
46
|
+
return values
|
|
47
|
+
|
|
48
|
+
if values.dtype.kind in ("f", "i", "u"):
|
|
49
|
+
units = attrs.get("units") if attrs else None
|
|
50
|
+
if units is None:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Cannot decode numeric dtype {values.dtype!r}: no 'units' attr found. "
|
|
53
|
+
"Expected a units string like 'seconds since 1970-01-01'."
|
|
54
|
+
)
|
|
55
|
+
units_str = str(units)
|
|
56
|
+
if "since" not in units_str:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Cannot decode numeric dtype {values.dtype!r}: 'units' attr {units_str!r} "
|
|
59
|
+
"does not contain 'since'. Expected a reference-epoch string like "
|
|
60
|
+
"'seconds since 1970-01-01'."
|
|
61
|
+
)
|
|
62
|
+
from xarray.coding.times import decode_cf_datetime
|
|
63
|
+
|
|
64
|
+
calendar = str(attrs.get("calendar", "standard"))
|
|
65
|
+
decoded = decode_cf_datetime(values, units=units_str, calendar=calendar)
|
|
66
|
+
return np.asarray(decoded)
|
|
67
|
+
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Cannot decode time array with dtype {values.dtype!r}: not a datetime64 or "
|
|
70
|
+
"a numeric type with 'units' containing 'since'."
|
|
71
|
+
)
|