icmd-python 3.3.5__py3-none-any.whl → 3.11.0__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.
- icmd/__init__.py +12 -0
- icmd/_vendor/assembler.py +16 -8
- icmd/apps/__init__.py +2 -0
- icmd/apps/agent.py +5 -1
- icmd/apps/base.py +42 -22
- icmd/apps/comments.py +34 -0
- icmd/apps/jobs.py +860 -127
- icmd/apps/results.py +353 -104
- icmd/apps/results_store.py +309 -0
- icmd/apps/suite.py +26 -1
- icmd/apps/transfer.py +895 -0
- icmd/cli/commands.py +286 -48
- icmd/cli/registry.py +30 -1
- icmd/client.py +16 -5
- icmd/compose.py +12 -9
- icmd/core/advisory.py +80 -28
- icmd/core/contracts.py +33 -18
- icmd/core/exceptions.py +143 -0
- icmd/core/groups.py +13 -6
- icmd/core/model_classes.py +14 -8
- icmd/core/session.py +7 -4
- icmd/types.py +66 -40
- icmd/version.py +1 -1
- {icmd_python-3.3.5.dist-info → icmd_python-3.11.0.dist-info}/METADATA +13 -4
- icmd_python-3.11.0.dist-info/RECORD +53 -0
- {icmd_python-3.3.5.dist-info → icmd_python-3.11.0.dist-info}/WHEEL +1 -1
- icmd_python-3.3.5.dist-info/RECORD +0 -50
- {icmd_python-3.3.5.dist-info → icmd_python-3.11.0.dist-info}/entry_points.txt +0 -0
- {icmd_python-3.3.5.dist-info → icmd_python-3.11.0.dist-info}/licenses/LICENSE +0 -0
- {icmd_python-3.3.5.dist-info → icmd_python-3.11.0.dist-info}/top_level.txt +0 -0
icmd/__init__.py
CHANGED
|
@@ -6,14 +6,20 @@ from .core.advisory import AdvisoryFinding, ContractProvider, check_input_dict
|
|
|
6
6
|
from .core.contracts import ContractClient, ContractTransport
|
|
7
7
|
from .core.exceptions import (
|
|
8
8
|
ICMDAuthenticationError,
|
|
9
|
+
ICMDChecksumMismatchError,
|
|
9
10
|
ICMDConnectionError,
|
|
10
11
|
ICMDContractUnavailableError,
|
|
12
|
+
ICMDMissingDependencyError,
|
|
13
|
+
ICMDModelMigrationError,
|
|
11
14
|
ICMDRefreshNetworkError,
|
|
12
15
|
ICMDRefreshRateLimitedError,
|
|
13
16
|
ICMDRefreshResponseError,
|
|
14
17
|
ICMDRefreshTokenInvalidError,
|
|
15
18
|
ICMDRefreshUnavailableError,
|
|
19
|
+
ICMDReleaseRefusedError,
|
|
16
20
|
ICMDRequestError,
|
|
21
|
+
ICMDResultsChangedError,
|
|
22
|
+
ICMDResultsRefusedError,
|
|
17
23
|
ICMDTokenRefreshError,
|
|
18
24
|
ICMDValidationError,
|
|
19
25
|
)
|
|
@@ -83,14 +89,20 @@ __all__ = [
|
|
|
83
89
|
"GridSampling",
|
|
84
90
|
"GridVariable",
|
|
85
91
|
"ICMDAuthenticationError",
|
|
92
|
+
"ICMDChecksumMismatchError",
|
|
86
93
|
"ICMDConnectionError",
|
|
87
94
|
"ICMDContractUnavailableError",
|
|
95
|
+
"ICMDMissingDependencyError",
|
|
96
|
+
"ICMDModelMigrationError",
|
|
88
97
|
"ICMDRefreshNetworkError",
|
|
89
98
|
"ICMDRefreshRateLimitedError",
|
|
90
99
|
"ICMDRefreshResponseError",
|
|
91
100
|
"ICMDRefreshTokenInvalidError",
|
|
92
101
|
"ICMDRefreshUnavailableError",
|
|
102
|
+
"ICMDReleaseRefusedError",
|
|
93
103
|
"ICMDRequestError",
|
|
104
|
+
"ICMDResultsChangedError",
|
|
105
|
+
"ICMDResultsRefusedError",
|
|
94
106
|
"ICMDTokenRefreshError",
|
|
95
107
|
"ICMDValidationError",
|
|
96
108
|
"LatinHypercubeSampling",
|
icmd/_vendor/assembler.py
CHANGED
|
@@ -23,7 +23,9 @@ __all__ = [
|
|
|
23
23
|
"build_design_document",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
-
#: The scenario discriminator
|
|
26
|
+
#: The scenario discriminator inside ``qt_model_inputs``. A catalog node
|
|
27
|
+
#: carries ``qt_model.scenario_key`` instead and the backend derives this from
|
|
28
|
+
#: it; a trained-model node, whose scenarios have no key, still carries it.
|
|
27
29
|
#: Exported so consumers that must recognize it -- notably the advisory check,
|
|
28
30
|
#: which excludes it from contract validation -- bind to this module rather than
|
|
29
31
|
#: re-spelling the literal. Renaming it here must not leave a stale copy behind.
|
|
@@ -83,23 +85,29 @@ def assemble_model_inputs(inputs: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
|
83
85
|
|
|
84
86
|
|
|
85
87
|
def assemble_model_node(
|
|
86
|
-
|
|
88
|
+
key: str,
|
|
87
89
|
model_uuid: str,
|
|
88
90
|
inputs: Mapping[str, Any],
|
|
89
|
-
|
|
91
|
+
scenario_key: str | None = None,
|
|
90
92
|
node_id: str | None = None,
|
|
91
93
|
) -> dict[str, Any]:
|
|
92
94
|
"""Build one model workflow node without model lookup or validation.
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
The node is identified by ``qt_model.key`` and ``qt_model.scenario_key``.
|
|
97
|
+
The backend derives the ``__meta_scenario`` title from the scenario key, so
|
|
98
|
+
a discriminator in *inputs* is dropped when one is given rather than sent
|
|
99
|
+
as a second, possibly stale, answer. ``node_id`` identifies this workflow
|
|
100
|
+
instance. The catalog ``model_uuid`` remains in ``qt_model.uuid`` and is
|
|
101
|
+
used only as the default node id.
|
|
96
102
|
"""
|
|
97
103
|
model_inputs = deepcopy(dict(inputs))
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
qt_model = {"key": key, "uuid": model_uuid}
|
|
105
|
+
if scenario_key is not None:
|
|
106
|
+
qt_model["scenario_key"] = scenario_key
|
|
107
|
+
model_inputs.pop(SCENARIO_KEY, None)
|
|
100
108
|
return {
|
|
101
109
|
"id": model_uuid if node_id is None else node_id,
|
|
102
|
-
"qt_model":
|
|
110
|
+
"qt_model": qt_model,
|
|
103
111
|
"qt_model_inputs": assemble_model_inputs(model_inputs),
|
|
104
112
|
}
|
|
105
113
|
|
icmd/apps/__init__.py
CHANGED
|
@@ -11,6 +11,7 @@ Provides a clean nested-attribute API::
|
|
|
11
11
|
|
|
12
12
|
from .account import AccountApp
|
|
13
13
|
from .agent import AgentApp
|
|
14
|
+
from .comments import CommentsApp
|
|
14
15
|
from .constants import ProjectKind
|
|
15
16
|
from .studio import StudioApp
|
|
16
17
|
from .suite import SuiteApp
|
|
@@ -19,6 +20,7 @@ from .workspace import WorkspaceApp
|
|
|
19
20
|
__all__ = [
|
|
20
21
|
"AccountApp",
|
|
21
22
|
"AgentApp",
|
|
23
|
+
"CommentsApp",
|
|
22
24
|
"ProjectKind",
|
|
23
25
|
"StudioApp",
|
|
24
26
|
"SuiteApp",
|
icmd/apps/agent.py
CHANGED
|
@@ -55,7 +55,11 @@ def _poll_message(
|
|
|
55
55
|
elapsed = 0
|
|
56
56
|
while True:
|
|
57
57
|
msg = _expect_dict(
|
|
58
|
-
client.request_json(
|
|
58
|
+
client.request_json(
|
|
59
|
+
"get",
|
|
60
|
+
f"{_MESSAGE_PATH}/{message_uuid}",
|
|
61
|
+
inject_group=client.has_owner_group,
|
|
62
|
+
),
|
|
59
63
|
_MESSAGE_PATH,
|
|
60
64
|
)
|
|
61
65
|
if (msg.get("status") or "").lower() in _TERMINAL_MESSAGE_STATUSES:
|
icmd/apps/base.py
CHANGED
|
@@ -214,7 +214,7 @@ class ReadOnlyResource:
|
|
|
214
214
|
uuid: str,
|
|
215
215
|
*,
|
|
216
216
|
params: dict[str, Any] | None = None,
|
|
217
|
-
inject_group: bool =
|
|
217
|
+
inject_group: bool | None = None,
|
|
218
218
|
) -> dict:
|
|
219
219
|
"""Get a single resource by UUID.
|
|
220
220
|
|
|
@@ -223,21 +223,26 @@ class ReadOnlyResource:
|
|
|
223
223
|
caller's server-side ``current_group`` (whatever they last
|
|
224
224
|
selected in the web UI). A resource owned by any other group
|
|
225
225
|
answers ``404`` — this is not a cross-group lookup, and omitting
|
|
226
|
-
``owner_group`` does not make it one.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
``owner_group`` does not make it one. By default the client's group
|
|
227
|
+
is sent when it already holds one (see :attr:`ICMD.has_owner_group`),
|
|
228
|
+
so a resource created through this client is found again. For jobs
|
|
229
|
+
specifically, the Studio action ``studio/jobs/{uuid}/accessible-group/``
|
|
230
|
+
answers which of your groups can ordinarily see a job; it walks plain
|
|
231
|
+
project access, so it performs no admin widening.
|
|
230
232
|
|
|
231
233
|
Parameters
|
|
232
234
|
----------
|
|
233
235
|
uuid : str
|
|
234
236
|
The resource UUID.
|
|
235
237
|
params : dict | None
|
|
236
|
-
Query parameters, forwarded only when not ``None``.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
``
|
|
240
|
-
|
|
238
|
+
Query parameters, forwarded only when not ``None``. An
|
|
239
|
+
``owner_group`` named here is sent as given.
|
|
240
|
+
inject_group : bool | None
|
|
241
|
+
``None`` (default) pins the lookup to the client's
|
|
242
|
+
``owner_group`` only when the client already holds one, and never
|
|
243
|
+
resolves or prompts for one. ``True`` always pins, resolving the
|
|
244
|
+
group if needed; ``False`` never pins, leaving the lookup to the
|
|
245
|
+
caller's ``current_group``.
|
|
241
246
|
|
|
242
247
|
Returns
|
|
243
248
|
-------
|
|
@@ -245,6 +250,8 @@ class ReadOnlyResource:
|
|
|
245
250
|
The resource record.
|
|
246
251
|
"""
|
|
247
252
|
endpoint = f"{self._path}/{uuid}"
|
|
253
|
+
if inject_group is None:
|
|
254
|
+
inject_group = self._client.has_owner_group
|
|
248
255
|
kwargs: dict[str, Any] = {"inject_group": inject_group}
|
|
249
256
|
if params is not None:
|
|
250
257
|
kwargs["params"] = params
|
|
@@ -268,13 +275,15 @@ class ReadOnlyResource:
|
|
|
268
275
|
*method*.
|
|
269
276
|
|
|
270
277
|
The active ``owner_group`` is injected as a query parameter for
|
|
271
|
-
*list* actions (no *uuid*)
|
|
272
|
-
same convention :meth:`list` and
|
|
278
|
+
*list* actions (no *uuid*); *detail* actions send it only when the
|
|
279
|
+
client already holds one — the same convention :meth:`list` and
|
|
280
|
+
:meth:`get` follow. An ``owner_group`` named in *params* is sent as
|
|
281
|
+
given either way.
|
|
273
282
|
|
|
274
283
|
Parameters
|
|
275
284
|
----------
|
|
276
285
|
name : str
|
|
277
|
-
The action's ``url_path`` (e.g. ``"
|
|
286
|
+
The action's ``url_path`` (e.g. ``"submit-draft"``,
|
|
278
287
|
``"queue-position"``, ``"save-calibration"``).
|
|
279
288
|
uuid : str | None
|
|
280
289
|
When given, the action is detail-scoped to this resource UUID.
|
|
@@ -292,7 +301,7 @@ class ReadOnlyResource:
|
|
|
292
301
|
list, some a single object).
|
|
293
302
|
"""
|
|
294
303
|
endpoint = "/".join(part for part in (self._path, uuid, name) if part)
|
|
295
|
-
kwargs: dict[str, Any] = {"inject_group": uuid is None}
|
|
304
|
+
kwargs: dict[str, Any] = {"inject_group": uuid is None or self._client.has_owner_group}
|
|
296
305
|
if data is not None:
|
|
297
306
|
kwargs["json"] = data
|
|
298
307
|
if params is not None:
|
|
@@ -328,8 +337,8 @@ class Resource(ReadOnlyResource):
|
|
|
328
337
|
supported across the API — several viewsets accept ``PATCH`` but
|
|
329
338
|
block ``PUT`` to keep their state-machine guards intact (e.g.
|
|
330
339
|
studio jobs, suite ML models). Use :meth:`replace` for a full
|
|
331
|
-
``PUT`` replacement where the resource allows it.
|
|
332
|
-
is
|
|
340
|
+
``PUT`` replacement where the resource allows it. Like :meth:`get`,
|
|
341
|
+
the request is pinned to the client's group only when it holds one.
|
|
333
342
|
|
|
334
343
|
Parameters
|
|
335
344
|
----------
|
|
@@ -345,7 +354,9 @@ class Resource(ReadOnlyResource):
|
|
|
345
354
|
"""
|
|
346
355
|
endpoint = f"{self._path}/{uuid}"
|
|
347
356
|
return _expect_dict(
|
|
348
|
-
self._client.request_json(
|
|
357
|
+
self._client.request_json(
|
|
358
|
+
"patch", endpoint, inject_group=self._client.has_owner_group, json=data
|
|
359
|
+
),
|
|
349
360
|
endpoint,
|
|
350
361
|
)
|
|
351
362
|
|
|
@@ -369,7 +380,9 @@ class Resource(ReadOnlyResource):
|
|
|
369
380
|
"""
|
|
370
381
|
endpoint = f"{self._path}/{uuid}"
|
|
371
382
|
return _expect_dict(
|
|
372
|
-
self._client.request_json(
|
|
383
|
+
self._client.request_json(
|
|
384
|
+
"put", endpoint, inject_group=self._client.has_owner_group, json=data
|
|
385
|
+
),
|
|
373
386
|
endpoint,
|
|
374
387
|
)
|
|
375
388
|
|
|
@@ -399,7 +412,9 @@ class Resource(ReadOnlyResource):
|
|
|
399
412
|
"""
|
|
400
413
|
endpoint = f"{self._path}/{uuid}"
|
|
401
414
|
return _expect_dict(
|
|
402
|
-
self._client.request_json(
|
|
415
|
+
self._client.request_json(
|
|
416
|
+
"delete", endpoint, inject_group=self._client.has_owner_group
|
|
417
|
+
),
|
|
403
418
|
endpoint,
|
|
404
419
|
)
|
|
405
420
|
|
|
@@ -435,7 +450,8 @@ class SoftDeleteMixin:
|
|
|
435
450
|
"""
|
|
436
451
|
endpoint = f"{self._path}/{uuid}/trash"
|
|
437
452
|
return _expect_dict(
|
|
438
|
-
self._client.request_json("post", endpoint, inject_group=
|
|
453
|
+
self._client.request_json("post", endpoint, inject_group=self._client.has_owner_group),
|
|
454
|
+
endpoint,
|
|
439
455
|
)
|
|
440
456
|
|
|
441
457
|
def restore(self, uuid: str) -> dict:
|
|
@@ -453,7 +469,8 @@ class SoftDeleteMixin:
|
|
|
453
469
|
"""
|
|
454
470
|
endpoint = f"{self._path}/{uuid}/restore"
|
|
455
471
|
return _expect_dict(
|
|
456
|
-
self._client.request_json("post", endpoint, inject_group=
|
|
472
|
+
self._client.request_json("post", endpoint, inject_group=self._client.has_owner_group),
|
|
473
|
+
endpoint,
|
|
457
474
|
)
|
|
458
475
|
|
|
459
476
|
def permanent_delete(self, uuid: str, *, confirm: bool = False) -> dict:
|
|
@@ -483,7 +500,10 @@ class SoftDeleteMixin:
|
|
|
483
500
|
)
|
|
484
501
|
endpoint = f"{self._path}/{uuid}/permanent-delete"
|
|
485
502
|
return _expect_dict(
|
|
486
|
-
self._client.request_json(
|
|
503
|
+
self._client.request_json(
|
|
504
|
+
"delete", endpoint, inject_group=self._client.has_owner_group
|
|
505
|
+
),
|
|
506
|
+
endpoint,
|
|
487
507
|
)
|
|
488
508
|
|
|
489
509
|
|
icmd/apps/comments.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Discussion comments and your comment notifications (``/api/v2/comments/``).
|
|
2
|
+
|
|
3
|
+
A discussion belongs to one item (a job, dataset, composition, ...), named by
|
|
4
|
+
``target_type`` and ``target_id``. Listing needs both, and a new comment
|
|
5
|
+
carries them in its body::
|
|
6
|
+
|
|
7
|
+
icmd = ICMD("icmd.ai")
|
|
8
|
+
on_job = {"target_type": "job", "target_id": "<uuid>"}
|
|
9
|
+
icmd.comments.comment.list(**on_job)
|
|
10
|
+
icmd.comments.comment.add({**on_job, "body": "Looks good"})
|
|
11
|
+
|
|
12
|
+
A comment's ``delete`` is final: nothing restores it. Notifications are the
|
|
13
|
+
caller's own feed, list-only; mark them read through
|
|
14
|
+
``icmd.comments.notification.action("mark_all_read", method="post")``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
from .base import ReadOnlyResource, Resource
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from icmd.core.session import ResourceClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CommentsApp:
|
|
28
|
+
"""Discussion comments and comment notifications."""
|
|
29
|
+
|
|
30
|
+
__slots__ = ("comment", "notification")
|
|
31
|
+
|
|
32
|
+
def __init__(self, client: ResourceClient) -> None:
|
|
33
|
+
self.comment = Resource(client, "comments")
|
|
34
|
+
self.notification = ReadOnlyResource(client, "comments/notifications")
|