nl2data-admin-service 0.1.0__tar.gz

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.
Files changed (36) hide show
  1. nl2data_admin_service-0.1.0/PKG-INFO +127 -0
  2. nl2data_admin_service-0.1.0/README.md +103 -0
  3. nl2data_admin_service-0.1.0/pyproject.toml +56 -0
  4. nl2data_admin_service-0.1.0/setup.cfg +4 -0
  5. nl2data_admin_service-0.1.0/src/nl2data_admin_service/__init__.py +21 -0
  6. nl2data_admin_service-0.1.0/src/nl2data_admin_service/assembly_admin.py +358 -0
  7. nl2data_admin_service-0.1.0/src/nl2data_admin_service/audit_admin.py +89 -0
  8. nl2data_admin_service-0.1.0/src/nl2data_admin_service/auth.py +90 -0
  9. nl2data_admin_service-0.1.0/src/nl2data_admin_service/authoring_admin.py +166 -0
  10. nl2data_admin_service-0.1.0/src/nl2data_admin_service/bundle_admin.py +428 -0
  11. nl2data_admin_service-0.1.0/src/nl2data_admin_service/common.py +268 -0
  12. nl2data_admin_service-0.1.0/src/nl2data_admin_service/config.py +40 -0
  13. nl2data_admin_service-0.1.0/src/nl2data_admin_service/dtos.py +729 -0
  14. nl2data_admin_service-0.1.0/src/nl2data_admin_service/errors.py +91 -0
  15. nl2data_admin_service-0.1.0/src/nl2data_admin_service/lint_admin.py +139 -0
  16. nl2data_admin_service-0.1.0/src/nl2data_admin_service/metadata_admin.py +301 -0
  17. nl2data_admin_service-0.1.0/src/nl2data_admin_service/protocols.py +364 -0
  18. nl2data_admin_service-0.1.0/src/nl2data_admin_service/schema.py +101 -0
  19. nl2data_admin_service-0.1.0/src/nl2data_admin_service/service.py +469 -0
  20. nl2data_admin_service-0.1.0/src/nl2data_admin_service/verification_admin.py +257 -0
  21. nl2data_admin_service-0.1.0/src/nl2data_admin_service.egg-info/PKG-INFO +127 -0
  22. nl2data_admin_service-0.1.0/src/nl2data_admin_service.egg-info/SOURCES.txt +34 -0
  23. nl2data_admin_service-0.1.0/src/nl2data_admin_service.egg-info/dependency_links.txt +1 -0
  24. nl2data_admin_service-0.1.0/src/nl2data_admin_service.egg-info/requires.txt +8 -0
  25. nl2data_admin_service-0.1.0/src/nl2data_admin_service.egg-info/top_level.txt +1 -0
  26. nl2data_admin_service-0.1.0/tests/test_admin_import_boundary.py +62 -0
  27. nl2data_admin_service-0.1.0/tests/test_assembly_lifecycle_service.py +352 -0
  28. nl2data_admin_service-0.1.0/tests/test_audit_inspection_service.py +403 -0
  29. nl2data_admin_service-0.1.0/tests/test_authoring_service.py +181 -0
  30. nl2data_admin_service-0.1.0/tests/test_capability_services.py +171 -0
  31. nl2data_admin_service-0.1.0/tests/test_host_integration.py +87 -0
  32. nl2data_admin_service-0.1.0/tests/test_lint_service.py +284 -0
  33. nl2data_admin_service-0.1.0/tests/test_schema.py +33 -0
  34. nl2data_admin_service-0.1.0/tests/test_security.py +24 -0
  35. nl2data_admin_service-0.1.0/tests/test_service_contract.py +190 -0
  36. nl2data_admin_service-0.1.0/tests/test_verification_service.py +294 -0
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: nl2data-admin-service
3
+ Version: 0.1.0
4
+ Summary: Optional transport-neutral admin control-plane service for the nl2data-core metadata-to-Bundle lifecycle.
5
+ Author: NL2Data Contributors
6
+ License: Apache-2.0
7
+ Keywords: nl2data,admin,service,semantic catalog,metadata
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: nl2data-core>=0.1.0
18
+ Requires-Dist: pydantic<3,>=2.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8.0; extra == "dev"
21
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
22
+ Requires-Dist: mypy>=1.10; extra == "dev"
23
+ Requires-Dist: ruff>=0.5; extra == "dev"
24
+
25
+ # nl2data-admin-service
26
+
27
+ Optional transport-neutral admin control-plane service for the nl2data-core metadata-to-Bundle lifecycle.
28
+
29
+ ## Overview
30
+
31
+ This package exposes a framework-neutral application service that hosts can wrap with their chosen transport (HTTP, CLI, UI, etc.). It intentionally has no HTTP, web-framework, authentication, or database-driver dependencies, keeping `nl2data` and `nl2data_core` transport-free and embeddable.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install -e packages/nl2data-admin-service
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ from nl2data_admin_service import AdminService, AdminServiceConfig
43
+ from nl2data_admin_service.auth import AuthContext, Permission
44
+
45
+ # Host provides dependencies and authentication context.
46
+ service = AdminService(dependencies, AdminServiceConfig())
47
+ context = AuthContext(
48
+ operator_id="operator-1",
49
+ tenant_scope_fingerprint="sha256:...",
50
+ permissions=frozenset([Permission.SNAPSHOT_READ]),
51
+ )
52
+ snapshot = service.get_snapshot("sha256:...", auth_context=context)
53
+ ```
54
+
55
+ ## Design
56
+
57
+ - **Transport-neutral**: Service methods accept and return bounded Pydantic DTOs.
58
+ - **Host-owned authentication**: The host supplies a trusted `AuthContext`; the service fails closed when it is missing.
59
+ - **Tenant/source scoped**: Every read and mutation is scoped by the host-provided tenant/source fingerprint.
60
+ - **Safe projection**: DTOs expose only opaque identifiers, fingerprints, versions, and bounded provenance.
61
+ - **Idempotent**: Mutating commands require an idempotency key and expected fingerprints/revisions.
62
+
63
+ ## Assembly lifecycle
64
+
65
+ The service projects the core lifecycle without reimplementing it:
66
+
67
+ 1. `create_draft(...)` or `create_draft_from_proposals(...)` creates an
68
+ `AssemblyDraft` in `draft` state. Draft DTOs expose state, assertion counts,
69
+ safe payload hashes, provenance kinds, redacted deployment-reference schemes,
70
+ and `draft_revision`, but no Bundle fingerprint.
71
+ 2. `submit_draft_for_review(...)` moves the draft to `review`.
72
+ `decide_draft_assertion(...)` approves, rejects, or edits one assertion; each
73
+ mutation requires the current revision, and payload edits invalidate review.
74
+ 3. `approve_assembly_draft(...)` freezes a fully reviewed draft in `approved`.
75
+ 4. `publish_draft(...)` delegates verification, publish-time fingerprinting,
76
+ accepted-assertion manifest derivation, audit creation, idempotency, and
77
+ supersession to core/catalog. Direct `publish_bundle(...)` is rejected.
78
+ 5. `activate_published_fingerprint(...)` and
79
+ `rollback_published_fingerprint(...)` move the active pointer among immutable
80
+ publications. `list_published_versions(...)` reports `available`, `active`,
81
+ `superseded`, `deprecated`, and `retired` state with predecessor/successor and
82
+ audit references.
83
+
84
+ Lifecycle mutations require both the matching `Permission` and a trusted
85
+ host-authorized Author, Reviewer, Approver, or Publisher role. Draft revision
86
+ conflicts return normalized `conflict`; missing roles return
87
+ `authorization_denied`. Publish outcomes contain bounded issue codes and safe
88
+ references only, never assertion payloads, resolved connections, or raw operator
89
+ identities.
90
+
91
+ ## Verification Suite
92
+
93
+ `await verify_draft(...)` requires `assembly:verify`, an expected draft revision,
94
+ and a configured policy profile. The host supplies the trusted verification
95
+ executor and candidate-bound execution-context factory. Verification may use
96
+ temporary fixtures and ephemeral secret resolution, but it does not mutate the
97
+ draft, approval state, catalog, or active pointer.
98
+
99
+ Planned publication uses `PublishDraftCommand` with the selected policy and
100
+ fresh `VerificationSuiteEvidence`. The service passes those bindings to core;
101
+ it does not reinterpret skipped, unavailable, timed-out, failed, or not-run
102
+ outcomes. `get_verification_evidence(...)` requires `assembly:audit` and returns
103
+ only bounded suite/layer/case statuses, counts, issue codes, identities, and
104
+ fingerprints. Queries, observations, expected or actual values, physical names,
105
+ deployment references, credentials, and backend exceptions never enter Admin
106
+ result DTOs.
107
+
108
+ ## Semantic authoring
109
+
110
+ `validate_authoring(...)` accepts `AuthoringDocumentCommand`, requires
111
+ `bundle:validate` permission and authorized source scope, and returns only a
112
+ bounded semantic summary or safe source-located diagnostics. It never reads or
113
+ writes the draft store.
114
+
115
+ `import_authoring(...)` accepts `ImportAuthoringCommand`, requires
116
+ `assembly:write` permission plus the trusted `author` lifecycle role, derives
117
+ the author reference from `AuthContext`, and persists through `create_draft`.
118
+ Imported assertions always begin pending in a revision-zero `draft`; authoring
119
+ cannot review, approve, publish, or activate content.
120
+
121
+ Capabilities advertise the supported authoring API version and effective
122
+ maximum input size. The built-in limit is 1 MiB and may be reduced by
123
+ `AdminServiceConfig.max_body_size_bytes`.
124
+
125
+ ## Service Contract Versioning
126
+
127
+ The service exposes a versioned command/result schema via `nl2data_admin_service.schema.build_schema(contract_version)`. Host transports can introspect this schema to bind requests and responses without hard-coding DTO shapes.
@@ -0,0 +1,103 @@
1
+ # nl2data-admin-service
2
+
3
+ Optional transport-neutral admin control-plane service for the nl2data-core metadata-to-Bundle lifecycle.
4
+
5
+ ## Overview
6
+
7
+ This package exposes a framework-neutral application service that hosts can wrap with their chosen transport (HTTP, CLI, UI, etc.). It intentionally has no HTTP, web-framework, authentication, or database-driver dependencies, keeping `nl2data` and `nl2data_core` transport-free and embeddable.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install -e packages/nl2data-admin-service
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from nl2data_admin_service import AdminService, AdminServiceConfig
19
+ from nl2data_admin_service.auth import AuthContext, Permission
20
+
21
+ # Host provides dependencies and authentication context.
22
+ service = AdminService(dependencies, AdminServiceConfig())
23
+ context = AuthContext(
24
+ operator_id="operator-1",
25
+ tenant_scope_fingerprint="sha256:...",
26
+ permissions=frozenset([Permission.SNAPSHOT_READ]),
27
+ )
28
+ snapshot = service.get_snapshot("sha256:...", auth_context=context)
29
+ ```
30
+
31
+ ## Design
32
+
33
+ - **Transport-neutral**: Service methods accept and return bounded Pydantic DTOs.
34
+ - **Host-owned authentication**: The host supplies a trusted `AuthContext`; the service fails closed when it is missing.
35
+ - **Tenant/source scoped**: Every read and mutation is scoped by the host-provided tenant/source fingerprint.
36
+ - **Safe projection**: DTOs expose only opaque identifiers, fingerprints, versions, and bounded provenance.
37
+ - **Idempotent**: Mutating commands require an idempotency key and expected fingerprints/revisions.
38
+
39
+ ## Assembly lifecycle
40
+
41
+ The service projects the core lifecycle without reimplementing it:
42
+
43
+ 1. `create_draft(...)` or `create_draft_from_proposals(...)` creates an
44
+ `AssemblyDraft` in `draft` state. Draft DTOs expose state, assertion counts,
45
+ safe payload hashes, provenance kinds, redacted deployment-reference schemes,
46
+ and `draft_revision`, but no Bundle fingerprint.
47
+ 2. `submit_draft_for_review(...)` moves the draft to `review`.
48
+ `decide_draft_assertion(...)` approves, rejects, or edits one assertion; each
49
+ mutation requires the current revision, and payload edits invalidate review.
50
+ 3. `approve_assembly_draft(...)` freezes a fully reviewed draft in `approved`.
51
+ 4. `publish_draft(...)` delegates verification, publish-time fingerprinting,
52
+ accepted-assertion manifest derivation, audit creation, idempotency, and
53
+ supersession to core/catalog. Direct `publish_bundle(...)` is rejected.
54
+ 5. `activate_published_fingerprint(...)` and
55
+ `rollback_published_fingerprint(...)` move the active pointer among immutable
56
+ publications. `list_published_versions(...)` reports `available`, `active`,
57
+ `superseded`, `deprecated`, and `retired` state with predecessor/successor and
58
+ audit references.
59
+
60
+ Lifecycle mutations require both the matching `Permission` and a trusted
61
+ host-authorized Author, Reviewer, Approver, or Publisher role. Draft revision
62
+ conflicts return normalized `conflict`; missing roles return
63
+ `authorization_denied`. Publish outcomes contain bounded issue codes and safe
64
+ references only, never assertion payloads, resolved connections, or raw operator
65
+ identities.
66
+
67
+ ## Verification Suite
68
+
69
+ `await verify_draft(...)` requires `assembly:verify`, an expected draft revision,
70
+ and a configured policy profile. The host supplies the trusted verification
71
+ executor and candidate-bound execution-context factory. Verification may use
72
+ temporary fixtures and ephemeral secret resolution, but it does not mutate the
73
+ draft, approval state, catalog, or active pointer.
74
+
75
+ Planned publication uses `PublishDraftCommand` with the selected policy and
76
+ fresh `VerificationSuiteEvidence`. The service passes those bindings to core;
77
+ it does not reinterpret skipped, unavailable, timed-out, failed, or not-run
78
+ outcomes. `get_verification_evidence(...)` requires `assembly:audit` and returns
79
+ only bounded suite/layer/case statuses, counts, issue codes, identities, and
80
+ fingerprints. Queries, observations, expected or actual values, physical names,
81
+ deployment references, credentials, and backend exceptions never enter Admin
82
+ result DTOs.
83
+
84
+ ## Semantic authoring
85
+
86
+ `validate_authoring(...)` accepts `AuthoringDocumentCommand`, requires
87
+ `bundle:validate` permission and authorized source scope, and returns only a
88
+ bounded semantic summary or safe source-located diagnostics. It never reads or
89
+ writes the draft store.
90
+
91
+ `import_authoring(...)` accepts `ImportAuthoringCommand`, requires
92
+ `assembly:write` permission plus the trusted `author` lifecycle role, derives
93
+ the author reference from `AuthContext`, and persists through `create_draft`.
94
+ Imported assertions always begin pending in a revision-zero `draft`; authoring
95
+ cannot review, approve, publish, or activate content.
96
+
97
+ Capabilities advertise the supported authoring API version and effective
98
+ maximum input size. The built-in limit is 1 MiB and may be reduced by
99
+ `AdminServiceConfig.max_body_size_bytes`.
100
+
101
+ ## Service Contract Versioning
102
+
103
+ The service exposes a versioned command/result schema via `nl2data_admin_service.schema.build_schema(contract_version)`. Host transports can introspect this schema to bind requests and responses without hard-coding DTO shapes.
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nl2data-admin-service"
7
+ version = "0.1.0"
8
+ description = "Optional transport-neutral admin control-plane service for the nl2data-core metadata-to-Bundle lifecycle."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "NL2Data Contributors" }]
13
+ keywords = ["nl2data", "admin", "service", "semantic catalog", "metadata"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Software Development :: Libraries",
22
+ ]
23
+ dependencies = [
24
+ "nl2data-core>=0.1.0",
25
+ "pydantic>=2.0,<3",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.0",
31
+ "pytest-asyncio>=0.23",
32
+ "mypy>=1.10",
33
+ "ruff>=0.5",
34
+ ]
35
+
36
+ [tool.setuptools.packages.find]
37
+ where = ["src"]
38
+
39
+ [tool.mypy]
40
+ python_version = "3.11"
41
+ files = ["src"]
42
+ mypy_path = ["../../src"]
43
+ check_untyped_defs = true
44
+ disallow_untyped_defs = true
45
+ no_implicit_optional = true
46
+ warn_redundant_casts = true
47
+ warn_unused_ignores = true
48
+ warn_return_any = true
49
+
50
+ [tool.ruff]
51
+ line-length = 100
52
+ target-version = "py311"
53
+ src = ["src", "tests"]
54
+
55
+ [tool.ruff.lint]
56
+ select = ["E", "F", "I", "UP", "B", "SIM"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ """Optional transport-neutral admin control-plane service for nl2data-core.
2
+
3
+ The ``nl2data-admin-service`` package exposes a framework-neutral application
4
+ service for the metadata-to-Bundle lifecycle. It intentionally has no HTTP,
5
+ authentication, web-framework, or database-driver dependencies so that hosts
6
+ can wrap it in their chosen transport (HTTP/CLI/UI) without coupling core to
7
+ any transport technology.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .config import AdminServiceConfig, AdminServiceProfile
13
+ from .protocols import AuthoringAdminService
14
+ from .service import AdminService
15
+
16
+ __all__ = [
17
+ "AdminService",
18
+ "AdminServiceConfig",
19
+ "AdminServiceProfile",
20
+ "AuthoringAdminService",
21
+ ]
@@ -0,0 +1,358 @@
1
+ """Assembly draft lifecycle Admin capability service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from nl2data_core.assembly import (
6
+ AssemblyDraft,
7
+ AssemblyState,
8
+ AssertionProvenance,
9
+ AssertionProvenanceKind,
10
+ DeploymentBinding,
11
+ LifecycleAction,
12
+ LifecycleRole,
13
+ ReviewState,
14
+ SemanticAssertion,
15
+ require_lifecycle_authorization,
16
+ )
17
+ from nl2data_core.assembly import approve_draft as core_approve_draft
18
+ from nl2data_core.assembly import create_discovery_draft as core_create_discovery_draft
19
+ from nl2data_core.assembly import decide_assertion as core_decide_assertion
20
+ from nl2data_core.assembly import edit_assertion as core_edit_assertion
21
+ from nl2data_core.assembly import submit_for_review as core_submit_for_review
22
+
23
+ from .auth import AuthContext, Permission
24
+ from .common import (
25
+ AdminDependencyAccess,
26
+ lifecycle_context,
27
+ load_draft,
28
+ require_permission,
29
+ require_source,
30
+ store_draft,
31
+ )
32
+ from .common import (
33
+ normalize_errors as _normalize_errors,
34
+ )
35
+ from .config import AdminServiceConfig
36
+ from .dtos import (
37
+ AssemblyAssertionSummary,
38
+ AssemblyDraftDetail,
39
+ AssemblyDraftSummary,
40
+ AssertionDecisionAction,
41
+ AssertionDecisionCommand,
42
+ DeploymentBindingSummary,
43
+ DraftMutationResult,
44
+ DraftRevisionCommand,
45
+ )
46
+ from .errors import ConflictError, NotFoundError, ValidationError
47
+ from .protocols import AdminServiceDependencies
48
+
49
+
50
+ def _draft_to_summary(draft: AssemblyDraft) -> AssemblyDraftSummary:
51
+ states = [assertion.review_state for assertion in draft.assertions]
52
+ return AssemblyDraftSummary(
53
+ draft_id=draft.draft_id,
54
+ bundle_id=draft.bundle_id,
55
+ model_version=draft.model_version,
56
+ state=draft.state.value,
57
+ draft_revision=draft.draft_revision,
58
+ assertion_count=len(draft.assertions),
59
+ pending_count=states.count(ReviewState.PENDING),
60
+ approved_count=states.count(ReviewState.APPROVED),
61
+ rejected_count=states.count(ReviewState.REJECTED),
62
+ )
63
+
64
+
65
+ def _draft_to_detail(draft: AssemblyDraft) -> AssemblyDraftDetail:
66
+ summary = _draft_to_summary(draft)
67
+ return AssemblyDraftDetail(
68
+ **summary.model_dump(),
69
+ assertions=tuple(
70
+ AssemblyAssertionSummary(
71
+ assertion_id=assertion.id,
72
+ assertion_type=assertion.type.value,
73
+ review_state=assertion.review_state.value,
74
+ payload_hash=assertion.payload_hash(),
75
+ provenance_kind=assertion.provenance.kind.value,
76
+ )
77
+ for assertion in draft.assertions
78
+ ),
79
+ deployment_bindings=tuple(
80
+ DeploymentBindingSummary(
81
+ binding_id=binding.binding_id,
82
+ environment=binding.environment,
83
+ source_id=binding.source_id,
84
+ reference_scheme=binding.reference_scheme,
85
+ )
86
+ for binding in draft.deployment_bindings
87
+ ),
88
+ )
89
+
90
+
91
+ class AssemblyLifecycleAdminCapability:
92
+ """Assembly draft creation, editing, review, approval, and provenance."""
93
+
94
+ def __init__(self, dependencies: AdminServiceDependencies, config: AdminServiceConfig) -> None:
95
+ self._deps = dependencies
96
+ self._access = AdminDependencyAccess(dependencies)
97
+ self._config = config
98
+
99
+ @_normalize_errors
100
+ def create_draft(
101
+ self,
102
+ draft: AssemblyDraft,
103
+ *,
104
+ auth_context: AuthContext,
105
+ ) -> DraftMutationResult:
106
+ require_permission(auth_context, Permission.ASSEMBLY_WRITE)
107
+ require_source(auth_context, draft.source_id)
108
+ authorization = lifecycle_context(self._deps, auth_context, draft.source_id)
109
+ require_lifecycle_authorization(
110
+ context=authorization,
111
+ authorizer=self._access.lifecycle_authorizer(),
112
+ required_role=LifecycleRole.AUTHOR,
113
+ action=LifecycleAction.CREATE_DRAFT,
114
+ resource_id=draft.draft_id,
115
+ )
116
+ if draft.state is not AssemblyState.DRAFT or draft.draft_revision != 0:
117
+ raise ValidationError("new assembly drafts must start at draft revision 0")
118
+ if draft.review_submitted_by is not None or draft.approved_by is not None:
119
+ raise ValidationError("new assembly drafts cannot carry review metadata")
120
+ if any(
121
+ assertion.review_state is not ReviewState.PENDING
122
+ or assertion.review_binding is not None
123
+ for assertion in draft.assertions
124
+ ):
125
+ raise ValidationError("new assembly draft assertions must be pending")
126
+ draft = AssemblyDraft.model_validate(
127
+ {
128
+ **draft.model_dump(mode="python", by_alias=True),
129
+ "author_reference": authorization.operator_reference,
130
+ }
131
+ )
132
+ try:
133
+ self._access.draft_store().create(
134
+ draft,
135
+ tenant_scope_fingerprint=auth_context.tenant_scope_fingerprint,
136
+ )
137
+ except ValueError as error:
138
+ raise ConflictError("Assembly draft already exists") from error
139
+ return DraftMutationResult(
140
+ draft=_draft_to_summary(draft),
141
+ audit_reference=authorization.operator_reference,
142
+ )
143
+
144
+ @_normalize_errors
145
+ def get_draft(
146
+ self,
147
+ draft_id: str,
148
+ *,
149
+ auth_context: AuthContext,
150
+ ) -> AssemblyDraftDetail:
151
+ require_permission(auth_context, Permission.ASSEMBLY_READ)
152
+ return _draft_to_detail(load_draft(self._access, draft_id, auth_context))
153
+
154
+ @_normalize_errors
155
+ def edit_draft(
156
+ self,
157
+ draft_id: str,
158
+ *,
159
+ expected_revision: int,
160
+ assertions: tuple[SemanticAssertion, ...] | None = None,
161
+ deployment_bindings: tuple[DeploymentBinding, ...] | None = None,
162
+ auth_context: AuthContext,
163
+ ) -> DraftMutationResult:
164
+ require_permission(auth_context, Permission.ASSEMBLY_WRITE)
165
+ current = load_draft(self._access, draft_id, auth_context)
166
+ authorization = lifecycle_context(self._deps, auth_context, current.source_id)
167
+ require_lifecycle_authorization(
168
+ context=authorization,
169
+ authorizer=self._access.lifecycle_authorizer(),
170
+ required_role=LifecycleRole.AUTHOR,
171
+ action=LifecycleAction.EDIT_DRAFT,
172
+ resource_id=draft_id,
173
+ )
174
+ changes: dict[str, object] = {}
175
+ if assertions is not None:
176
+ current_by_id = {assertion.id: assertion for assertion in current.assertions}
177
+ normalized: list[SemanticAssertion] = []
178
+ for assertion in assertions:
179
+ authoritative = current_by_id.get(assertion.id)
180
+ if (
181
+ authoritative is not None
182
+ and authoritative.type is assertion.type
183
+ and authoritative.payload_hash() == assertion.payload_hash()
184
+ and authoritative.provenance == assertion.provenance
185
+ ):
186
+ normalized.append(authoritative)
187
+ continue
188
+ normalized.append(
189
+ SemanticAssertion.create(
190
+ type=assertion.type,
191
+ payload=assertion.payload,
192
+ provenance=AssertionProvenance(
193
+ kind=AssertionProvenanceKind.MANUAL,
194
+ source_reference=(
195
+ f"seed:{authoritative.id}"
196
+ if authoritative is not None
197
+ else None
198
+ ),
199
+ ),
200
+ )
201
+ )
202
+ changes["assertions"] = tuple(normalized)
203
+ if deployment_bindings is not None:
204
+ changes["deployment_bindings"] = deployment_bindings
205
+ updated = current.mutate(expected_revision=expected_revision, **changes)
206
+ store_draft(
207
+ self._access,
208
+ updated,
209
+ expected_revision=expected_revision,
210
+ auth_context=auth_context,
211
+ )
212
+ return DraftMutationResult(
213
+ draft=_draft_to_summary(updated),
214
+ audit_reference=authorization.operator_reference,
215
+ )
216
+
217
+ @_normalize_errors
218
+ def submit_draft_for_review(
219
+ self,
220
+ draft_id: str,
221
+ command: DraftRevisionCommand,
222
+ *,
223
+ auth_context: AuthContext,
224
+ ) -> DraftMutationResult:
225
+ require_permission(auth_context, Permission.ASSEMBLY_WRITE)
226
+ current = load_draft(self._access, draft_id, auth_context)
227
+ authorization = lifecycle_context(self._deps, auth_context, current.source_id)
228
+ outcome = core_submit_for_review(
229
+ current,
230
+ expected_revision=command.expected_revision,
231
+ authorization=authorization,
232
+ authorizer=self._access.lifecycle_authorizer(),
233
+ )
234
+ store_draft(
235
+ self._access,
236
+ outcome.draft,
237
+ expected_revision=command.expected_revision,
238
+ auth_context=auth_context,
239
+ )
240
+ return DraftMutationResult(
241
+ draft=_draft_to_summary(outcome.draft),
242
+ audit_reference=authorization.operator_reference,
243
+ )
244
+
245
+ @_normalize_errors
246
+ def decide_draft_assertion(
247
+ self,
248
+ draft_id: str,
249
+ command: AssertionDecisionCommand,
250
+ *,
251
+ auth_context: AuthContext,
252
+ ) -> DraftMutationResult:
253
+ require_permission(auth_context, Permission.ASSEMBLY_REVIEW)
254
+ current = load_draft(self._access, draft_id, auth_context)
255
+ authorization = lifecycle_context(self._deps, auth_context, current.source_id)
256
+ if command.action is AssertionDecisionAction.EDIT:
257
+ if command.semantic_payload is None:
258
+ raise ValidationError("assertion edit requires semantic_payload")
259
+ outcome = core_edit_assertion(
260
+ current,
261
+ assertion_id=command.assertion_id,
262
+ payload=command.semantic_payload,
263
+ expected_revision=command.expected_revision,
264
+ authorization=authorization,
265
+ authorizer=self._access.lifecycle_authorizer(),
266
+ reason=command.reason,
267
+ )
268
+ else:
269
+ decision = (
270
+ ReviewState.APPROVED
271
+ if command.action is AssertionDecisionAction.APPROVE
272
+ else ReviewState.REJECTED
273
+ )
274
+ outcome = core_decide_assertion(
275
+ current,
276
+ assertion_id=command.assertion_id,
277
+ decision=decision,
278
+ expected_revision=command.expected_revision,
279
+ authorization=authorization,
280
+ authorizer=self._access.lifecycle_authorizer(),
281
+ reason=command.reason,
282
+ )
283
+ store_draft(
284
+ self._access,
285
+ outcome.draft,
286
+ expected_revision=command.expected_revision,
287
+ auth_context=auth_context,
288
+ )
289
+ return DraftMutationResult(
290
+ draft=_draft_to_summary(outcome.draft),
291
+ audit_reference=authorization.operator_reference,
292
+ )
293
+
294
+ @_normalize_errors
295
+ def approve_assembly_draft(
296
+ self,
297
+ draft_id: str,
298
+ command: DraftRevisionCommand,
299
+ *,
300
+ auth_context: AuthContext,
301
+ ) -> DraftMutationResult:
302
+ require_permission(auth_context, Permission.ASSEMBLY_APPROVE)
303
+ current = load_draft(self._access, draft_id, auth_context)
304
+ authorization = lifecycle_context(self._deps, auth_context, current.source_id)
305
+ outcome = core_approve_draft(
306
+ current,
307
+ expected_revision=command.expected_revision,
308
+ authorization=authorization,
309
+ authorizer=self._access.lifecycle_authorizer(),
310
+ )
311
+ store_draft(
312
+ self._access,
313
+ outcome.draft,
314
+ expected_revision=command.expected_revision,
315
+ auth_context=auth_context,
316
+ )
317
+ return DraftMutationResult(
318
+ draft=_draft_to_summary(outcome.draft),
319
+ audit_reference=authorization.operator_reference,
320
+ )
321
+
322
+ @_normalize_errors
323
+ def create_draft_from_proposals(
324
+ self,
325
+ snapshot_fingerprint: str,
326
+ *,
327
+ descriptor_id: str,
328
+ draft_id: str,
329
+ bundle_id: str,
330
+ model_version: str,
331
+ auth_context: AuthContext,
332
+ ) -> DraftMutationResult:
333
+ require_permission(auth_context, Permission.ASSEMBLY_WRITE)
334
+ catalog = self._access.catalog()
335
+ proposal_set = catalog.proposal_set(
336
+ snapshot_fingerprint,
337
+ tenant_scope_fingerprint=auth_context.tenant_scope_fingerprint,
338
+ )
339
+ if proposal_set is None:
340
+ raise NotFoundError("proposal set")
341
+ snapshot = catalog.snapshot(
342
+ snapshot_fingerprint,
343
+ tenant_scope_fingerprint=auth_context.tenant_scope_fingerprint,
344
+ )
345
+ if snapshot is None:
346
+ raise NotFoundError("snapshot")
347
+ require_source(auth_context, snapshot.source.source_id)
348
+ draft = core_create_discovery_draft(
349
+ proposal_set,
350
+ descriptor_id=descriptor_id,
351
+ draft_id=draft_id,
352
+ bundle_id=bundle_id,
353
+ source_id=snapshot.source.source_id,
354
+ model_version=model_version,
355
+ author_reference=auth_context.audit_reference or self._deps.audit_reference,
356
+ expected_snapshot_fingerprint=snapshot_fingerprint,
357
+ )
358
+ return self.create_draft(draft, auth_context=auth_context)