Skip to content

Commit fb67941

Browse files
Expose namespace controls in Python SDK
1 parent 28cbbc2 commit fb67941

8 files changed

Lines changed: 396 additions & 1 deletion

File tree

src/durable_workflow/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from .client import (
2020
BridgeAdapterOutcome,
2121
Client,
22+
NamespaceDescription,
23+
NamespaceList,
2224
ScheduleAction,
2325
ScheduleBackfillResult,
2426
ScheduleDescription,
@@ -147,6 +149,8 @@
147149
"ChildWorkflowFailed",
148150
"Client",
149151
"ContinueAsNew",
152+
"NamespaceDescription",
153+
"NamespaceList",
150154
"AUTH_COMPOSITION_CONTRACT_SCHEMA",
151155
"AUTH_COMPOSITION_CONTRACT_VERSION",
152156
"AUTH_COMPOSITION_REQUIRED_EFFECTIVE_CONFIG_FIELDS",

src/durable_workflow/client.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def _route_for_metrics(path: str) -> str:
7070
parts[3] = "{run_id}"
7171
elif parts[0] == "schedules" and len(parts) >= 2:
7272
parts[1] = "{schedule_id}"
73-
elif parts[0] == "search-attributes" and len(parts) >= 2:
73+
elif parts[0] in {"namespaces", "search-attributes"} and len(parts) >= 2:
7474
parts[1] = "{name}"
7575
elif parts[0] == "workers" and len(parts) >= 2:
7676
parts[1] = "{worker_id}"
@@ -109,6 +109,36 @@ class WorkflowList:
109109
next_page_token: str | None = None
110110

111111

112+
@dataclass
113+
class NamespaceDescription:
114+
"""Server configuration for one workflow namespace."""
115+
116+
name: str
117+
description: str | None = None
118+
retention_days: int | None = None
119+
status: str | None = None
120+
created_at: str | None = None
121+
updated_at: str | None = None
122+
123+
@classmethod
124+
def from_dict(cls, data: dict[str, Any]) -> NamespaceDescription:
125+
return cls(
126+
name=str(data.get("name", "")),
127+
description=data.get("description"),
128+
retention_days=data.get("retention_days"),
129+
status=data.get("status"),
130+
created_at=data.get("created_at"),
131+
updated_at=data.get("updated_at"),
132+
)
133+
134+
135+
@dataclass
136+
class NamespaceList:
137+
"""Namespaces visible to the current control-plane identity."""
138+
139+
namespaces: list[NamespaceDescription]
140+
141+
112142
@dataclass
113143
class WorkflowRun:
114144
"""Current server view of one durable run in a workflow execution chain."""
@@ -1040,6 +1070,93 @@ async def health(self) -> dict[str, Any]:
10401070
)
10411071
return result
10421072

