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,76 @@
|
|
|
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
|
|
18
|
+
|
|
19
|
+
from firecube.core.storage.uri import StorageUri
|
|
20
|
+
|
|
21
|
+
VALID_FORMATS = frozenset({"zarr", "parquet", "tensogram"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class ProductIdentity:
|
|
26
|
+
product_name: str
|
|
27
|
+
product_uri: StorageUri
|
|
28
|
+
control_root_uri: StorageUri
|
|
29
|
+
format: str
|
|
30
|
+
|
|
31
|
+
def __post_init__(self) -> None:
|
|
32
|
+
if self.format not in VALID_FORMATS:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
f"Invalid format {self.format!r}; expected one of {sorted(VALID_FORMATS)}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_uri(cls, uri: StorageUri, format: str, product_name: str) -> ProductIdentity:
|
|
39
|
+
"""Create a ProductIdentity from a URI with an explicit product name.
|
|
40
|
+
|
|
41
|
+
The product_name must be provided explicitly; it is not derived from the
|
|
42
|
+
URI basename. Use CLI --product-name, config default_product_name, or
|
|
43
|
+
plugin PRODUCT_NAME as the source.
|
|
44
|
+
"""
|
|
45
|
+
if not product_name:
|
|
46
|
+
raise ValueError(f"product_name is required; cannot be empty. URI: {uri}")
|
|
47
|
+
control_root = uri.join(".firecube")
|
|
48
|
+
return cls(
|
|
49
|
+
product_name=product_name,
|
|
50
|
+
product_uri=uri,
|
|
51
|
+
control_root_uri=control_root,
|
|
52
|
+
format=format,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def ensure_product_uri(base_uri: str, product: str) -> str:
|
|
57
|
+
"""Append `product` to `base_uri` if the last path segment doesn't exactly match.
|
|
58
|
+
|
|
59
|
+
Uses URI path parsing to avoid substring false positives (e.g. 'daily' matching
|
|
60
|
+
'daily_archive').
|
|
61
|
+
"""
|
|
62
|
+
from firecube.core.uris import parse_uri
|
|
63
|
+
|
|
64
|
+
base_uri = str(base_uri or "").rstrip("/")
|
|
65
|
+
product = str(product or "")
|
|
66
|
+
|
|
67
|
+
if not product:
|
|
68
|
+
return base_uri
|
|
69
|
+
|
|
70
|
+
parsed = parse_uri(base_uri)
|
|
71
|
+
path = parsed.get("path", "").rstrip("/")
|
|
72
|
+
last_segment = path.rsplit("/", 1)[-1] if "/" in path else path
|
|
73
|
+
if last_segment == product:
|
|
74
|
+
return base_uri
|
|
75
|
+
|
|
76
|
+
return base_uri + "/" + product
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
|
18
|
+
from enum import StrEnum
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from firecube.core.product.identity import ProductIdentity, ensure_product_uri
|
|
22
|
+
from firecube.core.storage.uri import StorageUri
|
|
23
|
+
from firecube.core.uris import (
|
|
24
|
+
is_remote_target,
|
|
25
|
+
is_windows_absolute_path,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CompletionRoute(StrEnum):
|
|
30
|
+
DIRECT = "direct"
|
|
31
|
+
STAGED = "staged"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class WriteModePolicy:
|
|
36
|
+
name: str
|
|
37
|
+
resolves_to_workspace: bool
|
|
38
|
+
seeds_staged_metadata: bool
|
|
39
|
+
storage_handled_by_engine: bool
|
|
40
|
+
completion_route: CompletionRoute
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
STAGED_WRITE_MODE = WriteModePolicy(
|
|
44
|
+
name="staged",
|
|
45
|
+
resolves_to_workspace=True,
|
|
46
|
+
seeds_staged_metadata=True,
|
|
47
|
+
storage_handled_by_engine=False,
|
|
48
|
+
completion_route=CompletionRoute.STAGED,
|
|
49
|
+
)
|
|
50
|
+
DIRECT_WRITE_MODE = WriteModePolicy(
|
|
51
|
+
name="direct",
|
|
52
|
+
resolves_to_workspace=False,
|
|
53
|
+
seeds_staged_metadata=False,
|
|
54
|
+
storage_handled_by_engine=True,
|
|
55
|
+
completion_route=CompletionRoute.DIRECT,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
_WRITE_MODE_POLICIES = {
|
|
59
|
+
STAGED_WRITE_MODE.name: STAGED_WRITE_MODE,
|
|
60
|
+
DIRECT_WRITE_MODE.name: DIRECT_WRITE_MODE,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def write_mode_policy(write_mode: str | WriteModePolicy) -> WriteModePolicy:
|
|
65
|
+
if isinstance(write_mode, WriteModePolicy):
|
|
66
|
+
return write_mode
|
|
67
|
+
try:
|
|
68
|
+
return _WRITE_MODE_POLICIES[str(write_mode)]
|
|
69
|
+
except KeyError as exc:
|
|
70
|
+
valid = ", ".join(sorted(_WRITE_MODE_POLICIES))
|
|
71
|
+
raise ValueError(f"Unknown write mode {write_mode!r}; expected one of: {valid}") from exc
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ProductResolver:
|
|
75
|
+
@staticmethod
|
|
76
|
+
def resolve(target: str, format: str, product_name: str) -> ProductIdentity:
|
|
77
|
+
"""Boundary-only: parse target string into typed ProductIdentity."""
|
|
78
|
+
try:
|
|
79
|
+
uri = StorageUri.parse(target)
|
|
80
|
+
except ValueError as e:
|
|
81
|
+
msg = str(e).lower()
|
|
82
|
+
if "scheme" in msg or "file://" in msg or "bare paths" in msg:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"--target must be a full URI like 's3://bucket/path/x.zarr' or "
|
|
85
|
+
"'file:///abs/path/x.zarr'. Bare names and relative paths are rejected. "
|
|
86
|
+
f"Original error: {e}"
|
|
87
|
+
) from e
|
|
88
|
+
raise
|
|
89
|
+
return ProductIdentity.from_uri(uri, format=format, product_name=product_name)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def resolve_dataset_target(
|
|
93
|
+
target: str,
|
|
94
|
+
*,
|
|
95
|
+
write_mode: str | WriteModePolicy = "staged",
|
|
96
|
+
temp_root: Path | None = None,
|
|
97
|
+
direct_base_uri: str | None = None,
|
|
98
|
+
) -> str:
|
|
99
|
+
"""Resolve the URI for a dataset target (Dataset Directory).
|
|
100
|
+
|
|
101
|
+
This determines the "Package Root" for Zarr/Parquet output, considering:
|
|
102
|
+
1. Absolute/Full URIs (s3://, /abs/path) -> Used as-is.
|
|
103
|
+
2. Relative paths + Staged mode -> Anchored to temporary workspace.
|
|
104
|
+
3. Relative paths + Direct mode -> Anchored to the typed product URI's parent.
|
|
105
|
+
"""
|
|
106
|
+
if (
|
|
107
|
+
is_remote_target(target)
|
|
108
|
+
or Path(target).is_absolute() # firecube: STORAGE-URI
|
|
109
|
+
or target.startswith("file://")
|
|
110
|
+
or is_windows_absolute_path(target)
|
|
111
|
+
):
|
|
112
|
+
return target
|
|
113
|
+
|
|
114
|
+
if write_mode_policy(write_mode).resolves_to_workspace:
|
|
115
|
+
workspace = temp_root if temp_root else Path.cwd()
|
|
116
|
+
return str(workspace / target)
|
|
117
|
+
|
|
118
|
+
if direct_base_uri:
|
|
119
|
+
return ensure_product_uri(direct_base_uri, target)
|
|
120
|
+
|
|
121
|
+
return target
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from firecube.core.storage.driver_config import (
|
|
21
|
+
StorageDriverConfig,
|
|
22
|
+
)
|
|
23
|
+
from firecube.core.storage.uri import StorageUri
|
|
24
|
+
from firecube.core.uris import parse_uri
|
|
25
|
+
|
|
26
|
+
_OBSTORE_SUPPORTED_PROTOCOLS = {"s3", "file", "gs", "az"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class ResolvedProduct:
|
|
31
|
+
product_name: str
|
|
32
|
+
product_uri: StorageUri
|
|
33
|
+
output_base_uri: StorageUri
|
|
34
|
+
control_root_uri: StorageUri
|
|
35
|
+
format: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ProductTarget:
|
|
39
|
+
@classmethod
|
|
40
|
+
def resolve(
|
|
41
|
+
cls,
|
|
42
|
+
target_arg: str,
|
|
43
|
+
driver_config: StorageDriverConfig,
|
|
44
|
+
*,
|
|
45
|
+
product_name: str,
|
|
46
|
+
plugin_default_format: str | None = None,
|
|
47
|
+
default_base_uri: StorageUri | None = None,
|
|
48
|
+
) -> ResolvedProduct:
|
|
49
|
+
target_value = str(target_arg or "")
|
|
50
|
+
if not product_name:
|
|
51
|
+
raise ValueError("product_name is required; cannot be empty.")
|
|
52
|
+
output_format = str(plugin_default_format or "zarr")
|
|
53
|
+
parsed = parse_uri(target_value)
|
|
54
|
+
protocol = parsed["protocol"]
|
|
55
|
+
|
|
56
|
+
if cls._is_bare_name_target(target_value, protocol):
|
|
57
|
+
if default_base_uri is None:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"Bare-name targets require either a fully-qualified URI/path or default_base_uri."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
product_uri = default_base_uri.join(target_value)
|
|
63
|
+
cls._validate_driver_protocol(driver_config, product_uri.protocol)
|
|
64
|
+
return ResolvedProduct(
|
|
65
|
+
product_name=product_name,
|
|
66
|
+
product_uri=product_uri,
|
|
67
|
+
output_base_uri=default_base_uri,
|
|
68
|
+
control_root_uri=cls._control_root(product_uri),
|
|
69
|
+
format=output_format,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if protocol != "file":
|
|
73
|
+
product_uri = StorageUri.parse(target_value)
|
|
74
|
+
cls._validate_driver_protocol(driver_config, product_uri.protocol)
|
|
75
|
+
return ResolvedProduct(
|
|
76
|
+
product_name=product_name,
|
|
77
|
+
product_uri=product_uri,
|
|
78
|
+
output_base_uri=product_uri.parent(),
|
|
79
|
+
control_root_uri=cls._control_root(product_uri),
|
|
80
|
+
format=output_format,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if target_value.startswith("file://"):
|
|
84
|
+
product_uri = StorageUri.parse(target_value)
|
|
85
|
+
resolved_path = Path(product_uri.path).resolve()
|
|
86
|
+
else:
|
|
87
|
+
resolved_path = Path(target_value).resolve()
|
|
88
|
+
product_uri = StorageUri.from_local_path(resolved_path)
|
|
89
|
+
cls._validate_driver_protocol(driver_config, product_uri.protocol)
|
|
90
|
+
return ResolvedProduct(
|
|
91
|
+
product_name=product_name,
|
|
92
|
+
product_uri=product_uri,
|
|
93
|
+
output_base_uri=product_uri.parent(),
|
|
94
|
+
control_root_uri=cls._control_root(product_uri),
|
|
95
|
+
format=output_format,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _is_bare_name_target(target_arg: str, protocol: str) -> bool:
|
|
100
|
+
return (
|
|
101
|
+
protocol == "file"
|
|
102
|
+
and "/" not in target_arg
|
|
103
|
+
and "\\" not in target_arg
|
|
104
|
+
and not target_arg.startswith("file://")
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _control_root(product_uri: StorageUri) -> StorageUri:
|
|
109
|
+
return product_uri.join(".firecube")
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _validate_driver_protocol(
|
|
113
|
+
driver_config: StorageDriverConfig,
|
|
114
|
+
protocol: str,
|
|
115
|
+
) -> None:
|
|
116
|
+
if driver_config.driver == "obstore" and protocol not in _OBSTORE_SUPPORTED_PROTOCOLS:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Storage driver 'obstore' does not support protocol '{protocol}'; "
|
|
119
|
+
"use --storage-driver=fsspec instead."
|
|
120
|
+
)
|
firecube/core/runtime.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
"""Runtime wiring and global state for Firecube configuration.
|
|
16
|
+
|
|
17
|
+
This module handles side-effects like setting global variables and exporting
|
|
18
|
+
environment variables. It depends on `firecube.core.config` for types
|
|
19
|
+
and loading logic.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import os
|
|
26
|
+
from collections.abc import Mapping, MutableMapping
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from firecube.core.config import (
|
|
31
|
+
StorageConfig,
|
|
32
|
+
build_storage_config,
|
|
33
|
+
derive_target_uri,
|
|
34
|
+
load_config_file,
|
|
35
|
+
)
|
|
36
|
+
from firecube.core.product.identity import ProductIdentity
|
|
37
|
+
from firecube.core.storage.uri import StorageUri
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger("firecube.core.runtime")
|
|
40
|
+
|
|
41
|
+
_global_storage_config: StorageConfig | None = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _target_uri_from_config(identity: ProductIdentity) -> str:
|
|
45
|
+
"""Render the FIRECUBE_TARGET_URI value from a `ProductIdentity`.
|
|
46
|
+
|
|
47
|
+
Preserves legacy env-var semantics: bare local path for `file://` URIs,
|
|
48
|
+
canonical URI form for remote (`s3://`, `gs://`) URIs.
|
|
49
|
+
"""
|
|
50
|
+
if identity.product_uri.protocol == "file":
|
|
51
|
+
return identity.product_uri.path
|
|
52
|
+
return identity.product_uri.to_str()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def set_global_storage_config(config: StorageConfig) -> None:
|
|
56
|
+
"""Set the global storage configuration."""
|
|
57
|
+
global _global_storage_config
|
|
58
|
+
config.validate()
|
|
59
|
+
_global_storage_config = config
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_global_storage_config() -> StorageConfig:
|
|
63
|
+
"""Return the global storage configuration."""
|
|
64
|
+
if _global_storage_config is None:
|
|
65
|
+
raise RuntimeError("Global storage configuration not set. Initialise with CLI args first.")
|
|
66
|
+
return _global_storage_config
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def export_storage_config_to_env(
|
|
70
|
+
config: StorageConfig,
|
|
71
|
+
identity: ProductIdentity | None = None,
|
|
72
|
+
*,
|
|
73
|
+
env: MutableMapping[str, str] | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Publish storage configuration to environment variables for child processes.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
config:
|
|
80
|
+
Non-location storage config (storage_type, endpoint_url, credentials,
|
|
81
|
+
region, path_style, storage_driver).
|
|
82
|
+
identity:
|
|
83
|
+
Optional product identity providing location fields (bucket /
|
|
84
|
+
target_path / target_uri). When ``None``, location vars are omitted.
|
|
85
|
+
env:
|
|
86
|
+
Optional mutable mapping to write into; defaults to ``os.environ``.
|
|
87
|
+
|
|
88
|
+
Notes
|
|
89
|
+
-----
|
|
90
|
+
We intentionally export both FIRECUBE_* and AWS_* style variables:
|
|
91
|
+
- FIRECUBE_* is used by Firecube internals and helpers.
|
|
92
|
+
- AWS_* allows libraries such as fsspec/xarray/zarr to discover
|
|
93
|
+
credentials and endpoints when working with S3-compatible storage.
|
|
94
|
+
"""
|
|
95
|
+
target_env: MutableMapping[str, str] = os.environ if env is None else env
|
|
96
|
+
|
|
97
|
+
target_env["FIRECUBE_STORAGE_TYPE"] = config.storage_type
|
|
98
|
+
if config.endpoint_url:
|
|
99
|
+
target_env["FIRECUBE_ENDPOINT_URL"] = config.endpoint_url
|
|
100
|
+
if config.access_key:
|
|
101
|
+
target_env["FIRECUBE_ACCESS_KEY"] = config.access_key
|
|
102
|
+
if config.secret_key:
|
|
103
|
+
target_env["FIRECUBE_SECRET_KEY"] = config.secret_key
|
|
104
|
+
if config.region:
|
|
105
|
+
target_env["FIRECUBE_REGION"] = config.region
|
|
106
|
+
target_env["FIRECUBE_PATH_STYLE"] = str(config.path_style).lower()
|
|
107
|
+
target_env["FIRECUBE_STORAGE_DRIVER"] = config.storage_driver
|
|
108
|
+
|
|
109
|
+
if identity is not None:
|
|
110
|
+
if identity.product_uri.authority:
|
|
111
|
+
target_env["FIRECUBE_BUCKET"] = identity.product_uri.authority
|
|
112
|
+
if identity.product_uri.protocol == "file":
|
|
113
|
+
target_env["FIRECUBE_TARGET_PATH"] = identity.product_uri.path
|
|
114
|
+
target_env["FIRECUBE_TARGET_URI"] = _target_uri_from_config(identity)
|
|
115
|
+
|
|
116
|
+
if config.access_key:
|
|
117
|
+
target_env["AWS_ACCESS_KEY_ID"] = config.access_key
|
|
118
|
+
if config.secret_key:
|
|
119
|
+
target_env["AWS_SECRET_ACCESS_KEY"] = config.secret_key
|
|
120
|
+
if config.region:
|
|
121
|
+
target_env["AWS_DEFAULT_REGION"] = config.region
|
|
122
|
+
if config.endpoint_url:
|
|
123
|
+
target_env["AWS_ENDPOINT_URL"] = config.endpoint_url
|
|
124
|
+
target_env["AWS_S3_ADDRESSING_STYLE"] = "path" if config.path_style else "virtual"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def identity_from_storage_config(storage_config: StorageConfig) -> ProductIdentity | None:
|
|
128
|
+
"""Synthesize a `ProductIdentity` from a resolved `StorageConfig`.
|
|
129
|
+
|
|
130
|
+
Bridges the legacy bridge-attr storage view (``target_path`` / ``bucket``
|
|
131
|
+
set on ``StorageConfig`` by ``build_storage_config``) to the
|
|
132
|
+
``ProductIdentity`` shape consumed by env-export and CLI bridge sites.
|
|
133
|
+
The remaining bridge-attr reads live inside ``derive_target_uri`` (see
|
|
134
|
+
config.py); this helper is the single seam between resolved config and
|
|
135
|
+
identity.
|
|
136
|
+
|
|
137
|
+
Returns ``None`` when the resolved config lacks a base URI (e.g. local
|
|
138
|
+
storage_type with no ``target_path`` set yet).
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
base_uri_str = derive_target_uri(storage_config)
|
|
142
|
+
except ValueError:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
if storage_config.storage_type == "local":
|
|
146
|
+
base_uri = StorageUri.from_local_path(base_uri_str)
|
|
147
|
+
else:
|
|
148
|
+
base_uri = StorageUri.parse(base_uri_str)
|
|
149
|
+
|
|
150
|
+
name = base_uri.path.rstrip("/").rsplit("/", 1)[-1]
|
|
151
|
+
return ProductIdentity(
|
|
152
|
+
product_name=name,
|
|
153
|
+
product_uri=base_uri,
|
|
154
|
+
control_root_uri=base_uri.join(".firecube"),
|
|
155
|
+
format="zarr",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def resolve_storage_config(
|
|
160
|
+
*,
|
|
161
|
+
config_file: Path | str | None = None,
|
|
162
|
+
env: Mapping[str, str] = os.environ, # type: ignore[assignment]
|
|
163
|
+
overrides: dict[str, Any] | None = None,
|
|
164
|
+
export_env: bool = True,
|
|
165
|
+
set_global: bool = True,
|
|
166
|
+
) -> StorageConfig:
|
|
167
|
+
"""Resolve StorageConfig from config.toml + env + overrides.
|
|
168
|
+
|
|
169
|
+
This is a convenience wrapper used by CLI entrypoints to keep the
|
|
170
|
+
load/build/validate/export wiring consistent across commands.
|
|
171
|
+
|
|
172
|
+
Notes
|
|
173
|
+
-----
|
|
174
|
+
- Precedence is implemented in `build_storage_config(...)`.
|
|
175
|
+
- This helper does not depend on click (so it can be used by non-CLI code).
|
|
176
|
+
"""
|
|
177
|
+
cfg = load_config_file(config_file)
|
|
178
|
+
storage_config = build_storage_config(cfg, env, overrides or {})
|
|
179
|
+
storage_config.validate()
|
|
180
|
+
|
|
181
|
+
if set_global:
|
|
182
|
+
set_global_storage_config(storage_config)
|
|
183
|
+
if export_env:
|
|
184
|
+
identity = identity_from_storage_config(storage_config)
|
|
185
|
+
export_storage_config_to_env(storage_config, identity)
|
|
186
|
+
|
|
187
|
+
return storage_config
|