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.
- nl2data_admin_service-0.1.0/PKG-INFO +127 -0
- nl2data_admin_service-0.1.0/README.md +103 -0
- nl2data_admin_service-0.1.0/pyproject.toml +56 -0
- nl2data_admin_service-0.1.0/setup.cfg +4 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/__init__.py +21 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/assembly_admin.py +358 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/audit_admin.py +89 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/auth.py +90 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/authoring_admin.py +166 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/bundle_admin.py +428 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/common.py +268 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/config.py +40 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/dtos.py +729 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/errors.py +91 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/lint_admin.py +139 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/metadata_admin.py +301 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/protocols.py +364 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/schema.py +101 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/service.py +469 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service/verification_admin.py +257 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service.egg-info/PKG-INFO +127 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service.egg-info/SOURCES.txt +34 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service.egg-info/dependency_links.txt +1 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service.egg-info/requires.txt +8 -0
- nl2data_admin_service-0.1.0/src/nl2data_admin_service.egg-info/top_level.txt +1 -0
- nl2data_admin_service-0.1.0/tests/test_admin_import_boundary.py +62 -0
- nl2data_admin_service-0.1.0/tests/test_assembly_lifecycle_service.py +352 -0
- nl2data_admin_service-0.1.0/tests/test_audit_inspection_service.py +403 -0
- nl2data_admin_service-0.1.0/tests/test_authoring_service.py +181 -0
- nl2data_admin_service-0.1.0/tests/test_capability_services.py +171 -0
- nl2data_admin_service-0.1.0/tests/test_host_integration.py +87 -0
- nl2data_admin_service-0.1.0/tests/test_lint_service.py +284 -0
- nl2data_admin_service-0.1.0/tests/test_schema.py +33 -0
- nl2data_admin_service-0.1.0/tests/test_security.py +24 -0
- nl2data_admin_service-0.1.0/tests/test_service_contract.py +190 -0
- 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,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)
|