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,144 @@
|
|
|
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
|
+
"""Plugin registry and discovery for Firecube ingestors.
|
|
16
|
+
|
|
17
|
+
Core responsibilities:
|
|
18
|
+
- Provide the @register_ingestor decorator for PipelinedIngestor subclasses.
|
|
19
|
+
- Keep an in-memory mapping of available ingestors by name.
|
|
20
|
+
- Discover built-in plugins under firecube.ingestor.*
|
|
21
|
+
- Discover external plugins via entry points (group: 'firecube.plugins').
|
|
22
|
+
|
|
23
|
+
Non-goals:
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import importlib
|
|
30
|
+
import pkgutil
|
|
31
|
+
from importlib.metadata import entry_points
|
|
32
|
+
from types import ModuleType
|
|
33
|
+
from typing import TypeVar
|
|
34
|
+
|
|
35
|
+
from firecube.ingestor.contracts.interfaces import Ingestor
|
|
36
|
+
|
|
37
|
+
AVAILABLE_INGESTORS: dict[str, type[Ingestor]] = {}
|
|
38
|
+
_LOADED: bool = False
|
|
39
|
+
_IngestorT = TypeVar("_IngestorT", bound=Ingestor)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def register_ingestor(name: str):
|
|
43
|
+
"""Decorator to register a plugin under a given name."""
|
|
44
|
+
|
|
45
|
+
def decorator(cls: type[_IngestorT]) -> type[_IngestorT]:
|
|
46
|
+
# Using runtime protocol check
|
|
47
|
+
if not issubclass(cls, Ingestor):
|
|
48
|
+
raise TypeError("Ingestor must satisfy the Ingestor protocol")
|
|
49
|
+
|
|
50
|
+
AVAILABLE_INGESTORS[name] = cls
|
|
51
|
+
cls.name = name # pyright: ignore[reportAttributeAccessIssue]
|
|
52
|
+
return cls
|
|
53
|
+
|
|
54
|
+
return decorator
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_ingestor(name: str) -> type[Ingestor]:
|
|
58
|
+
"""Retrieve a registered ingestor class by name.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
name: The name used in @register_ingestor.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
The registered Ingestor class.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
KeyError: If the ingestor is not found.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
return AVAILABLE_INGESTORS[name]
|
|
72
|
+
except KeyError as exc:
|
|
73
|
+
raise KeyError(f"Ingestor '{name}' not registered") from exc
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def discover_ingestors() -> dict[str, type[Ingestor]]:
|
|
77
|
+
"""Import all plugin modules so that decorators can register them.
|
|
78
|
+
|
|
79
|
+
Discovery strategy:
|
|
80
|
+
1) If already loaded, return cached copy.
|
|
81
|
+
2) Load entry points from group 'firecube.plugins'.
|
|
82
|
+
This triggers module imports, ensuring @register_ingestor runs.
|
|
83
|
+
3) If FIRECUBE_LOAD_BUILTIN_PLUGINS=1 is set, fallback to walking
|
|
84
|
+
the `firecube.plugins` namespace.
|
|
85
|
+
"""
|
|
86
|
+
global _LOADED
|
|
87
|
+
if _LOADED:
|
|
88
|
+
return AVAILABLE_INGESTORS.copy()
|
|
89
|
+
|
|
90
|
+
import os
|
|
91
|
+
|
|
92
|
+
# 1. Entry Points (Standard)
|
|
93
|
+
group = "firecube.plugins"
|
|
94
|
+
eps = entry_points(group=group)
|
|
95
|
+
|
|
96
|
+
from firecube.ingestor.registry.version_compat import warn_if_incompatible
|
|
97
|
+
|
|
98
|
+
for ep in eps:
|
|
99
|
+
try:
|
|
100
|
+
ep.load()
|
|
101
|
+
except Exception as e:
|
|
102
|
+
import logging
|
|
103
|
+
|
|
104
|
+
logging.getLogger("firecube.registry").warning(
|
|
105
|
+
f"Failed to load plugin entry point '{ep.name}': {e}"
|
|
106
|
+
)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
registered = AVAILABLE_INGESTORS.get(ep.name)
|
|
110
|
+
if registered is not None:
|
|
111
|
+
warn_if_incompatible(ep.name, registered)
|
|
112
|
+
|
|
113
|
+
# 2. Legacy Fallback (Dev-only)
|
|
114
|
+
if os.environ.get("FIRECUBE_LOAD_BUILTIN_PLUGINS") == "1":
|
|
115
|
+
try:
|
|
116
|
+
import firecube.plugins as plugins_pkg # pyright: ignore[reportMissingImports]
|
|
117
|
+
except ImportError:
|
|
118
|
+
plugins_pkg = None
|
|
119
|
+
|
|
120
|
+
def _import_submodules(package: ModuleType, prefix: str) -> None:
|
|
121
|
+
for mod_info in pkgutil.iter_modules(package.__path__):
|
|
122
|
+
full_name = f"{prefix}.{mod_info.name}"
|
|
123
|
+
try:
|
|
124
|
+
module = importlib.import_module(full_name)
|
|
125
|
+
if mod_info.ispkg:
|
|
126
|
+
_import_submodules(module, full_name)
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
if plugins_pkg is not None:
|
|
131
|
+
_import_submodules(plugins_pkg, plugins_pkg.__name__)
|
|
132
|
+
|
|
133
|
+
_LOADED = True
|
|
134
|
+
return AVAILABLE_INGESTORS.copy()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def reset_plugin_discovery_cache() -> None:
|
|
138
|
+
"""Invalidate the plugin discovery cache.
|
|
139
|
+
|
|
140
|
+
Used by tests and plugin management CLI commands to force re-discovery.
|
|
141
|
+
"""
|
|
142
|
+
global _LOADED
|
|
143
|
+
AVAILABLE_INGESTORS.clear()
|
|
144
|
+
_LOADED = False
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
"""
|
|
16
|
+
Shared helpers for surfacing plugin metadata to CLI and API layers.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
|
|
23
|
+
from firecube.ingestor.registry.loader import AVAILABLE_INGESTORS, discover_ingestors, get_ingestor
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class PluginDescriptor:
|
|
28
|
+
name: str
|
|
29
|
+
module: str
|
|
30
|
+
description: str | None
|
|
31
|
+
products: dict[str, object]
|
|
32
|
+
defaults: dict[str, object] | None
|
|
33
|
+
required_options: dict[str, object] | None
|
|
34
|
+
product_name: str | None = None
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict[str, object]:
|
|
37
|
+
return {
|
|
38
|
+
"name": self.name,
|
|
39
|
+
"module": self.module,
|
|
40
|
+
"description": self.description,
|
|
41
|
+
"products": self.products,
|
|
42
|
+
"defaults": self.defaults,
|
|
43
|
+
"required_options": self.required_options,
|
|
44
|
+
"product_name": self.product_name,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _ensure_loaded() -> None:
|
|
49
|
+
discover_ingestors()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def list_plugins() -> list[str]:
|
|
53
|
+
_ensure_loaded()
|
|
54
|
+
return sorted(AVAILABLE_INGESTORS.keys())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_plugin_descriptor(plugin: str) -> PluginDescriptor:
|
|
58
|
+
_ensure_loaded()
|
|
59
|
+
cls = get_ingestor(plugin)
|
|
60
|
+
description = (cls.__doc__ or "").strip() or None
|
|
61
|
+
|
|
62
|
+
products: dict[str, object] = {}
|
|
63
|
+
spec = getattr(cls, "spec", None)
|
|
64
|
+
if spec:
|
|
65
|
+
for attr in ("product", "variable", "file_glob"):
|
|
66
|
+
value = getattr(spec, attr, None)
|
|
67
|
+
if value:
|
|
68
|
+
products[attr] = value
|
|
69
|
+
|
|
70
|
+
defaults = None
|
|
71
|
+
if hasattr(cls, "plugin_defaults"):
|
|
72
|
+
defaults = cls.plugin_defaults() # type: ignore[attr-defined]
|
|
73
|
+
|
|
74
|
+
required = None
|
|
75
|
+
if hasattr(cls, "required_options"):
|
|
76
|
+
required = cls.required_options() # type: ignore[attr-defined]
|
|
77
|
+
|
|
78
|
+
resolved_product_name = getattr(cls, "PRODUCT_NAME", None)
|
|
79
|
+
product_name = (
|
|
80
|
+
resolved_product_name
|
|
81
|
+
if isinstance(resolved_product_name, str) and resolved_product_name
|
|
82
|
+
else None
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return PluginDescriptor(
|
|
86
|
+
name=plugin,
|
|
87
|
+
module=cls.__module__,
|
|
88
|
+
description=description,
|
|
89
|
+
products=products,
|
|
90
|
+
defaults=defaults,
|
|
91
|
+
required_options=required,
|
|
92
|
+
product_name=product_name,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def list_plugin_descriptors() -> list[PluginDescriptor]:
|
|
97
|
+
return [get_plugin_descriptor(name) for name in list_plugins()]
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
"""Plugin version compatibility check.
|
|
16
|
+
|
|
17
|
+
A plugin may declare ``firecube_core_min_version`` (string, PEP 440) on its
|
|
18
|
+
registered ingestor class. When ``discover_ingestors()`` discovers a plugin, the
|
|
19
|
+
declared minimum is compared against the running ``firecube.ingestor`` version.
|
|
20
|
+
|
|
21
|
+
The check is intentionally permissive: a missing declaration is fine, an
|
|
22
|
+
unparseable value is fine, and even an incompatible value only emits a warning.
|
|
23
|
+
The goal is to surface *informational* drift between the core plugin contract
|
|
24
|
+
and out-of-tree plugin builds, not to block ingestion.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from firecube.ingestor import __version__ as CORE_VERSION
|
|
33
|
+
|
|
34
|
+
_LOG = logging.getLogger("firecube.registry.version_compat")
|
|
35
|
+
|
|
36
|
+
VERSION_ATTR = "firecube_core_min_version"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse(value: str):
|
|
40
|
+
from packaging.version import InvalidVersion, Version
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
return Version(value)
|
|
44
|
+
except InvalidVersion:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check_plugin_compatibility(
|
|
49
|
+
plugin_name: str,
|
|
50
|
+
plugin_cls: type[Any],
|
|
51
|
+
*,
|
|
52
|
+
core_version: str = CORE_VERSION,
|
|
53
|
+
) -> str | None:
|
|
54
|
+
"""Compare a plugin's declared minimum core version against ``core_version``.
|
|
55
|
+
|
|
56
|
+
Returns a human-readable warning message when the plugin requires a newer
|
|
57
|
+
core than the one currently running, otherwise ``None``. The message is
|
|
58
|
+
purely informational — callers decide whether to log/raise.
|
|
59
|
+
"""
|
|
60
|
+
declared = getattr(plugin_cls, VERSION_ATTR, None)
|
|
61
|
+
if declared is None:
|
|
62
|
+
return None
|
|
63
|
+
if not isinstance(declared, str):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
plugin_min = _parse(declared)
|
|
67
|
+
core_now = _parse(core_version)
|
|
68
|
+
if plugin_min is None or core_now is None:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
if core_now >= plugin_min:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
f"Plugin '{plugin_name}' requires firecube.ingestor>={declared}, "
|
|
76
|
+
f"but {core_version} is installed. The plugin may not work correctly."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def warn_if_incompatible(
|
|
81
|
+
plugin_name: str,
|
|
82
|
+
plugin_cls: type[Any],
|
|
83
|
+
*,
|
|
84
|
+
core_version: str = CORE_VERSION,
|
|
85
|
+
logger: logging.Logger | None = None,
|
|
86
|
+
) -> bool:
|
|
87
|
+
"""Emit a warning via ``logger`` when the plugin declares an incompatible minimum.
|
|
88
|
+
|
|
89
|
+
Returns True when a warning was emitted, False otherwise.
|
|
90
|
+
"""
|
|
91
|
+
message = check_plugin_compatibility(plugin_name, plugin_cls, core_version=core_version)
|
|
92
|
+
if message is None:
|
|
93
|
+
return False
|
|
94
|
+
(logger or _LOG).warning(message)
|
|
95
|
+
return True
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
|
|
@@ -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
|
+
"""Run-level metric aggregation utilities for plugin and engine integration."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from firecube.ingestor.types.context import PipelineRunState, RuntimeIngestContext
|
|
24
|
+
|
|
25
|
+
_RESERVED_AGGREGATE_METRICS_KEYS = frozenset({"pipeline"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def merge_batch_metrics(ctx: RuntimeIngestContext, state: PipelineRunState) -> dict[str, Any]:
|
|
29
|
+
"""Merge successful batch metrics into one run-level mapping.
|
|
30
|
+
|
|
31
|
+
Default policy:
|
|
32
|
+
- Numeric values are summed.
|
|
33
|
+
- List values are concatenated.
|
|
34
|
+
- Other values keep the first successful value.
|
|
35
|
+
- Zarr coverage is always merged under ``metrics["zarr"]["coverage"]``.
|
|
36
|
+
"""
|
|
37
|
+
merged_metrics: dict[str, Any] = {}
|
|
38
|
+
if ctx.output_format == "zarr":
|
|
39
|
+
merged_metrics["zarr"] = {"coverage": []}
|
|
40
|
+
|
|
41
|
+
for res in state.results:
|
|
42
|
+
if not res.success or not res.metrics:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
if "zarr" in res.metrics:
|
|
46
|
+
cov = res.metrics["zarr"].get("coverage")
|
|
47
|
+
if cov:
|
|
48
|
+
if "zarr" not in merged_metrics:
|
|
49
|
+
merged_metrics["zarr"] = {"coverage": []}
|
|
50
|
+
merged_metrics["zarr"]["coverage"].extend(cov)
|
|
51
|
+
|
|
52
|
+
for key, value in res.metrics.items():
|
|
53
|
+
if key == "zarr":
|
|
54
|
+
continue
|
|
55
|
+
if isinstance(value, (int, float)):
|
|
56
|
+
current = merged_metrics.get(key, 0)
|
|
57
|
+
if isinstance(current, (int, float)):
|
|
58
|
+
merged_metrics[key] = current + value
|
|
59
|
+
else:
|
|
60
|
+
merged_metrics[key] = value
|
|
61
|
+
elif isinstance(value, list):
|
|
62
|
+
if key not in merged_metrics:
|
|
63
|
+
merged_metrics[key] = []
|
|
64
|
+
if isinstance(merged_metrics[key], list):
|
|
65
|
+
merged_metrics[key].extend(value)
|
|
66
|
+
else:
|
|
67
|
+
merged_metrics.setdefault(key, value)
|
|
68
|
+
|
|
69
|
+
return merged_metrics
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def normalize_plugin_aggregate_metrics(
|
|
73
|
+
raw_metrics: Any,
|
|
74
|
+
*,
|
|
75
|
+
logger: logging.Logger,
|
|
76
|
+
plugin_name: str,
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
"""Normalize plugin aggregate metrics and enforce engine-owned key boundaries."""
|
|
79
|
+
if raw_metrics is None:
|
|
80
|
+
return {}
|
|
81
|
+
if not isinstance(raw_metrics, Mapping):
|
|
82
|
+
logger.warning(
|
|
83
|
+
"Plugin '%s' returned non-mapping aggregate metrics (%s); using empty metrics.",
|
|
84
|
+
plugin_name,
|
|
85
|
+
type(raw_metrics).__name__,
|
|
86
|
+
)
|
|
87
|
+
return {}
|
|
88
|
+
|
|
89
|
+
normalized: dict[str, Any] = {}
|
|
90
|
+
dropped_keys: list[str] = []
|
|
91
|
+
for key, value in raw_metrics.items():
|
|
92
|
+
if not isinstance(key, str):
|
|
93
|
+
dropped_keys.append(repr(key))
|
|
94
|
+
continue
|
|
95
|
+
normalized[key] = value
|
|
96
|
+
|
|
97
|
+
if dropped_keys:
|
|
98
|
+
logger.warning(
|
|
99
|
+
"Plugin '%s' returned non-string aggregate metric keys; ignoring: %s",
|
|
100
|
+
plugin_name,
|
|
101
|
+
", ".join(sorted(dropped_keys)),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
for reserved_key in _RESERVED_AGGREGATE_METRICS_KEYS:
|
|
105
|
+
if reserved_key in normalized:
|
|
106
|
+
logger.warning(
|
|
107
|
+
"Plugin '%s' returned reserved aggregate metrics key '%s'; ignoring plugin value.",
|
|
108
|
+
plugin_name,
|
|
109
|
+
reserved_key,
|
|
110
|
+
)
|
|
111
|
+
normalized.pop(reserved_key, None)
|
|
112
|
+
return normalized
|