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
firecube/cli/main.py ADDED
@@ -0,0 +1,866 @@
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
+ """Unified Click-based CLI entrypoint for Firecube."""
16
+
17
+ # pyright: reportMissingImports=false, reportFunctionMemberAccess=false, reportCallIssue=false
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import logging
23
+ import os
24
+ from pathlib import Path
25
+
26
+ import click
27
+
28
+ from firecube.cli._command_schemas import IngestCommandConfig
29
+ from firecube.cli._ctx import get_storage_config
30
+ from firecube.cli._errors import MissingProductNameError, wrap_user_facing_errors
31
+ from firecube.cli._formatter import (
32
+ COMMAND_GROUPS,
33
+ OPTION_GROUPS,
34
+ FirecubeGroup,
35
+ install_option_groups_patch,
36
+ )
37
+ from firecube.cli._product import require_full_uri
38
+ from firecube.cli._rename_hints import install_rename_hints
39
+ from firecube.cli._shared_options import (
40
+ storage_driver_option,
41
+ storage_type_option,
42
+ write_mode_option,
43
+ )
44
+ from firecube.cli._typed_options import TypedOptionsParam, coerce_options_for_plugin
45
+ from firecube.cli.advise import advise as advise_group
46
+ from firecube.cli.archive import archive as archive_group
47
+ from firecube.cli.catalog import catalog as catalog_group
48
+ from firecube.cli.chunks import chunks as chunks_group
49
+ from firecube.cli.completion import completion_cmd
50
+ from firecube.cli.parquet import parquet as parquet_group
51
+ from firecube.cli.plugins import plugins as plugins_group
52
+ from firecube.cli.zarr import zarr as zarr_group
53
+ from firecube.core.config import get_plugin_defaults, load_config_file
54
+ from firecube.core.observability import set_current_span_attribute, span
55
+ from firecube.core.product.resolver import ProductResolver
56
+ from firecube.core.storage.binding import StorageBinding
57
+ from firecube.core.storage.driver_config import StorageDriverConfig
58
+ from firecube.core.storage.session import StorageSession
59
+ from firecube.core.uris import (
60
+ is_remote_target,
61
+ local_path_from_target,
62
+ )
63
+ from firecube.ingestor.api import IngestContext, StorageContext
64
+ from firecube.ingestor.registry.loader import discover_ingestors
65
+
66
+ log = logging.getLogger("firecube.cli")
67
+
68
+ install_option_groups_patch()
69
+ install_rename_hints()
70
+
71
+ CONTEXT_SETTINGS = {
72
+ "help_option_names": ["-h", "--help"],
73
+ }
74
+
75
+ COMMAND_GROUPS["firecube"] = [
76
+ {
77
+ "name": "Core",
78
+ "commands": ["ingest", "archive"],
79
+ },
80
+ {
81
+ "name": "Inspect",
82
+ "commands": ["zarr", "parquet", "chunks", "catalog"],
83
+ },
84
+ {
85
+ "name": "Tools",
86
+ "commands": ["plugins", "advise", "completion"],
87
+ },
88
+ ]
89
+
90
+ OPTION_GROUPS.update(
91
+ {
92
+ "firecube ingest": [
93
+ {
94
+ "name": "Input",
95
+ "options": ["--input-data", "--target", "--product-name", "--output-format"],
96
+ },
97
+ {
98
+ "name": "Execution",
99
+ "options": [
100
+ "--write-mode",
101
+ "--storage-type",
102
+ "--storage-driver",
103
+ "--in-memory",
104
+ "--slot-start",
105
+ "--slot-end",
106
+ "--slot-size",
107
+ ],
108
+ },
109
+ {
110
+ "name": "Advanced",
111
+ "options": ["--option", "--show-options"],
112
+ },
113
+ ],
114
+ "firecube archive create": [
115
+ {
116
+ "name": "Required",
117
+ "options": ["--source", "--archive"],
118
+ },
119
+ {
120
+ "name": "Filtering",
121
+ "options": ["--start-date", "--end-date", "--group", "--variables"],
122
+ },
123
+ {
124
+ "name": "Encoding",
125
+ "options": ["--compression"],
126
+ },
127
+ {
128
+ "name": "Behavior",
129
+ "options": ["--overwrite", "--allow-nan", "--allow-inf"],
130
+ },
131
+ ],
132
+ "firecube archive restore": [
133
+ {
134
+ "name": "Required",
135
+ "options": ["--archive", "--target"],
136
+ },
137
+ {
138
+ "name": "Behavior",
139
+ "options": ["--overwrite"],
140
+ },
141
+ ],
142
+ "firecube chunks list": [
143
+ {
144
+ "name": "Filtering",
145
+ "options": [
146
+ "--product-name",
147
+ "--start-date",
148
+ "--end-date",
149
+ "--time-range",
150
+ "--include-span",
151
+ "--type",
152
+ "--pattern",
153
+ "--meta",
154
+ ],
155
+ },
156
+ {
157
+ "name": "Output",
158
+ "options": ["--format", "--limit"],
159
+ },
160
+ {
161
+ "name": "Advanced",
162
+ "options": ["--workspace"],
163
+ },
164
+ ],
165
+ "firecube chunks delete": [
166
+ {
167
+ "name": "Scope",
168
+ "options": ["--product-name", "--all-products"],
169
+ },
170
+ {
171
+ "name": "Filters",
172
+ "options": [
173
+ "--pattern",
174
+ "--type",
175
+ "--meta",
176
+ "--start-date",
177
+ "--end-date",
178
+ "--range",
179
+ ],
180
+ },
181
+ {
182
+ "name": "Mode",
183
+ "options": ["--manifest-only", "--storage-only", "--include-metadata"],
184
+ },
185
+ {
186
+ "name": "Safety",
187
+ "options": ["--dry-run", "--yes-i-really-mean-it"],
188
+ },
189
+ {
190
+ "name": "Context",
191
+ "options": ["--workspace"],
192
+ },
193
+ ],
194
+ "firecube chunks delete-span": [
195
+ {
196
+ "name": "Required",
197
+ "options": ["--product-name"],
198
+ },
199
+ {
200
+ "name": "Filtering",
201
+ "options": ["--run-id", "--batch-id", "--group", "--meta", "--include-replaced"],
202
+ },
203
+ {
204
+ "name": "Safety",
205
+ "options": ["--dry-run", "--force", "--yes-i-really-mean-it"],
206
+ },
207
+ {
208
+ "name": "Advanced",
209
+ "options": ["--workspace"],
210
+ },
211
+ ],
212
+ "firecube chunks runs list": [
213
+ {
214
+ "name": "Required",
215
+ "options": ["--product-name"],
216
+ },
217
+ {
218
+ "name": "Output",
219
+ "options": ["--status", "--format", "--workspace"],
220
+ },
221
+ ],
222
+ "firecube chunks runs abandon": [
223
+ {
224
+ "name": "Required",
225
+ "options": ["--product-name", "--run-id", "--reason"],
226
+ },
227
+ {
228
+ "name": "Safety",
229
+ "options": ["--dry-run", "--yes-i-really-mean-it"],
230
+ },
231
+ {
232
+ "name": "Advanced",
233
+ "options": ["--workspace"],
234
+ },
235
+ ],
236
+ "firecube chunks claims clear": [
237
+ {
238
+ "name": "Scope",
239
+ "options": ["--domain", "--all-stale"],
240
+ },
241
+ {
242
+ "name": "Required",
243
+ "options": ["--product-name"],
244
+ },
245
+ {
246
+ "name": "Safety",
247
+ "options": ["--dry-run", "--yes-i-really-mean-it"],
248
+ },
249
+ {
250
+ "name": "Advanced",
251
+ "options": ["--workspace", "--force"],
252
+ },
253
+ ],
254
+ "firecube catalog intake": [
255
+ {
256
+ "name": "Required",
257
+ "options": ["--product", "--output", "--collection-id"],
258
+ },
259
+ {
260
+ "name": "Optional",
261
+ "options": ["--include-storage-options", "--no-storage-options"],
262
+ },
263
+ ],
264
+ "firecube zarr validate": [
265
+ {
266
+ "name": "Required",
267
+ "options": ["--product", "--group"],
268
+ },
269
+ {
270
+ "name": "Options",
271
+ "options": [
272
+ "--timeout",
273
+ "--max-chunks",
274
+ "--on-timeout",
275
+ "--storage-type",
276
+ "--storage-driver",
277
+ ],
278
+ },
279
+ ],
280
+ "firecube zarr slots": [
281
+ {
282
+ "name": "Required",
283
+ "options": [
284
+ "--target",
285
+ "--product-name",
286
+ "--storage-type",
287
+ "--storage-driver",
288
+ "--write-mode",
289
+ ],
290
+ },
291
+ {
292
+ "name": "Options",
293
+ "options": ["--slot-size", "--no-resume", "--format"],
294
+ },
295
+ ],
296
+ "firecube zarr multires": [
297
+ {
298
+ "name": "Required",
299
+ "options": ["--target", "--product-name", "--storage-type", "--storage-driver"],
300
+ },
301
+ {
302
+ "name": "Options",
303
+ "options": ["--resolutions"],
304
+ },
305
+ ],
306
+ "firecube zarr preallocate": [
307
+ {
308
+ "name": "Required",
309
+ "options": [
310
+ "--target",
311
+ "--product-name",
312
+ "--storage-type",
313
+ "--storage-driver",
314
+ "--write-mode",
315
+ ],
316
+ },
317
+ {
318
+ "name": "Input",
319
+ "options": ["--input-data"],
320
+ },
321
+ {
322
+ "name": "Advanced",
323
+ "options": ["--option"],
324
+ },
325
+ ],
326
+ "firecube parquet validate": [
327
+ {
328
+ "name": "Required",
329
+ "options": ["--product-name"],
330
+ },
331
+ ],
332
+ "firecube parquet consolidate": [
333
+ {
334
+ "name": "Required",
335
+ "options": ["--product", "--output"],
336
+ },
337
+ {
338
+ "name": "Options",
339
+ "options": ["--codec", "--storage-type", "--storage-driver"],
340
+ },
341
+ ],
342
+ "firecube plugins list": [
343
+ {
344
+ "name": "Options",
345
+ "options": ["--format"],
346
+ },
347
+ ],
348
+ "firecube plugins describe": [
349
+ {
350
+ "name": "Options",
351
+ "options": ["--format"],
352
+ },
353
+ ],
354
+ "firecube plugins explain": [
355
+ {
356
+ "name": "Options",
357
+ "options": ["--format"],
358
+ },
359
+ ],
360
+ "firecube plugins install": [
361
+ {
362
+ "name": "Options",
363
+ "options": ["--editable"],
364
+ },
365
+ ],
366
+ "firecube advise batch-size": [
367
+ {
368
+ "name": "Required",
369
+ "options": ["--product", "--group"],
370
+ },
371
+ ],
372
+ "firecube advise compliance": [
373
+ {
374
+ "name": "Required",
375
+ "options": ["--profile", "--product", "--group"],
376
+ },
377
+ {
378
+ "name": "Options",
379
+ "options": [
380
+ "--format",
381
+ "--strict",
382
+ "--storage-type",
383
+ "--storage-driver",
384
+ ],
385
+ },
386
+ ],
387
+ "firecube archive info": [
388
+ {
389
+ "name": "Required",
390
+ "options": ["--archive"],
391
+ },
392
+ {
393
+ "name": "Options",
394
+ "options": ["--format"],
395
+ },
396
+ ],
397
+ "firecube archive validate": [
398
+ {
399
+ "name": "Required",
400
+ "options": ["--archive"],
401
+ },
402
+ {
403
+ "name": "Options",
404
+ "options": ["--quick"],
405
+ },
406
+ ],
407
+ "firecube archive list": [
408
+ {
409
+ "name": "Required",
410
+ "options": ["--archive"],
411
+ },
412
+ ],
413
+ "firecube chunks claims list": [
414
+ {
415
+ "name": "Filtering",
416
+ "options": ["--product-name"],
417
+ },
418
+ {
419
+ "name": "Output",
420
+ "options": ["--format"],
421
+ },
422
+ {
423
+ "name": "Advanced",
424
+ "options": ["--workspace"],
425
+ },
426
+ ],
427
+ "firecube chunks snapshots rebuild": [
428
+ {
429
+ "name": "Required",
430
+ "options": ["--product-name"],
431
+ },
432
+ {
433
+ "name": "Safety",
434
+ "options": ["--dry-run"],
435
+ },
436
+ {
437
+ "name": "Output",
438
+ "options": ["--format"],
439
+ },
440
+ {
441
+ "name": "Advanced",
442
+ "options": ["--workspace"],
443
+ },
444
+ ],
445
+ "firecube chunks snapshots status": [
446
+ {
447
+ "name": "Required",
448
+ "options": ["--product-name"],
449
+ },
450
+ {
451
+ "name": "Output",
452
+ "options": ["--format"],
453
+ },
454
+ {
455
+ "name": "Advanced",
456
+ "options": ["--workspace"],
457
+ },
458
+ ],
459
+ }
460
+ )
461
+
462
+
463
+ @click.group(cls=FirecubeGroup, context_settings=CONTEXT_SETTINGS)
464
+ @click.version_option(package_name="firecube", message="Firecube %(version)s")
465
+ @click.option(
466
+ "--config-file",
467
+ type=click.Path(path_type=Path),
468
+ help="Firecube TOML config file (default: ~/.config/firecube/config.toml)",
469
+ )
470
+ @click.pass_context
471
+ def cli(ctx: click.Context, config_file: Path | None) -> None:
472
+ """batch ingestion tool for EO datasets
473
+
474
+ Batch ingestion and management tool for Earth Observation datasets.
475
+ Writes Zarr and Parquet stores with built-in resume safety, idempotent
476
+ chunk tracking, and multi-resolution support. Run firecube ingest to
477
+ start a job, or explore subcommands for storage management and diagnostics.
478
+ """
479
+ ctx.ensure_object(dict)
480
+ if config_file is not None:
481
+ ctx.obj["config_file"] = config_file
482
+
483
+
484
+ @cli.command(
485
+ epilog="""\b
486
+ Examples:
487
+ # list available options for a plugin
488
+ firecube ingest <plugin> --show-options
489
+ # run ingestion from a local source to a local target
490
+ firecube ingest <plugin> \\
491
+ --input-data /path/to/input \\
492
+ --target file:///path/to/output.zarr \\
493
+ --product-name <name> \\
494
+ --storage-type local \\
495
+ --storage-driver fsspec \\
496
+ --output-format zarr \\
497
+ --write-mode direct
498
+ # run with staged write mode (write locally then upload)
499
+ firecube ingest <plugin> \\
500
+ --input-data /data/raw \\
501
+ --target s3://bucket/output.zarr \\
502
+ --product-name <name> \\
503
+ --storage-type s3 \\
504
+ --storage-driver fsspec \\
505
+ --output-format zarr \\
506
+ --write-mode staged
507
+ # pass plugin-specific options
508
+ firecube ingest <plugin> \\
509
+ --input-data /data/ \\
510
+ --target file:///tmp/output.zarr \\
511
+ --product-name <name> \\
512
+ --storage-type local \\
513
+ --storage-driver fsspec \\
514
+ --output-format zarr \\
515
+ --write-mode direct \\
516
+ --option horizon=LST \\
517
+ --option no_progress=true
518
+ See also: firecube plugins list, firecube chunks list, firecube advise batch-size
519
+ """,
520
+ )
521
+ @click.argument("plugin")
522
+ @click.option(
523
+ "-i",
524
+ "--input-data",
525
+ "input_data",
526
+ type=str,
527
+ required=False,
528
+ help="Raw plugin input data: local path, file:///abs/path, or s3:// prefix. Interpreted by the plugin.",
529
+ )
530
+ @click.option(
531
+ "-t",
532
+ "--target",
533
+ type=str,
534
+ required=False,
535
+ help=("Target product URI required (file:///abs/path or s3://bucket/key); -t short available."),
536
+ )
537
+ @click.option(
538
+ "-n",
539
+ "--product-name",
540
+ "product_name",
541
+ default=None,
542
+ help=(
543
+ "Logical product name. Overrides the plugin class default and the "
544
+ "per-plugin config default."
545
+ ),
546
+ )
547
+ @click.option(
548
+ "--output-format", required=False, help="output format to deliver (e.g. parquet, zarr)"
549
+ )
550
+ @storage_type_option(
551
+ required=False,
552
+ extra_help=(
553
+ "Inferred from URI scheme when omitted (file://→local, s3://→s3). "
554
+ "Explicit value overrides inference and is rejected on mismatch."
555
+ ),
556
+ )
557
+ @storage_driver_option(
558
+ required=False,
559
+ extra_help=("Defaults to fsspec; overridable via FIRECUBE_STORAGE_DRIVER or [storage].driver."),
560
+ )
561
+ @write_mode_option(required=False, extra_help="Required. No local-target inference.")
562
+ @click.option(
563
+ "--slot-start",
564
+ "slot_start",
565
+ type=int,
566
+ default=None,
567
+ help="Parallel ingestion: first slot index (inclusive).",
568
+ )
569
+ @click.option(
570
+ "--slot-end",
571
+ "slot_end",
572
+ type=int,
573
+ default=None,
574
+ help="Parallel ingestion: last slot index (exclusive).",
575
+ )
576
+ @click.option(
577
+ "--slot-size",
578
+ "slot_size",
579
+ type=int,
580
+ default=None,
581
+ help="Slot size for orchestrated parallel ingestion range derivation.",
582
+ )
583
+ @click.option(
584
+ "--slot-group",
585
+ default=None,
586
+ help=(
587
+ "Group name this worker owns when running multi-group parallel "
588
+ "ingestion. Only workers with matching group are scheduled writes for "
589
+ "that group. Defaults to None (all groups — single-group or "
590
+ "non-parallel mode)."
591
+ ),
592
+ )
593
+ @click.option("--in-memory", is_flag=True, help="use in-memory DuckDB")
594
+ @click.option(
595
+ "--option",
596
+ "extra_options",
597
+ multiple=True,
598
+ type=TypedOptionsParam(),
599
+ help="Plugin/engine option in key=value form.",
600
+ )
601
+ @click.option(
602
+ "--show-options",
603
+ is_flag=True,
604
+ help="show available options for the specified plugin and exit",
605
+ )
606
+ @click.pass_context
607
+ @wrap_user_facing_errors
608
+ def ingest(
609
+ ctx: click.Context,
610
+ plugin: str,
611
+ input_data: str | None,
612
+ target: str | None,
613
+ product_name: str | None,
614
+ storage_type: str | None,
615
+ storage_driver: str | None,
616
+ output_format: str | None,
617
+ write_mode: str | None,
618
+ slot_start: int | None,
619
+ slot_end: int | None,
620
+ slot_size: int | None,
621
+ slot_group: str | None,
622
+ in_memory: bool,
623
+ extra_options,
624
+ show_options: bool,
625
+ ) -> None:
626
+ """run an ingestion job for a plugin
627
+
628
+ The plugin handles source reading; firecube manages write strategy (staged
629
+ or direct), pipeline parallelism, and chunk tracking. Use --option to pass
630
+ plugin-specific overrides, or --show-options to list available options for
631
+ a plugin.
632
+ """
633
+ extra_options = coerce_options_for_plugin(plugin, extra_options)
634
+ ingest_cfg: IngestCommandConfig | None = None
635
+ if not show_options:
636
+ from firecube.cli._uri_policy import apply_smart_default, parse_product_uri
637
+
638
+ storage_type_resolved: str | None = storage_type
639
+ if target is not None:
640
+ parsed_target = parse_product_uri(target)
641
+ storage_type_resolved = apply_smart_default(parsed_target, storage_type)
642
+ # storage_driver flows through unresolved; build_storage_config handles
643
+ # env > config > default precedence downstream.
644
+
645
+ try:
646
+ ingest_cfg = IngestCommandConfig(
647
+ plugin=plugin,
648
+ input_data=input_data,
649
+ target=target or "",
650
+ write_mode=write_mode,
651
+ storage_type=storage_type_resolved,
652
+ storage_driver=storage_driver,
653
+ product_name=product_name,
654
+ output_format=output_format or "zarr",
655
+ in_memory=in_memory,
656
+ options=dict(extra_options),
657
+ )
658
+ except click.UsageError:
659
+ raise
660
+
661
+ if not show_options:
662
+ if target is None:
663
+ raise click.ClickException("Missing option '--target'.")
664
+ require_full_uri(str(target), option_name="--target")
665
+
666
+ if (slot_start is None) ^ (slot_end is None):
667
+ raise click.UsageError("--slot-start and --slot-end must be provided together")
668
+
669
+ plugins = discover_ingestors()
670
+ if plugin not in plugins:
671
+ raise click.ClickException(f"Unknown plugin: {plugin}")
672
+
673
+ ingestor_cls = plugins[plugin]
674
+
675
+ if show_options:
676
+ _print_plugin_options(plugin, ingestor_cls)
677
+ return
678
+
679
+ if input_data:
680
+ input_data_str = str(input_data)
681
+ if not is_remote_target(input_data_str):
682
+ local_source = local_path_from_target(input_data_str)
683
+ if not local_source.exists():
684
+ raise click.ClickException(f"Input data not found: {input_data}")
685
+ input_data = str(local_source)
686
+ else:
687
+ input_data = input_data_str
688
+
689
+ from firecube.core import observability
690
+
691
+ observability.init_observability(f"firecube-ingestor-{plugin}")
692
+ try:
693
+ with span(
694
+ "firecube.cli.ingest",
695
+ attributes={
696
+ "firecube.plugin": plugin,
697
+ "firecube.write_mode": str(write_mode or ""),
698
+ },
699
+ ):
700
+ explicit_config = ctx.obj.get("config_file")
701
+ cfg = load_config_file(explicit_config, strict=explicit_config is not None)
702
+ assert ingest_cfg is not None
703
+ options = get_plugin_defaults(cfg, plugin)
704
+ from firecube.cli._slot_env import resolve_slot_range_from_env
705
+
706
+ resolved_slot_start, resolved_slot_end, resolved_slot_group = (
707
+ resolve_slot_range_from_env(
708
+ slot_start,
709
+ slot_end,
710
+ slot_size,
711
+ slot_group,
712
+ )
713
+ )
714
+ options["slot_start"] = resolved_slot_start
715
+ options["slot_end"] = resolved_slot_end
716
+ options["slot_size"] = slot_size
717
+ options["slot_group"] = resolved_slot_group
718
+ resolved_product_name = (
719
+ ingest_cfg.product_name
720
+ or get_plugin_defaults(cfg, plugin).get("default_product_name")
721
+ or getattr(ingestor_cls, "PRODUCT_NAME", None)
722
+ )
723
+ if not resolved_product_name:
724
+ raise MissingProductNameError(plugin)
725
+
726
+ target_str = ingest_cfg.target
727
+ require_full_uri(target_str, option_name="--target")
728
+ try:
729
+ identity = ProductResolver.resolve(
730
+ target_str,
731
+ format=ingest_cfg.output_format,
732
+ product_name=str(resolved_product_name),
733
+ )
734
+ except ValueError as exc:
735
+ raise click.UsageError(str(exc)) from exc
736
+ storage_overrides: dict[str, str | None] = {
737
+ "storage_type": str(ingest_cfg.storage_type)
738
+ if ingest_cfg.storage_type is not None
739
+ else None,
740
+ "storage_driver": str(ingest_cfg.storage_driver)
741
+ if ingest_cfg.storage_driver is not None
742
+ else None,
743
+ }
744
+ storage_config = get_storage_config(ctx, overrides=storage_overrides, cache=False)
745
+ options.setdefault("storage", {"type": storage_config.storage_type})
746
+
747
+ # Per HIGH-3 fix: read every option from the validated typed config, not raw Click params.
748
+ # IngestCommandConfig has applied defaults and validation; the raw params are inputs to
749
+ # its construction only. This ensures typed defaults (e.g. output_format="zarr") flow through.
750
+ final_write_mode = ingest_cfg.write_mode
751
+ set_current_span_attribute("firecube.write_mode", str(final_write_mode))
752
+ options["write_mode"] = final_write_mode
753
+
754
+ final_output_format = ingest_cfg.output_format
755
+ if final_output_format == "tensogram":
756
+ from firecube.ingestor.api import is_dataset_producer
757
+
758
+ if not is_dataset_producer(ingestor_cls):
759
+ raise click.ClickException(
760
+ f"Plugin '{plugin}' does not support --output-format tensogram. "
761
+ "It must implement the DatasetProducer protocol "
762
+ "(build_dataset + get_batch_groups)."
763
+ )
764
+
765
+ # Instantiate the ingestor *after* setting the global storage config so
766
+ # BaseIngestor can initialise ChunkManager with the correct output base.
767
+ ingestor = ingestor_cls()
768
+
769
+ for key, coerced_value in extra_options:
770
+ options[key] = coerced_value
771
+
772
+ try:
773
+ upload_workers = int(options.get("upload_workers", 4))
774
+ except (TypeError, ValueError) as exc:
775
+ raise click.BadParameter("upload_workers must be an integer >= 1.") from exc
776
+ if upload_workers < 1:
777
+ raise click.BadParameter("upload_workers must be at least 1.")
778
+
779
+ driver_config = StorageDriverConfig.from_storage_config(storage_config)
780
+ session = StorageSession(
781
+ StorageBinding(
782
+ identity=identity,
783
+ driver=driver_config,
784
+ )
785
+ )
786
+ plugin_target = identity.product_uri.to_str()
787
+ log.debug(
788
+ "Ingest target wiring (target=%s plugin_target=%s product_name=%s resolved_product_name=%s write_mode=%s storage_type=%s)",
789
+ target_str,
790
+ plugin_target,
791
+ identity.product_name,
792
+ resolved_product_name,
793
+ final_write_mode,
794
+ storage_config.storage_type,
795
+ )
796
+
797
+ # Generate Run ID (Engine-owned)
798
+ run_id = str(options.get("run_id") or options.get("manifest_run_id") or "")
799
+ if not run_id:
800
+ import uuid
801
+
802
+ hostname = os.getenv("HOSTNAME", "host")
803
+ run_id = f"{plugin}-{hostname}-{uuid.uuid4().hex}"
804
+
805
+ ingest_ctx = IngestContext(
806
+ source=input_data or "",
807
+ target=plugin_target,
808
+ in_memory=ingest_cfg.in_memory,
809
+ output_format=final_output_format,
810
+ options=options,
811
+ storage=StorageContext(output=session),
812
+ run_id=run_id,
813
+ )
814
+ result = ingestor.run(ingest_ctx)
815
+
816
+ if result.manifest:
817
+ click.echo(json.dumps(result.manifest, indent=2))
818
+ else:
819
+ click.echo(
820
+ json.dumps(
821
+ {
822
+ "output_path": result.outputs.primary,
823
+ "output_format": result.output_format,
824
+ },
825
+ indent=2,
826
+ )
827
+ )
828
+ finally:
829
+ from firecube.core.observability import shutdown_observability
830
+
831
+ shutdown_observability(timeout_millis=5000)
832
+
833
+
834
+ cli.add_command(archive_group, name="archive")
835
+ cli.add_command(advise_group, name="advise")
836
+ cli.add_command(chunks_group, name="chunks")
837
+ cli.add_command(plugins_group, name="plugins")
838
+ cli.add_command(zarr_group, name="zarr")
839
+ cli.add_command(catalog_group, name="catalog")
840
+ cli.add_command(parquet_group, name="parquet")
841
+ cli.add_command(completion_cmd, name="completion")
842
+
843
+
844
+ def _print_plugin_options(plugin_name: str, ingestor_cls: type) -> None:
845
+ """Introspect and print available options for a plugin using tiered config."""
846
+ click.echo(f"Available options for plugin '{plugin_name}':\n")
847
+
848
+ if not hasattr(ingestor_cls, "describe_options"):
849
+ click.echo(" (Plugin does not support option introspection)")
850
+ return
851
+
852
+ # Helper method on BaseIngestor returning dict[Tier, list[str]]
853
+ tiers = ingestor_cls.describe_options()
854
+
855
+ for tier, keys in tiers.items():
856
+ if not keys:
857
+ continue
858
+
859
+ click.echo(f"[{tier}]")
860
+ for key in keys:
861
+ click.echo(f" --option {key}")
862
+ click.echo("")
863
+
864
+
865
+ if __name__ == "__main__":
866
+ cli()