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 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 this assembler emits into ``qt_model_inputs``.
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
- name: str,
88
+ key: str,
87
89
  model_uuid: str,
88
90
  inputs: Mapping[str, Any],
89
- scenario: str | None = None,
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
- ``node_id`` identifies this workflow instance. The catalog ``model_uuid``
95
- remains in ``qt_model.uuid`` and is used only as the default node id.
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
- if scenario is not None:
99
- model_inputs[SCENARIO_KEY] = {"title": scenario}
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": {"name": name, "uuid": model_uuid},
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("get", f"{_MESSAGE_PATH}/{message_uuid}", inject_group=False),
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 = False,
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. For jobs specifically, the
227
- Studio action ``studio/jobs/{uuid}/accessible-group/`` answers which
228
- of your groups can ordinarily see a job; it walks plain project
229
- access, so it performs no admin widening.
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
- inject_group : bool
238
- When ``True``, pin the lookup to the client's active
239
- ``owner_group`` (resolving it if needed) instead of leaving it
240
- to the caller's ``current_group``.
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*) and skipped for *detail* actions — the
272
- same convention :meth:`list` and :meth:`get` follow.
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. ``"resubmit"``,
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. Group injection
332
- is skipped — the UUID fully identifies the target.
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("patch", endpoint, inject_group=False, json=data),
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("put", endpoint, inject_group=False, json=data),
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("delete", endpoint, inject_group=False),
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=False), endpoint
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=False), endpoint
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("delete", endpoint, inject_group=False), endpoint
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")