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,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
+ )