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
firecube/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
"""Firecube package."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from firecube._version import VERSION
|
|
20
|
+
|
|
21
|
+
__version__ = VERSION
|
|
22
|
+
|
|
23
|
+
__all__ = ["__version__"]
|
firecube/__main__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
"""Firecube CLI entrypoint."""
|
|
16
|
+
|
|
17
|
+
from firecube.cli.main import cli
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
cli()
|
firecube/_version.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
"""Resolve Firecube's package version from installed metadata or pyproject.toml."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import importlib.metadata
|
|
20
|
+
import tomllib
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
_DISTRIBUTION_NAME = "firecube"
|
|
24
|
+
_UNKNOWN_VERSION = "0.0.0+unknown"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _pyproject_version() -> str | None:
|
|
28
|
+
current = Path(__file__).resolve()
|
|
29
|
+
for parent in current.parents[:4]:
|
|
30
|
+
pyproject = parent / "pyproject.toml"
|
|
31
|
+
if not pyproject.exists():
|
|
32
|
+
continue
|
|
33
|
+
data = tomllib.loads(pyproject.read_text())
|
|
34
|
+
version = data.get("project", {}).get("version")
|
|
35
|
+
return version if isinstance(version, str) and version else None
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_version() -> str:
|
|
40
|
+
try:
|
|
41
|
+
return importlib.metadata.version(_DISTRIBUTION_NAME)
|
|
42
|
+
except importlib.metadata.PackageNotFoundError:
|
|
43
|
+
return _pyproject_version() or _UNKNOWN_VERSION
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
VERSION = get_version()
|
|
@@ -0,0 +1,135 @@
|
|
|
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 typing import Literal
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
USER_FACING_PATHS: frozenset[tuple[str, ...]] = frozenset(
|
|
22
|
+
{
|
|
23
|
+
("ingest",),
|
|
24
|
+
("zarr", "validate"),
|
|
25
|
+
("zarr", "multires"),
|
|
26
|
+
("zarr", "preallocate"),
|
|
27
|
+
("zarr", "slots"),
|
|
28
|
+
("parquet", "validate"),
|
|
29
|
+
("parquet", "consolidate"),
|
|
30
|
+
("plugins", "list"),
|
|
31
|
+
("plugins", "describe"),
|
|
32
|
+
("plugins", "explain"),
|
|
33
|
+
("plugins", "create"),
|
|
34
|
+
("plugins", "install"),
|
|
35
|
+
("plugins", "uninstall"),
|
|
36
|
+
("advise", "batch-size"),
|
|
37
|
+
("advise", "compliance"),
|
|
38
|
+
("completion",),
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
OPERATOR_FACING_PATHS: frozenset[tuple[str, ...]] = frozenset(
|
|
43
|
+
{
|
|
44
|
+
("chunks", "list"),
|
|
45
|
+
("chunks", "delete"),
|
|
46
|
+
("chunks", "delete-span"),
|
|
47
|
+
("chunks", "claims", "list"),
|
|
48
|
+
("chunks", "claims", "clear"),
|
|
49
|
+
("chunks", "runs", "list"),
|
|
50
|
+
("chunks", "runs", "abandon"),
|
|
51
|
+
("chunks", "snapshots", "rebuild"),
|
|
52
|
+
("chunks", "snapshots", "status"),
|
|
53
|
+
("archive", "create"),
|
|
54
|
+
("archive", "restore"),
|
|
55
|
+
("archive", "info"),
|
|
56
|
+
("archive", "validate"),
|
|
57
|
+
("archive", "list"),
|
|
58
|
+
("catalog", "intake"),
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
TIER_1_PATHS: frozenset[tuple[str, ...]] = frozenset(
|
|
63
|
+
{
|
|
64
|
+
("chunks", "delete"),
|
|
65
|
+
("chunks", "delete-span"),
|
|
66
|
+
("archive", "create"),
|
|
67
|
+
("archive", "restore"),
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
TIER_2_PATHS: frozenset[tuple[str, ...]] = frozenset(
|
|
72
|
+
{
|
|
73
|
+
("chunks", "runs", "abandon"),
|
|
74
|
+
("chunks", "claims", "clear"),
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
TIER_3_PATHS: frozenset[tuple[str, ...]] = frozenset(
|
|
79
|
+
{
|
|
80
|
+
("chunks", "snapshots", "rebuild"),
|
|
81
|
+
("zarr", "preallocate"),
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
INTERNAL_TOKENS_USER_FORBIDDEN = frozenset(
|
|
86
|
+
{
|
|
87
|
+
".firecube",
|
|
88
|
+
"WAL",
|
|
89
|
+
"control-plane",
|
|
90
|
+
"DirectZarrIngestor",
|
|
91
|
+
"JOB_COMPLETION_INDEX",
|
|
92
|
+
"SUPPORTS_SLOT_RANGE_PARALLELISM",
|
|
93
|
+
"SchemaSizeMismatchError",
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
GROUP_AUDIENCE: dict[str, Literal["user", "operator"]] = {
|
|
98
|
+
"zarr": "user",
|
|
99
|
+
"archive": "operator",
|
|
100
|
+
"parquet": "user",
|
|
101
|
+
"chunks": "operator",
|
|
102
|
+
"advise": "user",
|
|
103
|
+
"plugins": "user",
|
|
104
|
+
"catalog": "operator",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_AUDIENCE_PATHS: dict[tuple[str, ...], Literal["user", "operator"]] = dict.fromkeys(
|
|
108
|
+
USER_FACING_PATHS, "user"
|
|
109
|
+
)
|
|
110
|
+
_AUDIENCE_PATHS.update(dict.fromkeys(OPERATOR_FACING_PATHS, "operator"))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def classify(command_path: tuple[str, ...]) -> Literal["user", "operator"]:
|
|
114
|
+
"""Return audience class for a command path.
|
|
115
|
+
|
|
116
|
+
Raises ``click.ClickException`` for unknown paths so the CLI boundary
|
|
117
|
+
renders a clean error instead of a Python traceback.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
return _AUDIENCE_PATHS[command_path]
|
|
122
|
+
except KeyError as exc:
|
|
123
|
+
raise click.ClickException(f"Unknown command path: {command_path!r}") from exc
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__all__ = [
|
|
127
|
+
"GROUP_AUDIENCE",
|
|
128
|
+
"INTERNAL_TOKENS_USER_FORBIDDEN",
|
|
129
|
+
"OPERATOR_FACING_PATHS",
|
|
130
|
+
"TIER_1_PATHS",
|
|
131
|
+
"TIER_2_PATHS",
|
|
132
|
+
"TIER_3_PATHS",
|
|
133
|
+
"USER_FACING_PATHS",
|
|
134
|
+
"classify",
|
|
135
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
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 dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
|
|
23
|
+
from firecube.cli._uri_policy import SCHEME_TO_STORAGE_TYPE
|
|
24
|
+
from firecube.core.uris import infer_target_protocol
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(slots=True)
|
|
28
|
+
class IngestCommandConfig:
|
|
29
|
+
"""Validated ingest command configuration.
|
|
30
|
+
|
|
31
|
+
Uses aggregate validation to report all missing/invalid command-level values at once.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
plugin: str
|
|
35
|
+
input_data: str | Path | None
|
|
36
|
+
target: str
|
|
37
|
+
write_mode: str | None
|
|
38
|
+
storage_type: str | None
|
|
39
|
+
storage_driver: str | None
|
|
40
|
+
product_name: str | None = None
|
|
41
|
+
output_format: str = "zarr"
|
|
42
|
+
in_memory: bool = False
|
|
43
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
_VALID_WRITE_MODES = frozenset({"staged", "direct"})
|
|
46
|
+
_VALID_STORAGE_TYPES = frozenset({"local", "s3"})
|
|
47
|
+
_VALID_STORAGE_DRIVERS = frozenset({"fsspec", "obstore"})
|
|
48
|
+
|
|
49
|
+
def __post_init__(self) -> None:
|
|
50
|
+
"""Aggregate all validation errors into a single Click usage error."""
|
|
51
|
+
errors: list[str] = []
|
|
52
|
+
|
|
53
|
+
if self.product_name is not None and self.product_name == "":
|
|
54
|
+
errors.append(
|
|
55
|
+
"--product-name was provided but is empty. Either omit it (use plugin "
|
|
56
|
+
"PRODUCT_NAME or config default_product_name), or provide a non-empty value."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if not self.target:
|
|
60
|
+
errors.append("--target is required.")
|
|
61
|
+
|
|
62
|
+
if self.write_mode is None:
|
|
63
|
+
errors.append(
|
|
64
|
+
"--write-mode is required. No inference from target locality. "
|
|
65
|
+
"Choose: staged (workspace-first then upload) or direct (stream to target)."
|
|
66
|
+
)
|
|
67
|
+
elif self.write_mode not in self._VALID_WRITE_MODES:
|
|
68
|
+
errors.append(
|
|
69
|
+
f"--write-mode must be one of: {', '.join(sorted(self._VALID_WRITE_MODES))} "
|
|
70
|
+
f"(got '{self.write_mode}')"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if self.storage_type is not None and self.storage_type not in self._VALID_STORAGE_TYPES:
|
|
74
|
+
errors.append(
|
|
75
|
+
f"--storage-type must be one of: {', '.join(sorted(self._VALID_STORAGE_TYPES))} "
|
|
76
|
+
f"(got '{self.storage_type}')"
|
|
77
|
+
)
|
|
78
|
+
if self.storage_type is not None and self.target:
|
|
79
|
+
try:
|
|
80
|
+
scheme = infer_target_protocol(self.target)
|
|
81
|
+
except ValueError as exc:
|
|
82
|
+
errors.append(str(exc))
|
|
83
|
+
else:
|
|
84
|
+
expected_storage_type = SCHEME_TO_STORAGE_TYPE.get(scheme)
|
|
85
|
+
if expected_storage_type is None:
|
|
86
|
+
errors.append(
|
|
87
|
+
f"Target URI scheme '{scheme}' is not supported. Use a file:// URI for "
|
|
88
|
+
"--storage-type local, or an s3:// URI for --storage-type s3."
|
|
89
|
+
)
|
|
90
|
+
elif expected_storage_type != self.storage_type:
|
|
91
|
+
alternate_target = (
|
|
92
|
+
"an s3://-compatible URI" if self.storage_type == "s3" else "a file:// URI"
|
|
93
|
+
)
|
|
94
|
+
errors.append(
|
|
95
|
+
f"Target URI scheme '{scheme}' is incompatible with --storage-type "
|
|
96
|
+
f"'{self.storage_type}'. Use --storage-type {expected_storage_type} "
|
|
97
|
+
f"for {scheme}:// targets, or change --target to {alternate_target}."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
self.storage_driver is not None
|
|
102
|
+
and self.storage_driver not in self._VALID_STORAGE_DRIVERS
|
|
103
|
+
):
|
|
104
|
+
errors.append(
|
|
105
|
+
f"--storage-driver must be one of: {', '.join(sorted(self._VALID_STORAGE_DRIVERS))} "
|
|
106
|
+
f"(got '{self.storage_driver}')"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if errors:
|
|
110
|
+
raise click.UsageError(
|
|
111
|
+
"Invalid ingest configuration:\n" + "\n".join(f" - {error}" for error in errors)
|
|
112
|
+
)
|
firecube/cli/_ctx.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
from collections.abc import Mapping
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
|
|
24
|
+
from firecube.core.config import StorageConfig, load_config_file
|
|
25
|
+
from firecube.core.runtime import identity_from_storage_config, resolve_storage_config
|
|
26
|
+
|
|
27
|
+
log = logging.getLogger("firecube.cli")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_config_file(ctx: click.Context) -> Path | None:
|
|
31
|
+
ctx.ensure_object(dict)
|
|
32
|
+
value = ctx.obj.get("config_file")
|
|
33
|
+
return value if isinstance(value, Path) else None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_config(ctx: click.Context) -> dict[str, Any]:
|
|
37
|
+
"""Load config TOML as a plain dictionary."""
|
|
38
|
+
return load_config_file(get_config_file(ctx))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_storage_config(
|
|
42
|
+
ctx: click.Context,
|
|
43
|
+
*,
|
|
44
|
+
overrides: Mapping[str, str | None] | None = None,
|
|
45
|
+
set_global: bool = False,
|
|
46
|
+
cache: bool = True,
|
|
47
|
+
) -> StorageConfig:
|
|
48
|
+
"""Resolve StorageConfig from config + env + optional overrides.
|
|
49
|
+
|
|
50
|
+
This centralizes common CLI behavior:
|
|
51
|
+
- reads `--config-file` stored in `ctx.obj["config_file"]`
|
|
52
|
+
- caches the resolved StorageConfig in `ctx.obj["storage_config"]`
|
|
53
|
+
- wraps errors as Click-friendly exceptions
|
|
54
|
+
"""
|
|
55
|
+
ctx.ensure_object(dict)
|
|
56
|
+
if cache and overrides is None:
|
|
57
|
+
cached = ctx.obj.get("storage_config")
|
|
58
|
+
if isinstance(cached, StorageConfig):
|
|
59
|
+
return cached
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
storage_config = resolve_storage_config(
|
|
63
|
+
config_file=get_config_file(ctx),
|
|
64
|
+
overrides=dict(overrides) if overrides is not None else None,
|
|
65
|
+
set_global=set_global,
|
|
66
|
+
)
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
raise click.ClickException(f"Failed to resolve storage configuration: {exc}") from exc
|
|
69
|
+
|
|
70
|
+
if cache and overrides is None:
|
|
71
|
+
ctx.obj["storage_config"] = storage_config
|
|
72
|
+
identity = identity_from_storage_config(storage_config)
|
|
73
|
+
product_uri = identity.product_uri if identity is not None else None
|
|
74
|
+
bucket = product_uri.authority if product_uri is not None else None
|
|
75
|
+
target_path = (
|
|
76
|
+
product_uri.path if product_uri is not None and product_uri.protocol == "file" else None
|
|
77
|
+
)
|
|
78
|
+
log.debug(
|
|
79
|
+
"Resolved StorageConfig (type=%s bucket=%s endpoint=%s target=%s)",
|
|
80
|
+
storage_config.storage_type,
|
|
81
|
+
bucket,
|
|
82
|
+
storage_config.endpoint_url,
|
|
83
|
+
target_path,
|
|
84
|
+
)
|
|
85
|
+
return storage_config
|
firecube/cli/_errors.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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 errno
|
|
18
|
+
import functools
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
|
|
23
|
+
_KNOWN_USER_ERROR_TYPE_NAMES: frozenset[str] = frozenset(
|
|
24
|
+
{
|
|
25
|
+
"ConfigurationError",
|
|
26
|
+
"GroupNotFoundError",
|
|
27
|
+
"FileNotFoundError",
|
|
28
|
+
"NodeNotFoundError",
|
|
29
|
+
"NotADirectoryError",
|
|
30
|
+
"PathNotFoundError",
|
|
31
|
+
"PermissionError",
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _is_known_user_error(exc: BaseException) -> bool:
|
|
37
|
+
return type(exc).__name__ in _KNOWN_USER_ERROR_TYPE_NAMES
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_known_user_oserror(exc: BaseException) -> bool:
|
|
41
|
+
"""True when exc is an OS-level error that is the user's fault (not infra)."""
|
|
42
|
+
return isinstance(exc, OSError) and exc.errno in {errno.ENOENT, errno.ENOTDIR, errno.EACCES}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def wrap_user_facing_errors[**P, R](func: Callable[P, R]) -> Callable[P, R]:
|
|
46
|
+
"""Convert known downstream errors to ``click.ClickException`` at the CLI boundary.
|
|
47
|
+
|
|
48
|
+
Only exceptions whose class name appears in ``_KNOWN_USER_ERROR_TYPE_NAMES``
|
|
49
|
+
are wrapped; ``click.ClickException`` and ``click.exceptions.Exit`` pass
|
|
50
|
+
through unchanged, and any other exception propagates so operators see the
|
|
51
|
+
real traceback.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@functools.wraps(func)
|
|
55
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
56
|
+
try:
|
|
57
|
+
return func(*args, **kwargs)
|
|
58
|
+
except click.exceptions.Exit:
|
|
59
|
+
raise
|
|
60
|
+
except click.ClickException:
|
|
61
|
+
raise
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
if _is_known_user_error(exc) or _is_known_user_oserror(exc):
|
|
64
|
+
raise click.ClickException(str(exc)) from exc
|
|
65
|
+
raise
|
|
66
|
+
|
|
67
|
+
return wrapper
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class UnknownOptionError(click.BadParameter):
|
|
71
|
+
def __init__(self, key: str, plugin: str, valid_keys: list[str]) -> None:
|
|
72
|
+
msg = (
|
|
73
|
+
f"Unknown option '{key}' for plugin '{plugin}'.\n"
|
|
74
|
+
f"Valid keys ({len(valid_keys)}): {', '.join(sorted(valid_keys))}\n"
|
|
75
|
+
f"Hint: run `firecube ingest {plugin} --show-options` to inspect. "
|
|
76
|
+
f"Experimental options must use the x_ prefix (e.g. --option x_foo=...)."
|
|
77
|
+
)
|
|
78
|
+
super().__init__(msg)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MissingProductNameError(click.UsageError):
|
|
82
|
+
def __init__(self, plugin: str) -> None:
|
|
83
|
+
super().__init__(
|
|
84
|
+
f"Missing product name for plugin '{plugin}'. Provide one of:\n"
|
|
85
|
+
f" 1. CLI flag: --product-name <name>\n"
|
|
86
|
+
f" 2. Config file: [plugins.{plugin}]\n default_product_name = '<name>'\n"
|
|
87
|
+
f" 3. Plugin class attr: {plugin}.PRODUCT_NAME = '<name>' (preferred; declared at plugin source)"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class MissingStorageTypeError(click.UsageError):
|
|
92
|
+
def __init__(self, target_uri: str) -> None:
|
|
93
|
+
super().__init__(
|
|
94
|
+
f"Missing --storage-type for target '{target_uri}'.\n"
|
|
95
|
+
f"Storage type is no longer inferred from URI scheme. Provide explicitly:\n"
|
|
96
|
+
f" --storage-type [local|s3]"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class MissingStorageDriverError(click.UsageError):
|
|
101
|
+
def __init__(self, target_uri: str) -> None:
|
|
102
|
+
super().__init__(
|
|
103
|
+
f"Missing --storage-driver for target '{target_uri}'. Provide explicitly:\n"
|
|
104
|
+
f" --storage-driver [fsspec|obstore]"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class MissingWriteModeError(click.UsageError):
|
|
109
|
+
def __init__(self) -> None:
|
|
110
|
+
super().__init__(
|
|
111
|
+
"Missing --write-mode. Required for all targets (no local-default inference). "
|
|
112
|
+
"Choose:\n"
|
|
113
|
+
" --write-mode staged : workspace-first, then upload\n"
|
|
114
|
+
" --write-mode direct : stream directly to target\n"
|
|
115
|
+
)
|