1073+
# ── Namespaces ────────────────────────────────────────────────────
1074+
async def list_namespaces(self) -> NamespaceList:
1075+
"""List namespaces visible to the current control-plane identity."""
1076+
data = await self._request("GET", "/namespaces")
1077+
if not isinstance(data, dict):
1078+
raise ServerError(
1079+
200,
1080+
{
1081+
"reason": "invalid_namespace_response",
1082+
"message": f"expected JSON object, got {type(data).__name__}",
1083+
},
1084+
)
1085+
items = data.get("namespaces", [])
1086+
return NamespaceList(
1087+
namespaces=[
1088+
NamespaceDescription.from_dict(item)
1089+
for item in items
1090+
if isinstance(item, dict)
1091+
],
1092+
)
1093+
1094+
async def describe_namespace(self, name: str) -> NamespaceDescription:
1095+
"""Return configuration and status for one namespace."""
1096+
data = await self._request("GET", f"/namespaces/{quote(name, safe='')}", context=name)
1097+
if not isinstance(data, dict):
1098+
raise ServerError(
1099+
200,
1100+
{
1101+
"reason": "invalid_namespace_response",
1102+
"message": f"expected JSON object, got {type(data).__name__}",
1103+
},
1104+
)
1105+
return NamespaceDescription.from_dict(data)
1106+
1107+
async def create_namespace(
1108+
self,
1109+
name: str,
1110+
*,
1111+
description: str | None = None,
1112+
retention_days: int = 30,
1113+
) -> NamespaceDescription:
1114+
"""Create a workflow namespace and return the server representation."""
1115+
data = await self._request(
1116+
"POST",
1117+
"/namespaces",
1118+
json={
1119+
"name": name,
1120+
"description": description,
1121+
"retention_days": retention_days,
1122+
},
1123+
context=name,
1124+
)
1125+
if not isinstance(data, dict):
1126+
raise ServerError(
1127+
200,
1128+
{
1129+
"reason": "invalid_namespace_response",
1130+
"message": f"expected JSON object, got {type(data).__name__}",
1131+
},
1132+
)
1133+
return NamespaceDescription.from_dict(data)
1134+
1135+
async def update_namespace(
1136+
self,
1137+
name: str,
1138+
*,
1139+
description: str | None = None,
1140+
retention_days: int | None = None,
1141+
) -> NamespaceDescription:
1142+
"""Update namespace metadata. Only provided fields are sent."""
1143+
body: dict[str, Any] = {}
1144+
if description is not None:
1145+
body["description"] = description
1146+
if retention_days is not None:
1147+
body["retention_days"] = retention_days
1148+
1149+
data = await self._request("PUT", f"/namespaces/{quote(name, safe='')}", json=body, context=name)
1150+
if not isinstance(data, dict):
1151+
raise ServerError(
1152+
200,
1153+
{
1154+
"reason": "invalid_namespace_response",
1155+
"message": f"expected JSON object, got {type(data).__name__}",
1156+
},
1157+
)
1158+
return NamespaceDescription.from_dict(data)
1159+
10431160
# ── Task queues ────────────────────────────────────────────────────
10441161
async def list_task_queues(self) -> TaskQueueList:
10451162
"""List task queues with server-side admission status.

src/durable_workflow/sync.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
from .client import Client as AsyncClient
99
from .client import (
10+
NamespaceDescription,
11+
NamespaceList,
1012
ScheduleAction,
1113
ScheduleBackfillResult,
1214
ScheduleDescription,
@@ -281,6 +283,46 @@ def describe_task_queue(self, name: str) -> TaskQueueDescription:
281283
result: TaskQueueDescription = _run(self._async.describe_task_queue(name))
282284
return result
283285

286+
def list_namespaces(self) -> NamespaceList:
287+
result: NamespaceList = _run(self._async.list_namespaces())
288+
return result
289+
290+
def describe_namespace(self, name: str) -> NamespaceDescription:
291+
result: NamespaceDescription = _run(self._async.describe_namespace(name))
292+
return result
293+
294+
def create_namespace(
295+
self,
296+
name: str,
297+
*,
298+
description: str | None = None,
299+
retention_days: int = 30,
300+
) -> NamespaceDescription:
301+
result: NamespaceDescription = _run(
302+
self._async.create_namespace(
303+
name,
304+
description=description,
305+
retention_days=retention_days,
306+
)
307+
)
308+
return result
309+
310+
def update_namespace(
311+
self,
312+
name: str,
313+
*,
314+
description: str | None = None,
315+
retention_days: int | None = None,
316+
) -> NamespaceDescription:
317+
result: NamespaceDescription = _run(
318+
self._async.update_namespace(
319+
name,
320+
description=description,
321+
retention_days=retention_days,
322+
)
323+
)
324+
return result
325+
284326
def get_history(self, workflow_id: str, run_id: str) -> Any:
285327
return _run(self._async.get_history(workflow_id, run_id))
286328

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"schema": "durable-workflow.polyglot.control-plane-request-fixture",
3+
"operation": "namespace.create",
4+
"request": {
5+
"method": "POST",
6+
"path": "/namespaces",
7+
"body": {
8+
"name": "billing",
9+
"description": "Billing workflows",
10+
"retention_days": 90
11+
}
12+
},
13+
"cli": {
14+
"argv": {
15+
"name": "billing",
16+
"--description": "Billing workflows",
17+
"--retention": "90",
18+
"--json": true
19+
}
20+
},
21+
"sdk_python": {
22+
"args": {
23+
"name": "billing",
24+
"description": "Billing workflows",
25+
"retention_days": 90
26+
}
27+
},
28+
"response_body": {
29+
"name": "billing",
30+
"description": "Billing workflows",
31+
"retention_days": 90,
32+
"status": "active",
33+
"created_at": "2026-04-02T00:00:00Z",
34+
"updated_at": "2026-04-02T00:00:00Z"
35+
},
36+
"semantic_body": {
37+
"name": "billing",
38+
"description": "Billing workflows",
39+
"retention_days": 90,
40+
"status": "active"
41+
}
42+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"schema": "durable-workflow.polyglot.control-plane-request-fixture",
3+
"operation": "namespace.describe",
4+
"request": {
5+
"method": "GET",
6+
"path": "/namespaces/billing"
7+
},
8+
"cli": {
9+
"argv": {
10+
"name": "billing",
11+
"--json": true
12+
}
13+
},
14+
"sdk_python": {
15+
"args": {
16+
"name": "billing"
17+
}
18+
},
19+
"response_body": {
20+
"name": "billing",
21+
"description": "Billing workflows",
22+
"retention_days": 90,
23+
"status": "active",
24+
"created_at": "2026-04-02T00:00:00Z",
25+
"updated_at": "2026-04-16T12:00:00Z"
26+
},
27+
"semantic_body": {
28+
"name": "billing",
29+
"description": "Billing workflows",
30+
"retention_days": 90,
31+
"status": "active"
32+
}
33+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"schema": "durable-workflow.polyglot.control-plane-request-fixture",
3+
"operation": "namespace.list",
4+
"request": {
5+
"method": "GET",
6+
"path": "/namespaces"
7+
},
8+
"cli": {
9+
"argv": {
10+
"--json": true
11+
}
12+
},
13+
"sdk_python": {
14+
"args": {}
15+
},
16+
"response_body": {
17+
"namespaces": [
18+
{
19+
"name": "default",
20+
"description": "Default namespace",
21+
"retention_days": 30,
22+
"status": "active",
23+
"created_at": "2026-04-01T00:00:00Z",
24+
"updated_at": "2026-04-15T12:00:00Z"
25+
},
26+
{
27+
"name": "billing",
28+
"description": "Billing workflows",
29+
"retention_days": 90,
30+
"status": "active",
31+
"created_at": "2026-04-02T00:00:00Z",
32+
"updated_at": "2026-04-16T12:00:00Z"
33+
}
34+
]
35+
},
36+
"semantic_body": {
37+
"namespace_names": ["default", "billing"],
38+
"retention_days": {
39+
"default": 30,
40+
"billing": 90
41+
},
42+
"statuses": {
43+
"default": "active",
44+
"billing": "active"
45+
}
46+
}
47+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"schema": "durable-workflow.polyglot.control-plane-request-fixture",
3+
"operation": "namespace.update",
4+
"request": {
5+
"method": "PUT",
6+
"path": "/namespaces/billing",
7+
"body": {
8+
"description": "Billing workflows and reports",
9+
"retention_days": 120
10+
}
11+
},
12+
"cli": {
13+
"argv": {
14+
"name": "billing",
15+
"--description": "Billing workflows and reports",
16+
"--retention": "120",
17+
"--json": true
18+
}
19+
},
20+
"sdk_python": {
21+
"args": {
22+
"name": "billing",
23+
"description": "Billing workflows and reports",
24+
"retention_days": 120
25+
}
26+
},
27+
"response_body": {
28+
"name": "billing",
29+
"description": "Billing workflows and reports",
30+
"retention_days": 120,
31+
"status": "active",
32+
"created_at": "2026-04-02T00:00:00Z",
33+
"updated_at": "2026-04-17T12:00:00Z"
34+
},
35+
"semantic_body": {
36+
"name": "billing",
37+
"description": "Billing workflows and reports",
38+
"retention_days": 120,
39+
"status": "active"
40+
}
41+
}

0 commit comments

Comments
 (0)