Release v0.1.16
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-08-05 19:53:46 +02:00
parent add7a99f6d
commit f1a5be2a93
35 changed files with 1467 additions and 195 deletions
+1 -1
View File
@@ -356,7 +356,7 @@ def consume_first_admin_credential(
tenant = Tenant(
slug=clean_tenant_slug,
name=clean_tenant_name,
default_locale="en",
default_locale="de",
settings={},
is_active=True,
)
@@ -0,0 +1,236 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Literal, Mapping, cast
InformationGovernanceAdoption = Literal[
"not_applicable",
"contract_only",
"partial",
"enforced",
]
INFORMATION_GOVERNANCE_ADOPTION_ORDER: tuple[InformationGovernanceAdoption, ...] = (
"not_applicable",
"contract_only",
"partial",
"enforced",
)
class InformationGovernanceDeclarationError(ValueError):
pass
@dataclass(frozen=True, slots=True)
class InformationGovernanceDimension:
"""Truthful module-level adoption claim for one cross-cutting dimension."""
adoption: InformationGovernanceAdoption = "contract_only"
object_types: tuple[str, ...] = ()
evidence: tuple[str, ...] = ()
limitation: str | None = None
def __post_init__(self) -> None:
if self.adoption not in INFORMATION_GOVERNANCE_ADOPTION_ORDER:
raise InformationGovernanceDeclarationError(
f"Unsupported information-governance adoption: {self.adoption!r}."
)
for field_name in ("object_types", "evidence"):
values = getattr(self, field_name)
if len(values) != len(set(values)) or any(not item.strip() for item in values):
raise InformationGovernanceDeclarationError(
f"Information-governance {field_name.replace('_', ' ')} must "
"contain unique non-empty values."
)
if self.adoption == "enforced" and not self.evidence:
raise InformationGovernanceDeclarationError(
"An enforced information-governance dimension requires evidence."
)
if self.adoption in {"partial", "enforced"} and not self.object_types:
raise InformationGovernanceDeclarationError(
"Partial and enforced information-governance dimensions must "
"name their covered object types."
)
if self.adoption == "not_applicable" and self.object_types:
raise InformationGovernanceDeclarationError(
"A non-applicable information-governance dimension cannot declare object types."
)
if self.adoption in {"contract_only", "partial"} and not str(
self.limitation or ""
).strip():
raise InformationGovernanceDeclarationError(
"Contract-only and partial adoption must state the current limitation."
)
def to_dict(self) -> dict[str, object]:
return {
"adoption": self.adoption,
"object_types": list(self.object_types),
"evidence": list(self.evidence),
"limitation": self.limitation,
}
def _contract_only_dimension() -> InformationGovernanceDimension:
return InformationGovernanceDimension(
adoption="contract_only",
limitation=(
"The platform contract applies, but module-specific adoption evidence "
"has not been declared."
),
)
@dataclass(frozen=True, slots=True)
class ModuleInformationGovernance:
"""Cross-cutting data-use requirements and honest adoption evidence."""
temporal_browsing: InformationGovernanceDimension = field(
default_factory=_contract_only_dimension
)
purpose_aware_access: InformationGovernanceDimension = field(
default_factory=_contract_only_dimension
)
retention: InformationGovernanceDimension = field(
default_factory=_contract_only_dimension
)
institutional_context: InformationGovernanceDimension = field(
default_factory=_contract_only_dimension
)
current_authorization_for_historical_reads: bool = True
contract_version: str = "1"
def __post_init__(self) -> None:
if self.contract_version != "1":
raise InformationGovernanceDeclarationError(
"Unsupported module information-governance contract version."
)
if not self.current_authorization_for_historical_reads:
raise InformationGovernanceDeclarationError(
"Historical reads must always use current authorization."
)
for name, dimension in self.dimensions.items():
if not isinstance(dimension, InformationGovernanceDimension):
raise InformationGovernanceDeclarationError(
f"Information-governance dimension {name!r} has an invalid value."
)
@property
def dimensions(self) -> Mapping[str, InformationGovernanceDimension]:
return {
"temporal_browsing": self.temporal_browsing,
"purpose_aware_access": self.purpose_aware_access,
"retention": self.retention,
"institutional_context": self.institutional_context,
}
def to_dict(self) -> dict[str, object]:
return {
"contract_version": self.contract_version,
"current_authorization_for_historical_reads": (
self.current_authorization_for_historical_reads
),
"dimensions": {
name: dimension.to_dict()
for name, dimension in self.dimensions.items()
},
}
def information_governance_from_mapping(
value: Mapping[str, object],
) -> ModuleInformationGovernance:
raw_dimensions = value.get("dimensions")
if not isinstance(raw_dimensions, Mapping):
raise InformationGovernanceDeclarationError(
"Information-governance dimensions must be an object."
)
def dimension(name: str) -> InformationGovernanceDimension:
raw_dimension = raw_dimensions.get(name)
if not isinstance(raw_dimension, Mapping):
raise InformationGovernanceDeclarationError(
f"Information-governance dimension {name!r} must be an object."
)
def text_tuple(field_name: str) -> tuple[str, ...]:
raw_values = raw_dimension.get(field_name, ())
if not isinstance(raw_values, (list, tuple)):
raise InformationGovernanceDeclarationError(
f"Information-governance {name}.{field_name} must be a list."
)
if any(not isinstance(item, str) for item in raw_values):
raise InformationGovernanceDeclarationError(
f"Information-governance {name}.{field_name} must contain strings."
)
return tuple(raw_values)
raw_limitation = raw_dimension.get("limitation")
raw_adoption = raw_dimension.get("adoption") or "contract_only"
if not isinstance(raw_adoption, str):
raise InformationGovernanceDeclarationError(
f"Information-governance {name}.adoption must be a string."
)
if raw_limitation is not None and not isinstance(raw_limitation, str):
raise InformationGovernanceDeclarationError(
f"Information-governance {name}.limitation must be a string."
)
return InformationGovernanceDimension(
adoption=cast(
InformationGovernanceAdoption,
raw_adoption,
),
object_types=text_tuple("object_types"),
evidence=text_tuple("evidence"),
limitation=raw_limitation,
)
current_authorization = value.get(
"current_authorization_for_historical_reads",
True,
)
if not isinstance(current_authorization, bool):
raise InformationGovernanceDeclarationError(
"current_authorization_for_historical_reads must be boolean."
)
return ModuleInformationGovernance(
contract_version=str(value.get("contract_version") or "1"),
current_authorization_for_historical_reads=current_authorization,
temporal_browsing=dimension("temporal_browsing"),
purpose_aware_access=dimension("purpose_aware_access"),
retention=dimension("retention"),
institutional_context=dimension("institutional_context"),
)
def information_governance_maturity_issues(
declaration: ModuleInformationGovernance,
*,
maturity: str | None,
) -> tuple[str, ...]:
if maturity not in {"reference_ready", "supported", "lts"}:
return ()
incomplete = [
name
for name, dimension in declaration.dimensions.items()
if dimension.adoption not in {"not_applicable", "enforced"}
]
if not incomplete:
return ()
return (
f"Maturity {maturity!r} requires enforced or explicitly non-applicable "
"information governance for: " + ", ".join(incomplete),
)
__all__ = [
"INFORMATION_GOVERNANCE_ADOPTION_ORDER",
"InformationGovernanceAdoption",
"InformationGovernanceDeclarationError",
"InformationGovernanceDimension",
"ModuleInformationGovernance",
"information_governance_from_mapping",
"information_governance_maturity_issues",
]
@@ -17,6 +17,10 @@ from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from govoplan_core.core.versioning import format_version_range, version_range_is_valid, version_satisfies_range
from govoplan_core.core.information_governance import (
information_governance_from_mapping,
information_governance_maturity_issues,
)
from govoplan_core.core.provider_governance import (
external_provider_from_mapping,
module_architecture_from_mapping,
@@ -630,6 +634,7 @@ def _normalize_catalog_item(value: Any) -> dict[str, object]:
"tags": _string_list(value.get("tags")),
}
raw_architecture = value.get("architecture")
architecture_maturity: str | None = None
if raw_architecture is not None:
if not isinstance(raw_architecture, Mapping):
raise ValueError(
@@ -647,6 +652,27 @@ def _normalize_catalog_item(value: Any) -> dict[str, object]:
+ "; ".join(issues)
)
item["architecture"] = architecture.to_dict()
architecture_maturity = architecture.maturity
raw_information_governance = value.get("information_governance")
if raw_information_governance is not None:
if not isinstance(raw_information_governance, Mapping):
raise ValueError(
"Module package catalog information_governance for "
f"{module_id!r} must be an object."
)
information_governance = information_governance_from_mapping(
raw_information_governance
)
governance_issues = information_governance_maturity_issues(
information_governance,
maturity=architecture_maturity,
)
if governance_issues:
raise ValueError(
"Module package catalog information_governance for "
f"{module_id!r} is invalid: " + "; ".join(governance_issues)
)
item["information_governance"] = information_governance.to_dict()
raw_providers = value.get("external_providers")
if raw_providers is not None:
if not isinstance(raw_providers, list):
+4
View File
@@ -4,6 +4,7 @@ from collections.abc import Callable, Iterable, Mapping, Sequence
from dataclasses import dataclass, field
from typing import Any, Literal, Protocol, TYPE_CHECKING
from govoplan_core.core.information_governance import ModuleInformationGovernance
from govoplan_core.core.ownership import OwnershipProviderRegistration
from govoplan_core.core.provider_governance import (
ExternalProviderDeclaration,
@@ -446,6 +447,9 @@ class ModuleManifest:
...,
] = ()
architecture: ModuleArchitectureDeclaration | None = None
information_governance: ModuleInformationGovernance = field(
default_factory=ModuleInformationGovernance
)
external_providers: tuple[ExternalProviderDeclaration, ...] = ()
external_provider_state_providers: tuple[
ExternalProviderStateProviderRegistration,
+10
View File
@@ -32,6 +32,9 @@ from govoplan_core.core.module_entitlements import (
current_tenant_execution_context,
tenant_execution_scope,
)
from govoplan_core.core.information_governance import (
information_governance_maturity_issues,
)
from govoplan_core.core.ownership import (
OwnershipProviderRegistration,
ResourceOwnershipProvider,
@@ -788,6 +791,13 @@ def _validate_architecture_declarations(manifest: ModuleManifest) -> None:
raise RegistryError(
f"Module {manifest.id!r} architecture declaration: {issue}"
)
for issue in information_governance_maturity_issues(
manifest.information_governance,
maturity=architecture.maturity if architecture is not None else None,
):
raise RegistryError(
f"Module {manifest.id!r} information-governance declaration: {issue}"
)
provider_ids: set[str] = set()
declared_capabilities = {