mlspace-plugin 0.2.1__py3-none-any.whl
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.
- mlspace_mcp/__init__.py +3 -0
- mlspace_mcp/__main__.py +125 -0
- mlspace_mcp/_status.py +18 -0
- mlspace_mcp/auth.py +109 -0
- mlspace_mcp/bodyspec.py +184 -0
- mlspace_mcp/client.py +170 -0
- mlspace_mcp/client_setup.py +235 -0
- mlspace_mcp/config.py +283 -0
- mlspace_mcp/errors.py +241 -0
- mlspace_mcp/formatting.py +502 -0
- mlspace_mcp/init_cli.py +356 -0
- mlspace_mcp/job_hooks.py +242 -0
- mlspace_mcp/jsonc.py +90 -0
- mlspace_mcp/overview.py +421 -0
- mlspace_mcp/registry.py +632 -0
- mlspace_mcp/resource_refs.py +188 -0
- mlspace_mcp/server.py +190 -0
- mlspace_mcp/setup_ui.py +103 -0
- mlspace_mcp/skills/mlspace-call-deployed-model-predict/SKILL.md +36 -0
- mlspace_mcp/skills/mlspace-deploy-inference-service/SKILL.md +42 -0
- mlspace_mcp/skills/mlspace-diagnose-data-transfer/SKILL.md +41 -0
- mlspace_mcp/skills/mlspace-diagnose-failed-training-job/SKILL.md +64 -0
- mlspace_mcp/skills/mlspace-diagnose-job-launch-rejection/SKILL.md +44 -0
- mlspace_mcp/skills/mlspace-diagnose-unhealthy-inference/SKILL.md +40 -0
- mlspace_mcp/skills/mlspace-discover-launchable-resources/SKILL.md +45 -0
- mlspace_mcp/skills/mlspace-launch-training-job/SKILL.md +66 -0
- mlspace_mcp/skills/mlspace-list-running-jobs/SKILL.md +43 -0
- mlspace_mcp/skills/mlspace-resume-notebook-and-set-autoshutdown/SKILL.md +44 -0
- mlspace_mcp/skills/mlspace-stage-s3-to-nfs/SKILL.md +45 -0
- mlspace_mcp/skills/mlspace-transfer-compute-node/SKILL.md +63 -0
- mlspace_mcp/spec/body_schemas.json +7506 -0
- mlspace_mcp/spec/openapi.json +28347 -0
- mlspace_mcp/tls.py +46 -0
- mlspace_mcp/tools/__init__.py +1 -0
- mlspace_mcp/tools/allocations.py +92 -0
- mlspace_mcp/tools/async_inference.py +77 -0
- mlspace_mcp/tools/build_image.py +76 -0
- mlspace_mcp/tools/dalle.py +52 -0
- mlspace_mcp/tools/data_transfer.py +260 -0
- mlspace_mcp/tools/docker_registry.py +156 -0
- mlspace_mcp/tools/inference.py +88 -0
- mlspace_mcp/tools/jobs.py +155 -0
- mlspace_mcp/tools/notebooks.py +174 -0
- mlspace_mcp/tools/queues.py +202 -0
- mlspace_mcp/tools/resources.py +86 -0
- mlspace_mcp/tools/tensorboards.py +112 -0
- mlspace_mcp/tools/workspaces.py +92 -0
- mlspace_mcp/workspace_context.py +107 -0
- mlspace_plugin-0.2.1.dist-info/METADATA +427 -0
- mlspace_plugin-0.2.1.dist-info/RECORD +53 -0
- mlspace_plugin-0.2.1.dist-info/WHEEL +4 -0
- mlspace_plugin-0.2.1.dist-info/entry_points.txt +2 -0
- mlspace_plugin-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""``mlspace_queues`` — manage MLSpace allocation queues and shared-cluster queues.
|
|
2
|
+
|
|
3
|
+
Pure declarative data following the EXEMPLAR (``inference.py``). The core owns
|
|
4
|
+
all request mechanics; this module only declares ``ACTIONS`` / ``PARAMS`` and the
|
|
5
|
+
``DomainTool`` metadata for the ``queues`` domain.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from ..registry import DomainTool, Op, Param
|
|
13
|
+
|
|
14
|
+
# action -> Op. (method, path) pairs are the single source of truth for drift.
|
|
15
|
+
ACTIONS: dict[str, Op] = {
|
|
16
|
+
"list": Op(
|
|
17
|
+
"GET", "/public/v2/queues/",
|
|
18
|
+
query_params={"allocation_id": "allocation_id", "workspace_id": "workspace_id"},
|
|
19
|
+
required=("allocation_id",),
|
|
20
|
+
kind="list",
|
|
21
|
+
help="List queues for an allocation.",
|
|
22
|
+
),
|
|
23
|
+
"get": Op(
|
|
24
|
+
"GET", "/public/v2/queues/{queue_id}",
|
|
25
|
+
path_params={"queue_id": "queue_id"},
|
|
26
|
+
required=("queue_id",),
|
|
27
|
+
help="Get detailed information about one queue.",
|
|
28
|
+
),
|
|
29
|
+
"defaults": Op(
|
|
30
|
+
"GET", "/public/v2/queues/defaults",
|
|
31
|
+
query_params={"allocation_id": "allocation_id"},
|
|
32
|
+
required=("allocation_id",),
|
|
33
|
+
kind="list",
|
|
34
|
+
help="List default queues (with types) for an allocation.",
|
|
35
|
+
),
|
|
36
|
+
"instance_types": Op(
|
|
37
|
+
"GET", "/public/v2/queues/{queue_id}/instance-types",
|
|
38
|
+
path_params={"queue_id": "queue_id"},
|
|
39
|
+
required=("queue_id",),
|
|
40
|
+
kind="list",
|
|
41
|
+
help="List instance types available for a queue.",
|
|
42
|
+
),
|
|
43
|
+
"jobs": Op(
|
|
44
|
+
"GET", "/public/v2/queues/{queue_id}/jobs",
|
|
45
|
+
path_params={"queue_id": "queue_id"},
|
|
46
|
+
required=("queue_id",),
|
|
47
|
+
kind="list",
|
|
48
|
+
help="List jobs in a queue.",
|
|
49
|
+
),
|
|
50
|
+
"notebooks": Op(
|
|
51
|
+
"GET", "/public/v2/queues/{queue_id}/notebooks",
|
|
52
|
+
path_params={"queue_id": "queue_id"},
|
|
53
|
+
required=("queue_id",),
|
|
54
|
+
kind="list",
|
|
55
|
+
help="List notebooks in a queue.",
|
|
56
|
+
),
|
|
57
|
+
"pods": Op(
|
|
58
|
+
"GET", "/public/v2/queues/{queue_id}/pods",
|
|
59
|
+
path_params={"queue_id": "queue_id"},
|
|
60
|
+
required=("queue_id",),
|
|
61
|
+
kind="list",
|
|
62
|
+
help="List pods in a queue.",
|
|
63
|
+
),
|
|
64
|
+
"awaiting_launch_resources": Op(
|
|
65
|
+
"GET", "/public/v2/queues/{queue_id}/awaiting-launch-resources",
|
|
66
|
+
path_params={"queue_id": "queue_id"},
|
|
67
|
+
query_params={"priority": "priority", "limit": "limit", "offset": "offset"},
|
|
68
|
+
required=("queue_id", "priority", "limit", "offset"),
|
|
69
|
+
kind="list",
|
|
70
|
+
help="List one priority page; priority, limit and offset are required. Use mlspace_queue_inspect for all-priority coverage. List the resources a queue is waiting on. THE way to answer 'why is my "
|
|
71
|
+
"job still pending?' — `jobs get` only reports the bare status `pending` and "
|
|
72
|
+
"`jobs list_nodes` answers 404 while the job waits.",
|
|
73
|
+
),
|
|
74
|
+
"queue_defaults": Op(
|
|
75
|
+
"GET", "/public/v2/queues/{queue_id}/defaults",
|
|
76
|
+
path_params={"queue_id": "queue_id"},
|
|
77
|
+
required=("queue_id",),
|
|
78
|
+
kind="list",
|
|
79
|
+
help="List default settings of ONE queue (the `defaults` action lists default "
|
|
80
|
+
"queues of an allocation — different question, similar name).",
|
|
81
|
+
),
|
|
82
|
+
"assign_workspace": Op(
|
|
83
|
+
"POST", "/public/v2/queues/{queue_id}/workspaces/assign",
|
|
84
|
+
path_params={"queue_id": "queue_id"},
|
|
85
|
+
body_field="body",
|
|
86
|
+
required=("queue_id", "body"),
|
|
87
|
+
write=True,
|
|
88
|
+
help="Assign workspaces to a queue; `body` = workspace assignment request.",
|
|
89
|
+
),
|
|
90
|
+
"delete": Op(
|
|
91
|
+
"DELETE", "/public/v2/queues/{queue_id}",
|
|
92
|
+
path_params={"queue_id": "queue_id"},
|
|
93
|
+
required=("queue_id",),
|
|
94
|
+
write=True,
|
|
95
|
+
confirm=True,
|
|
96
|
+
help="Delete a queue. Irreversible; jobs and notebooks bound to it lose their "
|
|
97
|
+
"scheduling target.",
|
|
98
|
+
),
|
|
99
|
+
"create": Op(
|
|
100
|
+
"POST", "/public/v2/queues/",
|
|
101
|
+
body_field="body",
|
|
102
|
+
required=("body",),
|
|
103
|
+
write=True,
|
|
104
|
+
help="Create a queue; `body` = CreateQueueRequest.",
|
|
105
|
+
),
|
|
106
|
+
"update": Op(
|
|
107
|
+
"PUT", "/public/v2/queues/{queue_id}",
|
|
108
|
+
path_params={"queue_id": "queue_id"},
|
|
109
|
+
body_field="body",
|
|
110
|
+
required=("queue_id", "body"),
|
|
111
|
+
write=True,
|
|
112
|
+
help="Update a queue; `body` = UpdateQueueRequest.",
|
|
113
|
+
),
|
|
114
|
+
"assign": Op(
|
|
115
|
+
"POST", "/public/v2/queues/{queue_id}/assign",
|
|
116
|
+
path_params={"queue_id": "queue_id"},
|
|
117
|
+
body_field="body",
|
|
118
|
+
required=("queue_id", "body"),
|
|
119
|
+
write=True,
|
|
120
|
+
help="Assign a queue to a workspace; `body` = AssignmentRequest.",
|
|
121
|
+
),
|
|
122
|
+
"unassign": Op(
|
|
123
|
+
"DELETE", "/public/v2/queues/{queue_id}/assign",
|
|
124
|
+
path_params={"queue_id": "queue_id"},
|
|
125
|
+
body_field="body",
|
|
126
|
+
required=("queue_id", "body"),
|
|
127
|
+
write=True,
|
|
128
|
+
help="Unassign a queue from a workspace; `body` = AssignmentRequest.",
|
|
129
|
+
),
|
|
130
|
+
"set_default": Op(
|
|
131
|
+
"POST", "/public/v2/queues/{queue_id}/defaults",
|
|
132
|
+
path_params={"queue_id": "queue_id"},
|
|
133
|
+
body_field="body",
|
|
134
|
+
required=("queue_id", "body"),
|
|
135
|
+
write=True,
|
|
136
|
+
help="Set the queue as default for a job/notebook; "
|
|
137
|
+
"`body` = DefaultQueueRequest: {org_structure, default_for}.",
|
|
138
|
+
),
|
|
139
|
+
"unset_default": Op(
|
|
140
|
+
"DELETE", "/public/v2/queues/{queue_id}/defaults",
|
|
141
|
+
path_params={"queue_id": "queue_id"},
|
|
142
|
+
body_field="body",
|
|
143
|
+
required=("queue_id", "body"),
|
|
144
|
+
write=True,
|
|
145
|
+
help="Unset the queue as default for a job/notebook; "
|
|
146
|
+
"`body` = DefaultQueueRequest: {org_structure, default_for}.",
|
|
147
|
+
),
|
|
148
|
+
"add_nodes": Op(
|
|
149
|
+
"POST", "/public/v2/queues/{queue_id}/nodes",
|
|
150
|
+
path_params={"queue_id": "queue_id"},
|
|
151
|
+
body_field="body",
|
|
152
|
+
required=("queue_id", "body"),
|
|
153
|
+
write=True,
|
|
154
|
+
help="Add compute nodes to the destination queue; `body` = AddNodesRequest "
|
|
155
|
+
"{nodes, force_withdrawal}. Inspect source workload with mlspace_queue_inspect first. "
|
|
156
|
+
"force_withdrawal=true forces withdrawal of all load; never escalate silently.",
|
|
157
|
+
),
|
|
158
|
+
"assign_shared": Op(
|
|
159
|
+
"POST", "/public/v2/shared-cluster-queues/assign",
|
|
160
|
+
body_field="body",
|
|
161
|
+
required=("body",),
|
|
162
|
+
write=True,
|
|
163
|
+
help="Assign a shared cluster queue to a workspace; "
|
|
164
|
+
"`body` = AssignmentRequest: {region_key, org_structure}.",
|
|
165
|
+
),
|
|
166
|
+
"unassign_shared": Op(
|
|
167
|
+
"DELETE", "/public/v2/shared-cluster-queues/assign",
|
|
168
|
+
body_field="body",
|
|
169
|
+
required=("body",),
|
|
170
|
+
write=True,
|
|
171
|
+
help="Unassign a shared cluster queue from a workspace; "
|
|
172
|
+
"`body` = AssignmentRequest: {region_key, org_structure}.",
|
|
173
|
+
),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
PARAMS: list[Param] = [
|
|
177
|
+
Param("priority", Literal["shared-low", "shared-medium", "shared-cluster", "low", "medium", "high"], "Required priority for awaiting_launch_resources; one priority is not complete demand."),
|
|
178
|
+
Param("limit", int, "Required pending page size."),
|
|
179
|
+
Param("offset", int, "Required pending page offset (start at zero)."),
|
|
180
|
+
Param("queue_id", str, "Queue unique ID (UUID); required for queue-scoped actions."),
|
|
181
|
+
Param("allocation_id", str, "Allocation unique ID (UUID); required for list/defaults."),
|
|
182
|
+
Param("workspace_id", str, "Optional workspace UUID to filter the queue list by."),
|
|
183
|
+
Param(
|
|
184
|
+
"body",
|
|
185
|
+
dict,
|
|
186
|
+
"JSON body. create=CreateQueueRequest; update=UpdateQueueRequest; "
|
|
187
|
+
"assign/unassign=AssignmentRequest {org_structure}; "
|
|
188
|
+
"set_default/unset_default=DefaultQueueRequest {org_structure, default_for}; "
|
|
189
|
+
"add_nodes=AddNodesRequest {nodes, force_withdrawal}; "
|
|
190
|
+
"assign_shared/unassign_shared=AssignmentRequest {region_key, org_structure}.",
|
|
191
|
+
),
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
DOMAIN = DomainTool(
|
|
195
|
+
domain="queues",
|
|
196
|
+
name="mlspace_queues",
|
|
197
|
+
title="MLSpace Queues",
|
|
198
|
+
summary="Manage MLSpace allocation queues and shared-cluster queues "
|
|
199
|
+
"(list, inspect, create, update, assign, set defaults, add nodes).",
|
|
200
|
+
actions=ACTIONS,
|
|
201
|
+
params=PARAMS,
|
|
202
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""``mlspace_resources`` — read MLSpace compute catalog and node load.
|
|
2
|
+
|
|
3
|
+
A merged read-only catalog over configs, available instance types, nodes, and
|
|
4
|
+
node load. Pure declarative data; the core owns all request mechanics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from ..registry import DomainTool, Op, Param
|
|
12
|
+
|
|
13
|
+
# action -> Op. The (method, path) pairs are the single source of truth for the
|
|
14
|
+
# drift test; paths are copied verbatim from the spec, placeholders included.
|
|
15
|
+
ACTIONS: dict[str, Op] = {
|
|
16
|
+
"configs": Op(
|
|
17
|
+
"GET", "/public/v2/configs",
|
|
18
|
+
query_params={"cluster_type": "cluster_type"},
|
|
19
|
+
required=("cluster_type",),
|
|
20
|
+
kind="catalog",
|
|
21
|
+
help="Get region configs: available instance_types and images for Jobs/Notebooks.",
|
|
22
|
+
),
|
|
23
|
+
"instance_types": Op(
|
|
24
|
+
"GET", "/public/v2/instance_types/{region}/available",
|
|
25
|
+
path_params={"region": "region"},
|
|
26
|
+
query_params={"allocation_name": "allocation_name", "queue_name": "queue_name"},
|
|
27
|
+
required=("region",),
|
|
28
|
+
kind="list",
|
|
29
|
+
help="List available instance types by connected allocation in a region.",
|
|
30
|
+
),
|
|
31
|
+
"nodes": Op(
|
|
32
|
+
"GET", "/public/v2/nodes/{region}/nodes",
|
|
33
|
+
path_params={"region": "region"},
|
|
34
|
+
required=("region",),
|
|
35
|
+
kind="list",
|
|
36
|
+
help="List available nodes to allocation in a region.",
|
|
37
|
+
),
|
|
38
|
+
"nodes_load": Op(
|
|
39
|
+
"GET", "/public/v2/nodes/load",
|
|
40
|
+
query_params={
|
|
41
|
+
"queue_id": "queue_id",
|
|
42
|
+
"allocation_id": "allocation_id",
|
|
43
|
+
"node_names": "node_names",
|
|
44
|
+
},
|
|
45
|
+
required=("node_names",),
|
|
46
|
+
kind="list",
|
|
47
|
+
help="Get load on nodes; pass node_names (and optionally queue_id/allocation_id).",
|
|
48
|
+
),
|
|
49
|
+
"rate_limits": Op(
|
|
50
|
+
"GET", "/public/v2/limiter/",
|
|
51
|
+
kind="list",
|
|
52
|
+
help="List this account's API rate limits and current usage, per endpoint and "
|
|
53
|
+
"period. Call this BEFORE a burst of writes: the contract documents 429 on "
|
|
54
|
+
"only ~45% of operations, so this is the only reliable way to know the budget.",
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
PARAMS: list[Param] = [
|
|
59
|
+
Param(
|
|
60
|
+
"cluster_type",
|
|
61
|
+
Literal["MT", "INF"],
|
|
62
|
+
"Cluster type to fetch configs for (required for configs).",
|
|
63
|
+
),
|
|
64
|
+
Param(
|
|
65
|
+
"region",
|
|
66
|
+
str,
|
|
67
|
+
"Model-training region, e.g. DGX2-MT, A100-MT, SR003-SR006, SR008 "
|
|
68
|
+
"(authoritative list via the configs action). Required for instance_types/nodes. "
|
|
69
|
+
"Optional for configs: filters the catalog client-side to this one region "
|
|
70
|
+
"(case-insensitive); omit to get all regions.",
|
|
71
|
+
),
|
|
72
|
+
Param("allocation_name", str, "Filter instance_types by allocation name."),
|
|
73
|
+
Param("queue_name", str, "Filter instance_types by queue name."),
|
|
74
|
+
Param("queue_id", str, "Filter node load by queue UUID (nodes_load)."),
|
|
75
|
+
Param("allocation_id", str, "Filter node load by allocation UUID (nodes_load)."),
|
|
76
|
+
Param("node_names", list[str], "Node names to query load for (nodes_load)."),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
DOMAIN = DomainTool(
|
|
80
|
+
domain="resources",
|
|
81
|
+
name="mlspace_resources",
|
|
82
|
+
title="MLSpace Resources",
|
|
83
|
+
summary="Read MLSpace compute catalog: configs, available instance types, nodes, node load.",
|
|
84
|
+
actions=ACTIONS,
|
|
85
|
+
params=PARAMS,
|
|
86
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""``mlspace_tensorboards`` — manage MLSpace TensorBoard instances.
|
|
2
|
+
|
|
3
|
+
Pure declarative data following the inference.py exemplar. No request
|
|
4
|
+
mechanics here; the core (registry.py + client.py) owns headers, path
|
|
5
|
+
templating, query encoding, bodies, readonly, confirm, and formatting.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from ..registry import DomainTool, Op, Param
|
|
13
|
+
|
|
14
|
+
# action -> Op. The (method, path) pairs are the single source of truth for the
|
|
15
|
+
# drift test; paths are copied verbatim from the spec, placeholders included.
|
|
16
|
+
ACTIONS: dict[str, Op] = {
|
|
17
|
+
"list": Op(
|
|
18
|
+
"GET", "/public/v2/tensorboards/v2/tensorboards",
|
|
19
|
+
query_params={
|
|
20
|
+
"order_by": "order_by",
|
|
21
|
+
"desc": "desc",
|
|
22
|
+
"limit": "limit",
|
|
23
|
+
"offset": "offset",
|
|
24
|
+
"search": "search",
|
|
25
|
+
"status": "status",
|
|
26
|
+
},
|
|
27
|
+
kind="list",
|
|
28
|
+
help="List TensorBoards in the workspace (filters: order_by, desc, "
|
|
29
|
+
"limit, offset, search, status).",
|
|
30
|
+
),
|
|
31
|
+
"get": Op(
|
|
32
|
+
"GET", "/public/v2/tensorboards/v2/tensorboard/{tensorboard_uuid}",
|
|
33
|
+
path_params={"tensorboard_uuid": "tensorboard_uuid"},
|
|
34
|
+
required=("tensorboard_uuid",),
|
|
35
|
+
help="Get one TensorBoard by uuid.",
|
|
36
|
+
),
|
|
37
|
+
"create": Op(
|
|
38
|
+
"POST", "/public/v2/tensorboards/v2/{namespace}/tensorboard",
|
|
39
|
+
path_params={"namespace": "namespace"},
|
|
40
|
+
body_field="body",
|
|
41
|
+
required=("namespace", "body"),
|
|
42
|
+
write=True,
|
|
43
|
+
help="Create a TensorBoard; `body` = CreateTensorboardPayloadV2 "
|
|
44
|
+
"({name, image, instance_type, logdir, ...}).",
|
|
45
|
+
),
|
|
46
|
+
"modify": Op(
|
|
47
|
+
"POST", "/public/v2/tensorboards/v2/tensorboard/{tensorboard_uuid}/modify",
|
|
48
|
+
path_params={"tensorboard_uuid": "tensorboard_uuid"},
|
|
49
|
+
body_field="body",
|
|
50
|
+
required=("tensorboard_uuid", "body"),
|
|
51
|
+
write=True,
|
|
52
|
+
help="Modify a TensorBoard; `body` = ModifyTensorboardRequest "
|
|
53
|
+
"({autoshutdown_config, description, s3_credentials, s3_buckets}).",
|
|
54
|
+
),
|
|
55
|
+
"pause": Op(
|
|
56
|
+
"POST", "/public/v2/tensorboards/v2/tensorboard/{tensorboard_uuid}/pause",
|
|
57
|
+
path_params={"tensorboard_uuid": "tensorboard_uuid"},
|
|
58
|
+
required=("tensorboard_uuid",),
|
|
59
|
+
write=True,
|
|
60
|
+
help="Pause a TensorBoard.",
|
|
61
|
+
),
|
|
62
|
+
"resume": Op(
|
|
63
|
+
"POST",
|
|
64
|
+
"/public/v2/tensorboards/v2/{namespace}/tensorboard/{tensorboard_uuid}/resume",
|
|
65
|
+
path_params={"namespace": "namespace", "tensorboard_uuid": "tensorboard_uuid"},
|
|
66
|
+
body_field="body",
|
|
67
|
+
required=("namespace", "tensorboard_uuid", "body"),
|
|
68
|
+
write=True,
|
|
69
|
+
help="Resume a TensorBoard; `body` = ResumeTensorboardPayload "
|
|
70
|
+
"({region, instance_type}).",
|
|
71
|
+
),
|
|
72
|
+
"delete": Op(
|
|
73
|
+
"DELETE", "/public/v2/tensorboards/v2/tensorboard/{tensorboard_uuid}",
|
|
74
|
+
path_params={"tensorboard_uuid": "tensorboard_uuid"},
|
|
75
|
+
required=("tensorboard_uuid",),
|
|
76
|
+
write=True,
|
|
77
|
+
confirm=True,
|
|
78
|
+
help="Delete a TensorBoard.",
|
|
79
|
+
),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
PARAMS: list[Param] = [
|
|
83
|
+
Param("tensorboard_uuid", str, "TensorBoard UUID (get/modify/pause/resume/delete)."),
|
|
84
|
+
Param("namespace", str, "Namespace the TensorBoard belongs to (create/resume). "
|
|
85
|
+
"Optional: auto-filled from the server's workspace if omitted."),
|
|
86
|
+
Param(
|
|
87
|
+
"order_by",
|
|
88
|
+
Literal["name", "creationTimestamp"],
|
|
89
|
+
"List ordering field (default creationTimestamp).",
|
|
90
|
+
),
|
|
91
|
+
Param("desc", bool, "Sort descending (list)."),
|
|
92
|
+
Param("limit", int, "Max results to return (list)."),
|
|
93
|
+
Param("offset", int, "Pagination offset (list)."),
|
|
94
|
+
Param("search", str, "Filter by name substring (list)."),
|
|
95
|
+
Param("status", str, "Filter by comma-separated statuses, e.g. 'creating,running' (list)."),
|
|
96
|
+
Param(
|
|
97
|
+
"body",
|
|
98
|
+
dict,
|
|
99
|
+
"JSON body. create=CreateTensorboardPayloadV2; modify="
|
|
100
|
+
"ModifyTensorboardRequest; resume=ResumeTensorboardPayload.",
|
|
101
|
+
),
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
DOMAIN = DomainTool(
|
|
105
|
+
domain="tensorboards",
|
|
106
|
+
name="mlspace_tensorboards",
|
|
107
|
+
title="MLSpace TensorBoards",
|
|
108
|
+
summary="Manage MLSpace TensorBoard instances (list, inspect, create, "
|
|
109
|
+
"modify, pause, resume, delete).",
|
|
110
|
+
actions=ACTIONS,
|
|
111
|
+
params=PARAMS,
|
|
112
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""``mlspace_workspaces`` — inspect MLSpace workspaces (read-only).
|
|
2
|
+
|
|
3
|
+
Pure declarative data following the ``inference`` exemplar: the core owns all
|
|
4
|
+
request mechanics (headers, path templating, query encoding, bodies, readonly,
|
|
5
|
+
confirm, formatting). This module only declares ``ACTIONS`` / ``PARAMS``.
|
|
6
|
+
|
|
7
|
+
SAFETY: workspace bootstrap (POST /workspaces/v3/) and debootstrap (DELETE
|
|
8
|
+
/workspaces/v3/) are DELIBERATELY NOT exposed — creating/deleting an entire
|
|
9
|
+
workspace is a catastrophic blast radius for a shared, company-wide server, so
|
|
10
|
+
those ops are intentionally omitted (and listed in the drift test's exclusion
|
|
11
|
+
set so the bijection check stays green). Do workspace lifecycle in the console.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from ..registry import DomainTool, Op, Param
|
|
17
|
+
|
|
18
|
+
# action -> Op. The (method, path) pairs are the single source of truth for the
|
|
19
|
+
# drift test; paths are copied verbatim from the spec, placeholders included.
|
|
20
|
+
ACTIONS: dict[str, Op] = {
|
|
21
|
+
"get_api_key": Op(
|
|
22
|
+
"GET", "/public/v2/workspaces/v1/x_api_key",
|
|
23
|
+
query_params={"workspace_id": "workspace_id"},
|
|
24
|
+
required=("workspace_id",),
|
|
25
|
+
help="Get the workspace X-API-KEY (secret, redacted in output).",
|
|
26
|
+
),
|
|
27
|
+
"status": Op(
|
|
28
|
+
"GET", "/public/v2/workspaces/v1/{workspace_id}/status",
|
|
29
|
+
path_params={"workspace_id": "workspace_id"},
|
|
30
|
+
required=("workspace_id",),
|
|
31
|
+
help="Get a workspace's status.",
|
|
32
|
+
),
|
|
33
|
+
"users": Op(
|
|
34
|
+
"GET", "/public/v2/workspaces/v3/{workspace_id}/users",
|
|
35
|
+
path_params={"workspace_id": "workspace_id"},
|
|
36
|
+
required=("workspace_id",),
|
|
37
|
+
kind="list",
|
|
38
|
+
help="List the users who have access to a workspace.",
|
|
39
|
+
),
|
|
40
|
+
"list": Op(
|
|
41
|
+
"GET", "/public/v2/workspaces/v3/",
|
|
42
|
+
query_params={"customer_id": "customer_id"},
|
|
43
|
+
kind="list",
|
|
44
|
+
help="List user workspaces (optionally filtered by customer_id).",
|
|
45
|
+
),
|
|
46
|
+
"get": Op(
|
|
47
|
+
"GET", "/public/v2/workspaces/v3/{workspace_id}",
|
|
48
|
+
path_params={"workspace_id": "workspace_id"},
|
|
49
|
+
required=("workspace_id",),
|
|
50
|
+
help="Get workspace details.",
|
|
51
|
+
),
|
|
52
|
+
"allocations": Op(
|
|
53
|
+
"GET", "/public/v2/workspaces/v3/{workspace_id}/allocations",
|
|
54
|
+
path_params={"workspace_id": "workspace_id"},
|
|
55
|
+
required=("workspace_id",),
|
|
56
|
+
kind="list",
|
|
57
|
+
help="List a workspace's allocations.",
|
|
58
|
+
),
|
|
59
|
+
"allocation_queues": Op(
|
|
60
|
+
"GET",
|
|
61
|
+
"/public/v2/workspaces/v3/{workspace_id}/allocations/{allocation_id}/queues",
|
|
62
|
+
path_params={"workspace_id": "workspace_id", "allocation_id": "allocation_id"},
|
|
63
|
+
required=("workspace_id", "allocation_id"),
|
|
64
|
+
kind="list",
|
|
65
|
+
help="List queues for one allocation in a workspace.",
|
|
66
|
+
),
|
|
67
|
+
# NOTE: bootstrap (POST /workspaces/v3/) and debootstrap (DELETE /workspaces/v3/)
|
|
68
|
+
# are intentionally NOT exposed — see the SAFETY note in the module docstring.
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
PARAMS: list[Param] = [
|
|
72
|
+
Param(
|
|
73
|
+
"workspace_id",
|
|
74
|
+
str,
|
|
75
|
+
"Workspace UUID (path/query param for get_api_key/status/get/"
|
|
76
|
+
"allocations/allocation_queues). Distinct from the x-workspace-id header. "
|
|
77
|
+
"Optional: defaults to the server's configured workspace if omitted; pass only to target another.",
|
|
78
|
+
),
|
|
79
|
+
Param("allocation_id", str, "Allocation UUID (allocation_queues only)."),
|
|
80
|
+
Param("customer_id", str, "Filter workspaces by customer (list only)."),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
DOMAIN = DomainTool(
|
|
84
|
+
domain="workspaces",
|
|
85
|
+
name="mlspace_workspaces",
|
|
86
|
+
title="MLSpace Workspaces",
|
|
87
|
+
summary="Inspect MLSpace workspaces (status, details, allocations, queues, "
|
|
88
|
+
"api-key). Read-only: workspace create/delete is intentionally not exposed.",
|
|
89
|
+
actions=ACTIONS,
|
|
90
|
+
params=PARAMS,
|
|
91
|
+
redact_result_keys=("x-api-key",),
|
|
92
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""A fixed selected catalogue and independent, lazily initialized HTTP clients."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import hashlib
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from .auth import TokenManager
|
|
12
|
+
from .client import MLSpaceClient
|
|
13
|
+
from .config import Settings
|
|
14
|
+
from .errors import MLSpaceError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def normalized_endpoint(endpoint: str) -> str:
|
|
18
|
+
parsed = urlsplit(endpoint)
|
|
19
|
+
host = (parsed.hostname or "").lower()
|
|
20
|
+
if ":" in host:
|
|
21
|
+
host = f"[{host}]"
|
|
22
|
+
port = parsed.port
|
|
23
|
+
if port and (parsed.scheme.lower(), port) not in {("https", 443), ("http", 80)}:
|
|
24
|
+
host += f":{port}"
|
|
25
|
+
return urlunsplit((parsed.scheme.lower(), host, parsed.path.rstrip("/"), "", ""))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def environment_id(endpoint: str) -> str:
|
|
29
|
+
return "mlspace:" + hashlib.sha256(normalized_endpoint(endpoint).encode()).hexdigest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class WorkspaceSelection:
|
|
34
|
+
id: str
|
|
35
|
+
name: str
|
|
36
|
+
project_name: str
|
|
37
|
+
api_key: str = field(default="", repr=False)
|
|
38
|
+
namespace: str = ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def selected_workspaces(settings: Settings) -> tuple[WorkspaceSelection, ...]:
|
|
42
|
+
if settings.workspaces:
|
|
43
|
+
return tuple(WorkspaceSelection(
|
|
44
|
+
item.id, item.name, item.project_name,
|
|
45
|
+
item.api_key.get_secret_value() if item.api_key else "",
|
|
46
|
+
settings.namespace if len(settings.workspaces) == 1
|
|
47
|
+
and item.id == settings.workspace_id else "",
|
|
48
|
+
) for item in settings.workspaces)
|
|
49
|
+
return (WorkspaceSelection(settings.workspace_id, settings.workspace_id, "",
|
|
50
|
+
settings.api_key.get_secret_value(), settings.namespace),)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def select_target(selected: tuple[WorkspaceSelection, ...], target: str | None) -> WorkspaceSelection:
|
|
54
|
+
if target is None:
|
|
55
|
+
if len(selected) == 1:
|
|
56
|
+
return selected[0]
|
|
57
|
+
raise MLSpaceError("Choose target from mlspace_contexts; several workspaces are configured.")
|
|
58
|
+
by_id = [item for item in selected if item.id == target]
|
|
59
|
+
matches = by_id or [item for item in selected if item.name == target]
|
|
60
|
+
if len(matches) > 1:
|
|
61
|
+
raise MLSpaceError("Workspace target is ambiguous; use a configured workspace ID.")
|
|
62
|
+
if not matches:
|
|
63
|
+
raise MLSpaceError("Workspace target is not configured; see mlspace_contexts.")
|
|
64
|
+
return matches[0]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class WorkspaceClients:
|
|
68
|
+
"""Never discover membership or switch an existing client's credentials."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, settings: Settings, http: httpx.AsyncClient, tokens: TokenManager):
|
|
71
|
+
self.selected = selected_workspaces(settings)
|
|
72
|
+
if len({item.id for item in self.selected}) != len(self.selected):
|
|
73
|
+
raise MLSpaceError("Duplicate configured workspace IDs.")
|
|
74
|
+
self.endpoint = normalized_endpoint(settings.base_url)
|
|
75
|
+
self.environment = environment_id(self.endpoint)
|
|
76
|
+
self._http = http
|
|
77
|
+
self._tokens = tokens
|
|
78
|
+
self._clients: dict[str, MLSpaceClient] = {}
|
|
79
|
+
self._locks = {item.id: asyncio.Lock() for item in self.selected}
|
|
80
|
+
|
|
81
|
+
def select(self, target: str | None) -> WorkspaceSelection:
|
|
82
|
+
"""Resolve membership without HTTP; use before validating operation addresses."""
|
|
83
|
+
return select_target(self.selected, target)
|
|
84
|
+
|
|
85
|
+
def catalogue(self) -> dict:
|
|
86
|
+
return {"environment": self.environment, "endpoint": self.endpoint,
|
|
87
|
+
"workspaces": [{"id": item.id, "name": item.name,
|
|
88
|
+
"project_name": item.project_name} for item in self.selected]}
|
|
89
|
+
|
|
90
|
+
async def resolve(self, target: str | None) -> MLSpaceClient:
|
|
91
|
+
selection = self.select(target)
|
|
92
|
+
async with self._locks[selection.id]:
|
|
93
|
+
if selection.id not in self._clients:
|
|
94
|
+
key = selection.api_key
|
|
95
|
+
if not key:
|
|
96
|
+
bootstrap = MLSpaceClient(self._http, self._tokens, base_url=self.endpoint,
|
|
97
|
+
api_key="", workspace_id=selection.id)
|
|
98
|
+
data = await bootstrap.request("GET", "/public/v2/workspaces/v1/x_api_key")
|
|
99
|
+
resolved_key = data.get("x-api-key") if isinstance(data, dict) else None
|
|
100
|
+
if not isinstance(resolved_key, str) or not resolved_key.strip():
|
|
101
|
+
raise MLSpaceError("API key unavailable for the selected workspace.")
|
|
102
|
+
key = resolved_key
|
|
103
|
+
self._clients[selection.id] = MLSpaceClient(
|
|
104
|
+
self._http, self._tokens, base_url=self.endpoint, api_key=key,
|
|
105
|
+
workspace_id=selection.id, namespace=selection.namespace,
|
|
106
|
+
)
|
|
107
|
+
return self._clients[selection.id]
|