Files
BlackVortexAI-bv_nodepack/py/util/regional/context.py
T

401 lines
18 KiB
Python

from __future__ import annotations
import copy
import json
import math
import re
from collections.abc import Callable, Iterator, Mapping
from dataclasses import dataclass, field
from types import MappingProxyType
from typing import Any
from .document import RegionalValidationError, parse_document
ENVELOPE_VERSION = 3
CORE_VERSION = 1
_PACK_ID = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
_LOCAL_KEY = re.compile(r"^[a-z0-9]+(?:[._-][a-z0-9]+)*$")
class RegionalContextError(ValueError):
pass
class RegistryCollisionError(RegionalContextError):
pass
class UnsupportedCapabilityVersionError(RegionalContextError):
pass
Validator = Callable[[dict[str, Any]], None]
Migration = Callable[[dict[str, Any]], dict[str, Any]]
Operation = Callable[[dict[str, Any], Any], dict[str, Any]]
@dataclass(frozen=True)
class CapabilityRegistration:
version: int
validator: Validator
version_validators: Mapping[int, Validator] = field(default_factory=dict)
migrations: Mapping[int, Migration] = field(default_factory=dict)
operations: Mapping[str, Operation] = field(default_factory=dict)
metadata: Mapping[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not isinstance(self.version, int) or isinstance(self.version, bool) or self.version < 1:
raise RegionalContextError("capability version must be a positive integer")
unsupported = set(self.operations) - {"replace", "merge", "subtract", "clear"}
if unsupported:
raise RegionalContextError(f"unsupported capability operations: {sorted(unsupported)}")
object.__setattr__(self, "version_validators", MappingProxyType(dict(self.version_validators)))
object.__setattr__(self, "migrations", MappingProxyType(dict(self.migrations)))
object.__setattr__(self, "operations", MappingProxyType(dict(self.operations)))
object.__setattr__(self, "metadata", MappingProxyType(_json_copy(dict(self.metadata), path="capability metadata")))
@dataclass(frozen=True)
class ResourceRegistration:
validator: Callable[[dict[str, Any]], None]
metadata: Mapping[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
object.__setattr__(self, "metadata", MappingProxyType(_json_copy(dict(self.metadata), path="resource metadata")))
def _normalized_pack_id(pack_id: str) -> str:
normalized = re.sub(r"[-_.\s]+", "-", pack_id.strip().lower())
if not _PACK_ID.fullmatch(normalized):
raise RegionalContextError(f"invalid provider pack id: {pack_id!r}")
return normalized
def _registry_key(pack_id: str, local_key: str) -> str:
normalized_local = local_key.strip().lower()
if not _LOCAL_KEY.fullmatch(normalized_local):
raise RegionalContextError(f"invalid local registry key: {local_key!r}")
return f"{_normalized_pack_id(pack_id)}.{normalized_local}"
class CapabilityRegistry:
"""Internal, explicitly owned capability registry with fail-closed collisions."""
def __init__(self) -> None:
self._entries: dict[str, tuple[CapabilityRegistration, str]] = {}
def register(self, pack_id: str, local_key: str, registration: CapabilityRegistration, *, source: str) -> str:
key = _registry_key(pack_id, local_key)
if key in self._entries:
first_source = self._entries[key][1]
raise RegistryCollisionError(
f"capability {key!r} already registered by {first_source!r}; rejected {source!r}"
)
self._entries[key] = (registration, source)
return key
def get(self, key: str) -> CapabilityRegistration | None:
entry = self._entries.get(key)
return entry[0] if entry else None
def source(self, key: str) -> str | None:
entry = self._entries.get(key)
return entry[1] if entry else None
def keys(self) -> tuple[str, ...]:
return tuple(self._entries)
class ResourceRegistry:
"""Internal registry for serializable resource-reference types, never runtime objects."""
def __init__(self) -> None:
self._entries: dict[str, tuple[ResourceRegistration, str]] = {}
def register(self, pack_id: str, local_key: str, registration: ResourceRegistration, *, source: str) -> str:
key = _registry_key(pack_id, local_key)
if key in self._entries:
first_source = self._entries[key][1]
raise RegistryCollisionError(
f"resource type {key!r} already registered by {first_source!r}; rejected {source!r}"
)
self._entries[key] = (registration, source)
return key
def get(self, key: str) -> ResourceRegistration | None:
entry = self._entries.get(key)
return entry[0] if entry else None
def validate(self, key: str, reference: Any) -> dict[str, Any]:
registration = self.get(key)
if registration is None:
raise RegionalContextError(f"unregistered resource type: {key}")
clean = _json_copy(reference, path=f"resource[{key}]")
if not isinstance(clean, dict):
raise RegionalContextError(f"resource[{key}] must be an object")
registration.validator(clean)
return clean
def keys(self) -> tuple[str, ...]:
return tuple(self._entries)
def _json_copy(value: Any, *, path: str = "context") -> Any:
def visit(item: Any, item_path: str) -> None:
if item is None or isinstance(item, (str, bool)):
return
if isinstance(item, int) and not isinstance(item, bool):
return
if isinstance(item, float):
if math.isfinite(item):
return
raise RegionalContextError(f"{item_path} contains a non-finite number")
if isinstance(item, list):
for index, child in enumerate(item):
visit(child, f"{item_path}[{index}]")
return
if isinstance(item, dict):
for key, child in item.items():
if not isinstance(key, str):
raise RegionalContextError(f"{item_path} contains a non-string object key")
visit(child, f"{item_path}.{key}")
return
raise RegionalContextError(f"{item_path} contains non-JSON value {type(item).__name__}")
visit(value, path)
return copy.deepcopy(value)
def _only_keys(value: Any, allowed: set[str], path: str) -> None:
if not isinstance(value, dict):
return
unknown = sorted(set(value) - allowed)
if unknown:
raise RegionalContextError(f"{path} contains unknown fields: {unknown}")
def _validate_core_shape(core: Any) -> dict[str, Any]:
clean = _json_copy(core, path="core")
if not isinstance(clean, dict):
raise RegionalContextError("core must be an object")
core_keys = {"version", "document_id", "title", "canvas", "prompts", "negative_mode", "overlap", "regions"}
_only_keys(clean, core_keys, "core")
missing = sorted(core_keys - set(clean))
if missing:
raise RegionalContextError(f"core is missing required fields: {missing}")
if clean.get("version") != CORE_VERSION:
raise RegionalContextError(f"core.version must be {CORE_VERSION}")
if not isinstance(clean.get("title"), str):
raise RegionalContextError("core.title must be a string")
_only_keys(clean.get("canvas"), {"width", "height"}, "core.canvas")
_only_keys(clean.get("prompts"), {"global", "background"}, "core.prompts")
prompts = clean.get("prompts")
if isinstance(prompts, dict):
for scope in ("global", "background"):
_only_keys(prompts.get(scope), {"positive_source", "negative_source"}, f"core.prompts.{scope}")
_only_keys(clean.get("overlap"), {"mode"}, "core.overlap")
regions = clean.get("regions")
if isinstance(regions, list):
region_keys = {"id", "name", "parent_region_id", "enabled", "usage", "strength", "priority", "prompts", "mask", "geometry", "authoring"}
geometry_common = {"id", "layer_id", "mask_group_id", "type", "operation", "enabled", "authoring"}
geometry_specific = {
"rect": {"x", "y", "width", "height"}, "ellipse": {"x", "y", "width", "height"},
"polygon": {"points"}, "brush_stroke": {"shape", "pressure_mode", "size", "hardness", "opacity", "points"},
"raster_mask": {"x", "y", "width", "height", "pixel_width", "pixel_height", "data_url"},
}
for index, region in enumerate(regions):
path = f"core.regions[{index}]"
_only_keys(region, region_keys, path)
if not isinstance(region, dict):
continue
_only_keys(region.get("prompts"), {"positive_source", "negative_source"}, f"{path}.prompts")
_only_keys(region.get("mask"), {"feather"}, f"{path}.mask")
_only_keys(region.get("authoring"), {"visible", "locked", "color"}, f"{path}.authoring")
geometry = region.get("geometry")
if isinstance(geometry, list):
for shape_index, shape in enumerate(geometry):
shape_path = f"{path}.geometry[{shape_index}]"
shape_type = shape.get("type") if isinstance(shape, dict) else None
_only_keys(shape, geometry_common | geometry_specific.get(shape_type, set()), shape_path)
if isinstance(shape, dict):
_only_keys(shape.get("authoring"), {"name", "visible", "locked"}, f"{shape_path}.authoring")
points = shape.get("points")
if isinstance(points, list):
for point_index, point in enumerate(points):
_only_keys(point, {"x", "y", "pressure"}, f"{shape_path}.points[{point_index}]")
legacy = {"schema": "bv.regional", "version": 2, **{key: value for key, value in clean.items() if key != "version"}}
try:
parse_document(legacy)
except RegionalValidationError as exc:
raise RegionalContextError(str(exc).replace("BV Regional document", "BV Regional core")) from exc
return clean
def _normalize_registered_capability(key: str, payload: Any, registry: CapabilityRegistry | None) -> dict[str, Any]:
clean = _json_copy(payload, path=f"capabilities.{key}")
if not isinstance(clean, dict):
raise RegionalContextError(f"capabilities.{key} must be an object")
version = clean.get("version")
if not isinstance(version, int) or isinstance(version, bool) or version < 1:
raise RegionalContextError(f"capabilities.{key}.version must be a positive integer")
registration = registry.get(key) if registry else None
if registration is None or version > registration.version:
return clean
while version < registration.version:
migration = registration.migrations.get(version)
if migration is None:
raise RegionalContextError(f"capability {key} has no migration from version {version}")
clean = _json_copy(migration(copy.deepcopy(clean)), path=f"capabilities.{key}")
if not isinstance(clean, dict) or clean.get("version") != version + 1:
raise RegionalContextError(f"capability {key} migration {version} must produce version {version + 1}")
version += 1
step_validator = registration.version_validators.get(version)
if step_validator is not None:
step_validator(copy.deepcopy(clean))
registration.validator(copy.deepcopy(clean))
return clean
class RegionalContext(Mapping[str, Any]):
"""Immutable-by-interface v3 context facade with copy-on-write transforms."""
__slots__ = ("__wire", "__registry")
def __init__(self, wire: dict[str, Any], registry: CapabilityRegistry | None = None) -> None:
clean = _json_copy(wire)
if not isinstance(clean, dict):
raise RegionalContextError("regional context must be an object")
if clean.get("schema") != "bv.regional" or clean.get("version") != ENVELOPE_VERSION:
raise RegionalContextError("RegionalContext requires a BV Regional v3 envelope")
_only_keys(clean, {"schema", "version", "core", "capabilities"}, "context")
capabilities = clean.get("capabilities")
if not isinstance(capabilities, dict):
raise RegionalContextError("capabilities must be an object")
normalized_capabilities: dict[str, Any] = {}
for key, payload in capabilities.items():
if "." not in key:
raise RegionalContextError(f"capability key must be namespaced: {key!r}")
normalized_capabilities[key] = _normalize_registered_capability(key, payload, registry)
self.__wire = {
"schema": "bv.regional",
"version": ENVELOPE_VERSION,
"core": _validate_core_shape(clean.get("core")),
"capabilities": normalized_capabilities,
}
self.__registry = registry
def __getitem__(self, key: str) -> Any:
return copy.deepcopy(self.__wire[key])
def __iter__(self) -> Iterator[str]:
return iter(self.__wire)
def __len__(self) -> int:
return len(self.__wire)
def to_dict(self) -> dict[str, Any]:
return copy.deepcopy(self.__wire)
@property
def core(self) -> dict[str, Any]:
return copy.deepcopy(self.__wire["core"])
@property
def capabilities(self) -> dict[str, Any]:
return copy.deepcopy(self.__wire["capabilities"])
def with_core(self, core: Any) -> RegionalContext:
wire = self.to_dict()
wire["core"] = _validate_core_shape(core)
return RegionalContext(wire, self.__registry)
def with_capability(self, key: str, payload: Any) -> RegionalContext:
if not isinstance(key, str) or "." not in key:
raise RegionalContextError("capability key must be namespaced")
wire = self.to_dict()
wire["capabilities"][key] = _normalize_registered_capability(key, payload, self.__registry)
return RegionalContext(wire, self.__registry)
def without_capability(self, key: str) -> RegionalContext:
wire = self.to_dict()
wire["capabilities"].pop(key, None)
return RegionalContext(wire, self.__registry)
def require_capability(self, key: str) -> dict[str, Any]:
payload = self.__wire["capabilities"].get(key)
if payload is None:
raise RegionalContextError(f"required capability is missing: {key}")
registration = self.__registry.get(key) if self.__registry else None
if registration is None:
raise RegionalContextError(f"required capability is unregistered: {key}")
if payload["version"] > registration.version:
raise UnsupportedCapabilityVersionError(
f"capability {key} version {payload['version']} is newer than supported version {registration.version}"
)
registration.validator(copy.deepcopy(payload))
return copy.deepcopy(payload)
def normalize_context(value: Any, *, registry: CapabilityRegistry | None = None) -> RegionalContext:
if isinstance(value, RegionalContext):
value = value.to_dict()
try:
parsed = json.loads(value) if isinstance(value, str) else _json_copy(value)
except json.JSONDecodeError as exc:
raise RegionalContextError(f"invalid JSON: {exc}") from exc
if not isinstance(parsed, dict):
raise RegionalContextError("regional context must be an object")
if parsed.get("schema") != "bv.regional":
raise RegionalContextError("schema must be 'bv.regional'")
version = parsed.get("version")
if version in {1, 2}:
document = parse_document(parsed)
core = {"version": CORE_VERSION, **{key: value for key, value in document.items() if key not in {"schema", "version"}}}
wire = {"schema": "bv.regional", "version": ENVELOPE_VERSION, "core": _validate_core_shape(core), "capabilities": {}}
return RegionalContext(wire, registry)
if version != ENVELOPE_VERSION:
raise RegionalContextError(f"unsupported BV Regional envelope version: {version}")
_only_keys(parsed, {"schema", "version", "core", "capabilities"}, "context")
capabilities = parsed.get("capabilities")
if not isinstance(capabilities, dict):
raise RegionalContextError("capabilities must be an object")
normalized_capabilities: dict[str, Any] = {}
for key, payload in capabilities.items():
if not isinstance(key, str) or "." not in key:
raise RegionalContextError(f"capability key must be namespaced: {key!r}")
normalized_capabilities[key] = _normalize_registered_capability(key, payload, registry)
wire = {
"schema": "bv.regional",
"version": ENVELOPE_VERSION,
"core": _validate_core_shape(parsed.get("core")),
"capabilities": normalized_capabilities,
}
return RegionalContext(wire, registry)
def serialize_context(value: Any, *, registry: CapabilityRegistry | None = None, pretty: bool = False) -> str:
clean = normalize_context(value, registry=registry).to_dict()
return json.dumps(clean, ensure_ascii=False, sort_keys=True, indent=2 if pretty else None, separators=None if pretty else (",", ":"))
def context_document(value: Any, *, registry: CapabilityRegistry | None = None) -> dict[str, Any]:
"""Return the validated v2-compatible document projection used by existing consumers."""
core = normalize_context(value, registry=registry).core
return {
"schema": "bv.regional",
"version": 2,
**{key: item for key, item in core.items() if key != "version"},
}
def is_v3_context(value: Any) -> bool:
if isinstance(value, RegionalContext):
return True
if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError:
return False
return isinstance(value, dict) and value.get("schema") == "bv.regional" and value.get("version") == ENVELOPE_VERSION