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,192 @@
|
|
|
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
|
+
"""ZIP archive helpers for ingestion plugins.
|
|
16
|
+
|
|
17
|
+
Reusable helpers for extracting and streaming HDF5 members from ZIP archives.
|
|
18
|
+
Plugin-agnostic; callers should depend on the ``firecube.core.formats`` public
|
|
19
|
+
re-exports rather than this submodule path.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import concurrent.futures
|
|
25
|
+
import logging
|
|
26
|
+
import tempfile
|
|
27
|
+
import zipfile
|
|
28
|
+
from collections.abc import Callable
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
log = logging.getLogger("firecube.core.formats")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def extract_hdf5_from_zip(
|
|
35
|
+
zip_path: Path, dest: Path, *, logger: logging.Logger | None = None
|
|
36
|
+
) -> Path | None:
|
|
37
|
+
"""Extract the first HDF5-like member from a ZIP archive into dest.
|
|
38
|
+
|
|
39
|
+
Selection:
|
|
40
|
+
- Prefer members ending with .h5
|
|
41
|
+
- Fallback: first non-directory member whose filename contains "HDF5"
|
|
42
|
+
"""
|
|
43
|
+
logger = logger or log
|
|
44
|
+
try:
|
|
45
|
+
with zipfile.ZipFile(zip_path) as zf:
|
|
46
|
+
members = [name for name in zf.namelist() if name.lower().endswith(".h5")]
|
|
47
|
+
if not members:
|
|
48
|
+
candidates = [
|
|
49
|
+
name
|
|
50
|
+
for name in zf.namelist()
|
|
51
|
+
if not name.endswith("/") and "HDF5" in Path(name).name.upper()
|
|
52
|
+
]
|
|
53
|
+
if not candidates:
|
|
54
|
+
return None
|
|
55
|
+
members = [candidates[0]]
|
|
56
|
+
member = members[0]
|
|
57
|
+
|
|
58
|
+
# Stream optimization: read to memory then write (often faster than extract()).
|
|
59
|
+
try:
|
|
60
|
+
hdf5_data = zf.read(member)
|
|
61
|
+
extracted_path = dest / member
|
|
62
|
+
extracted_path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
with extracted_path.open("wb") as handle:
|
|
64
|
+
handle.write(hdf5_data)
|
|
65
|
+
logger.debug(
|
|
66
|
+
"Streamed %s bytes from %s/%s", f"{len(hdf5_data):,}", zip_path.name, member
|
|
67
|
+
)
|
|
68
|
+
return extracted_path
|
|
69
|
+
except Exception as stream_exc:
|
|
70
|
+
logger.debug(
|
|
71
|
+
"Stream extraction failed for %s (%s); falling back to ZipFile.extract()",
|
|
72
|
+
zip_path,
|
|
73
|
+
stream_exc,
|
|
74
|
+
)
|
|
75
|
+
extracted_path = Path(zf.extract(member, dest))
|
|
76
|
+
return extracted_path
|
|
77
|
+
except zipfile.BadZipFile:
|
|
78
|
+
logger.warning("Invalid ZIP archive: %s", zip_path)
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def stream_hdf5_from_zip(zip_path: Path, *, logger: logging.Logger | None = None) -> bytes | None:
|
|
83
|
+
"""Stream HDF5 content directly from ZIP to memory (no temp files)."""
|
|
84
|
+
logger = logger or log
|
|
85
|
+
try:
|
|
86
|
+
with zipfile.ZipFile(zip_path) as zf:
|
|
87
|
+
members = [name for name in zf.namelist() if name.lower().endswith(".h5")]
|
|
88
|
+
if not members:
|
|
89
|
+
candidates = [
|
|
90
|
+
name
|
|
91
|
+
for name in zf.namelist()
|
|
92
|
+
if not name.endswith("/") and "HDF5" in Path(name).name.upper()
|
|
93
|
+
]
|
|
94
|
+
if not candidates:
|
|
95
|
+
return None
|
|
96
|
+
members = [candidates[0]]
|
|
97
|
+
member = members[0]
|
|
98
|
+
logger.debug("Streaming HDF5 data from %s/%s", zip_path.name, member)
|
|
99
|
+
return zf.read(member)
|
|
100
|
+
except zipfile.BadZipFile:
|
|
101
|
+
logger.warning("Invalid ZIP archive: %s", zip_path)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def extract_zip_files_parallel(
|
|
106
|
+
zip_files: list[Path],
|
|
107
|
+
*,
|
|
108
|
+
tempdir_factory: Callable[[], tempfile.TemporaryDirectory],
|
|
109
|
+
extract_fn: Callable[[Path, Path], Path | None] = extract_hdf5_from_zip,
|
|
110
|
+
max_workers: int = 4,
|
|
111
|
+
record_temp_dirs: bool = False,
|
|
112
|
+
temp_dir_map: dict[str, tempfile.TemporaryDirectory] | None = None,
|
|
113
|
+
logger: logging.Logger | None = None,
|
|
114
|
+
) -> tuple[list[Path], list[tempfile.TemporaryDirectory]]:
|
|
115
|
+
"""Extract ZIP files in parallel using ThreadPoolExecutor for I/O-bound operations.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
(resolved_paths, temp_dirs)
|
|
119
|
+
|
|
120
|
+
If record_temp_dirs=True and temp_dir_map is provided, it will map each
|
|
121
|
+
extracted file path (resolved) to the TemporaryDirectory that produced it.
|
|
122
|
+
"""
|
|
123
|
+
logger = logger or log
|
|
124
|
+
resolved: list[Path] = []
|
|
125
|
+
temp_dirs: list[tempfile.TemporaryDirectory] = []
|
|
126
|
+
|
|
127
|
+
if not zip_files:
|
|
128
|
+
return resolved, temp_dirs
|
|
129
|
+
|
|
130
|
+
def _maybe_record(path: Path, tmp: tempfile.TemporaryDirectory) -> None:
|
|
131
|
+
if not record_temp_dirs or temp_dir_map is None:
|
|
132
|
+
return
|
|
133
|
+
try:
|
|
134
|
+
temp_dir_map[str(path.resolve())] = tmp
|
|
135
|
+
except Exception:
|
|
136
|
+
temp_dir_map[str(path)] = tmp
|
|
137
|
+
|
|
138
|
+
# For small numbers of files, don't bother with parallelism overhead
|
|
139
|
+
if len(zip_files) <= 2:
|
|
140
|
+
for zip_path in zip_files:
|
|
141
|
+
tmp = tempdir_factory()
|
|
142
|
+
extracted = extract_fn(zip_path, Path(tmp.name))
|
|
143
|
+
if extracted:
|
|
144
|
+
_maybe_record(extracted, tmp)
|
|
145
|
+
temp_dirs.append(tmp)
|
|
146
|
+
resolved.append(extracted)
|
|
147
|
+
else:
|
|
148
|
+
tmp.cleanup()
|
|
149
|
+
logger.warning("No HDF5 file found inside archive %s", zip_path)
|
|
150
|
+
return resolved, temp_dirs
|
|
151
|
+
|
|
152
|
+
def extract_single_zip(
|
|
153
|
+
zip_path: Path,
|
|
154
|
+
) -> tuple[Path | None, tempfile.TemporaryDirectory | None]:
|
|
155
|
+
try:
|
|
156
|
+
tmp = tempdir_factory()
|
|
157
|
+
extracted = extract_fn(zip_path, Path(tmp.name))
|
|
158
|
+
if extracted:
|
|
159
|
+
return extracted, tmp
|
|
160
|
+
tmp.cleanup()
|
|
161
|
+
logger.warning("No HDF5 file found inside archive %s", zip_path)
|
|
162
|
+
return None, None
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
logger.error("Failed to extract ZIP file %s: %s", zip_path, exc)
|
|
165
|
+
return None, None
|
|
166
|
+
|
|
167
|
+
actual_workers = min(int(max_workers), len(zip_files))
|
|
168
|
+
logger.debug(
|
|
169
|
+
"Extracting %d ZIP files using %d parallel workers", len(zip_files), actual_workers
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=actual_workers) as executor:
|
|
173
|
+
future_to_zip = {
|
|
174
|
+
executor.submit(extract_single_zip, zip_path): zip_path for zip_path in zip_files
|
|
175
|
+
}
|
|
176
|
+
for future in concurrent.futures.as_completed(future_to_zip):
|
|
177
|
+
zip_path = future_to_zip[future]
|
|
178
|
+
try:
|
|
179
|
+
extracted_path, tmp_dir = future.result()
|
|
180
|
+
if extracted_path and tmp_dir:
|
|
181
|
+
_maybe_record(extracted_path, tmp_dir)
|
|
182
|
+
resolved.append(extracted_path)
|
|
183
|
+
temp_dirs.append(tmp_dir)
|
|
184
|
+
except Exception as exc:
|
|
185
|
+
logger.error("ZIP extraction failed for %s: %s", zip_path, exc)
|
|
186
|
+
|
|
187
|
+
logger.debug(
|
|
188
|
+
"Parallel ZIP extraction completed: %d files extracted from %d archives",
|
|
189
|
+
len(resolved),
|
|
190
|
+
len(zip_files),
|
|
191
|
+
)
|
|
192
|
+
return resolved, temp_dirs
|