Skip to content

Commit e156dea

Browse files
[cross-repo from server#351] Conformance blocker: complete namespaces parity coverage (#145)
1 parent f656fdf commit e156dea

5 files changed

Lines changed: 84 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Added
10+
- `Client.delete_namespace()` now exercises the namespace lifecycle cleanup
11+
control-plane surface and returns the server's per-table cleanup counts on
12+
`NamespaceDescription.deleted`.
13+
914
### Fixed
1015
- Python parent workflows now decode successful child workflow completions from
1116
the server's documented `ChildRunCompleted.output` history payload, while

CONFORMANCE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ The Python SDK claims two targets from the suite's matrix:
2525
| --- | --- | --- |
2626
| `control_plane_request_response` | `tests/fixtures/control-plane/` | stable, parity-shared with `cli` |
2727
| `signal_query_runtime_contract` | `tests/test_signals.py`, `tests/test_queries.py`, `tests/test_worker.py` and the public scenario manifest at <https://durable-workflow.github.io/platform-conformance/signal-query-runtime-scenarios.json> | stable, parity-shared with PHP worker, CLI, and server routes |
28-
| `namespace_runtime_contract` | public scenario manifest at <https://durable-workflow.github.io/platform-conformance/namespace-runtime-scenarios.json> | stable, suite v7 runtime coverage for namespace isolation and SDK namespace selection |
28+
| `namespace_runtime_contract` | public scenario manifest at <https://durable-workflow.github.io/platform-conformance/namespace-runtime-scenarios.json> | stable, suite v8 runtime coverage for namespace isolation and SDK namespace selection |
2929
| `child_workflow_runtime_contract` | public scenario manifest at <https://durable-workflow.github.io/platform-conformance/child-workflow-runtime-scenarios.json> | stable, parity-shared with PHP worker and server child-workflow runtime behavior |
3030
| `worker_task_lifecycle` | `tests/fixtures/external-task-input/`, `tests/fixtures/external-task-result/` | stable |
3131
| `history_replay_bundles` | `tests/fixtures/golden_history/` and the public replay scenario manifest at <https://durable-workflow.github.io/platform-conformance/replay-runtime-scenarios.json> | stable, parity-shared with `workflow` golden bundles and the full runtime replay scenario matrix |
@@ -55,7 +55,7 @@ result document before tag, with the conformance level at `full` or
5555
| Field | Value |
5656
| --- | --- |
5757
| Required claimed targets | `official_sdk`, `worker_protocol_implementation` |
58-
| Required suite version | `PlatformConformanceSuite::VERSION` (currently `7`, mirrored at `/platform-conformance-contract.json`) |
58+
| Required suite version | `PlatformConformanceSuite::VERSION` (currently `8`, mirrored at `/platform-conformance-contract.json`) |
5959
| CI job | `platform-conformance` (lands when the harness reference implementation publishes; until then `cli-parity` and `test_history_event_contract.py` cover the same ground) |
6060
| Block on `nonconforming` | yes |
6161
| Artifact attached to release | harness result document, schema `durable-workflow.v2.platform-conformance.result` |

src/durable_workflow/client.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,21 @@ class NamespaceDescription:
148148
status: str | None = None
149149
created_at: str | None = None
150150
updated_at: str | None = None
151+
deleted: dict[str, int] | None = None
151152
external_payload_storage: ExternalPayloadStoragePolicy | None = None
152153

153154
@classmethod
154155
def from_dict(cls, data: dict[str, Any]) -> NamespaceDescription:
155156
external_payload_storage = None
156157
if isinstance(data.get("external_payload_storage"), dict):
157158
external_payload_storage = ExternalPayloadStoragePolicy.from_dict(data)
159+
deleted = None
160+
if isinstance(data.get("deleted"), dict):
161+
deleted = {
162+
str(key): value
163+
for key, value in data["deleted"].items()
164+
if isinstance(value, int)
165+
}
158166

159167
return cls(
160168
name=str(data.get("name", "")),
@@ -163,6 +171,7 @@ def from_dict(cls, data: dict[str, Any]) -> NamespaceDescription:
163171
status=data.get("status"),
164172
created_at=data.get("created_at"),
165173
updated_at=data.get("updated_at"),
174+
deleted=deleted,
166175
external_payload_storage=external_payload_storage,
167176
)
168177

@@ -1572,6 +1581,19 @@ async def update_namespace(
15721581
)
15731582
return NamespaceDescription.from_dict(data)
15741583

1584+
async def delete_namespace(self, name: str) -> NamespaceDescription:
1585+
"""Delete a namespace through the server lifecycle surface."""
1586+
data = await self._request("DELETE", f"/namespaces/{quote(name, safe='')}", context=name)
1587+
if not isinstance(data, dict):
1588+
raise ServerError(
1589+
200,
1590+
{
1591+
"reason": "invalid_namespace_response",
1592+
"message": f"expected JSON object, got {type(data).__name__}",
1593+
},
1594+
)
1595+
return NamespaceDescription.from_dict(data)
1596+
15751597
async def set_namespace_external_storage(
15761598
self,
15771599
name: str | None = None,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"schema": "durable-workflow.polyglot.control-plane-request-fixture",
3+
"version": 1,
4+
"operation": "namespace.delete",
5+
"request": {
6+
"method": "DELETE",
7+
"path": "/namespaces/billing"
8+
},
9+
"cli": {
10+
"argv": {
11+
"name": "billing",
12+
"--json": true
13+
}
14+
},
15+
"sdk_python": {
16+
"args": {
17+
"name": "billing"
18+
}
19+
},
20+
"response_body": {
21+
"name": "billing",
22+
"status": "deleted",
23+
"deleted": {
24+
"workflow_runs": 2,
25+
"workflow_schedules": 1,
26+
"search_attribute_definitions": 3,
27+
"workflow_worker_registrations": 1
28+
}
29+
},
30+
"semantic_body": {
31+
"name": "billing",
32+
"status": "deleted",
33+
"deleted_keys": [
34+
"workflow_runs",
35+
"workflow_schedules",
36+
"search_attribute_definitions",
37+
"workflow_worker_registrations"
38+
]
39+
}
40+
}

tests/test_client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,21 @@ async def test_update_namespace_matches_polyglot_fixture(self, client: Client) -
10801080
assert result.description == fixture["semantic_body"]["description"]
10811081
assert result.retention_days == fixture["semantic_body"]["retention_days"]
10821082

1083+
@pytest.mark.asyncio
1084+
async def test_delete_namespace_matches_polyglot_fixture(self, client: Client) -> None:
1085+
fixture_path = Path(__file__).parent / "fixtures" / "control-plane" / "namespace-delete-parity.json"
1086+
fixture = json.loads(fixture_path.read_text())
1087+
resp = _mock_response(200, fixture["response_body"])
1088+
1089+
with patch.object(client._http, "request", new_callable=AsyncMock, return_value=resp) as mock:
1090+
result = await client.delete_namespace(**fixture["sdk_python"]["args"])
1091+
1092+
assert mock.call_args.args[:2] == (fixture["request"]["method"], f"/api{fixture['request']['path']}")
1093+
assert result.name == fixture["semantic_body"]["name"]
1094+
assert result.status == fixture["semantic_body"]["status"]
1095+
assert result.deleted is not None
1096+
assert sorted(result.deleted) == sorted(fixture["semantic_body"]["deleted_keys"])
1097+
10831098
@pytest.mark.asyncio
10841099
async def test_set_namespace_external_storage_matches_polyglot_fixture(self, client: Client) -> None:
10851100
fixture_path = Path(__file__).parent / "fixtures" / "control-plane" / "namespace-set-storage-driver-parity.json"

0 commit comments

Comments
 (0)