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.
Files changed (197) hide show
  1. firecube/__init__.py +23 -0
  2. firecube/__main__.py +20 -0
  3. firecube/_version.py +46 -0
  4. firecube/cli/_audience.py +135 -0
  5. firecube/cli/_command_schemas.py +112 -0
  6. firecube/cli/_ctx.py +85 -0
  7. firecube/cli/_errors.py +115 -0
  8. firecube/cli/_formatter.py +195 -0
  9. firecube/cli/_product.py +47 -0
  10. firecube/cli/_rename_hints.py +123 -0
  11. firecube/cli/_shared_options.py +247 -0
  12. firecube/cli/_slot_env.py +102 -0
  13. firecube/cli/_slot_planning.py +95 -0
  14. firecube/cli/_typed_options.py +181 -0
  15. firecube/cli/_uri_policy.py +110 -0
  16. firecube/cli/advise.py +321 -0
  17. firecube/cli/archive.py +870 -0
  18. firecube/cli/catalog.py +196 -0
  19. firecube/cli/chunks/__init__.py +74 -0
  20. firecube/cli/chunks/_claims.py +229 -0
  21. firecube/cli/chunks/_common.py +84 -0
  22. firecube/cli/chunks/_delete.py +376 -0
  23. firecube/cli/chunks/_list.py +334 -0
  24. firecube/cli/chunks/_manager.py +161 -0
  25. firecube/cli/chunks/_runs.py +294 -0
  26. firecube/cli/chunks/_snapshots.py +165 -0
  27. firecube/cli/completion.py +73 -0
  28. firecube/cli/main.py +866 -0
  29. firecube/cli/parquet.py +289 -0
  30. firecube/cli/plugins/__init__.py +28 -0
  31. firecube/cli/plugins/commands.py +262 -0
  32. firecube/cli/plugins/introspect.py +304 -0
  33. firecube/cli/plugins/mgmt.py +198 -0
  34. firecube/cli/plugins/registry.py +132 -0
  35. firecube/cli/zarr.py +1043 -0
  36. firecube/core/__init__.py +14 -0
  37. firecube/core/api.py +88 -0
  38. firecube/core/cf/__init__.py +23 -0
  39. firecube/core/cf/check_ids.py +126 -0
  40. firecube/core/cf/report.py +57 -0
  41. firecube/core/cf/validator.py +313 -0
  42. firecube/core/config.py +307 -0
  43. firecube/core/controlplane/__init__.py +47 -0
  44. firecube/core/controlplane/_event_processor.py +136 -0
  45. firecube/core/controlplane/_helpers.py +114 -0
  46. firecube/core/controlplane/_projection.py +253 -0
  47. firecube/core/controlplane/_snapshot.py +215 -0
  48. firecube/core/controlplane/_wal_reader.py +402 -0
  49. firecube/core/controlplane/_wal_writer.py +589 -0
  50. firecube/core/controlplane/claims.py +294 -0
  51. firecube/core/controlplane/deletion.py +704 -0
  52. firecube/core/controlplane/events.py +219 -0
  53. firecube/core/controlplane/manager.py +990 -0
  54. firecube/core/controlplane/metrics.py +97 -0
  55. firecube/core/controlplane/repo.py +804 -0
  56. firecube/core/controlplane/repo_utils.py +73 -0
  57. firecube/core/controlplane/time_dim.py +111 -0
  58. firecube/core/controlplane/types.py +445 -0
  59. firecube/core/credentials.py +36 -0
  60. firecube/core/duckdb/__init__.py +27 -0
  61. firecube/core/duckdb/bridge.py +82 -0
  62. firecube/core/duckdb/io.py +30 -0
  63. firecube/core/errors.py +74 -0
  64. firecube/core/filesystem/__init__.py +60 -0
  65. firecube/core/filesystem/_obstore_compat.py +60 -0
  66. firecube/core/filesystem/fsspec_backend.py +308 -0
  67. firecube/core/filesystem/instrumentation.py +260 -0
  68. firecube/core/filesystem/obstore_backend.py +406 -0
  69. firecube/core/filesystem/ops.py +412 -0
  70. firecube/core/filesystem/protocol.py +130 -0
  71. firecube/core/filesystem/store_factory.py +149 -0
  72. firecube/core/formats/__init__.py +46 -0
  73. firecube/core/formats/_iso.py +84 -0
  74. firecube/core/formats/discovery.py +165 -0
  75. firecube/core/formats/hdf5.py +137 -0
  76. firecube/core/formats/netcdf.py +226 -0
  77. firecube/core/formats/zip.py +192 -0
  78. firecube/core/intake.py +530 -0
  79. firecube/core/observability/__init__.py +72 -0
  80. firecube/core/observability/_state.py +24 -0
  81. firecube/core/observability/logging.py +147 -0
  82. firecube/core/observability/metrics.py +230 -0
  83. firecube/core/observability/telemetry/__init__.py +87 -0
  84. firecube/core/observability/telemetry/pushgateway.py +339 -0
  85. firecube/core/observability/telemetry/sinks.py +155 -0
  86. firecube/core/observability/tracing.py +126 -0
  87. firecube/core/product/__init__.py +45 -0
  88. firecube/core/product/identity.py +76 -0
  89. firecube/core/product/resolver.py +121 -0
  90. firecube/core/product/target.py +120 -0
  91. firecube/core/runtime.py +187 -0
  92. firecube/core/slot_index.py +205 -0
  93. firecube/core/storage/__init__.py +45 -0
  94. firecube/core/storage/binding.py +39 -0
  95. firecube/core/storage/cache_key.py +40 -0
  96. firecube/core/storage/completion.py +245 -0
  97. firecube/core/storage/driver_config.py +125 -0
  98. firecube/core/storage/results.py +30 -0
  99. firecube/core/storage/session.py +431 -0
  100. firecube/core/storage/transfer.py +142 -0
  101. firecube/core/storage/uri.py +173 -0
  102. firecube/core/tensogram/__init__.py +37 -0
  103. firecube/core/tensogram/_compat.py +36 -0
  104. firecube/core/tensogram/controlplane_codec.py +230 -0
  105. firecube/core/tensogram/converter.py +390 -0
  106. firecube/core/tensogram/metadata.py +274 -0
  107. firecube/core/tensogram/restore.py +303 -0
  108. firecube/core/tensogram/schema.py +93 -0
  109. firecube/core/uris.py +182 -0
  110. firecube/core/workspaces.py +68 -0
  111. firecube/core/zarr/__init__.py +69 -0
  112. firecube/core/zarr/_reserved_attrs.py +47 -0
  113. firecube/core/zarr/_reserved_root_attrs.py +74 -0
  114. firecube/core/zarr/io.py +88 -0
  115. firecube/core/zarr/layers.py +184 -0
  116. firecube/core/zarr/multires.py +178 -0
  117. firecube/core/zarr/region_writer.py +782 -0
  118. firecube/core/zarr/scrub.py +200 -0
  119. firecube/core/zarr/state.py +258 -0
  120. firecube/core/zarr/time_decode.py +71 -0
  121. firecube/core/zarr/validation.py +622 -0
  122. firecube/ingestor/__init__.py +27 -0
  123. firecube/ingestor/api.py +215 -0
  124. firecube/ingestor/config/__init__.py +19 -0
  125. firecube/ingestor/config/coercion.py +157 -0
  126. firecube/ingestor/config/engine.py +188 -0
  127. firecube/ingestor/contracts/__init__.py +14 -0
  128. firecube/ingestor/contracts/interfaces.py +235 -0
  129. firecube/ingestor/devtools/_templates/__init__.py +14 -0
  130. firecube/ingestor/devtools/_templates/ingestor_base.py.tpl +72 -0
  131. firecube/ingestor/devtools/_templates/ingestor_direct_zarr.py.tpl +66 -0
  132. firecube/ingestor/devtools/_templates/ingestor_parquet.py.tpl +59 -0
  133. firecube/ingestor/devtools/_templates/ingestor_zarr.py.tpl +55 -0
  134. firecube/ingestor/devtools/_templates/readme.md.tpl +40 -0
  135. firecube/ingestor/devtools/scaffolding.py +229 -0
  136. firecube/ingestor/errors.py +79 -0
  137. firecube/ingestor/extensions/_binning.py +152 -0
  138. firecube/ingestor/extensions/duck.py +153 -0
  139. firecube/ingestor/extensions/grid.py +287 -0
  140. firecube/ingestor/extensions/healpix.py +330 -0
  141. firecube/ingestor/registry/__init__.py +14 -0
  142. firecube/ingestor/registry/loader.py +144 -0
  143. firecube/ingestor/registry/metadata.py +97 -0
  144. firecube/ingestor/registry/version_compat.py +95 -0
  145. firecube/ingestor/runtime/__init__.py +14 -0
  146. firecube/ingestor/runtime/aggregation.py +112 -0
  147. firecube/ingestor/runtime/base.py +836 -0
  148. firecube/ingestor/runtime/base_hooks.py +129 -0
  149. firecube/ingestor/runtime/batching.py +161 -0
  150. firecube/ingestor/runtime/configure.py +195 -0
  151. firecube/ingestor/runtime/coverage.py +177 -0
  152. firecube/ingestor/runtime/engine.py +793 -0
  153. firecube/ingestor/runtime/parallel_evidence.py +75 -0
  154. firecube/ingestor/runtime/parallel_execution_state.py +28 -0
  155. firecube/ingestor/runtime/parallel_gate.py +164 -0
  156. firecube/ingestor/runtime/parallel_run_id.py +48 -0
  157. firecube/ingestor/runtime/preflight.py +40 -0
  158. firecube/ingestor/runtime/recording.py +425 -0
  159. firecube/ingestor/runtime/resume_guard.py +556 -0
  160. firecube/ingestor/runtime/resume_types.py +65 -0
  161. firecube/ingestor/runtime/telemetry.py +171 -0
  162. firecube/ingestor/runtime/tensogram/__init__.py +14 -0
  163. firecube/ingestor/runtime/tensogram/strategy.py +164 -0
  164. firecube/ingestor/runtime/workspace.py +220 -0
  165. firecube/ingestor/runtime/zarr/__init__.py +34 -0
  166. firecube/ingestor/runtime/zarr/append.py +466 -0
  167. firecube/ingestor/runtime/zarr/append_services.py +650 -0
  168. firecube/ingestor/runtime/zarr/batch_runner.py +190 -0
  169. firecube/ingestor/runtime/zarr/contracts.py +58 -0
  170. firecube/ingestor/runtime/zarr/existing_cube_check.py +282 -0
  171. firecube/ingestor/runtime/zarr/resume_cache.py +92 -0
  172. firecube/ingestor/runtime/zarr/staged_metadata.py +249 -0
  173. firecube/ingestor/runtime/zarr/strategies/__init__.py +15 -0
  174. firecube/ingestor/runtime/zarr/strategies/append.py +172 -0
  175. firecube/ingestor/runtime/zarr/strategies/indexed_region.py +426 -0
  176. firecube/ingestor/runtime/zarr/write.py +212 -0
  177. firecube/ingestor/runtime/zarr/write_context.py +139 -0
  178. firecube/ingestor/templates/__init__.py +19 -0
  179. firecube/ingestor/templates/config.py +118 -0
  180. firecube/ingestor/templates/direct_zarr.py +790 -0
  181. firecube/ingestor/templates/generic.py +462 -0
  182. firecube/ingestor/templates/generic_tensogram.py +170 -0
  183. firecube/ingestor/types/__init__.py +19 -0
  184. firecube/ingestor/types/config.py +89 -0
  185. firecube/ingestor/types/context.py +409 -0
  186. firecube/ingestor/types/manifest.py +53 -0
  187. firecube/ingestor/types/planned_range.py +152 -0
  188. firecube/ingestor/types/result_metrics.py +386 -0
  189. firecube/ingestor/utils/__init__.py +14 -0
  190. firecube/ingestor/utils/duckdb_utils.py +235 -0
  191. firecube/ingestor/validation.py +55 -0
  192. firecube-0.1.4.dist-info/METADATA +242 -0
  193. firecube-0.1.4.dist-info/RECORD +197 -0
  194. firecube-0.1.4.dist-info/WHEEL +4 -0
  195. firecube-0.1.4.dist-info/entry_points.txt +2 -0
  196. firecube-0.1.4.dist-info/licenses/AUTHORS.md +5 -0
  197. 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