icmd-python 3.3.0__py3-none-any.whl → 3.3.5__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/apps/agent.py +12 -1
- icmd/apps/jobs.py +16 -16
- icmd/cli/__init__.py +16 -6
- icmd/cli/account.py +4 -1
- icmd/cli/commands.py +2 -2
- icmd/client.py +21 -14
- icmd/compose.py +5 -5
- icmd/core/credentials.py +41 -28
- icmd/core/session.py +52 -19
- icmd/types.py +153 -9
- icmd/version.py +1 -1
- {icmd_python-3.3.0.dist-info → icmd_python-3.3.5.dist-info}/METADATA +1 -1
- {icmd_python-3.3.0.dist-info → icmd_python-3.3.5.dist-info}/RECORD +17 -17
- {icmd_python-3.3.0.dist-info → icmd_python-3.3.5.dist-info}/WHEEL +0 -0
- {icmd_python-3.3.0.dist-info → icmd_python-3.3.5.dist-info}/entry_points.txt +0 -0
- {icmd_python-3.3.0.dist-info → icmd_python-3.3.5.dist-info}/licenses/LICENSE +0 -0
- {icmd_python-3.3.0.dist-info → icmd_python-3.3.5.dist-info}/top_level.txt +0 -0
icmd/apps/agent.py
CHANGED
|
@@ -212,5 +212,16 @@ class AgentApp:
|
|
|
212
212
|
return self._client.request_json("get", "agent/providers")
|
|
213
213
|
|
|
214
214
|
def usage(self) -> dict | list[dict]:
|
|
215
|
-
"""Get the group's
|
|
215
|
+
"""Get the group's Assist credit balance (``GET agent/assist/usage``).
|
|
216
|
+
|
|
217
|
+
Group admins only.
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
dict
|
|
222
|
+
``granted``, ``used``, ``balance`` and ``month_used`` in credits,
|
|
223
|
+
and ``status`` (``"ok"`` / ``"low"`` / ``"out"``). ``balance``
|
|
224
|
+
can be negative. An unmetered group returns ``None`` for all five
|
|
225
|
+
keys, ``status`` included.
|
|
226
|
+
"""
|
|
216
227
|
return self._client.request_json("get", "agent/assist/usage")
|
icmd/apps/jobs.py
CHANGED
|
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
|
|
|
29
29
|
from .types import JobLogResponse
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
_TERMINAL_STATUSES = frozenset({"successful", "failed", "partial", "cancelled"})
|
|
32
|
+
_TERMINAL_STATUSES = frozenset({"successful", "failed", "partial", "cancelled", "aborted"})
|
|
33
33
|
|
|
34
34
|
#: Consecutive transient failures :meth:`JobResource.poll` absorbs before
|
|
35
35
|
#: giving up. A training job outlives the 15-minute access token several
|
|
@@ -223,11 +223,12 @@ class JobResource(ReadOnlyResource):
|
|
|
223
223
|
The design-level CALPHAD engine, written to
|
|
224
224
|
``input_dict["engine"]`` — qt-designer reads it from there and
|
|
225
225
|
substitutes it into every model input spelled ``use_global``.
|
|
226
|
-
One of ``"thermocalc"``, ``"pandat"``, ``"
|
|
227
|
-
|
|
228
|
-
backend with ``400 invalid_engine``. Optional when
|
|
226
|
+
One of ``"thermocalc"``, ``"pandat"``, ``"pycalphad"``; anything
|
|
227
|
+
else is rejected by the backend with ``400 invalid_engine``. Optional when
|
|
229
228
|
*input_dict* already carries an ``engine``; supplying both is an
|
|
230
|
-
error unless they agree
|
|
229
|
+
error unless they agree. Omit both when no model defers to the
|
|
230
|
+
job-level engine; if one does, the backend refuses the job with
|
|
231
|
+
``400 missing_engine``.
|
|
231
232
|
folder : str
|
|
232
233
|
UUID of the job folder this job belongs to.
|
|
233
234
|
name : str | None
|
|
@@ -265,7 +266,7 @@ class JobResource(ReadOnlyResource):
|
|
|
265
266
|
------
|
|
266
267
|
ICMDValidationError
|
|
267
268
|
If *engine* and a non-empty ``input_dict["engine"]`` disagree, if
|
|
268
|
-
|
|
269
|
+
``input_dict["engine"]`` holds
|
|
269
270
|
something other than a string, or if a model named by name alone
|
|
270
271
|
matches more than one catalog model.
|
|
271
272
|
"""
|
|
@@ -285,12 +286,10 @@ class JobResource(ReadOnlyResource):
|
|
|
285
286
|
"Pass one or the other, or make them agree."
|
|
286
287
|
)
|
|
287
288
|
resolved = engine or document_engine
|
|
288
|
-
if resolved is None:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
)
|
|
293
|
-
inputs["engine"] = resolved
|
|
289
|
+
if resolved is not None:
|
|
290
|
+
inputs["engine"] = resolved
|
|
291
|
+
else:
|
|
292
|
+
inputs.pop("engine", None)
|
|
294
293
|
# Before the advisory, which needs a uuid per model to fetch a contract,
|
|
295
294
|
# and regardless of ``check`` -- completing the reference is what makes
|
|
296
295
|
# the document submittable, not a check on it.
|
|
@@ -432,9 +431,10 @@ class JobResource(ReadOnlyResource):
|
|
|
432
431
|
Returns
|
|
433
432
|
-------
|
|
434
433
|
str | None
|
|
435
|
-
The terminal status reached -- ``"failed"
|
|
436
|
-
are terminal too, so a caller that
|
|
437
|
-
must check this. ``None`` only when
|
|
434
|
+
The terminal status reached -- ``"failed"``, ``"partial"``,
|
|
435
|
+
``"cancelled"`` and ``"aborted"`` are terminal too, so a caller that
|
|
436
|
+
needs the job to have *worked* must check this. ``None`` only when
|
|
437
|
+
polling was interrupted.
|
|
438
438
|
|
|
439
439
|
Raises
|
|
440
440
|
------
|
|
@@ -646,7 +646,7 @@ class JobResource(ReadOnlyResource):
|
|
|
646
646
|
# -----------------------------------------------------------------
|
|
647
647
|
|
|
648
648
|
def resubmit(self, uuid: str) -> dict:
|
|
649
|
-
"""Re-run a
|
|
649
|
+
"""Re-run a successful/failed/partial/cancelled/aborted job (``POST {uuid}/resubmit``).
|
|
650
650
|
|
|
651
651
|
Takes no inputs, because the endpoint accepts none: it declares no request
|
|
652
652
|
body and re-resolves the job's *stored* ``input_dict`` against the owning
|
icmd/cli/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ Entry point: ``icmd`` (installed via ``[project.scripts]``).
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import contextlib
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
10
11
|
import click
|
|
@@ -23,7 +24,7 @@ from icmd.version import __version__
|
|
|
23
24
|
from .account import agent_usage, register_account_commands
|
|
24
25
|
from .auth import CLIAuthenticator
|
|
25
26
|
from .auth_status import auth_cmd
|
|
26
|
-
from .client import _build_account_client
|
|
27
|
+
from .client import _build_account_client, build_client
|
|
27
28
|
from .commands import (
|
|
28
29
|
_domain_option,
|
|
29
30
|
_format,
|
|
@@ -139,12 +140,21 @@ def login() -> None:
|
|
|
139
140
|
@cli.command()
|
|
140
141
|
@_domain_option
|
|
141
142
|
def logout() -> None:
|
|
142
|
-
"""
|
|
143
|
+
"""Sign out of an ICMD® instance and clear its cached credentials."""
|
|
143
144
|
ctx = click.get_current_context()
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
domain = _require_domain(ctx)
|
|
146
|
+
normalized = normalize_domain(domain)
|
|
147
|
+
store = CredentialManager()
|
|
148
|
+
# Revoke a saved login on the server first, or its refresh token stays
|
|
149
|
+
# usable until it expires. Gated on a stored refresh token so that with
|
|
150
|
+
# nothing saved no client is built (no request, no prompt). Best effort:
|
|
151
|
+
# the local clear runs whatever the server does, Ctrl-C included.
|
|
152
|
+
try:
|
|
153
|
+
if store.get_refresh_token(normalized):
|
|
154
|
+
with contextlib.suppress(Exception):
|
|
155
|
+
build_client(domain).logout(clear_saved_credentials=True)
|
|
156
|
+
finally:
|
|
157
|
+
store.clear_domain_credentials(normalized)
|
|
148
158
|
write_output({"status": "ok", "domain": normalized}, fmt=_format(ctx))
|
|
149
159
|
|
|
150
160
|
|
icmd/cli/account.py
CHANGED
|
@@ -371,7 +371,10 @@ def invite_cancel(uuid: str, yes: bool) -> None:
|
|
|
371
371
|
@click.command("usage")
|
|
372
372
|
@_global_options
|
|
373
373
|
def agent_usage() -> None:
|
|
374
|
-
"""Get the group's
|
|
374
|
+
"""Get the group's ICMD® Assist credit balance.
|
|
375
|
+
|
|
376
|
+
Prints ``granted`` / ``used`` / ``balance`` / ``month_used`` (credits) and
|
|
377
|
+
``status``; ``null`` values mean the group is unmetered.
|
|
375
378
|
|
|
376
379
|
Unlike the account commands, this is group-scoped: it honours ``--group``
|
|
377
380
|
and injects ``owner_group`` (the backend gates on group-admin membership).
|
icmd/cli/commands.py
CHANGED
|
@@ -1148,8 +1148,8 @@ def _make_submit_cmd(path: str) -> click.Command:
|
|
|
1148
1148
|
default=None,
|
|
1149
1149
|
help=(
|
|
1150
1150
|
"Design-level CALPHAD engine written to input_dict['engine'] "
|
|
1151
|
-
"(thermocalc, pandat,
|
|
1152
|
-
"
|
|
1151
|
+
"(thermocalc, pandat, pycalphad). Omit when the input_dict "
|
|
1152
|
+
"already carries one or no model defers to the job-level engine."
|
|
1153
1153
|
),
|
|
1154
1154
|
)
|
|
1155
1155
|
@click.option("--folder", required=True, help="UUID of the destination job folder.")
|
icmd/client.py
CHANGED
|
@@ -148,11 +148,9 @@ class ICMD:
|
|
|
148
148
|
def _save_credentials(self) -> None:
|
|
149
149
|
"""Persist the credentials produced by an interactive login.
|
|
150
150
|
|
|
151
|
-
Only ever called after an interactive flow
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
backend's absolute session cap forward on every refresh, so the refresh
|
|
155
|
-
path saves the access token alone (:meth:`_cache_access_token`).
|
|
151
|
+
Only ever called after an interactive flow: it replaces the whole
|
|
152
|
+
record unconditionally, whereas a refresh must write only while the
|
|
153
|
+
record still holds the token it presented (:meth:`_cache_access_token`).
|
|
156
154
|
"""
|
|
157
155
|
issued = self._session.refresh_token
|
|
158
156
|
self._credentials.save_domain_credentials(
|
|
@@ -167,8 +165,9 @@ class ICMD:
|
|
|
167
165
|
def _cache_access_token(self, refresh_token: str | None = None) -> None:
|
|
168
166
|
"""Cache the session's access token for the next process to reuse.
|
|
169
167
|
|
|
170
|
-
|
|
171
|
-
|
|
168
|
+
A refresh token the server rotated in exchange for *refresh_token* is
|
|
169
|
+
written with it, in the same compare-and-swap, so the next process
|
|
170
|
+
presents the current token rather than a superseded one.
|
|
172
171
|
|
|
173
172
|
Parameters
|
|
174
173
|
----------
|
|
@@ -185,11 +184,13 @@ class ICMD:
|
|
|
185
184
|
"""
|
|
186
185
|
if not self._session.has_valid_token():
|
|
187
186
|
return
|
|
187
|
+
current = self._session.refresh_token
|
|
188
188
|
self._credentials.save_access_token(
|
|
189
189
|
self._session.domain,
|
|
190
190
|
self._session.access_token,
|
|
191
|
-
|
|
191
|
+
expires=self._session.access_token_expires_at,
|
|
192
192
|
expected_refresh_token=refresh_token,
|
|
193
|
+
rotated_refresh_token=current if current and current != refresh_token else None,
|
|
193
194
|
)
|
|
194
195
|
|
|
195
196
|
def _adopt_cached_access_token(self) -> bool:
|
|
@@ -347,8 +348,11 @@ class ICMD:
|
|
|
347
348
|
"""Clear cached credentials for a domain.
|
|
348
349
|
|
|
349
350
|
Removes the whole record — refresh token and cached access token
|
|
350
|
-
alike — unconditionally
|
|
351
|
-
|
|
351
|
+
alike — unconditionally, not as a reaction to a token the server
|
|
352
|
+
rejected. Local only: the server session stays live; use
|
|
353
|
+
:meth:`logout` to end it. Processes already running on that login
|
|
354
|
+
may then sign it out, since each rotates its refresh token on its
|
|
355
|
+
own once the record is gone.
|
|
352
356
|
|
|
353
357
|
Can be called without an ``ICMD`` instance, which is useful when
|
|
354
358
|
instantiation itself fails (e.g. after a server-side auth-method
|
|
@@ -506,10 +510,13 @@ class ICMD:
|
|
|
506
510
|
Parameters
|
|
507
511
|
----------
|
|
508
512
|
clear_saved_credentials : bool
|
|
509
|
-
Also clear the stored refresh token
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
+
Also clear the stored refresh token. Every refresh token of a
|
|
514
|
+
login belongs to the session just ended, so once the server has
|
|
515
|
+
ended it the stored one is dead either way; clearing it only spares
|
|
516
|
+
the next client one rejected refresh before it authenticates
|
|
517
|
+
interactively. The cached access
|
|
518
|
+
token is dropped either way — it belongs to the session just ended,
|
|
519
|
+
and leaving it behind would let another process keep using it.
|
|
513
520
|
|
|
514
521
|
Notes
|
|
515
522
|
-----
|
icmd/compose.py
CHANGED
|
@@ -93,11 +93,11 @@ def compose_input_dict(
|
|
|
93
93
|
|
|
94
94
|
``databases`` and ``engine`` are the design-level values, written at the top
|
|
95
95
|
of the document rather than on any one model: a sequence of wire mappings or
|
|
96
|
-
:class:`~icmd.types.Database` objects, and one of the
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
:class:`~icmd.types.Database` objects, and one of the CALPHAD engine names
|
|
97
|
+
``thermocalc``, ``pandat``, ``pycalphad``. They are what a model input
|
|
98
|
+
spelled ``use_global`` resolves against. Omitting either emits no key for
|
|
99
|
+
it, which is what leaves the value unset -- an empty list or an empty string
|
|
100
|
+
reads downstream as no value at all.
|
|
101
101
|
|
|
102
102
|
:class:`~icmd.types.FromVariable` and :class:`~icmd.types.FromModel` hold
|
|
103
103
|
the object they reference, and resolve here to the variable's name and the
|
icmd/core/credentials.py
CHANGED
|
@@ -41,7 +41,7 @@ Per-domain record::
|
|
|
41
41
|
|
|
42
42
|
{
|
|
43
43
|
"auth_method": "SAML", # required for auth-method detection
|
|
44
|
-
"refresh_token": "…", # long-lived;
|
|
44
|
+
"refresh_token": "…", # long-lived; replaced when a refresh rotates it
|
|
45
45
|
"access_token": "…", # optional cache, short-lived
|
|
46
46
|
"access_token_expires_at": "…", # absolute UTC ISO-8601 timestamp
|
|
47
47
|
"saved_at": "…"
|
|
@@ -717,18 +717,15 @@ class CredentialManager:
|
|
|
717
717
|
domain: str,
|
|
718
718
|
access_token: str,
|
|
719
719
|
*,
|
|
720
|
-
|
|
721
|
-
expires_at: datetime | None = None,
|
|
720
|
+
expires: float | datetime | None = None,
|
|
722
721
|
expected_refresh_token: str | None = None,
|
|
722
|
+
rotated_refresh_token: str | None = None,
|
|
723
723
|
) -> bool:
|
|
724
|
-
"""Cache an access token for *domain
|
|
724
|
+
"""Cache an access token for *domain*, with the refresh token rotated beside it.
|
|
725
725
|
|
|
726
726
|
This is the only writer of the access-token fields, and it merges into
|
|
727
|
-
the existing record rather than replacing it
|
|
728
|
-
|
|
729
|
-
server mints a *new* refresh token on every exchange and persisting the
|
|
730
|
-
rotation would roll the backend's absolute session cap forward
|
|
731
|
-
indefinitely.
|
|
727
|
+
the existing record rather than replacing it, so the auth method and
|
|
728
|
+
the rest of the record survive.
|
|
732
729
|
|
|
733
730
|
Parameters
|
|
734
731
|
----------
|
|
@@ -736,11 +733,10 @@ class CredentialManager:
|
|
|
736
733
|
Normalised ICMD domain the token belongs to.
|
|
737
734
|
access_token : str
|
|
738
735
|
The access token to cache.
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
naive value is interpreted as UTC.
|
|
736
|
+
expires : float | datetime | None
|
|
737
|
+
A number is the lifetime in seconds as reported by the server,
|
|
738
|
+
measured from now; a datetime is the absolute expiry, and a naive
|
|
739
|
+
one is interpreted as UTC.
|
|
744
740
|
expected_refresh_token : str | None
|
|
745
741
|
When given, cache only while the stored refresh token is still
|
|
746
742
|
exactly this value — the compare-and-swap counterpart to
|
|
@@ -751,6 +747,12 @@ class CredentialManager:
|
|
|
751
747
|
would resurrect a signed-out record (an access-token-only leftover
|
|
752
748
|
that the next process happily adopts) or file one account's token
|
|
753
749
|
under another's. ``None`` merges unconditionally.
|
|
750
|
+
rotated_refresh_token : str | None
|
|
751
|
+
The refresh token the server returned with *access_token*, when it
|
|
752
|
+
differs from the one presented. Written in the same rewrite, after
|
|
753
|
+
the *expected_refresh_token* check, so both tokens land or neither
|
|
754
|
+
does. A failed write is logged at WARNING: the stored token is then
|
|
755
|
+
one the server has already superseded.
|
|
754
756
|
|
|
755
757
|
Returns
|
|
756
758
|
-------
|
|
@@ -765,24 +767,26 @@ class CredentialManager:
|
|
|
765
767
|
Raises
|
|
766
768
|
------
|
|
767
769
|
ValueError
|
|
768
|
-
If
|
|
770
|
+
If *expires* is not given.
|
|
769
771
|
"""
|
|
770
|
-
if
|
|
771
|
-
raise ValueError("save_access_token requires
|
|
772
|
+
if expires is None:
|
|
773
|
+
raise ValueError("save_access_token requires expires")
|
|
772
774
|
|
|
773
775
|
if not access_token or not isinstance(access_token, str):
|
|
774
776
|
logger.debug("Refusing to cache an empty access token for %s", domain)
|
|
775
777
|
return False
|
|
776
778
|
|
|
777
|
-
resolved = self._resolve_expiry(
|
|
779
|
+
resolved = self._resolve_expiry(expires)
|
|
778
780
|
if resolved is None:
|
|
779
781
|
logger.debug("Access token for %s has no usable expiry; not caching", domain)
|
|
780
782
|
return False
|
|
781
783
|
|
|
782
784
|
stamp = resolved.isoformat()
|
|
783
785
|
canonical = canonical_domain(domain)
|
|
786
|
+
attempted = False
|
|
784
787
|
|
|
785
788
|
def mutate(records: dict[str, Any]) -> bool:
|
|
789
|
+
nonlocal attempted
|
|
786
790
|
# Folded before the compare-and-swap, so the token compared against
|
|
787
791
|
# is the one this domain's single surviving record holds. Refusing
|
|
788
792
|
# below writes nothing and discards the fold with it; the next
|
|
@@ -804,23 +808,32 @@ class CredentialManager:
|
|
|
804
808
|
records[canonical] = record
|
|
805
809
|
record[_ACCESS_TOKEN_KEY] = access_token
|
|
806
810
|
record[_ACCESS_TOKEN_EXPIRES_AT_KEY] = stamp
|
|
811
|
+
if rotated_refresh_token:
|
|
812
|
+
record[_REFRESH_TOKEN_KEY] = rotated_refresh_token
|
|
813
|
+
attempted = True
|
|
807
814
|
return True
|
|
808
815
|
|
|
809
|
-
|
|
816
|
+
saved = self._mutate_store(mutate)
|
|
817
|
+
if attempted and not saved and rotated_refresh_token:
|
|
818
|
+
logger.warning(
|
|
819
|
+
"Could not save the rotated refresh token for %s; this process's next "
|
|
820
|
+
"refresh and any later process will present a superseded one, which "
|
|
821
|
+
"signs the login out.",
|
|
822
|
+
domain,
|
|
823
|
+
)
|
|
824
|
+
return saved
|
|
810
825
|
|
|
811
826
|
@staticmethod
|
|
812
|
-
def _resolve_expiry(
|
|
813
|
-
"""Resolve
|
|
814
|
-
if
|
|
815
|
-
if
|
|
816
|
-
return None
|
|
817
|
-
return expires_at.replace(tzinfo=UTC) if expires_at.tzinfo is None else expires_at
|
|
827
|
+
def _resolve_expiry(expires: float | datetime) -> datetime | None:
|
|
828
|
+
"""Resolve either expiry form into one absolute UTC timestamp."""
|
|
829
|
+
if isinstance(expires, datetime):
|
|
830
|
+
return expires.replace(tzinfo=UTC) if expires.tzinfo is None else expires
|
|
818
831
|
|
|
819
|
-
if not isinstance(
|
|
832
|
+
if not isinstance(expires, int | float) or isinstance(expires, bool):
|
|
820
833
|
return None
|
|
821
|
-
if
|
|
834
|
+
if expires <= 0:
|
|
822
835
|
return None
|
|
823
|
-
return datetime.now(UTC) + timedelta(seconds=float(
|
|
836
|
+
return datetime.now(UTC) + timedelta(seconds=float(expires))
|
|
824
837
|
|
|
825
838
|
def _mutate_store(self, mutate: Callable[[dict[str, Any]], bool]) -> bool:
|
|
826
839
|
"""Read, mutate, and atomically rewrite the store under the credential lock.
|
icmd/core/session.py
CHANGED
|
@@ -9,6 +9,7 @@ this module only manages token state.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import logging
|
|
12
13
|
import os
|
|
13
14
|
import re
|
|
14
15
|
import warnings
|
|
@@ -60,6 +61,8 @@ if TYPE_CHECKING:
|
|
|
60
61
|
from .constants import HTTPMethod
|
|
61
62
|
from .contracts import ContractClient
|
|
62
63
|
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
63
66
|
|
|
64
67
|
class ResourceClient(Protocol):
|
|
65
68
|
"""Structural type for objects that can make JSON API requests."""
|
|
@@ -622,6 +625,16 @@ class BearerAuth(AuthBase):
|
|
|
622
625
|
return r
|
|
623
626
|
|
|
624
627
|
|
|
628
|
+
def _no_auth(r: requests.PreparedRequest) -> requests.PreparedRequest:
|
|
629
|
+
"""Send one request without the session's bearer.
|
|
630
|
+
|
|
631
|
+
Passed as a per-request ``auth=``, which displaces ``Session.auth``;
|
|
632
|
+
``auth=None`` would not, since ``requests`` falls back to the session's.
|
|
633
|
+
"""
|
|
634
|
+
r.headers.pop("Authorization", None)
|
|
635
|
+
return r
|
|
636
|
+
|
|
637
|
+
|
|
625
638
|
# ---------------------------------------------------------------------------
|
|
626
639
|
# Session
|
|
627
640
|
# ---------------------------------------------------------------------------
|
|
@@ -708,7 +721,6 @@ class Session:
|
|
|
708
721
|
EXCLUDED_ENDPOINTS: ClassVar[set[str]] = {
|
|
709
722
|
LOGIN,
|
|
710
723
|
AUTH_REFRESH,
|
|
711
|
-
AUTH_LOGOUT,
|
|
712
724
|
MFA_VERIFY,
|
|
713
725
|
MFA_EMAIL_GENERATE,
|
|
714
726
|
SAML_LOGIN,
|
|
@@ -716,6 +728,11 @@ class Session:
|
|
|
716
728
|
PUBLIC_CONFIG,
|
|
717
729
|
}
|
|
718
730
|
|
|
731
|
+
#: ``AllowAny`` endpoints that never carry the bearer: DRF rejects a stale one
|
|
732
|
+
#: before the view runs. The refresh POST opts out itself in
|
|
733
|
+
#: :meth:`refresh_access_token`; everything else, logout included, keeps it.
|
|
734
|
+
_BEARERLESS_ENDPOINTS: ClassVar[frozenset[str]] = frozenset({LOGIN, PUBLIC_CONFIG})
|
|
735
|
+
|
|
719
736
|
def __init__(self, domain: str) -> None:
|
|
720
737
|
"""Initialise the session with a validated domain.
|
|
721
738
|
|
|
@@ -910,6 +927,7 @@ class Session:
|
|
|
910
927
|
f"{self.api_root}/{AUTH_REFRESH}/",
|
|
911
928
|
json={"refresh_token": self._refresh_token},
|
|
912
929
|
timeout=self._REFRESH_TIMEOUT,
|
|
930
|
+
auth=_no_auth,
|
|
913
931
|
)
|
|
914
932
|
except requests.RequestException as exc:
|
|
915
933
|
# Nothing came back, so nothing implicates the stored token.
|
|
@@ -1026,6 +1044,9 @@ class Session:
|
|
|
1026
1044
|
|
|
1027
1045
|
if "timeout" not in kwargs:
|
|
1028
1046
|
kwargs["timeout"] = self._timeout
|
|
1047
|
+
if endpoint in self._BEARERLESS_ENDPOINTS:
|
|
1048
|
+
# A default, so a caller's own ``auth=`` still wins.
|
|
1049
|
+
kwargs.setdefault("auth", _no_auth)
|
|
1029
1050
|
|
|
1030
1051
|
renewal_failure = self._renew_expired_token(endpoint)
|
|
1031
1052
|
|
|
@@ -1143,15 +1164,11 @@ class Session:
|
|
|
1143
1164
|
|
|
1144
1165
|
# The server already refused this exact access token on a 401 that a
|
|
1145
1166
|
# refresh did not fix, so exchanging again cannot help — and would not
|
|
1146
|
-
# be free
|
|
1147
|
-
#
|
|
1148
|
-
#
|
|
1149
|
-
#
|
|
1150
|
-
#
|
|
1151
|
-
# exhausts the bucket after 30 — the same refresh lockout the
|
|
1152
|
-
# empty-token guard above prevents, reached by another door. No reset
|
|
1153
|
-
# is needed: the guard compares against the token currently held, so
|
|
1154
|
-
# any path that installs a new one clears it.
|
|
1167
|
+
# be free. Without this, a 401 that is not about expiry (an
|
|
1168
|
+
# authorization failure, a proxy stripping ``Authorization``) costs one
|
|
1169
|
+
# refresh POST per *request*, each counted by the server's refresh
|
|
1170
|
+
# throttle. No reset is needed: the guard compares against the token
|
|
1171
|
+
# currently held, so any path that installs a new one clears it.
|
|
1155
1172
|
if self._access_token and self._access_token == self._refresh_rejected_token:
|
|
1156
1173
|
return response
|
|
1157
1174
|
|
|
@@ -1306,18 +1323,34 @@ class Session:
|
|
|
1306
1323
|
# -----------------------------------------------------------------
|
|
1307
1324
|
|
|
1308
1325
|
def logout(self) -> None:
|
|
1309
|
-
"""
|
|
1326
|
+
"""Revoke the server session and clear in-memory tokens.
|
|
1310
1327
|
|
|
1311
|
-
|
|
1328
|
+
The server revokes the session named by the bearer, so the POST takes
|
|
1329
|
+
the same renew-then-send path as any authenticated request, including
|
|
1330
|
+
the single refresh-and-retry on a ``401``. Best effort: it never
|
|
1331
|
+
raises, and the outcome is logged at DEBUG — callers decide how to
|
|
1332
|
+
communicate success.
|
|
1312
1333
|
"""
|
|
1313
1334
|
try:
|
|
1314
1335
|
if self._access_token:
|
|
1315
|
-
self.
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1336
|
+
self._renew_expired_token(AUTH_LOGOUT)
|
|
1337
|
+
if (
|
|
1338
|
+
self._refresh_coordinator is not None
|
|
1339
|
+
and self._proactive_renewal
|
|
1340
|
+
and self.is_token_expired()
|
|
1341
|
+
):
|
|
1342
|
+
# Renewal had its chance and failed; the server would only
|
|
1343
|
+
# answer this token with a 401.
|
|
1344
|
+
logger.debug("Logout skipped: the access token expired and was not renewed")
|
|
1345
|
+
else:
|
|
1346
|
+
url = f"{self.api_root}/{AUTH_LOGOUT}/"
|
|
1347
|
+
response = self._raw_request("POST", url, timeout=5)
|
|
1348
|
+
if response.status_code == HTTPStatus.UNAUTHORIZED:
|
|
1349
|
+
response = self._retry_on_unauthorized(
|
|
1350
|
+
response, AUTH_LOGOUT, "POST", url, timeout=5
|
|
1351
|
+
)
|
|
1352
|
+
logger.debug("Logout answered HTTP %s", response.status_code)
|
|
1353
|
+
except Exception as exc:
|
|
1354
|
+
logger.debug("Logout request failed: %s", exc)
|
|
1322
1355
|
|
|
1323
1356
|
self.clear_tokens()
|
icmd/types.py
CHANGED
|
@@ -22,6 +22,7 @@ import csv
|
|
|
22
22
|
import math
|
|
23
23
|
from collections.abc import Mapping, Sequence
|
|
24
24
|
from copy import deepcopy
|
|
25
|
+
from decimal import Decimal
|
|
25
26
|
from io import StringIO
|
|
26
27
|
from typing import TYPE_CHECKING, Any, Protocol, Self, runtime_checkable
|
|
27
28
|
|
|
@@ -458,6 +459,9 @@ class DatasetCompositionalVariable(_Envelope):
|
|
|
458
459
|
per element -- nothing downstream reads a dataset reference on the
|
|
459
460
|
composition itself. :meth:`DatasetSource.composition` mints the composition
|
|
460
461
|
and its variables together, which is the only way they stay consistent.
|
|
462
|
+
Constructing one here normalizes nothing and qt-designer samples on the
|
|
463
|
+
composition's own unit rather than the one labelled here, so fraction values
|
|
464
|
+
are unsupported: express them in percent through that method instead.
|
|
461
465
|
|
|
462
466
|
The component is keyed ``name`` on the wire: that is the key the sampler
|
|
463
467
|
matches against the composition's components.
|
|
@@ -550,6 +554,49 @@ _BALANCE = "bal"
|
|
|
550
554
|
#: 9958180, lines 106-109).
|
|
551
555
|
_PERCENT_UNITS = frozenset({"mass_percent", "mole_percent"})
|
|
552
556
|
|
|
557
|
+
#: The unit a fraction-labelled dataset's cells are in once expressed in
|
|
558
|
+
#: percent. A dataset may be labelled in fractions and a job may not, so these
|
|
559
|
+
#: are read off the dataset rather than added to :data:`_PERCENT_UNITS`.
|
|
560
|
+
_FRACTION_TO_PERCENT = {"mass_fraction": "mass_percent", "mole_fraction": "mole_percent"}
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
class _CompositionUnitUnset:
|
|
564
|
+
"""The type of :data:`_COMPOSITION_UNIT_UNSET`, so it renders as prose.
|
|
565
|
+
|
|
566
|
+
``inspect.signature`` renders a default by ``repr``, so a bare ``object()``
|
|
567
|
+
puts a memory address in :meth:`DatasetSource.composition`'s signature --
|
|
568
|
+
in ``help()`` and in every IDE tooltip on a public method.
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
def __repr__(self) -> str:
|
|
572
|
+
"""Return what the signature shows where no unit was passed."""
|
|
573
|
+
return "<from the dataset>"
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
#: :meth:`DatasetSource.composition`'s "not given" default, and nothing else's.
|
|
577
|
+
#: Not ``None`` (:data:`_UNSET`): a plain parameter keeps no record of what the
|
|
578
|
+
#: caller passed, the way a Pydantic field's ``model_fields_set`` does, so
|
|
579
|
+
#: ``composition_unit=None`` is a value passed -- refused rather than resolved
|
|
580
|
+
#: from the dataset.
|
|
581
|
+
_COMPOSITION_UNIT_UNSET: Any = _CompositionUnitUnset()
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _as_percent(values: list[_Number | str]) -> list[_Number | str]:
|
|
585
|
+
"""Return one composition column's fraction cells in percent, exactly.
|
|
586
|
+
|
|
587
|
+
``Decimal(str(value)) * 100`` rather than ``value * 100``: the web app
|
|
588
|
+
multiplies in BigNumber, so ``0.07`` becomes ``7`` there and must not become
|
|
589
|
+
``7.000000000000001`` here. Neither side scales the decimal the CSV cell was
|
|
590
|
+
written with -- both round it through a binary float first (``Number(value)``
|
|
591
|
+
there, :func:`_composition_values` here, and :func:`_as_numbers` before it
|
|
592
|
+
where the whole column parses) -- so what this buys is that the two agree,
|
|
593
|
+
cell for cell. ``bal`` markers are the strings here, and pass through
|
|
594
|
+
untouched.
|
|
595
|
+
"""
|
|
596
|
+
return [
|
|
597
|
+
value if isinstance(value, str) else float(Decimal(str(value)) * 100) for value in values
|
|
598
|
+
]
|
|
599
|
+
|
|
553
600
|
|
|
554
601
|
def _composition_values(column: str, values: list[_Number | str]) -> list[_Number | str]:
|
|
555
602
|
"""Return one composition column as numbers, keeping its ``bal`` markers.
|
|
@@ -564,15 +611,75 @@ def _composition_values(column: str, values: list[_Number | str]) -> list[_Numbe
|
|
|
564
611
|
coerced.append(_BALANCE)
|
|
565
612
|
continue
|
|
566
613
|
try:
|
|
567
|
-
|
|
614
|
+
number = float(value)
|
|
568
615
|
except ValueError:
|
|
569
616
|
raise ValueError(
|
|
570
617
|
f"Composition column {column!r} holds {value!r}, which is neither a "
|
|
571
618
|
f"number nor the {_BALANCE!r} balance marker."
|
|
572
619
|
) from None
|
|
620
|
+
# ``float`` reads ``nan`` and ``inf`` and ``Decimal`` scales them, so
|
|
621
|
+
# unguarded the balance element's remainder of 100 comes out ``nan`` and
|
|
622
|
+
# the cell reaches the wire as JSON no strict parser accepts.
|
|
623
|
+
if not math.isfinite(number):
|
|
624
|
+
raise ValueError(
|
|
625
|
+
f"Composition column {column!r} holds {value!r}, which is not a finite "
|
|
626
|
+
"number. Drop the row, or fill the cell in."
|
|
627
|
+
)
|
|
628
|
+
coerced.append(number)
|
|
573
629
|
return coerced
|
|
574
630
|
|
|
575
631
|
|
|
632
|
+
def _resolve_composition_unit(
|
|
633
|
+
dataset_name: str, declared_unit: str | None, passed_unit: str
|
|
634
|
+
) -> tuple[str, str | None]:
|
|
635
|
+
"""Return the percent unit a composition carries, and the one its cells are in.
|
|
636
|
+
|
|
637
|
+
The second is the percent unit a fraction-labelled dataset's cells reach
|
|
638
|
+
once scaled, and ``None`` for every other label, including none -- which is
|
|
639
|
+
what tells the caller whether to scale them at all.
|
|
640
|
+
|
|
641
|
+
``passed_unit`` left at :data:`_COMPOSITION_UNIT_UNSET` resolves from the
|
|
642
|
+
dataset: a fraction label through its matching percent unit, a percent label
|
|
643
|
+
as it stands, ``mass_percent`` only where the dataset declares nothing at
|
|
644
|
+
all. A label this SDK cannot read is refused with the dataset blamed -- an
|
|
645
|
+
empty one included, since a dataset carrying an empty label has declared a
|
|
646
|
+
value -- where a unit passed is refused with the caller blamed.
|
|
647
|
+
"""
|
|
648
|
+
scaled_unit = _FRACTION_TO_PERCENT.get(declared_unit or "")
|
|
649
|
+
from_dataset = passed_unit is _COMPOSITION_UNIT_UNSET
|
|
650
|
+
composition_unit = passed_unit
|
|
651
|
+
if from_dataset:
|
|
652
|
+
# A fraction label resolves through its scaled percent unit; every
|
|
653
|
+
# other label stands and is checked below, as a passed one is.
|
|
654
|
+
declared = scaled_unit or declared_unit
|
|
655
|
+
composition_unit = "mass_percent" if declared is None else declared
|
|
656
|
+
if composition_unit not in _PERCENT_UNITS:
|
|
657
|
+
if from_dataset:
|
|
658
|
+
raise ValueError(
|
|
659
|
+
f"Dataset {dataset_name!r} declares composition_unit "
|
|
660
|
+
f"{declared_unit!r}, which is neither one of "
|
|
661
|
+
f"{sorted(_PERCENT_UNITS)} nor a fraction of one, so its cells cannot "
|
|
662
|
+
"be read. Pass the unit they are in: "
|
|
663
|
+
"composition(name, composition_unit=...)."
|
|
664
|
+
)
|
|
665
|
+
raise ValueError(
|
|
666
|
+
f"composition_unit must be one of {sorted(_PERCENT_UNITS)}; got "
|
|
667
|
+
f"{composition_unit!r}. The balance element is filled in as what is left "
|
|
668
|
+
"of 100, which is a wrong number in any other unit."
|
|
669
|
+
)
|
|
670
|
+
# Both units are now one of the two in ``_PERCENT_UNITS``, so they differ
|
|
671
|
+
# only in basis. Scaling the cells cannot change what they measure, so a
|
|
672
|
+
# caller naming the other basis has the dataset wrong.
|
|
673
|
+
if scaled_unit is not None and composition_unit != scaled_unit:
|
|
674
|
+
raise ValueError(
|
|
675
|
+
f"Dataset {dataset_name!r} is labelled {declared_unit!r}, so its "
|
|
676
|
+
f"cells are {scaled_unit!r} once expressed in percent; "
|
|
677
|
+
f"composition_unit={composition_unit!r} is the other basis. Pass "
|
|
678
|
+
f"{scaled_unit!r}, or leave composition_unit unset."
|
|
679
|
+
)
|
|
680
|
+
return composition_unit, scaled_unit
|
|
681
|
+
|
|
682
|
+
|
|
576
683
|
class DatasetSource:
|
|
577
684
|
"""One uploaded dataset, read once, minting variables from its columns.
|
|
578
685
|
|
|
@@ -598,6 +705,13 @@ class DatasetSource:
|
|
|
598
705
|
Its name, carried onto every variable minted here.
|
|
599
706
|
columns : Mapping[str, list[int | float | str]]
|
|
600
707
|
The parsed columns, by column name.
|
|
708
|
+
composition_unit : str | None
|
|
709
|
+
The unit the dataset itself declares its composition columns are in, as
|
|
710
|
+
:meth:`load` reads it off the dataset's metadata. :meth:`composition`
|
|
711
|
+
scales fraction cells on the strength of it, and resolves the
|
|
712
|
+
composition's own unit from it when none is passed. Omit it on a
|
|
713
|
+
hand-built source, which then behaves as a dataset carrying no unit
|
|
714
|
+
does.
|
|
601
715
|
"""
|
|
602
716
|
|
|
603
717
|
def __init__(
|
|
@@ -605,16 +719,29 @@ class DatasetSource:
|
|
|
605
719
|
dataset_uuid: str,
|
|
606
720
|
dataset_name: str,
|
|
607
721
|
columns: Mapping[str, list[_Number | str]],
|
|
722
|
+
*,
|
|
723
|
+
composition_unit: str | None = None,
|
|
608
724
|
) -> None:
|
|
609
725
|
self.uuid = dataset_uuid
|
|
610
726
|
self.name = dataset_name
|
|
611
727
|
self._columns = dict(columns)
|
|
728
|
+
self._composition_unit = composition_unit
|
|
612
729
|
|
|
613
730
|
@classmethod
|
|
614
731
|
def load(cls, client: "ICMD", dataset_uuid: str) -> Self:
|
|
615
732
|
"""Fetch one dataset and return the source minting its variables."""
|
|
616
733
|
body = client.workspace.dataset.get(dataset_uuid, inject_group=True)
|
|
617
|
-
|
|
734
|
+
# This server always sends ``metadata`` as a dict; the inner ``dataset``
|
|
735
|
+
# block is the one that is really null, on a dataset saved without a
|
|
736
|
+
# unit. The outer guard is insurance for a tenant on an older backend.
|
|
737
|
+
metadata = body.get("metadata") or {}
|
|
738
|
+
declared_unit = (metadata.get("dataset") or {}).get("composition_unit")
|
|
739
|
+
return cls(
|
|
740
|
+
dataset_uuid,
|
|
741
|
+
body["name"],
|
|
742
|
+
_parse_columns(body["csv_data"]),
|
|
743
|
+
composition_unit=declared_unit,
|
|
744
|
+
)
|
|
618
745
|
|
|
619
746
|
def _column(self, column: str) -> list[_Number | str]:
|
|
620
747
|
"""Return one column's values, naming the ones there are when it is absent."""
|
|
@@ -641,7 +768,7 @@ class DatasetSource:
|
|
|
641
768
|
self,
|
|
642
769
|
name: str,
|
|
643
770
|
*,
|
|
644
|
-
composition_unit: str =
|
|
771
|
+
composition_unit: str = _COMPOSITION_UNIT_UNSET,
|
|
645
772
|
columns: Sequence[str] | None = None,
|
|
646
773
|
) -> tuple["Composition", list["DatasetCompositionalVariable"]]:
|
|
647
774
|
"""Resolve this dataset's compositions into the pair the wire accepts.
|
|
@@ -653,16 +780,29 @@ class DatasetSource:
|
|
|
653
780
|
app does too: a composition that only references a dataset is read by
|
|
654
781
|
nothing, and the job runs with no composition columns at all.
|
|
655
782
|
|
|
783
|
+
The dataset's declared label decides the **cells**: a ``mass_fraction``
|
|
784
|
+
or ``mole_fraction`` label has every numeric cell scaled by 100, since
|
|
785
|
+
no fraction unit reaches the wire at all; a percent label leaves them as
|
|
786
|
+
they stand. ``composition_unit`` decides the **label** the composition
|
|
787
|
+
carries and nothing else -- passing one does not turn that scaling off.
|
|
788
|
+
Left unset it follows the dataset as well: a fraction label's matching
|
|
789
|
+
percent unit, a percent label as it stands, ``mass_percent`` for a
|
|
790
|
+
dataset declaring nothing, and an error for a label this SDK does not
|
|
791
|
+
know, which passing a unit overrides.
|
|
792
|
+
|
|
793
|
+
That unit is cross-checked only where this SDK converts: against a
|
|
794
|
+
fraction-labelled dataset the other basis is refused, since scaling
|
|
795
|
+
cannot change what the cells measure. A percent-labelled dataset takes
|
|
796
|
+
whatever is passed -- the label is free-form metadata the caller may be
|
|
797
|
+
correcting.
|
|
798
|
+
|
|
656
799
|
``columns`` defaults to every column named after an element. Each row
|
|
657
800
|
must mark exactly one balance element with ``bal``, which is what the
|
|
658
801
|
uploaded composition dataset carries.
|
|
659
802
|
"""
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
f"{composition_unit!r}. The balance element is filled in as what is left "
|
|
664
|
-
"of 100, which is a wrong number in any other unit."
|
|
665
|
-
)
|
|
803
|
+
composition_unit, scaled_unit = _resolve_composition_unit(
|
|
804
|
+
self.name, self._composition_unit, composition_unit
|
|
805
|
+
)
|
|
666
806
|
selected = (
|
|
667
807
|
list(columns)
|
|
668
808
|
if columns is not None
|
|
@@ -677,6 +817,10 @@ class DatasetSource:
|
|
|
677
817
|
composition_columns = {
|
|
678
818
|
column: _composition_values(column, self._column(column)) for column in selected
|
|
679
819
|
}
|
|
820
|
+
if scaled_unit is not None:
|
|
821
|
+
composition_columns = {
|
|
822
|
+
column: _as_percent(values) for column, values in composition_columns.items()
|
|
823
|
+
}
|
|
680
824
|
if not all(composition_columns.values()):
|
|
681
825
|
raise ValueError(
|
|
682
826
|
f"Dataset {self.name!r} has no rows, so there is no composition to "
|
icmd/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "3.3.
|
|
1
|
+
__version__ = "3.3.5"
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
icmd/__init__.py,sha256=BoE6tl0PaBgXqXmL_QjebFos5eOvS91Dj6tjJwolezk,2739
|
|
2
|
-
icmd/client.py,sha256=
|
|
3
|
-
icmd/compose.py,sha256=
|
|
2
|
+
icmd/client.py,sha256=8PXqAskxzkuUwz2wtHYFp9kDAgQlyOxDNbEyVSKayeY,22643
|
|
3
|
+
icmd/compose.py,sha256=V_B4EYuX01oJexnGGvhIwDUKgfx_Fyd0lPVvSRUoH8c,6931
|
|
4
4
|
icmd/links.py,sha256=I8v8_xhI6lbUwrMjIUcYYcqKUi2PIPm6zQ7mXYP9A5E,6827
|
|
5
5
|
icmd/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
icmd/types.py,sha256=
|
|
7
|
-
icmd/version.py,sha256=
|
|
6
|
+
icmd/types.py,sha256=LizuU96hF7VRjl4OlyF0oPcao1cKe795YXEsLW9fKI4,79496
|
|
7
|
+
icmd/version.py,sha256=QO4UWv5ZUgBpuBbLNKWKecIDPPTa5HfOdEYEe2q2Afo,22
|
|
8
8
|
icmd/_vendor/__init__.py,sha256=VFA4EQd2l76sxgkHPUpgzZavFF6kFdpq9v71AZsPIdQ,579
|
|
9
9
|
icmd/_vendor/assembler.py,sha256=U8VPlqN1605RJQ5qMVoNrL3uJ7NpUe8BVmD8q06IN5w,5282
|
|
10
10
|
icmd/apps/__init__.py,sha256=ak2ch1UU-qaiyOT1eCdRwqSI0Avhzyw5DLkw-U2ivQQ,659
|
|
11
11
|
icmd/apps/account.py,sha256=BWDO0Zysv0ygLVhbeIk82BjsvuDVb_vBdu6jqBPw3tc,14557
|
|
12
|
-
icmd/apps/agent.py,sha256=
|
|
12
|
+
icmd/apps/agent.py,sha256=MmWuH1sYqo-hOhNNuTwh930H_BFXs7oMP0QgyzaC0Iw,8387
|
|
13
13
|
icmd/apps/base.py,sha256=y4uvIiomCLia89UL2rV4oufLBUQaODZ8qwl-EwDtjhM,18979
|
|
14
14
|
icmd/apps/constants.py,sha256=5xoIwFNGOaAp2cHAlVDlbzJ7ObQHYXJgVGMBfNLCHzI,757
|
|
15
15
|
icmd/apps/documents.py,sha256=8_wFc7NLUFQMYZA5hqsciXniHECZEWS-egX_ni8F4LE,3720
|
|
16
|
-
icmd/apps/jobs.py,sha256=
|
|
16
|
+
icmd/apps/jobs.py,sha256=mUwV1wD4NlxOuYbK6a2HFZvZOtd1cXt-NQMZnmsYo1c,35282
|
|
17
17
|
icmd/apps/navigation.py,sha256=mbEJG33Z-Nha5txvMpL63sT-UqsgFgZ-NMhTU7L7gI0,3738
|
|
18
18
|
icmd/apps/results.py,sha256=lPs9iBoW49h0HTIYaUIau7QZVok7RdpSeawnxyA0ETU,7246
|
|
19
19
|
icmd/apps/studio.py,sha256=XN6gVCqkTzp7qH9I-UOhW7UJ2RfkGv60H-G7ulkR850,2419
|
|
20
20
|
icmd/apps/suite.py,sha256=04MIHUima_RGfnHyBPjyxcghcIMHhgDLWPcex6tIWls,2761
|
|
21
21
|
icmd/apps/types.py,sha256=jOzykkXWj9e_lk805_ABCC-sWBwIpJGN3HEb2AyQ9vw,1290
|
|
22
22
|
icmd/apps/workspace.py,sha256=BXCOLQo5BMu-okW2Ink8L_FPhU8r7OWy2jDsvilU3Oo,3055
|
|
23
|
-
icmd/cli/__init__.py,sha256=
|
|
24
|
-
icmd/cli/account.py,sha256=
|
|
23
|
+
icmd/cli/__init__.py,sha256=mBpAlW1BCKglPzdDffTc4uLdr_2Ri7qnVC2I-Q9FjEA,7395
|
|
24
|
+
icmd/cli/account.py,sha256=equTAGWR3NCCMYEPTw4IZKHEtBEdGD8F4Pp8cfiSNoU,13859
|
|
25
25
|
icmd/cli/auth.py,sha256=UFpEf-lLlkbphivgBfd9NxAEqHcCsPmEl-DXet-NStg,3317
|
|
26
26
|
icmd/cli/auth_status.py,sha256=6wkI3Q0c-t8DBOfuPaN_xXEUcL9HLZm1jX4lQ76X3Ag,13505
|
|
27
27
|
icmd/cli/client.py,sha256=3iToPSABFYWWUPn-uDVFAv9GoBWQXP04l2Id_aYgd64,2186
|
|
28
|
-
icmd/cli/commands.py,sha256=
|
|
28
|
+
icmd/cli/commands.py,sha256=JdEsIlsMvThWaMHbNmOWyESKWRbnpdTt_qxY56RHpxo,54852
|
|
29
29
|
icmd/cli/diagnostics.py,sha256=1fXgKmpUMA5U-_79AzZ0uSibFmDTK_CLCe91YhBnM8s,2747
|
|
30
30
|
icmd/cli/domain.py,sha256=MiGzdKwKaOEvMshsPPJTmdBm7pCTjGM98MOg9RhAL_k,874
|
|
31
31
|
icmd/cli/links.py,sha256=7CMhKUJ-qKQmk2I-KCdn6oF06cdCq0v5jQDGU7gEJ30,14039
|
|
@@ -36,15 +36,15 @@ icmd/core/advisory.py,sha256=ErXByq5UgpSGkjIiAjnmVCBLdY76REiU-Ilf_HoJ06Y,20783
|
|
|
36
36
|
icmd/core/auth.py,sha256=bG6eLbSkmyDWLyprx7NJMDwLTGsOIrsRdBZ3f6vz1vk,29480
|
|
37
37
|
icmd/core/constants.py,sha256=P3a01sDt5WmGYqkp5hu4UqWbMgfp50o6rBV3q9jU4yA,2339
|
|
38
38
|
icmd/core/contracts.py,sha256=0Dw7tq_IxQY5YC4CKtaubJ0Npa4Ae66-sm-0UuqH_no,9901
|
|
39
|
-
icmd/core/credentials.py,sha256=
|
|
39
|
+
icmd/core/credentials.py,sha256=39vyoFH1_MrzD9TpTh4FVPhWbrY5X35zGCS22KeTi_8,46835
|
|
40
40
|
icmd/core/display.py,sha256=RkgJhr2Bfw3-ZjGvE100ddD6oyAAUUAJfBAy6DvX5ik,13060
|
|
41
41
|
icmd/core/exceptions.py,sha256=9iFCw93yZ0Eh1x_6-acPU8pVtaUy7EhSz-h1JkYS4M4,6985
|
|
42
42
|
icmd/core/groups.py,sha256=OFbQce42DLDK0rDAdMbh8H7imNPAkBJMl7o7tbKQ4Iw,7211
|
|
43
43
|
icmd/core/model_classes.py,sha256=lOwTwZzmQ247fddFMjPVo43Z_VZfdPhhM0NI8LfRqlc,40047
|
|
44
|
-
icmd/core/session.py,sha256=
|
|
45
|
-
icmd_python-3.3.
|
|
46
|
-
icmd_python-3.3.
|
|
47
|
-
icmd_python-3.3.
|
|
48
|
-
icmd_python-3.3.
|
|
49
|
-
icmd_python-3.3.
|
|
50
|
-
icmd_python-3.3.
|
|
44
|
+
icmd/core/session.py,sha256=_hdA3Yo_dH1A_qRH1xpyEB8sBkUNQKcUywc54WsPZos,60894
|
|
45
|
+
icmd_python-3.3.5.dist-info/licenses/LICENSE,sha256=644lGvCmDFa5hQl6xdhaYk3o-1oWqPXAIiiQlvqeZdE,1028
|
|
46
|
+
icmd_python-3.3.5.dist-info/METADATA,sha256=YXJOLdFBGc9bfWGYDvIWMAKdFNyPH_WSvSWU32oaBxY,4919
|
|
47
|
+
icmd_python-3.3.5.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
48
|
+
icmd_python-3.3.5.dist-info/entry_points.txt,sha256=CFX537PlyyPQL3Q_3TKUFeiiDSkOAboqaLEy9AIEwVU,39
|
|
49
|
+
icmd_python-3.3.5.dist-info/top_level.txt,sha256=olzM6Z_QEwg7RrFVOnkgO0I2Ps2m7vhSqYQZYeiawcw,5
|
|
50
|
+
icmd_python-3.3.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|