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,161 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import overload
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
|
|
24
|
+
from firecube.cli._ctx import get_config, get_storage_config
|
|
25
|
+
from firecube.cli._product import resolve_product_identity
|
|
26
|
+
from firecube.core.config import StorageConfig
|
|
27
|
+
from firecube.core.controlplane import ChunkManager
|
|
28
|
+
from firecube.core.product.identity import ProductIdentity
|
|
29
|
+
from firecube.core.runtime import identity_from_storage_config
|
|
30
|
+
from firecube.core.storage.binding import StorageBinding
|
|
31
|
+
from firecube.core.storage.driver_config import StorageDriverConfig
|
|
32
|
+
from firecube.core.storage.uri import StorageUri
|
|
33
|
+
|
|
34
|
+
log = logging.getLogger("firecube.cli")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_manager(
|
|
38
|
+
ctx: click.Context,
|
|
39
|
+
workspace: Path | None,
|
|
40
|
+
product_uri: StorageUri | None = None,
|
|
41
|
+
*,
|
|
42
|
+
product_name: str | None = None,
|
|
43
|
+
) -> ChunkManager:
|
|
44
|
+
"""Resolve a ChunkManager for chunk operations using shared config + overrides."""
|
|
45
|
+
ctx.ensure_object(dict)
|
|
46
|
+
base_workspace = workspace if workspace is not None else ctx.obj.get("workspace")
|
|
47
|
+
cfg = get_config(ctx)
|
|
48
|
+
storage_config: StorageConfig | None = None
|
|
49
|
+
env_storage_type_present = bool(os.environ.get("FIRECUBE_STORAGE_TYPE"))
|
|
50
|
+
if cfg.get("storage") or env_storage_type_present:
|
|
51
|
+
storage_config = get_storage_config(ctx, set_global=False)
|
|
52
|
+
|
|
53
|
+
if product_uri is not None and product_name is not None:
|
|
54
|
+
driver = StorageDriverConfig.from_storage_config_or_default(storage_config)
|
|
55
|
+
binding = StorageBinding(
|
|
56
|
+
identity=ProductIdentity.from_uri(
|
|
57
|
+
uri=product_uri,
|
|
58
|
+
format="zarr",
|
|
59
|
+
product_name=product_name,
|
|
60
|
+
),
|
|
61
|
+
driver=driver,
|
|
62
|
+
)
|
|
63
|
+
manager = ChunkManager(binding=binding, workspace=base_workspace)
|
|
64
|
+
ctx.call_on_close(manager.close)
|
|
65
|
+
log.info("Chunks using product URI target (product_uri=%s)", manager.base_uri)
|
|
66
|
+
return manager
|
|
67
|
+
|
|
68
|
+
base_output = None
|
|
69
|
+
if product_uri is not None:
|
|
70
|
+
base_output = product_uri.to_str()
|
|
71
|
+
|
|
72
|
+
if base_output is None and storage_config is not None:
|
|
73
|
+
base_uri_from_config = _base_uri_from_storage_config(storage_config)
|
|
74
|
+
base_output = base_uri_from_config.to_str() if base_uri_from_config is not None else None
|
|
75
|
+
|
|
76
|
+
if base_output is None:
|
|
77
|
+
msg = (
|
|
78
|
+
"Chunk operations require a full product URI or a [storage] configuration in config.toml "
|
|
79
|
+
"(no product URI or storage configuration found)."
|
|
80
|
+
)
|
|
81
|
+
log.error(msg)
|
|
82
|
+
raise click.ClickException(msg)
|
|
83
|
+
|
|
84
|
+
base_uri = _storage_uri_from_base(base_output)
|
|
85
|
+
driver = StorageDriverConfig.from_storage_config_or_default(storage_config)
|
|
86
|
+
binding = StorageBinding(
|
|
87
|
+
identity=ProductIdentity.from_uri(
|
|
88
|
+
base_uri.join("__firecube_controlplane__"),
|
|
89
|
+
"zarr",
|
|
90
|
+
product_name="__firecube_controlplane__",
|
|
91
|
+
),
|
|
92
|
+
driver=driver,
|
|
93
|
+
)
|
|
94
|
+
manager = ChunkManager(binding=binding, workspace=base_workspace)
|
|
95
|
+
ctx.call_on_close(manager.close)
|
|
96
|
+
|
|
97
|
+
if storage_config is not None:
|
|
98
|
+
config_identity = identity_from_storage_config(storage_config)
|
|
99
|
+
bucket = config_identity.product_uri.authority if config_identity is not None else None
|
|
100
|
+
log.info(
|
|
101
|
+
"Chunks using storage configuration (storage_type=%s, bucket=%s, base_uri=%s)",
|
|
102
|
+
storage_config.storage_type,
|
|
103
|
+
bucket,
|
|
104
|
+
manager.base_uri,
|
|
105
|
+
)
|
|
106
|
+
if not ctx.obj.get("chunks_quiet"):
|
|
107
|
+
source = "env" if env_storage_type_present else "config"
|
|
108
|
+
click.echo(
|
|
109
|
+
f"[chunks] storage: type={storage_config.storage_type} "
|
|
110
|
+
f"base_uri={manager.base_uri} (from {source})",
|
|
111
|
+
err=True,
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
log.info("Chunks using product URI base (base_uri=%s)", manager.base_uri)
|
|
115
|
+
|
|
116
|
+
return manager
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _storage_uri_from_base(raw: str) -> StorageUri:
|
|
120
|
+
if "://" not in raw:
|
|
121
|
+
raise click.UsageError(
|
|
122
|
+
"Product target must resolve to a full URI like 's3://bucket/path' or "
|
|
123
|
+
f"'file:///abs/path'. Bare paths are no longer supported. Got: {raw!r}"
|
|
124
|
+
)
|
|
125
|
+
return StorageUri.parse(raw)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _base_uri_from_storage_config(storage_config: StorageConfig) -> StorageUri | None:
|
|
129
|
+
identity = identity_from_storage_config(storage_config)
|
|
130
|
+
return identity.product_uri if identity is not None else None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@overload
|
|
134
|
+
def resolve_cli_product(product: str, *, format: str = "zarr") -> tuple[str, StorageUri]: ...
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@overload
|
|
138
|
+
def resolve_cli_product(product: None, *, format: str = "zarr") -> tuple[None, None]: ...
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def resolve_cli_product(
|
|
142
|
+
product: str | None, *, format: str = "zarr"
|
|
143
|
+
) -> tuple[str | None, StorageUri | None]:
|
|
144
|
+
if product is None:
|
|
145
|
+
return None, None
|
|
146
|
+
if "://" not in product:
|
|
147
|
+
return product, None
|
|
148
|
+
product_label = product.rstrip("/").rsplit("/", 1)[-1]
|
|
149
|
+
identity = resolve_product_identity(
|
|
150
|
+
product, format=format, product_name=product_label, option_name="--product-name"
|
|
151
|
+
)
|
|
152
|
+
return identity.product_name, identity.product_uri
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def storage_config_from_ctx(ctx: click.Context) -> StorageConfig:
|
|
156
|
+
storage_config_obj: StorageConfig | None = ctx.obj.get("storage_config")
|
|
157
|
+
if storage_config_obj is None:
|
|
158
|
+
raise click.ClickException(
|
|
159
|
+
"Storage configuration not available. Define [storage] in config.toml or use --manifest-only."
|
|
160
|
+
)
|
|
161
|
+
return storage_config_obj
|
|
@@ -0,0 +1,294 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
|
|
23
|
+
from firecube.cli._product import require_full_uri
|
|
24
|
+
from firecube.cli._shared_options import dry_run_flag, yes_flag
|
|
25
|
+
from firecube.core.controlplane.types import AbandonSweepResult
|
|
26
|
+
from firecube.core.errors import ManifestError
|
|
27
|
+
from firecube.core.storage.uri import StorageUri
|
|
28
|
+
|
|
29
|
+
from ._manager import resolve_cli_product, resolve_manager
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.group("runs")
|
|
33
|
+
def runs_group() -> None:
|
|
34
|
+
"""manage ingestion run records
|
|
35
|
+
|
|
36
|
+
inspect and manage tracked chunk runs."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _echo_abandon_run_result(*, product_name: str, run_id: str, result: dict[str, object]) -> None:
|
|
40
|
+
if not result.get("abandoned"):
|
|
41
|
+
click.echo(f"Run {run_id} already terminal with status={result.get('status')}")
|
|
42
|
+
return
|
|
43
|
+
click.echo(f"Abandoned run {run_id} for {product_name}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _echo_abandon_sweep_result(
|
|
47
|
+
*,
|
|
48
|
+
product_name: str,
|
|
49
|
+
result: AbandonSweepResult,
|
|
50
|
+
dry_run: bool,
|
|
51
|
+
) -> None:
|
|
52
|
+
if not result.previewed:
|
|
53
|
+
click.echo("No stale runs found.")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if dry_run:
|
|
57
|
+
click.echo(f"[dry-run] Would abandon stale runs for {product_name}:")
|
|
58
|
+
for run_id in result.previewed:
|
|
59
|
+
click.echo(f" - {run_id}")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
click.echo(f"Abandoned stale runs for {product_name}:")
|
|
63
|
+
if result.abandoned:
|
|
64
|
+
click.echo(" Abandoned:")
|
|
65
|
+
for run_id in result.abandoned:
|
|
66
|
+
click.echo(f" - {run_id}")
|
|
67
|
+
if result.skipped_fresh:
|
|
68
|
+
click.echo(" Skipped fresh:")
|
|
69
|
+
for run_id in result.skipped_fresh:
|
|
70
|
+
click.echo(f" - {run_id}")
|
|
71
|
+
if result.skipped_already_terminal:
|
|
72
|
+
click.echo(" Skipped already terminal:")
|
|
73
|
+
for run_id in result.skipped_already_terminal:
|
|
74
|
+
click.echo(f" - {run_id}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@runs_group.command(
|
|
78
|
+
"list",
|
|
79
|
+
epilog="""\b
|
|
80
|
+
Examples:
|
|
81
|
+
# list all runs for a product
|
|
82
|
+
firecube chunks runs list --product-name <product>
|
|
83
|
+
|
|
84
|
+
\b
|
|
85
|
+
# filter by status
|
|
86
|
+
firecube chunks runs list --product-name <product> --status started
|
|
87
|
+
|
|
88
|
+
\b
|
|
89
|
+
# output as JSON
|
|
90
|
+
firecube chunks runs list --product-name <product> -f json
|
|
91
|
+
|
|
92
|
+
See also: firecube chunks runs abandon, firecube chunks list
|
|
93
|
+
""",
|
|
94
|
+
)
|
|
95
|
+
@click.option("-n", "--product-name", "product_name", required=True, help="product to inspect")
|
|
96
|
+
@click.option(
|
|
97
|
+
"--target",
|
|
98
|
+
"target",
|
|
99
|
+
default=None,
|
|
100
|
+
metavar="URI",
|
|
101
|
+
help=(
|
|
102
|
+
"Explicit product target URI (file://... or s3://...). "
|
|
103
|
+
"Overrides config/env backend resolution. Use when --product-name "
|
|
104
|
+
"alone resolves to the wrong backend (e.g. after moving a product "
|
|
105
|
+
"or when config points elsewhere)."
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
@click.option(
|
|
109
|
+
"--status", default=None, help="filter runs by status (started, complete, failed, abandoned)"
|
|
110
|
+
)
|
|
111
|
+
@click.option("--workspace", type=click.Path(path_type=Path), help="workspace directory override")
|
|
112
|
+
@click.option(
|
|
113
|
+
"-f",
|
|
114
|
+
"--format",
|
|
115
|
+
"output_format",
|
|
116
|
+
type=click.Choice(["table", "json"]),
|
|
117
|
+
default="table",
|
|
118
|
+
help="output format",
|
|
119
|
+
)
|
|
120
|
+
@click.pass_context
|
|
121
|
+
def list_cmd(
|
|
122
|
+
ctx: click.Context,
|
|
123
|
+
product_name: str,
|
|
124
|
+
target: str | None,
|
|
125
|
+
status: str | None,
|
|
126
|
+
workspace: Path | None,
|
|
127
|
+
output_format: str,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""list tracked ingestion runs
|
|
130
|
+
|
|
131
|
+
list tracked ingestion runs for a product from the .firecube/ control
|
|
132
|
+
plane. shows run status (started, complete, failed, abandoned), timestamps,
|
|
133
|
+
and span counts. use to identify stuck or failed runs before abandoning"""
|
|
134
|
+
if target is not None:
|
|
135
|
+
require_full_uri(target, option_name="--target")
|
|
136
|
+
try:
|
|
137
|
+
product_uri = StorageUri.parse(target)
|
|
138
|
+
except ValueError as exc:
|
|
139
|
+
raise click.UsageError(str(exc)) from exc
|
|
140
|
+
else:
|
|
141
|
+
product_name, product_uri = resolve_cli_product(product_name)
|
|
142
|
+
manager = resolve_manager(ctx, workspace, product_uri=product_uri, product_name=product_name)
|
|
143
|
+
runs = manager.list_runs(product=product_name, status=status)
|
|
144
|
+
|
|
145
|
+
if output_format == "json":
|
|
146
|
+
click.echo(
|
|
147
|
+
json.dumps(
|
|
148
|
+
[
|
|
149
|
+
{
|
|
150
|
+
"product": run.product,
|
|
151
|
+
"run_id": run.run_id,
|
|
152
|
+
"status": run.status,
|
|
153
|
+
"started_at": run.started_at,
|
|
154
|
+
"updated_at": run.updated_at,
|
|
155
|
+
"completed_at": run.completed_at,
|
|
156
|
+
"slot_range": (
|
|
157
|
+
list(run.slot_range) if run.slot_range is not None else None
|
|
158
|
+
),
|
|
159
|
+
"slot_group": run.slot_group,
|
|
160
|
+
"events": run.events,
|
|
161
|
+
"parts": run.parts,
|
|
162
|
+
"stale": run.stale,
|
|
163
|
+
"error": run.error,
|
|
164
|
+
}
|
|
165
|
+
for run in runs
|
|
166
|
+
],
|
|
167
|
+
indent=2,
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
if not runs:
|
|
173
|
+
click.echo(f"No runs found for {product_name}.")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
click.echo(f"{'Run ID':<36} {'Status':<10} {'State':<8} {'Parts':<5} {'Events':<6}")
|
|
177
|
+
click.echo("-" * 80)
|
|
178
|
+
for run in runs:
|
|
179
|
+
state = "stale" if run.stale else "active"
|
|
180
|
+
click.echo(f"{run.run_id:<36} {run.status:<10} {state:<8} {run.parts:<5} {run.events:<6}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@runs_group.command(
|
|
184
|
+
"abandon",
|
|
185
|
+
epilog="""\b
|
|
186
|
+
Examples:
|
|
187
|
+
# preview what would be abandoned
|
|
188
|
+
firecube chunks runs abandon --product-name <product> --run-id <run-id> --reason "crashed" --dry-run
|
|
189
|
+
|
|
190
|
+
\b
|
|
191
|
+
# abandon a stuck run in non-interactive context
|
|
192
|
+
firecube chunks runs abandon --product-name <product> --run-id <run-id> --reason "process crashed" \\
|
|
193
|
+
--yes-i-really-mean-it
|
|
194
|
+
|
|
195
|
+
See also: firecube chunks runs list, firecube chunks list
|
|
196
|
+
""",
|
|
197
|
+
)
|
|
198
|
+
@click.option("-n", "--product-name", "product_name", required=True, help="product owning the run")
|
|
199
|
+
@click.option(
|
|
200
|
+
"--target",
|
|
201
|
+
"target",
|
|
202
|
+
default=None,
|
|
203
|
+
metavar="URI",
|
|
204
|
+
help=(
|
|
205
|
+
"Explicit product target URI (file://... or s3://...). "
|
|
206
|
+
"Overrides config/env backend resolution. Use when --product-name "
|
|
207
|
+
"alone resolves to the wrong backend (e.g. after moving a product "
|
|
208
|
+
"or when config points elsewhere)."
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
@click.option(
|
|
212
|
+
"--run-id",
|
|
213
|
+
default=None,
|
|
214
|
+
help="run identifier (mutually exclusive with --all-stale)",
|
|
215
|
+
)
|
|
216
|
+
@click.option(
|
|
217
|
+
"--all-stale",
|
|
218
|
+
is_flag=True,
|
|
219
|
+
default=False,
|
|
220
|
+
help=("abandon all stale non-terminal runs for the product (mutually exclusive with --run-id)"),
|
|
221
|
+
)
|
|
222
|
+
@click.option("--reason", required=True, help="operator reason for abandonment")
|
|
223
|
+
@click.option("--workspace", type=click.Path(path_type=Path), help="workspace directory override")
|
|
224
|
+
@dry_run_flag
|
|
225
|
+
@yes_flag
|
|
226
|
+
@click.pass_context
|
|
227
|
+
def abandon_cmd(
|
|
228
|
+
ctx: click.Context,
|
|
229
|
+
product_name: str,
|
|
230
|
+
target: str | None,
|
|
231
|
+
run_id: str | None,
|
|
232
|
+
all_stale: bool,
|
|
233
|
+
reason: str,
|
|
234
|
+
workspace: Path | None,
|
|
235
|
+
dry_run: bool,
|
|
236
|
+
yes_i_really_mean_it: bool,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""mark a stuck run as abandoned
|
|
239
|
+
|
|
240
|
+
mark a non-terminal ingestion run as abandoned to unblock resume. a run
|
|
241
|
+
stuck in 'started' state blocks future ingestion for that product. use this
|
|
242
|
+
after confirming the original process is no longer active"""
|
|
243
|
+
if target is not None:
|
|
244
|
+
require_full_uri(target, option_name="--target")
|
|
245
|
+
try:
|
|
246
|
+
product_uri = StorageUri.parse(target)
|
|
247
|
+
except ValueError as exc:
|
|
248
|
+
raise click.UsageError(str(exc)) from exc
|
|
249
|
+
else:
|
|
250
|
+
product_name, product_uri = resolve_cli_product(product_name)
|
|
251
|
+
|
|
252
|
+
if (run_id is not None) == all_stale:
|
|
253
|
+
raise click.UsageError("Provide exactly one of --run-id or --all-stale.")
|
|
254
|
+
|
|
255
|
+
if all_stale:
|
|
256
|
+
manager = resolve_manager(
|
|
257
|
+
ctx, workspace, product_uri=product_uri, product_name=product_name
|
|
258
|
+
)
|
|
259
|
+
effective_dry_run = dry_run or not yes_i_really_mean_it
|
|
260
|
+
result = manager.abandon_stale_runs(
|
|
261
|
+
product=product_name,
|
|
262
|
+
reason=reason,
|
|
263
|
+
dry_run=effective_dry_run,
|
|
264
|
+
)
|
|
265
|
+
_echo_abandon_sweep_result(
|
|
266
|
+
product_name=product_name,
|
|
267
|
+
result=result,
|
|
268
|
+
dry_run=effective_dry_run,
|
|
269
|
+
)
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
assert run_id is not None
|
|
273
|
+
|
|
274
|
+
if not dry_run and not yes_i_really_mean_it:
|
|
275
|
+
if not sys.stdin.isatty():
|
|
276
|
+
raise click.UsageError(
|
|
277
|
+
"Confirmation required: re-run with --yes-i-really-mean-it or run interactively."
|
|
278
|
+
)
|
|
279
|
+
click.confirm(f"Abandon run '{run_id}'?", abort=True)
|
|
280
|
+
|
|
281
|
+
if dry_run:
|
|
282
|
+
click.echo(
|
|
283
|
+
f"[dry-run] Would abandon run '{run_id}' for product "
|
|
284
|
+
f"'{product_name}' (reason: {reason})"
|
|
285
|
+
)
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
manager = resolve_manager(ctx, workspace, product_uri=product_uri, product_name=product_name)
|
|
289
|
+
try:
|
|
290
|
+
result = manager.abandon_run(product=product_name, run_id=run_id, reason=reason)
|
|
291
|
+
except ManifestError as exc:
|
|
292
|
+
raise click.ClickException(str(exc)) from exc
|
|
293
|
+
|
|
294
|
+
_echo_abandon_run_result(product_name=product_name, run_id=run_id, result=result)
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from firecube.cli._shared_options import dry_run_flag
|
|
23
|
+
|
|
24
|
+
from ._manager import resolve_cli_product, resolve_manager
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.group("snapshots")
|
|
28
|
+
def snapshots_group() -> None:
|
|
29
|
+
"""manage control-plane snapshots
|
|
30
|
+
|
|
31
|
+
rebuild derived chunk snapshots."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@snapshots_group.command(
|
|
35
|
+
"rebuild",
|
|
36
|
+
epilog="""\b
|
|
37
|
+
Examples:
|
|
38
|
+
# preview a snapshot rebuild
|
|
39
|
+
firecube chunks snapshots rebuild --product-name <product> --dry-run
|
|
40
|
+
|
|
41
|
+
\b
|
|
42
|
+
# rebuild snapshot for a product
|
|
43
|
+
firecube chunks snapshots rebuild --product-name <product>
|
|
44
|
+
|
|
45
|
+
\b
|
|
46
|
+
# rebuild and output result as JSON
|
|
47
|
+
firecube chunks snapshots rebuild --product-name <product> -f json
|
|
48
|
+
|
|
49
|
+
See also: firecube chunks snapshots status, firecube chunks list
|
|
50
|
+
""",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"-n",
|
|
54
|
+
"--product-name",
|
|
55
|
+
"product_name",
|
|
56
|
+
required=True,
|
|
57
|
+
help="product to rebuild into a fresh chunk snapshot",
|
|
58
|
+
)
|
|
59
|
+
@click.option("--workspace", type=click.Path(path_type=Path), help="workspace directory override")
|
|
60
|
+
@click.option(
|
|
61
|
+
"-f",
|
|
62
|
+
"--format",
|
|
63
|
+
"output_format",
|
|
64
|
+
type=click.Choice(["table", "json"]),
|
|
65
|
+
default="table",
|
|
66
|
+
help="output format",
|
|
67
|
+
)
|
|
68
|
+
@dry_run_flag
|
|
69
|
+
@click.pass_context
|
|
70
|
+
def rebuild_cmd(
|
|
71
|
+
ctx: click.Context,
|
|
72
|
+
product_name: str,
|
|
73
|
+
workspace: Path | None,
|
|
74
|
+
output_format: str,
|
|
75
|
+
dry_run: bool,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""rebuild the chunk snapshot
|
|
78
|
+
|
|
79
|
+
rebuild the derived chunk snapshot for a product from the chunk event log.
|
|
80
|
+
snapshots are cached read models; use after the snapshot is missing,
|
|
81
|
+
corrupt, or very stale to restore fast query performance for chunks list"""
|
|
82
|
+
product_name, product_uri = resolve_cli_product(product_name)
|
|
83
|
+
|
|
84
|
+
if dry_run:
|
|
85
|
+
click.echo(f"[dry-run] Would rebuild snapshot for product '{product_name}'")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
manager = resolve_manager(ctx, workspace, product_uri=product_uri, product_name=product_name)
|
|
89
|
+
result = manager.rebuild_snapshot(product_name)
|
|
90
|
+
|
|
91
|
+
if output_format == "json":
|
|
92
|
+
click.echo(json.dumps(result, indent=2))
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
if result.get("locked"):
|
|
96
|
+
click.echo(
|
|
97
|
+
f"Snapshot rebuild skipped for {product_name}: another rebuild is already in progress."
|
|
98
|
+
)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
click.echo(
|
|
102
|
+
f"Rebuilt snapshot for {product_name}: generation={result.get('generation')} "
|
|
103
|
+
f"records={result.get('records', 0)}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@snapshots_group.command(
|
|
108
|
+
"status",
|
|
109
|
+
epilog="""\b
|
|
110
|
+
Examples:
|
|
111
|
+
# check snapshot status for a product
|
|
112
|
+
firecube chunks snapshots status --product-name <product>
|
|
113
|
+
|
|
114
|
+
\b
|
|
115
|
+
# output as JSON
|
|
116
|
+
firecube chunks snapshots status --product-name <product> -f json
|
|
117
|
+
|
|
118
|
+
See also: firecube chunks snapshots rebuild, firecube chunks list
|
|
119
|
+
""",
|
|
120
|
+
)
|
|
121
|
+
@click.option(
|
|
122
|
+
"-n",
|
|
123
|
+
"--product-name",
|
|
124
|
+
"product_name",
|
|
125
|
+
required=True,
|
|
126
|
+
help="product to check snapshot status for",
|
|
127
|
+
)
|
|
128
|
+
@click.option("--workspace", type=click.Path(path_type=Path), help="workspace directory override")
|
|
129
|
+
@click.option(
|
|
130
|
+
"-f",
|
|
131
|
+
"--format",
|
|
132
|
+
"output_format",
|
|
133
|
+
type=click.Choice(["table", "json"]),
|
|
134
|
+
default="table",
|
|
135
|
+
help="output format",
|
|
136
|
+
)
|
|
137
|
+
@click.pass_context
|
|
138
|
+
def status_cmd(
|
|
139
|
+
ctx: click.Context,
|
|
140
|
+
product_name: str,
|
|
141
|
+
workspace: Path | None,
|
|
142
|
+
output_format: str,
|
|
143
|
+
) -> None:
|
|
144
|
+
"""show snapshot age and status
|
|
145
|
+
|
|
146
|
+
show snapshot age and status for a product's .firecube/ control plane.
|
|
147
|
+
reports whether the snapshot exists, how many events it covers, and when
|
|
148
|
+
it was last rebuilt. use to decide whether a rebuild is needed"""
|
|
149
|
+
product_name, product_uri = resolve_cli_product(product_name)
|
|
150
|
+
manager = resolve_manager(ctx, workspace, product_uri=product_uri, product_name=product_name)
|
|
151
|
+
result = manager.snapshot_status(product_name)
|
|
152
|
+
|
|
153
|
+
if output_format == "json":
|
|
154
|
+
click.echo(json.dumps(result, indent=2))
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
if not result.get("exists"):
|
|
158
|
+
click.echo(f"No snapshot found for {product_name}")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
click.echo(f"Product: {product_name}")
|
|
162
|
+
click.echo(f"Age: {result['age_human']}")
|
|
163
|
+
click.echo(f"Cutoff: {result.get('completed_before', 'unknown')}")
|
|
164
|
+
click.echo(f"Generation: {result.get('generation', 'unknown')}")
|
|
165
|
+
click.echo(f"Records: {result.get('records', 'unknown')}")
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
from click.shell_completion import get_completion_class
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.command(
|
|
24
|
+
"completion",
|
|
25
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
26
|
+
epilog="""\b
|
|
27
|
+
Examples:
|
|
28
|
+
# enable completion for bash (add to ~/.bashrc)
|
|
29
|
+
eval "$(firecube completion bash)"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# enable completion for zsh (add to ~/.zshrc)
|
|
33
|
+
eval "$(firecube completion zsh)"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# enable completion for fish
|
|
37
|
+
firecube completion fish | source
|
|
38
|
+
""",
|
|
39
|
+
)
|
|
40
|
+
@click.argument("shell", type=click.Choice(["bash", "zsh", "fish"], case_sensitive=False))
|
|
41
|
+
@click.option(
|
|
42
|
+
"--output",
|
|
43
|
+
"output_path",
|
|
44
|
+
type=click.Path(path_type=Path),
|
|
45
|
+
help="write the completion script to a file instead of stdout",
|
|
46
|
+
)
|
|
47
|
+
@click.pass_context
|
|
48
|
+
def completion_cmd(ctx: click.Context, shell: str, output_path: Path | None) -> None:
|
|
49
|
+
"""generate shell completion scripts
|
|
50
|
+
|
|
51
|
+
Generates a shell completion script for firecube for bash, zsh, or fish.
|
|
52
|
+
Run the output through eval in your shell profile to enable tab
|
|
53
|
+
completion for all firecube commands and options.
|
|
54
|
+
"""
|
|
55
|
+
root = ctx.find_root()
|
|
56
|
+
# Always generate scripts for the installed console command name.
|
|
57
|
+
prog_name = "firecube"
|
|
58
|
+
complete_var = "_FIRECUBE_COMPLETE"
|
|
59
|
+
|
|
60
|
+
complete_cls = get_completion_class(shell)
|
|
61
|
+
if complete_cls is None:
|
|
62
|
+
raise click.ClickException(f"Unsupported shell: {shell}")
|
|
63
|
+
|
|
64
|
+
script = complete_cls(root.command, {}, prog_name, complete_var).source()
|
|
65
|
+
if output_path is not None:
|
|
66
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
output_path.write_text(script)
|
|
68
|
+
click.echo(f"Wrote {shell} completion script to {output_path}")
|
|
69
|
+
return
|
|
70
|
+
click.echo(script)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
__all__ = ["completion_cmd"]
|