feat: 为每个 Sensor 绑定只读 SensorContext
构造时一次绑定 SN、配置、资产和当前生效的标定:只接受显式给出的 SN,不做猜测;标定路径只取当前生效的标定,清除后为 None;换标定时整体替换。帧身份复用 stream-contract 的 FrameIdentity。load_config 改为复用 resolve_config_source,使记录的配置来源与实际读取的文件一致,并补一致性测试。已发布 Sensor 数值和资产包解析不变。回归 275 项、lint 通过。
This commit is contained in:
+50
-19
@@ -283,6 +283,48 @@ def resolve_sensor_config_path(name: str) -> Path | None:
|
||||
return bundle.sensor_json if bundle is not None else None
|
||||
|
||||
|
||||
def resolve_config_source(config_name_or_path) -> Path | None:
|
||||
"""Return the JSON file :func:`load_config` would read for this name/path.
|
||||
|
||||
Uses the same lookup order as ``load_config``: exact file, additive sensor
|
||||
bundle, config directory stem, then the legacy ``"name"`` field scan. Returns
|
||||
``None`` when the name has no file (never inventing a path), and follows
|
||||
aliases exactly like loading does so a recorded path is the one that was
|
||||
actually read.
|
||||
"""
|
||||
config_path = Path(config_name_or_path)
|
||||
if config_path.exists() and config_path.is_file():
|
||||
return config_path.resolve()
|
||||
|
||||
if config_path.is_absolute():
|
||||
return None
|
||||
|
||||
requested = str(config_name_or_path)
|
||||
candidates = [requested]
|
||||
alias = CONFIG_ALIASES.get(requested)
|
||||
if alias and alias not in candidates:
|
||||
candidates.append(alias)
|
||||
|
||||
for name in candidates:
|
||||
bundle = resolve_sensor_bundle(name)
|
||||
if bundle is not None:
|
||||
return bundle.sensor_json
|
||||
|
||||
for name in candidates:
|
||||
direct = CONFIG_DIR / f"{name}.json"
|
||||
if direct.exists():
|
||||
return direct.resolve()
|
||||
|
||||
for candidate in _config_files():
|
||||
try:
|
||||
data = _load_json(candidate)
|
||||
except Exception: # noqa: BLE001, S112 - unreadable configs are skipped during the scan
|
||||
continue
|
||||
if data.get("name") in candidates:
|
||||
return candidate.resolve()
|
||||
return None
|
||||
|
||||
|
||||
def resolve_sensor_asset_dir(name: str) -> Path | None:
|
||||
"""Return the default asset directory for a resolved sensor bundle."""
|
||||
bundle = resolve_sensor_bundle(name)
|
||||
@@ -402,6 +444,10 @@ def load_config(config_name_or_path):
|
||||
2. Additive sensor bundle ``sensors/<name>/sensor.json``
|
||||
3. File stem under the config directory (e.g. ``box`` -> ``config/box.json``)
|
||||
4. JSON field ``"name"`` matching the requested name (keeps ``prototype`` compatible)
|
||||
|
||||
The lookup itself lives in :func:`resolve_config_source`, so a caller that
|
||||
records which file was read (for example ``SensorContext.config_source``)
|
||||
cannot drift from what was actually loaded here.
|
||||
"""
|
||||
config_path = Path(config_name_or_path)
|
||||
|
||||
@@ -413,31 +459,16 @@ def load_config(config_name_or_path):
|
||||
if config_path.is_absolute():
|
||||
raise FileNotFoundError(f"Config file not found at absolute path: {config_path}")
|
||||
|
||||
source = resolve_config_source(config_name_or_path)
|
||||
if source is not None:
|
||||
return _load_json(source)
|
||||
|
||||
requested = str(config_name_or_path)
|
||||
candidates = [requested]
|
||||
alias = CONFIG_ALIASES.get(requested)
|
||||
if alias and alias not in candidates:
|
||||
candidates.append(alias)
|
||||
|
||||
for name in candidates:
|
||||
bundle = resolve_sensor_bundle(name)
|
||||
if bundle is not None:
|
||||
return _load_json(bundle.sensor_json)
|
||||
|
||||
for name in candidates:
|
||||
direct = CONFIG_DIR / f"{name}.json"
|
||||
if direct.exists():
|
||||
return _load_json(direct)
|
||||
|
||||
# Backward-compatible alias resolution: support old names via JSON "name" field.
|
||||
for candidate in _config_files():
|
||||
try:
|
||||
data = _load_json(candidate)
|
||||
except Exception: # noqa: S112 - skip unreadable config during alias scan
|
||||
continue
|
||||
if data.get("name") in candidates:
|
||||
return data
|
||||
|
||||
available = [f.stem for f in _config_files()]
|
||||
available_str = ", ".join(available) if available else "none"
|
||||
raise FileNotFoundError(
|
||||
|
||||
@@ -11,6 +11,7 @@ import time
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
@@ -41,6 +42,9 @@ from .capture import (
|
||||
open_capture,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .sensors.context import SensorContext
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrameMeasurement:
|
||||
@@ -229,6 +233,10 @@ class Sensor:
|
||||
self.bind_calibration_context(build_context(
|
||||
self._config, self.filters_state(), preprocessing={"mirror": self.mirror},
|
||||
))
|
||||
# One read-only binding of SN/config/assets/cal_path per instance. The
|
||||
# later read-only SensorContext contract (T04) is additive; a binding
|
||||
# problem is reported on the context, never by changing construction.
|
||||
self._bind_sensor_context()
|
||||
|
||||
if self.verbose:
|
||||
if self.calibration is not None and self.calibration.source_path is not None:
|
||||
@@ -1435,6 +1443,48 @@ class Sensor:
|
||||
),
|
||||
}
|
||||
|
||||
def _bind_sensor_context(self) -> None:
|
||||
"""Bind this instance's read-only SN/config/assets/cal_path context.
|
||||
|
||||
Called once during construction and again only when the force
|
||||
calibration is explicitly replaced. The binding is local to this
|
||||
instance; no state is cached or shared between sensors, and an
|
||||
unexpected binding problem is reported on the context instead of
|
||||
changing whether a published Sensor can be constructed. Partially
|
||||
initialised Sensor doubles (tests build some with ``__new__``) are
|
||||
tolerated: their missing fields bind as ``None``.
|
||||
"""
|
||||
from .sensors.context import build_sensor_context, fallback_sensor_context
|
||||
|
||||
config_name = getattr(self, "config_name", "")
|
||||
config = getattr(self, "_config", None)
|
||||
device_id = getattr(self, "device_id", None)
|
||||
calibration = getattr(self, "calibration", None)
|
||||
try:
|
||||
bundle_candidates = self._bundle_name_candidates()
|
||||
except Exception: # noqa: BLE001 - a test double may not carry the config
|
||||
bundle_candidates = []
|
||||
try:
|
||||
self._sensor_context = build_sensor_context(
|
||||
config_name=config_name,
|
||||
config=config if isinstance(config, dict) else {},
|
||||
device_id=device_id,
|
||||
calibration=calibration,
|
||||
bundle_candidates=bundle_candidates,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - context binding must not break a Sensor
|
||||
self._sensor_context = fallback_sensor_context(
|
||||
config_name=config_name,
|
||||
config=config,
|
||||
device_id=device_id,
|
||||
reason=exc,
|
||||
)
|
||||
|
||||
@property
|
||||
def context(self) -> "SensorContext":
|
||||
"""Read-only SN/config/assets/calibration binding of this Sensor instance."""
|
||||
return self._sensor_context
|
||||
|
||||
@property
|
||||
def force_calibrated(self) -> bool:
|
||||
"""True when at least one channel curve is loaded."""
|
||||
@@ -1475,6 +1525,7 @@ class Sensor:
|
||||
self._calibration_context = None
|
||||
self._force_readout.reset()
|
||||
self._apply_force_filter()
|
||||
self._bind_sensor_context()
|
||||
return
|
||||
if isinstance(cal, cal_mod.Calibration) or getattr(cal, "schema_version", None) == 3:
|
||||
selected = cal
|
||||
@@ -1509,6 +1560,9 @@ class Sensor:
|
||||
if not self._uses_calibration_model():
|
||||
self._force_readout.reset()
|
||||
self._apply_force_filter()
|
||||
# The context is a value object: an explicit calibration swap atomically
|
||||
# replaces it (SN/config/assets stay the same; cal path/fingerprint update).
|
||||
self._bind_sensor_context()
|
||||
|
||||
def _apply_force_filter(self):
|
||||
convert = self.calibration.convert if self.calibration is not None else None
|
||||
|
||||
@@ -53,6 +53,12 @@ from .capabilities import (
|
||||
report,
|
||||
unavailable,
|
||||
)
|
||||
from .context import (
|
||||
SensorAssetBinding,
|
||||
SensorContext,
|
||||
build_sensor_context,
|
||||
fallback_sensor_context,
|
||||
)
|
||||
from .descriptor import (
|
||||
BACKEND_COMPOUND_EYE,
|
||||
BACKEND_SINGLE_CAMERA,
|
||||
@@ -100,6 +106,11 @@ __all__ = [
|
||||
"Availability",
|
||||
"AxisAvailability",
|
||||
"Issue",
|
||||
# read-only per-instance binding (T04)
|
||||
"SensorContext",
|
||||
"SensorAssetBinding",
|
||||
"build_sensor_context",
|
||||
"fallback_sensor_context",
|
||||
# descriptor
|
||||
"BundleDescriptor",
|
||||
"DescriptorError",
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
"""Read-only ``SensorContext``: the SN, configuration, assets and calibration path
|
||||
bound once for one Sensor instance.
|
||||
|
||||
Stage-1 architecture (K11.1, sensor-context task T04) makes one object own what
|
||||
must not be re-guessed per frame: which physical sensor this is, which
|
||||
``sensor.json``/configuration it runs, which bundle assets were selected, and
|
||||
which force calibration was chosen. The context is a frozen value object; a
|
||||
``Sensor`` binds it once during construction (and atomically replaces it only
|
||||
when its calibration is explicitly swapped), so two Sensor instances cannot
|
||||
share or overwrite each other's identity.
|
||||
|
||||
Identity fields are not duplicated here: :meth:`SensorContext.frame_identity`
|
||||
builds the frozen :class:`~orisys.stream_contract.FrameIdentity` from stage 1,
|
||||
so the per-frame stream contract remains the single home for frame identity and
|
||||
clock fields.
|
||||
|
||||
The context is plain data (paths as strings, hashes, tuples): no Qt, no device
|
||||
handle, no mutable mapping. ``build_sensor_context`` is tolerant by design - a
|
||||
bundle that cannot be described is reported in ``diagnostics`` instead of
|
||||
changing a published Sensor's construction behaviour.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .. import configs as sdk_configs
|
||||
from ..stream_contract import FrameIdentity
|
||||
from .descriptor import parse_descriptor
|
||||
from .legacy import backend_from_config
|
||||
|
||||
__all__ = [
|
||||
"SensorAssetBinding",
|
||||
"SensorContext",
|
||||
"build_sensor_context",
|
||||
"fallback_sensor_context",
|
||||
]
|
||||
|
||||
#: Manifest keys that describe the bundle itself rather than an asset file.
|
||||
_MANIFEST_META_KEYS = frozenset({"name", "sensor_json", "version", "schema_version"})
|
||||
|
||||
|
||||
def _json_digest(value: Any) -> tuple[str, str | None]:
|
||||
"""Canonical SHA-256 of a JSON value plus an optional diagnostic."""
|
||||
try:
|
||||
text = json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
||||
except (TypeError, ValueError) as exc: # a config that is not plain JSON
|
||||
return (
|
||||
hashlib.sha256(str(value).encode("utf-8")).hexdigest(),
|
||||
f"config is not plain JSON for fingerprinting: {exc}",
|
||||
)
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest(), None
|
||||
|
||||
|
||||
def _file_digest(path: Path) -> str | None:
|
||||
try:
|
||||
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def _relative_posix(path: Path, root: Path) -> str | None:
|
||||
try:
|
||||
return path.resolve().relative_to(root.resolve()).as_posix()
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _manifest_assets(
|
||||
bundle: sdk_configs.SensorBundle,
|
||||
manifest_raw: Mapping[str, Any] | None,
|
||||
descriptor,
|
||||
diagnostics: list[str],
|
||||
) -> tuple[SensorAssetBinding, ...]:
|
||||
if manifest_raw is None:
|
||||
return ()
|
||||
|
||||
if descriptor is not None:
|
||||
# Descriptor-shaped manifest: bind the declared computational assets.
|
||||
bindings = []
|
||||
for declared in (descriptor.computational_assets, descriptor.assets):
|
||||
for role, value in sorted(declared.items()):
|
||||
rel = _relative_posix(Path(value), bundle.root)
|
||||
if rel is None:
|
||||
diagnostics.append(f"asset {role!r} escapes the bundle folder")
|
||||
continue
|
||||
bindings.append(SensorAssetBinding(str(role), rel))
|
||||
return tuple(bindings)
|
||||
|
||||
# Legacy manifest (for example ``sensors/finger/manifest.json``): bind the
|
||||
# declared asset directory and any single-file entries it names.
|
||||
bindings = []
|
||||
for role, value in sorted(manifest_raw.items()):
|
||||
if (
|
||||
role in _MANIFEST_META_KEYS
|
||||
or not isinstance(value, str)
|
||||
or not value.strip()
|
||||
):
|
||||
continue
|
||||
candidate = (bundle.root / Path(value)).resolve()
|
||||
rel = _relative_posix(candidate, bundle.root)
|
||||
if rel is None:
|
||||
diagnostics.append(f"manifest entry {role!r} escapes the bundle folder")
|
||||
continue
|
||||
bindings.append(SensorAssetBinding(str(role), rel))
|
||||
return tuple(bindings)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SensorAssetBinding:
|
||||
"""One bundle asset the instance was bound to: a role and a bundle-relative path."""
|
||||
|
||||
role: str
|
||||
path: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SensorContext:
|
||||
"""The immutable identity/config/assets/calibration binding of one Sensor instance.
|
||||
|
||||
``sensor_id`` is only the **explicitly supplied** SN (``device_id``); it is
|
||||
``None`` when the caller did not bind one. The context never guesses an SN
|
||||
from enumeration order, bundle names or reserved camera indices - a frame
|
||||
that needs a stream identity therefore fails loudly instead of being
|
||||
attributed to the wrong device. ``calibration_path`` and
|
||||
``calibration_fingerprint`` describe the calibration currently in effect:
|
||||
``set_calibration(None)`` leaves them ``None``/``"none"`` even when the
|
||||
Sensor was constructed with an explicit path.
|
||||
"""
|
||||
|
||||
sensor_id: str | None
|
||||
product_family: str | None
|
||||
processing_backend: str
|
||||
config_name: str
|
||||
config_source: str | None
|
||||
config_fingerprint: str
|
||||
bundle_root: str | None
|
||||
manifest_digest: str | None
|
||||
assets: tuple[SensorAssetBinding, ...]
|
||||
calibration_path: str | None
|
||||
calibration_fingerprint: str
|
||||
diagnostics: tuple[str, ...] = field(default_factory=tuple)
|
||||
|
||||
def frame_identity(
|
||||
self,
|
||||
*,
|
||||
session_id: str,
|
||||
frame_id: int,
|
||||
source_frame_id: int | None,
|
||||
acquisition_id: str | None = None,
|
||||
sync_group_id: str | None = None,
|
||||
) -> FrameIdentity:
|
||||
"""Build this stream's frozen frame identity from the bound SN.
|
||||
|
||||
The capture clock is not part of ``SensorContext``: it belongs to the
|
||||
frame header and is produced with the frame. This method reuses
|
||||
:class:`~orisys.stream_contract.FrameIdentity` rather than defining a
|
||||
second identity shape.
|
||||
"""
|
||||
if self.sensor_id is None:
|
||||
raise ValueError(
|
||||
"SensorContext has no explicit sensor_id (SN); construct the Sensor "
|
||||
"with device_id (or bind one) before building a frame identity"
|
||||
)
|
||||
return FrameIdentity(
|
||||
sensor_id=self.sensor_id,
|
||||
session_id=session_id,
|
||||
frame_id=frame_id,
|
||||
source_frame_id=source_frame_id,
|
||||
acquisition_id=acquisition_id,
|
||||
sync_group_id=sync_group_id,
|
||||
)
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""JSON-serialisable snapshot for records and diagnostics."""
|
||||
return {
|
||||
"sensor_id": self.sensor_id,
|
||||
"product_family": self.product_family,
|
||||
"processing_backend": self.processing_backend,
|
||||
"config_name": self.config_name,
|
||||
"config_source": self.config_source,
|
||||
"config_fingerprint": self.config_fingerprint,
|
||||
"bundle_root": self.bundle_root,
|
||||
"manifest_digest": self.manifest_digest,
|
||||
"assets": [
|
||||
{"role": asset.role, "path": asset.path} for asset in self.assets
|
||||
],
|
||||
"calibration_path": self.calibration_path,
|
||||
"calibration_fingerprint": self.calibration_fingerprint,
|
||||
"diagnostics": list(self.diagnostics),
|
||||
}
|
||||
|
||||
|
||||
def build_sensor_context(
|
||||
*,
|
||||
config_name: str | Path,
|
||||
config: Mapping[str, Any],
|
||||
device_id: str | None = None,
|
||||
calibration: Any = None,
|
||||
bundle_candidates: tuple[str, ...] | list[str] = (),
|
||||
) -> SensorContext:
|
||||
"""Bind one Sensor's SN, configuration identity, assets and calibration path.
|
||||
|
||||
Every lookup is explicit and local to this call; nothing is shared between
|
||||
instances. Optional bundle description problems are collected in
|
||||
``diagnostics`` and never raised, so adding a context cannot change whether a
|
||||
published Sensor can be constructed.
|
||||
"""
|
||||
diagnostics: list[str] = []
|
||||
cfg = config if isinstance(config, Mapping) else {}
|
||||
config_fingerprint, digest_problem = _json_digest(dict(cfg))
|
||||
if digest_problem:
|
||||
diagnostics.append(digest_problem)
|
||||
|
||||
config_source: str | None = None
|
||||
try:
|
||||
source = sdk_configs.resolve_config_source(config_name)
|
||||
except Exception as exc: # noqa: BLE001 - diagnostics only
|
||||
source = None
|
||||
diagnostics.append(f"config source could not be resolved: {exc}")
|
||||
if source is not None:
|
||||
config_source = str(Path(source).resolve())
|
||||
|
||||
names = [str(candidate) for candidate in bundle_candidates]
|
||||
product_family = next(
|
||||
(name for name in names if name in sdk_configs.PRODUCT_BUNDLES), None
|
||||
)
|
||||
bundle = None
|
||||
for name in names:
|
||||
try:
|
||||
bundle = sdk_configs.resolve_sensor_bundle(name)
|
||||
except Exception as exc: # noqa: BLE001 - diagnostics only
|
||||
diagnostics.append(f"bundle {name!r} could not be resolved: {exc}")
|
||||
continue
|
||||
if bundle is not None:
|
||||
break
|
||||
|
||||
processing_backend = backend_from_config(cfg)
|
||||
bundle_root: str | None = None
|
||||
manifest_digest: str | None = None
|
||||
assets: tuple[SensorAssetBinding, ...] = ()
|
||||
if bundle is not None:
|
||||
bundle_root = str(bundle.root)
|
||||
if product_family is None and bundle.name in sdk_configs.PRODUCT_BUNDLES:
|
||||
product_family = bundle.name
|
||||
manifest_raw: Mapping[str, Any] | None = None
|
||||
descriptor = None
|
||||
if bundle.manifest_json is not None:
|
||||
manifest_digest = _file_digest(bundle.manifest_json)
|
||||
if manifest_digest is None:
|
||||
diagnostics.append("bundle manifest.json is unreadable")
|
||||
try:
|
||||
loaded = json.loads(
|
||||
Path(bundle.manifest_json).read_text(encoding="utf-8-sig")
|
||||
)
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
diagnostics.append(f"bundle manifest.json could not be read: {exc}")
|
||||
else:
|
||||
if isinstance(loaded, Mapping):
|
||||
manifest_raw = loaded
|
||||
if loaded.get("schema_version") is not None:
|
||||
try:
|
||||
descriptor = parse_descriptor(bundle.root)
|
||||
except Exception as exc: # noqa: BLE001 - reported, never fatal
|
||||
diagnostics.append(
|
||||
f"bundle manifest.json is not a valid descriptor: {exc}"
|
||||
)
|
||||
# Legacy manifests have no backend field; the config rule that
|
||||
# already drives create_frame_processor stays in force.
|
||||
else:
|
||||
diagnostics.append("bundle manifest.json is not a JSON object")
|
||||
if descriptor is not None:
|
||||
processing_backend = descriptor.processing_backend
|
||||
assets = _manifest_assets(bundle, manifest_raw, descriptor, diagnostics)
|
||||
else:
|
||||
diagnostics.append("no additive sensor bundle resolved for this config")
|
||||
|
||||
calibration_path: str | None = None
|
||||
calibration_fingerprint = "none"
|
||||
if calibration is not None:
|
||||
source_path = getattr(calibration, "source_path", None)
|
||||
if source_path is not None:
|
||||
calibration_path = str(Path(source_path).resolve())
|
||||
fingerprint = getattr(calibration, "fingerprint", None)
|
||||
if isinstance(fingerprint, str) and fingerprint:
|
||||
calibration_fingerprint = fingerprint
|
||||
else:
|
||||
calibration_fingerprint = "unknown"
|
||||
diagnostics.append(
|
||||
f"{type(calibration).__name__} exposes no content fingerprint"
|
||||
)
|
||||
|
||||
sensor_id = None
|
||||
if device_id is not None:
|
||||
text = str(device_id).strip()
|
||||
sensor_id = text or None
|
||||
if sensor_id is None:
|
||||
diagnostics.append("device_id was empty; no SN is bound")
|
||||
|
||||
return SensorContext(
|
||||
sensor_id=sensor_id,
|
||||
product_family=product_family,
|
||||
processing_backend=processing_backend,
|
||||
config_name=str(config_name),
|
||||
config_source=config_source,
|
||||
config_fingerprint=config_fingerprint,
|
||||
bundle_root=bundle_root,
|
||||
manifest_digest=manifest_digest,
|
||||
assets=assets,
|
||||
calibration_path=calibration_path,
|
||||
calibration_fingerprint=calibration_fingerprint,
|
||||
diagnostics=tuple(diagnostics),
|
||||
)
|
||||
|
||||
|
||||
def fallback_sensor_context(
|
||||
*,
|
||||
config_name: str | Path,
|
||||
config: Mapping[str, Any] | None,
|
||||
device_id: str | None,
|
||||
reason: BaseException,
|
||||
) -> SensorContext:
|
||||
"""A minimal context used when binding itself fails unexpectedly.
|
||||
|
||||
The Sensor keeps working; the failure and its cause are visible on the
|
||||
context instead of crashing a published construction path.
|
||||
"""
|
||||
config_fingerprint, _ = _json_digest(
|
||||
dict(config) if isinstance(config, Mapping) else {}
|
||||
)
|
||||
backend = (
|
||||
backend_from_config(config) if isinstance(config, Mapping) else "single_camera"
|
||||
)
|
||||
sensor_id = str(device_id).strip() if device_id is not None else None
|
||||
return SensorContext(
|
||||
sensor_id=sensor_id or None,
|
||||
product_family=None,
|
||||
processing_backend=backend,
|
||||
config_name=str(config_name),
|
||||
config_source=None,
|
||||
config_fingerprint=config_fingerprint,
|
||||
bundle_root=None,
|
||||
manifest_digest=None,
|
||||
assets=(),
|
||||
calibration_path=None,
|
||||
calibration_fingerprint="none",
|
||||
diagnostics=(f"context binding failed: {type(reason).__name__}: {reason}",),
|
||||
)
|
||||
Reference in New Issue
Block a user