nodii-grpc-interfaces 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.
- nodii_grpc_interfaces-0.1.0/.gitignore +51 -0
- nodii_grpc_interfaces-0.1.0/PKG-INFO +17 -0
- nodii_grpc_interfaces-0.1.0/proto/README.md +19 -0
- nodii_grpc_interfaces-0.1.0/proto/nodii/dsar/v1/dsar.proto +176 -0
- nodii_grpc_interfaces-0.1.0/proto/nodii/tenant/v1/tenant_snapshot.proto +93 -0
- nodii_grpc_interfaces-0.1.0/pyproject.toml +85 -0
- nodii_grpc_interfaces-0.1.0/src/nodii_grpc_interfaces/__init__.py +42 -0
- nodii_grpc_interfaces-0.1.0/src/nodii_grpc_interfaces/proto_loader.py +163 -0
- nodii_grpc_interfaces-0.1.0/src/nodii_grpc_interfaces/py.typed +0 -0
- nodii_grpc_interfaces-0.1.0/tests/__init__.py +0 -0
- nodii_grpc_interfaces-0.1.0/tests/test_proto_loader.py +91 -0
- nodii_grpc_interfaces-0.1.0/tests/test_proto_parity.py +51 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
build/
|
|
4
|
+
coverage/
|
|
5
|
+
.DS_Store
|
|
6
|
+
*.log
|
|
7
|
+
.env
|
|
8
|
+
.env.local
|
|
9
|
+
.env.*.local
|
|
10
|
+
.turbo/
|
|
11
|
+
.bun/
|
|
12
|
+
|
|
13
|
+
# AI review loop artifacts (scripts/review/ai-review.ts)
|
|
14
|
+
.claude-pre-review.md
|
|
15
|
+
.claude-pre-review.json
|
|
16
|
+
.claude/pending-patches/
|
|
17
|
+
|
|
18
|
+
# Agent session state (not committed; per-session)
|
|
19
|
+
.claude/endpoints-snapshot.json
|
|
20
|
+
.agent-todo.md
|
|
21
|
+
|
|
22
|
+
# Python (uv)
|
|
23
|
+
.venv/
|
|
24
|
+
__pycache__/
|
|
25
|
+
*.pyc
|
|
26
|
+
*.pyo
|
|
27
|
+
*.egg-info/
|
|
28
|
+
.mypy_cache/
|
|
29
|
+
.ruff_cache/
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
htmlcov/
|
|
32
|
+
.coverage
|
|
33
|
+
.coverage.*
|
|
34
|
+
coverage.xml
|
|
35
|
+
|
|
36
|
+
# Go
|
|
37
|
+
*.test
|
|
38
|
+
*.out
|
|
39
|
+
go.work.sum
|
|
40
|
+
|
|
41
|
+
# Generated proto artifacts (regenerate via `bun proto:gen`)
|
|
42
|
+
ts/_generated/
|
|
43
|
+
python/_generated/
|
|
44
|
+
go/_generated/
|
|
45
|
+
|
|
46
|
+
# Test-stack volumes (docker-compose persistent data)
|
|
47
|
+
infra/test-stack/data/
|
|
48
|
+
# synthetic consumer go binaries
|
|
49
|
+
synthetic-consumers/telemetry/go/nodii-synthetic-telemetry
|
|
50
|
+
synthetic-consumers/pii/go/nodii-synthetic-pii
|
|
51
|
+
synthetic-consumers/auth-sdk/go/nodii-synthetic-auth-sdk
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nodii-grpc-interfaces
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Canonical gRPC interface contracts for the Nodii stack. Ships the vendored .proto bytes + runtime-compiled grpcio stubs for nodii.dsar.v1.DataLifecycleService and nodii.tenant.v1.TenantSnapshotService so consumer services can add_*_to_server(...) without running buf codegen.
|
|
5
|
+
Project-URL: Homepage, https://github.com/cognion-nucleus/nodii-libs
|
|
6
|
+
Project-URL: Repository, https://github.com/cognion-nucleus/nodii-libs
|
|
7
|
+
Author-email: Nodii <ops@nodii.co>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: dsar,grpc,interfaces,nodii,proto,tenant-snapshot
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Requires-Dist: grpcio-tools>=1.60
|
|
16
|
+
Requires-Dist: grpcio>=1.60
|
|
17
|
+
Requires-Dist: protobuf>=4.25
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# proto/ — vendored canonical gRPC contracts
|
|
2
|
+
|
|
3
|
+
These `.proto` files are **vendored byte-for-byte** from the nodii-libs
|
|
4
|
+
canonical proto mirror at `proto/nodii/**` (sourced from the planning hub).
|
|
5
|
+
They ship inside the `@nodii/grpc-interfaces` npm artifact (see `files` in
|
|
6
|
+
`package.json`) so the library is self-contained and consumers can load the
|
|
7
|
+
ServiceDefinitions at runtime via `@grpc/proto-loader` — no buf-remote
|
|
8
|
+
pipeline required.
|
|
9
|
+
|
|
10
|
+
| File | Package | Service |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `nodii/dsar/v1/dsar.proto` | `nodii.dsar.v1` | `DataLifecycleService` (9 RPCs; D108 — 9th RPC is `ListActiveHolds`) |
|
|
13
|
+
| `nodii/tenant/v1/tenant_snapshot.proto` | `nodii.tenant.v1` | `TenantSnapshotService` (5 streaming bootstrap RPCs) |
|
|
14
|
+
|
|
15
|
+
## Parity contract
|
|
16
|
+
|
|
17
|
+
The vendored copies MUST stay byte-equal to the repo-root `proto/nodii/**`
|
|
18
|
+
canonical files. The `tests/proto-parity.test.ts` fence asserts this on every
|
|
19
|
+
run. To update, re-copy from the canonical mirror — never hand-edit here.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
// DataLifecycleService — ticketing's DSAR Pattern A gRPC surface over its OWN
|
|
4
|
+
// data subjects (DCO ticket rows + ticket-side PII), called by
|
|
5
|
+
// nodii-tenant-service.DsarOrchestratorService (compliance-dsar-pattern-a /
|
|
6
|
+
// global 11-dsar § 6 / D109). Wired via @nodii/dsar.bindPatternA — the lib's
|
|
7
|
+
// DsarServiceImpl handler map is mounted onto this service definition.
|
|
8
|
+
//
|
|
9
|
+
// D108 / drift 7a24d546: the 9th RPC is ListActiveHolds (NOT the legacy
|
|
10
|
+
// Acknowledge). @nodii/dsar@0.3.2's DsarServiceImpl implements ListActiveHolds;
|
|
11
|
+
// ticketing AUTHORS this contract per D190 (producer authors its own gRPC
|
|
12
|
+
// contract) because the canonical proto vendored in @nodii/role-catalog@0.1.5 is
|
|
13
|
+
// the pre-D108 (Acknowledge) artifact — see drift 33066346 / request 39e64d25
|
|
14
|
+
// (nodii-libs proto fix in flight). RPCs 1-8 + their messages are transcribed
|
|
15
|
+
// verbatim from that proto (which is correct for those); only the 9th is
|
|
16
|
+
// corrected to ListActiveHolds per the live decision.
|
|
17
|
+
//
|
|
18
|
+
// Uniform envelope: every request carries (request_id, tenant_id, subject_kind,
|
|
19
|
+
// subject_id, …); the 8 lifecycle RPCs return LifecycleResponse{status, …};
|
|
20
|
+
// ListActiveHolds returns ListActiveHoldsResponse{holds}.
|
|
21
|
+
|
|
22
|
+
package nodii.dsar.v1;
|
|
23
|
+
|
|
24
|
+
import "google/protobuf/timestamp.proto";
|
|
25
|
+
|
|
26
|
+
service DataLifecycleService {
|
|
27
|
+
// GDPR subject rights.
|
|
28
|
+
rpc AccessData(AccessDataRequest) returns (LifecycleResponse);
|
|
29
|
+
rpc EraseData(EraseDataRequest) returns (LifecycleResponse);
|
|
30
|
+
rpc RectifyData(RectifyDataRequest) returns (LifecycleResponse);
|
|
31
|
+
rpc ExportPortableData(ExportPortableDataRequest) returns (LifecycleResponse);
|
|
32
|
+
rpc RestrictProcessing(RestrictProcessingRequest) returns (LifecycleResponse);
|
|
33
|
+
|
|
34
|
+
// Retention hold pair.
|
|
35
|
+
rpc PlaceRetentionHold(PlaceRetentionHoldRequest) returns (LifecycleResponse);
|
|
36
|
+
rpc LiftRetentionHold(LiftRetentionHoldRequest) returns (LifecycleResponse);
|
|
37
|
+
|
|
38
|
+
// Visibility.
|
|
39
|
+
rpc GetLifecycleRequestStatus(GetLifecycleRequestStatusRequest)
|
|
40
|
+
returns (LifecycleResponse);
|
|
41
|
+
// D108 (drift 7a24d546): ListActiveHolds replaces the legacy Acknowledge.
|
|
42
|
+
rpc ListActiveHolds(ListActiveHoldsRequest) returns (ListActiveHoldsResponse);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
enum SubjectKind {
|
|
46
|
+
SUBJECT_KIND_UNSPECIFIED = 0;
|
|
47
|
+
SUBJECT_KIND_USER = 1;
|
|
48
|
+
SUBJECT_KIND_EMPLOYEE = 2;
|
|
49
|
+
SUBJECT_KIND_CUSTOMER = 3;
|
|
50
|
+
SUBJECT_KIND_PLATFORM_ADMIN = 4;
|
|
51
|
+
SUBJECT_KIND_TENANT = 5; // synthetic — tenant-level holds + per-tenant erase
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
enum Status {
|
|
55
|
+
STATUS_UNSPECIFIED = 0;
|
|
56
|
+
STATUS_ACCEPTED = 1;
|
|
57
|
+
STATUS_COMPLETED = 2;
|
|
58
|
+
STATUS_NO_DATA = 3;
|
|
59
|
+
STATUS_PARTIAL = 4;
|
|
60
|
+
STATUS_FAILED = 5;
|
|
61
|
+
STATUS_HELD = 6;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---- Request messages (RPCs 1-8 — verbatim from the canonical proto) -------
|
|
65
|
+
|
|
66
|
+
message AccessDataRequest {
|
|
67
|
+
string request_id = 1; // UUIDv7
|
|
68
|
+
string tenant_id = 2;
|
|
69
|
+
SubjectKind subject_kind = 3;
|
|
70
|
+
string subject_id = 4;
|
|
71
|
+
string reason = 5;
|
|
72
|
+
string requested_by = 6;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
message EraseDataRequest {
|
|
76
|
+
string request_id = 1;
|
|
77
|
+
string tenant_id = 2;
|
|
78
|
+
SubjectKind subject_kind = 3;
|
|
79
|
+
string subject_id = 4;
|
|
80
|
+
string reason = 5;
|
|
81
|
+
string requested_by = 6;
|
|
82
|
+
string scope = 7; // "subject" | "tenant"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
message RectifyDataRequest {
|
|
86
|
+
string request_id = 1;
|
|
87
|
+
string tenant_id = 2;
|
|
88
|
+
SubjectKind subject_kind = 3;
|
|
89
|
+
string subject_id = 4;
|
|
90
|
+
string reason = 5;
|
|
91
|
+
string requested_by = 6;
|
|
92
|
+
map<string, string> patches = 7;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
message ExportPortableDataRequest {
|
|
96
|
+
string request_id = 1;
|
|
97
|
+
string tenant_id = 2;
|
|
98
|
+
SubjectKind subject_kind = 3;
|
|
99
|
+
string subject_id = 4;
|
|
100
|
+
string reason = 5;
|
|
101
|
+
string requested_by = 6;
|
|
102
|
+
string region = 7; // for S3 artifact bucket selection
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
message RestrictProcessingRequest {
|
|
106
|
+
string request_id = 1;
|
|
107
|
+
string tenant_id = 2;
|
|
108
|
+
SubjectKind subject_kind = 3;
|
|
109
|
+
string subject_id = 4;
|
|
110
|
+
string reason = 5;
|
|
111
|
+
string requested_by = 6;
|
|
112
|
+
string restriction_kind = 7; // free-form
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
message PlaceRetentionHoldRequest {
|
|
116
|
+
string request_id = 1;
|
|
117
|
+
string tenant_id = 2;
|
|
118
|
+
SubjectKind subject_kind = 3;
|
|
119
|
+
string subject_id = 4;
|
|
120
|
+
string statute_citation = 5;
|
|
121
|
+
google.protobuf.Timestamp expires_at = 6;
|
|
122
|
+
string requested_by = 7;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
message LiftRetentionHoldRequest {
|
|
126
|
+
string request_id = 1;
|
|
127
|
+
string tenant_id = 2;
|
|
128
|
+
SubjectKind subject_kind = 3;
|
|
129
|
+
string subject_id = 4;
|
|
130
|
+
string justification = 5;
|
|
131
|
+
string requested_by = 6;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
message GetLifecycleRequestStatusRequest {
|
|
135
|
+
string request_id = 1;
|
|
136
|
+
string rpc_method = 2;
|
|
137
|
+
string tenant_id = 3;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- ListActiveHolds (D108 9th RPC — replaces Acknowledge) -----------------
|
|
141
|
+
|
|
142
|
+
message ListActiveHoldsRequest {
|
|
143
|
+
string request_id = 1;
|
|
144
|
+
string tenant_id = 2;
|
|
145
|
+
SubjectKind subject_kind = 3; // optional narrowing filter (UNSPECIFIED = any)
|
|
146
|
+
string subject_id = 4; // optional narrowing filter ("" = any)
|
|
147
|
+
string hold_kind = 5; // optional "erase" | "restrict" ("" = any)
|
|
148
|
+
string requested_by = 6;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
message ActiveHold {
|
|
152
|
+
string id = 1;
|
|
153
|
+
string tenant_id = 2;
|
|
154
|
+
string subject_id = 3; // "" / unset for tenant-level holds
|
|
155
|
+
string hold_kind = 4; // "erase" | "restrict"
|
|
156
|
+
string statute_citation = 5;
|
|
157
|
+
string placed_by = 6;
|
|
158
|
+
google.protobuf.Timestamp placed_at = 7;
|
|
159
|
+
google.protobuf.Timestamp expires_at = 8; // unset = no expiry
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
message ListActiveHoldsResponse {
|
|
163
|
+
repeated ActiveHold holds = 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- Uniform lifecycle response (8 RPCs) -----------------------------------
|
|
167
|
+
|
|
168
|
+
message LifecycleResponse {
|
|
169
|
+
string request_id = 1;
|
|
170
|
+
Status status = 2;
|
|
171
|
+
string reason = 3;
|
|
172
|
+
string result_artifact_uri = 4;
|
|
173
|
+
google.protobuf.Timestamp expires_at = 5;
|
|
174
|
+
string statute_citation = 6;
|
|
175
|
+
}
|
|
176
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
// TenantSnapshotService — tenant-service's cold-start bootstrap surface for
|
|
4
|
+
// every sibling service maintaining a tenant-aggregate replica, called by
|
|
5
|
+
// `@nodii/replica-consumer`'s snapshotRpc client per [`09-replication-doctrine
|
|
6
|
+
// § 7`](../../../../../../../global/09-replication-doctrine.md).
|
|
7
|
+
//
|
|
8
|
+
// Locked + shipped per drift 2b30f6c5 adjudication (R0: 09-replication § 7.6
|
|
9
|
+
// wins over the feature_doc's 11-typed-message draft). Authored by
|
|
10
|
+
// nodii-tenant-service per D190 (producer-authored gRPC contracts); ingested
|
|
11
|
+
// to canonical mirror via contract-lock-notice 4e683dea.
|
|
12
|
+
//
|
|
13
|
+
// Phase 1 (this lock): 5 of 11 RPCs — for the 5 aggregates that have tables +
|
|
14
|
+
// emitters today (Tenant + UserMembership + CustomerMembership + AuthPolicy +
|
|
15
|
+
// TenantSubscription per 09-replication § 2.2a lines 109-117).
|
|
16
|
+
//
|
|
17
|
+
// Deferred to subsequent locks (R1 — no RPCs over non-existent tables):
|
|
18
|
+
// - BootstrapAccessTokenSnapshot ← access-control-personal-access-tokens
|
|
19
|
+
// - BootstrapOAuthAppSnapshot ← access-control-oauth-apps
|
|
20
|
+
// - BootstrapOAuthAppInstallationSnapshot ← access-control-oauth-apps
|
|
21
|
+
// - BootstrapCustomDomainSnapshot ← custom-domains
|
|
22
|
+
// - BootstrapChildSubdomainSnapshot ← custom-domains
|
|
23
|
+
//
|
|
24
|
+
// Removed from § 2.2a as STALE (per drift 2b30f6c5 audit confirmed by
|
|
25
|
+
// tenant-service in 4e683dea):
|
|
26
|
+
// - TenantModuleSubscription — no owning table; tenant_subscriptions IS the
|
|
27
|
+
// TenantSubscription aggregate. Hub follow-up: doctrine PR removing this
|
|
28
|
+
// row from § 2.2a.
|
|
29
|
+
//
|
|
30
|
+
// HWM (stream_high_water_mark) emission is DEFERRED to the outbox→Redis Streams
|
|
31
|
+
// dispatcher landing per § 9.2 + D209 SET-NX-EX lease. Until the dispatcher
|
|
32
|
+
// ships, the server streams data chunks then closes the gRPC stream without
|
|
33
|
+
// a terminal HWM chunk. Consumers bootstrap via LWW(entity_version) monotonicity;
|
|
34
|
+
// the HWM stitch arrives with the dispatcher.
|
|
35
|
+
|
|
36
|
+
package nodii.tenant.v1;
|
|
37
|
+
|
|
38
|
+
service TenantSnapshotService {
|
|
39
|
+
// S2S scope: tenant:snapshot:read.
|
|
40
|
+
// Each RPC streams the full set of rows for its aggregate (no filtering —
|
|
41
|
+
// bootstrap is full-or-none per § 7.6). Pagination is implicit via gRPC
|
|
42
|
+
// backpressure on the streaming response.
|
|
43
|
+
|
|
44
|
+
rpc BootstrapTenantSnapshot(BootstrapSnapshotRequest)
|
|
45
|
+
returns (stream TenantSnapshotChunk);
|
|
46
|
+
|
|
47
|
+
rpc BootstrapUserMembershipSnapshot(BootstrapSnapshotRequest)
|
|
48
|
+
returns (stream TenantSnapshotChunk);
|
|
49
|
+
|
|
50
|
+
rpc BootstrapCustomerMembershipSnapshot(BootstrapSnapshotRequest)
|
|
51
|
+
returns (stream TenantSnapshotChunk);
|
|
52
|
+
|
|
53
|
+
rpc BootstrapAuthPolicySnapshot(BootstrapSnapshotRequest)
|
|
54
|
+
returns (stream TenantSnapshotChunk);
|
|
55
|
+
|
|
56
|
+
rpc BootstrapTenantSubscriptionSnapshot(BootstrapSnapshotRequest)
|
|
57
|
+
returns (stream TenantSnapshotChunk);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// No filtering — bootstrap is full or none per § 7.6.
|
|
61
|
+
message BootstrapSnapshotRequest {}
|
|
62
|
+
|
|
63
|
+
// The single canonical chunk message per § 7.6 (NOT 11 typed
|
|
64
|
+
// `<Aggregate>Snapshot`/`<Aggregate>Payload` messages — that draft was
|
|
65
|
+
// rejected per drift 2b30f6c5).
|
|
66
|
+
message TenantSnapshotChunk {
|
|
67
|
+
// Singular PascalCase aggregate name, byte-for-byte equal to the
|
|
68
|
+
// 09-replication § 2.2a enum value. Consumer's `@nodii/replica-consumer`
|
|
69
|
+
// `defineReplica({ aggregateKind: ... })` must match.
|
|
70
|
+
string aggregate_kind = 1;
|
|
71
|
+
|
|
72
|
+
// Aggregate root's primary key.
|
|
73
|
+
string aggregate_id = 2;
|
|
74
|
+
|
|
75
|
+
// Current monotonic version — the ordering primitive (NOT
|
|
76
|
+
// `google.protobuf.Timestamp`; that was rejected per drift 2b30f6c5).
|
|
77
|
+
int64 entity_version = 3;
|
|
78
|
+
|
|
79
|
+
// Identical byte-equal payload to the equivalent live event's payload
|
|
80
|
+
// (the `*.created.v1` payload for the aggregate). Status / lifecycle
|
|
81
|
+
// changes are NOT included — those arrive via the live stream's
|
|
82
|
+
// `*.status_changed.v1`, `*.closed.v1`, etc. events.
|
|
83
|
+
// Consumer requirement: apply `lead_id` (and other "absence-means-unknown"
|
|
84
|
+
// fields) set-if-absent on snapshot apply — do NOT LWW-null a non-null
|
|
85
|
+
// replicated lead_id from a snapshot.
|
|
86
|
+
string payload_json = 4;
|
|
87
|
+
|
|
88
|
+
// Populated ONLY on the final chunk of the stream (the HWM "I'm caught up
|
|
89
|
+
// to here on the live stream" marker). Empty string until the
|
|
90
|
+
// outbox→Redis Streams dispatcher lands per § 9.2 + D209; consumers
|
|
91
|
+
// bootstrap via LWW(entity_version) monotonicity in the interim.
|
|
92
|
+
string stream_high_water_mark = 5;
|
|
93
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "nodii-grpc-interfaces"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Canonical gRPC interface contracts for the Nodii stack. Ships the vendored .proto bytes + runtime-compiled grpcio stubs for nodii.dsar.v1.DataLifecycleService and nodii.tenant.v1.TenantSnapshotService so consumer services can add_*_to_server(...) without running buf codegen."
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Nodii", email = "ops@nodii.co" },
|
|
9
|
+
]
|
|
10
|
+
keywords = ["nodii", "grpc", "proto", "dsar", "tenant-snapshot", "interfaces"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"grpcio>=1.60",
|
|
19
|
+
"grpcio-tools>=1.60",
|
|
20
|
+
"protobuf>=4.25",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/cognion-nucleus/nodii-libs"
|
|
25
|
+
Repository = "https://github.com/cognion-nucleus/nodii-libs"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/nodii_grpc_interfaces"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.sdist]
|
|
35
|
+
include = ["src/", "tests/", "proto/", "README.md", "LICENSE", "pyproject.toml"]
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"ruff>=0.7",
|
|
40
|
+
"mypy>=1.13",
|
|
41
|
+
"pytest>=8.3",
|
|
42
|
+
"pytest-asyncio>=0.24",
|
|
43
|
+
"pytest-cov>=6.0",
|
|
44
|
+
"types-protobuf>=4.25",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 100
|
|
49
|
+
target-version = "py311"
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint]
|
|
52
|
+
select = [
|
|
53
|
+
"E", "W", "F", "I", "B", "C4", "UP", "N", "ASYNC", "S", "PT", "RET", "SIM",
|
|
54
|
+
]
|
|
55
|
+
ignore = [
|
|
56
|
+
"S101", # use of assert detected — fine in tests
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
"tests/**/*.py" = ["S101", "S105", "S106", "B017", "PT011"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.format]
|
|
63
|
+
quote-style = "double"
|
|
64
|
+
indent-style = "space"
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
strict = true
|
|
68
|
+
python_version = "3.11"
|
|
69
|
+
warn_redundant_casts = true
|
|
70
|
+
warn_unused_ignores = true
|
|
71
|
+
warn_unreachable = true
|
|
72
|
+
disallow_untyped_decorators = true
|
|
73
|
+
# grpcio + protobuf-generated stubs lack type info; allow them through.
|
|
74
|
+
[[tool.mypy.overrides]]
|
|
75
|
+
module = ["grpc", "grpc.*", "grpc_tools", "grpc_tools.*", "google.protobuf", "google.protobuf.*"]
|
|
76
|
+
ignore_missing_imports = true
|
|
77
|
+
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
testpaths = ["tests"]
|
|
80
|
+
asyncio_mode = "auto"
|
|
81
|
+
addopts = [
|
|
82
|
+
"-ra",
|
|
83
|
+
"--strict-markers",
|
|
84
|
+
"--strict-config",
|
|
85
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""nodii_grpc_interfaces — canonical gRPC interface contracts for the Nodii stack.
|
|
2
|
+
|
|
3
|
+
Ships the vendored .proto bytes (under proto/) + runtime-compiled grpcio stubs
|
|
4
|
+
for the two canonical services, so a consumer service can register its own
|
|
5
|
+
handler implementation without running buf codegen.
|
|
6
|
+
|
|
7
|
+
Resolves hub requests:
|
|
8
|
+
- daa248c0 — dsar.proto (nodii.dsar.v1.DataLifecycleService)
|
|
9
|
+
- d7cd41dc — tenant_snapshot.proto (nodii.tenant.v1.TenantSnapshotService)
|
|
10
|
+
- a0919cfa — DataLifecycleService ServiceDefinition codegen for consumers
|
|
11
|
+
|
|
12
|
+
Public API (parity with @nodii/grpc-interfaces):
|
|
13
|
+
- load_dsar_stubs / load_tenant_snapshot_stubs — (pb2, pb2_grpc) tuples.
|
|
14
|
+
- data_lifecycle_servicer_base / tenant_snapshot_servicer_base — grpcio
|
|
15
|
+
ServicerBase classes for the consumer to subclass.
|
|
16
|
+
- SERVICE_NAMES — fully-qualified service names.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from nodii_grpc_interfaces.proto_loader import (
|
|
20
|
+
data_lifecycle_servicer_base,
|
|
21
|
+
load_dsar_stubs,
|
|
22
|
+
load_tenant_snapshot_stubs,
|
|
23
|
+
tenant_snapshot_servicer_base,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
LIB_NAME = "grpc-interfaces"
|
|
28
|
+
|
|
29
|
+
SERVICE_NAMES = {
|
|
30
|
+
"DataLifecycleService": "nodii.dsar.v1.DataLifecycleService",
|
|
31
|
+
"TenantSnapshotService": "nodii.tenant.v1.TenantSnapshotService",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"LIB_NAME",
|
|
36
|
+
"SERVICE_NAMES",
|
|
37
|
+
"__version__",
|
|
38
|
+
"data_lifecycle_servicer_base",
|
|
39
|
+
"load_dsar_stubs",
|
|
40
|
+
"load_tenant_snapshot_stubs",
|
|
41
|
+
"tenant_snapshot_servicer_base",
|
|
42
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Dynamic gRPC stub loader for the canonical Nodii interface contracts.
|
|
2
|
+
|
|
3
|
+
We compile the vendored protos at import time using grpcio_tools.protoc into a
|
|
4
|
+
temporary import path. This keeps the library a single self-contained package
|
|
5
|
+
(no buf-codegen dependency) and matches what the TS lib does with
|
|
6
|
+
@grpc/proto-loader.
|
|
7
|
+
|
|
8
|
+
Two services are shipped:
|
|
9
|
+
- nodii.dsar.v1.DataLifecycleService (request daa248c0 / a0919cfa; D108)
|
|
10
|
+
- nodii.tenant.v1.TenantSnapshotService (request d7cd41dc)
|
|
11
|
+
|
|
12
|
+
Usage by a consumer service:
|
|
13
|
+
|
|
14
|
+
from nodii_grpc_interfaces.proto_loader import load_dsar_stubs
|
|
15
|
+
|
|
16
|
+
pb2, pb2_grpc = load_dsar_stubs()
|
|
17
|
+
pb2_grpc.add_DataLifecycleServiceServicer_to_server(MyServicer(), server)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import importlib
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import tempfile
|
|
26
|
+
import threading
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from types import ModuleType
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
_lock = threading.Lock()
|
|
32
|
+
_compiled: dict[str, tuple[ModuleType, ModuleType]] = {}
|
|
33
|
+
|
|
34
|
+
# (proto path relative to the proto root, pb2 module name, grpc module name).
|
|
35
|
+
_DSAR_PROTO = ("nodii/dsar/v1/dsar.proto",
|
|
36
|
+
"nodii.dsar.v1.dsar_pb2",
|
|
37
|
+
"nodii.dsar.v1.dsar_pb2_grpc")
|
|
38
|
+
_TENANT_PROTO = ("nodii/tenant/v1/tenant_snapshot.proto",
|
|
39
|
+
"nodii.tenant.v1.tenant_snapshot_pb2",
|
|
40
|
+
"nodii.tenant.v1.tenant_snapshot_pb2_grpc")
|
|
41
|
+
|
|
42
|
+
# Persistent gen dir (survives the call); subsequent imports reuse the cached
|
|
43
|
+
# sys.modules entries. Overridable for wheel-install layouts where proto/ is
|
|
44
|
+
# not co-located with the package source.
|
|
45
|
+
_GEN_DIR_ENV = "NODII_GRPC_INTERFACES_GEN_DIR"
|
|
46
|
+
_PROTO_ROOT_ENV = "NODII_GRPC_INTERFACES_PROTO_ROOT"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _find_proto_root() -> Path:
|
|
50
|
+
here = Path(__file__).resolve().parent
|
|
51
|
+
candidates: list[Path] = []
|
|
52
|
+
env_override = os.environ.get(_PROTO_ROOT_ENV)
|
|
53
|
+
if env_override:
|
|
54
|
+
candidates.append(Path(env_override))
|
|
55
|
+
candidates.extend(
|
|
56
|
+
[
|
|
57
|
+
here / "proto", # bundled with wheel (preferred)
|
|
58
|
+
here.parent.parent / "proto", # python/grpc-interfaces/proto (dev)
|
|
59
|
+
here.parent / "proto",
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
for c in candidates:
|
|
63
|
+
if (c / "nodii" / "dsar" / "v1" / "dsar.proto").is_file():
|
|
64
|
+
return c
|
|
65
|
+
raise FileNotFoundError(
|
|
66
|
+
f"nodii_grpc_interfaces: cannot locate the vendored protos. Tried: {candidates}. "
|
|
67
|
+
f"Set {_PROTO_ROOT_ENV} to point at the proto root, or install the wheel "
|
|
68
|
+
"with bundled protos."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _compile_all_protos() -> Path:
|
|
73
|
+
"""Compile every vendored proto into a persistent gen dir; return that dir.
|
|
74
|
+
|
|
75
|
+
Both protos are compiled in a single protoc invocation so cross-file +
|
|
76
|
+
well-known-type imports resolve consistently.
|
|
77
|
+
"""
|
|
78
|
+
try:
|
|
79
|
+
from grpc_tools import protoc as _protoc # type: ignore[import-not-found,import-untyped,unused-ignore] # noqa: WPS433, I001
|
|
80
|
+
except ImportError as e: # pragma: no cover
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
"nodii_grpc_interfaces requires grpcio-tools to compile its protos. "
|
|
83
|
+
"Install grpcio-tools."
|
|
84
|
+
) from e
|
|
85
|
+
|
|
86
|
+
proto_root = _find_proto_root()
|
|
87
|
+
gen_dir = Path(
|
|
88
|
+
os.environ.get(
|
|
89
|
+
_GEN_DIR_ENV,
|
|
90
|
+
Path(tempfile.gettempdir()) / "nodii_grpc_interfaces_gen",
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
gen_dir.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
|
|
95
|
+
grpc_tools_mod = importlib.import_module("grpc_tools")
|
|
96
|
+
grpc_tools_file = grpc_tools_mod.__file__
|
|
97
|
+
if grpc_tools_file is None:
|
|
98
|
+
raise RuntimeError("grpc_tools module has no __file__ — broken install?")
|
|
99
|
+
wkt_include = Path(grpc_tools_file).parent / "_proto"
|
|
100
|
+
|
|
101
|
+
args = [
|
|
102
|
+
"protoc",
|
|
103
|
+
f"-I{proto_root}",
|
|
104
|
+
f"-I{wkt_include}",
|
|
105
|
+
f"--python_out={gen_dir}",
|
|
106
|
+
f"--grpc_python_out={gen_dir}",
|
|
107
|
+
str(proto_root / _DSAR_PROTO[0]),
|
|
108
|
+
str(proto_root / _TENANT_PROTO[0]),
|
|
109
|
+
]
|
|
110
|
+
rc = _protoc.main(args)
|
|
111
|
+
if rc != 0:
|
|
112
|
+
raise RuntimeError(f"nodii_grpc_interfaces: grpc_tools.protoc exited with code {rc}")
|
|
113
|
+
|
|
114
|
+
# Ensure every parent dir is an importable package.
|
|
115
|
+
for sub in ("", "nodii", "nodii/dsar", "nodii/dsar/v1",
|
|
116
|
+
"nodii/tenant", "nodii/tenant/v1"):
|
|
117
|
+
init = gen_dir / sub / "__init__.py"
|
|
118
|
+
init.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
if not init.exists():
|
|
120
|
+
init.write_text("")
|
|
121
|
+
|
|
122
|
+
if str(gen_dir) not in sys.path:
|
|
123
|
+
sys.path.insert(0, str(gen_dir))
|
|
124
|
+
return gen_dir
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _load(pb2_name: str, grpc_name: str) -> tuple[ModuleType, ModuleType]:
|
|
128
|
+
with _lock:
|
|
129
|
+
cached = _compiled.get(pb2_name)
|
|
130
|
+
if cached is not None:
|
|
131
|
+
return cached
|
|
132
|
+
_compile_all_protos()
|
|
133
|
+
pb2 = importlib.import_module(pb2_name)
|
|
134
|
+
grpc_mod = importlib.import_module(grpc_name)
|
|
135
|
+
result = (pb2, grpc_mod)
|
|
136
|
+
_compiled[pb2_name] = result
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def load_dsar_stubs() -> tuple[ModuleType, ModuleType]:
|
|
141
|
+
"""Returns (pb2, pb2_grpc) for nodii.dsar.v1.DataLifecycleService."""
|
|
142
|
+
return _load(_DSAR_PROTO[1], _DSAR_PROTO[2])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def load_tenant_snapshot_stubs() -> tuple[ModuleType, ModuleType]:
|
|
146
|
+
"""Returns (pb2, pb2_grpc) for nodii.tenant.v1.TenantSnapshotService."""
|
|
147
|
+
return _load(_TENANT_PROTO[1], _TENANT_PROTO[2])
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def data_lifecycle_servicer_base() -> Any:
|
|
151
|
+
"""The grpcio ServicerBase for DataLifecycleService.
|
|
152
|
+
|
|
153
|
+
A consumer subclasses this and implements the 9 RPC methods, then calls
|
|
154
|
+
`add_DataLifecycleServiceServicer_to_server(impl, server)`.
|
|
155
|
+
"""
|
|
156
|
+
_pb2, grpc_mod = load_dsar_stubs()
|
|
157
|
+
return grpc_mod.DataLifecycleServiceServicer
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def tenant_snapshot_servicer_base() -> Any:
|
|
161
|
+
"""The grpcio ServicerBase for TenantSnapshotService."""
|
|
162
|
+
_pb2, grpc_mod = load_tenant_snapshot_stubs()
|
|
163
|
+
return grpc_mod.TenantSnapshotServiceServicer
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Asserts the runtime-compiled stubs load and expose the expected RPCs.
|
|
2
|
+
|
|
3
|
+
The grpcio-generated ``<Service>Servicer`` base class carries one method per
|
|
4
|
+
RPC (named with the proto-declared PascalCase RPC name), and the
|
|
5
|
+
``add_<Service>Servicer_to_server`` registrar is what a consumer calls — so
|
|
6
|
+
introspecting the servicer base is the consumer-facing contract surface.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from nodii_grpc_interfaces import SERVICE_NAMES
|
|
12
|
+
from nodii_grpc_interfaces.proto_loader import (
|
|
13
|
+
load_dsar_stubs,
|
|
14
|
+
load_tenant_snapshot_stubs,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
# The 9 canonical DataLifecycleService RPCs (D108 — ListActiveHolds, NOT Acknowledge).
|
|
18
|
+
DSAR_RPCS = {
|
|
19
|
+
"AccessData",
|
|
20
|
+
"EraseData",
|
|
21
|
+
"RectifyData",
|
|
22
|
+
"ExportPortableData",
|
|
23
|
+
"RestrictProcessing",
|
|
24
|
+
"PlaceRetentionHold",
|
|
25
|
+
"LiftRetentionHold",
|
|
26
|
+
"GetLifecycleRequestStatus",
|
|
27
|
+
"ListActiveHolds",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# The 5 phase-1 TenantSnapshotService streaming bootstrap RPCs.
|
|
31
|
+
TENANT_RPCS = {
|
|
32
|
+
"BootstrapTenantSnapshot",
|
|
33
|
+
"BootstrapUserMembershipSnapshot",
|
|
34
|
+
"BootstrapCustomerMembershipSnapshot",
|
|
35
|
+
"BootstrapAuthPolicySnapshot",
|
|
36
|
+
"BootstrapTenantSubscriptionSnapshot",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _servicer_rpc_names(servicer_cls: type) -> set[str]:
|
|
41
|
+
# grpcio servicer bases define one public method per RPC.
|
|
42
|
+
return {
|
|
43
|
+
name
|
|
44
|
+
for name in dir(servicer_cls)
|
|
45
|
+
if not name.startswith("_") and callable(getattr(servicer_cls, name))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_dsar_stubs_load_and_expose_nine_rpcs() -> None:
|
|
50
|
+
pb2, pb2_grpc = load_dsar_stubs()
|
|
51
|
+
assert hasattr(pb2_grpc, "DataLifecycleServiceServicer")
|
|
52
|
+
assert hasattr(pb2_grpc, "DataLifecycleServiceStub")
|
|
53
|
+
assert hasattr(pb2_grpc, "add_DataLifecycleServiceServicer_to_server")
|
|
54
|
+
|
|
55
|
+
rpcs = _servicer_rpc_names(pb2_grpc.DataLifecycleServiceServicer)
|
|
56
|
+
assert rpcs == DSAR_RPCS
|
|
57
|
+
assert len(rpcs) == 9
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_dsar_ninth_rpc_is_list_active_holds_not_acknowledge() -> None:
|
|
61
|
+
_pb2, pb2_grpc = load_dsar_stubs()
|
|
62
|
+
rpcs = _servicer_rpc_names(pb2_grpc.DataLifecycleServiceServicer)
|
|
63
|
+
assert "ListActiveHolds" in rpcs
|
|
64
|
+
assert "Acknowledge" not in rpcs
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_dsar_list_active_holds_wire_messages_present() -> None:
|
|
68
|
+
pb2, _pb2_grpc = load_dsar_stubs()
|
|
69
|
+
# The D108 wire messages exist; the retired Acknowledge message is gone.
|
|
70
|
+
assert hasattr(pb2, "ListActiveHoldsRequest")
|
|
71
|
+
assert hasattr(pb2, "ListActiveHoldsResponse")
|
|
72
|
+
assert hasattr(pb2, "ActiveHold")
|
|
73
|
+
assert not hasattr(pb2, "AcknowledgeRequest")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_tenant_snapshot_stubs_load_and_expose_five_rpcs() -> None:
|
|
77
|
+
pb2, pb2_grpc = load_tenant_snapshot_stubs()
|
|
78
|
+
assert hasattr(pb2_grpc, "TenantSnapshotServiceServicer")
|
|
79
|
+
assert hasattr(pb2_grpc, "TenantSnapshotServiceStub")
|
|
80
|
+
assert hasattr(pb2_grpc, "add_TenantSnapshotServiceServicer_to_server")
|
|
81
|
+
assert hasattr(pb2, "TenantSnapshotChunk")
|
|
82
|
+
assert hasattr(pb2, "BootstrapSnapshotRequest")
|
|
83
|
+
|
|
84
|
+
rpcs = _servicer_rpc_names(pb2_grpc.TenantSnapshotServiceServicer)
|
|
85
|
+
assert rpcs == TENANT_RPCS
|
|
86
|
+
assert len(rpcs) == 5
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_service_names_are_canonical() -> None:
|
|
90
|
+
assert SERVICE_NAMES["DataLifecycleService"] == "nodii.dsar.v1.DataLifecycleService"
|
|
91
|
+
assert SERVICE_NAMES["TenantSnapshotService"] == "nodii.tenant.v1.TenantSnapshotService"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Contract fence: the vendored proto bytes carry the canonical wire contract.
|
|
2
|
+
|
|
3
|
+
These protos are vendored byte-equal from the planning-hub grpc-interfaces
|
|
4
|
+
canonical mirror. nodii-grpc-interfaces IS the canonical proto/stub vendor
|
|
5
|
+
surface (daa248c0/d7cd41dc/D190); it is NOT bound to the repo-root proto/ tree
|
|
6
|
+
(which feeds dsar/role-catalog codegen and still carries the pre-D108 dsar proto
|
|
7
|
+
pending drift 39e64d25). So we assert the vendored bytes carry the canonical
|
|
8
|
+
contract markers (incl. D108 ListActiveHolds, NOT the legacy Acknowledge) rather
|
|
9
|
+
than byte-comparing to the stale root. A failure means the vendored contract
|
|
10
|
+
drifted — an R0 violation.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
_HERE = Path(__file__).resolve().parent
|
|
18
|
+
_VENDORED_ROOT = _HERE.parent / "proto"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _read(rel: str) -> str:
|
|
22
|
+
return (_VENDORED_ROOT / rel).read_text(encoding="utf-8")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_dsar_proto_is_canonical_d108() -> None:
|
|
26
|
+
src = _read("nodii/dsar/v1/dsar.proto")
|
|
27
|
+
assert "package nodii.dsar.v1;" in src
|
|
28
|
+
assert "service DataLifecycleService" in src
|
|
29
|
+
# D108: 9th RPC is ListActiveHolds, NOT the legacy Acknowledge.
|
|
30
|
+
assert "rpc ListActiveHolds" in src
|
|
31
|
+
assert "ListActiveHoldsRequest" in src
|
|
32
|
+
assert "ListActiveHoldsResponse" in src
|
|
33
|
+
assert "rpc Acknowledge" not in src
|
|
34
|
+
for rpc in (
|
|
35
|
+
"AccessData",
|
|
36
|
+
"EraseData",
|
|
37
|
+
"RectifyData",
|
|
38
|
+
"ExportPortableData",
|
|
39
|
+
"RestrictProcessing",
|
|
40
|
+
"PlaceRetentionHold",
|
|
41
|
+
"LiftRetentionHold",
|
|
42
|
+
"GetLifecycleRequestStatus",
|
|
43
|
+
"ListActiveHolds",
|
|
44
|
+
):
|
|
45
|
+
assert f"rpc {rpc}" in src, f"missing rpc {rpc}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_tenant_snapshot_proto_is_canonical() -> None:
|
|
49
|
+
src = _read("nodii/tenant/v1/tenant_snapshot.proto")
|
|
50
|
+
assert "package nodii.tenant.v1;" in src
|
|
51
|
+
assert "service TenantSnapshotService" in src
|