icmd-python 1.5.7__py3-none-any.whl → 1.6.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/__init__.py +14 -2
- icmd/core/__init__.py +18 -1
- icmd/core/apps.py +168 -0
- icmd/core/client.py +205 -9
- icmd/core/constants.py +12 -0
- icmd/core/display.py +143 -0
- icmd/core/pagination.py +91 -0
- icmd/core/slim_packages.py +6 -2
- icmd/core/urls.py +111 -0
- icmd/version.py +1 -1
- {icmd_python-1.5.7.dist-info → icmd_python-1.6.5.dist-info}/METADATA +3 -4
- icmd_python-1.6.5.dist-info/RECORD +15 -0
- {icmd_python-1.5.7.dist-info → icmd_python-1.6.5.dist-info}/WHEEL +1 -1
- icmd_python-1.5.7.dist-info/RECORD +0 -10
- {icmd_python-1.5.7.dist-info → icmd_python-1.6.5.dist-info}/top_level.txt +0 -0
icmd/__init__.py
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
"""Python client app for ICMD® API."""
|
|
2
2
|
|
|
3
|
-
from .core import
|
|
3
|
+
from .core import (
|
|
4
|
+
ICMD,
|
|
5
|
+
ICMDAuthenticationError,
|
|
6
|
+
ICMDConnectionError,
|
|
7
|
+
ICMDRequestError,
|
|
8
|
+
ICMDValidationError,
|
|
9
|
+
)
|
|
4
10
|
|
|
5
|
-
__all__ = [
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ICMD",
|
|
13
|
+
"ICMDAuthenticationError",
|
|
14
|
+
"ICMDConnectionError",
|
|
15
|
+
"ICMDRequestError",
|
|
16
|
+
"ICMDValidationError",
|
|
17
|
+
]
|
icmd/core/__init__.py
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
"""Core ICMD client modules."""
|
|
2
2
|
|
|
3
|
-
from .
|
|
3
|
+
from . import urls
|
|
4
|
+
from .apps import Design, Fatigue, Resource, Workspace
|
|
5
|
+
from .client import (
|
|
6
|
+
ICMD,
|
|
7
|
+
ICMDAuthenticationError,
|
|
8
|
+
ICMDConnectionError,
|
|
9
|
+
ICMDRequestError,
|
|
10
|
+
ICMDValidationError,
|
|
11
|
+
)
|
|
4
12
|
from .credentials import CredentialManager
|
|
13
|
+
from .display import display_designs, display_jobs
|
|
5
14
|
|
|
6
15
|
__all__ = [
|
|
7
16
|
"ICMD",
|
|
8
17
|
"CredentialManager",
|
|
18
|
+
"Design",
|
|
19
|
+
"Fatigue",
|
|
9
20
|
"ICMDAuthenticationError",
|
|
10
21
|
"ICMDConnectionError",
|
|
22
|
+
"ICMDRequestError",
|
|
11
23
|
"ICMDValidationError",
|
|
24
|
+
"Resource",
|
|
25
|
+
"Workspace",
|
|
26
|
+
"display_designs",
|
|
27
|
+
"display_jobs",
|
|
28
|
+
"urls",
|
|
12
29
|
]
|
icmd/core/apps.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Composition-based resource access for ICMD API.
|
|
2
|
+
|
|
3
|
+
Provides a clean nested-attribute API::
|
|
4
|
+
|
|
5
|
+
icmd = ICMD("icmd.questek.com")
|
|
6
|
+
icmd.design.job.list(page=1, page_size=10)
|
|
7
|
+
icmd.workspace.dataset.get("uuid-123")
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from functools import partial
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from . import urls
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
|
|
20
|
+
from .client import ICMD
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Resource:
|
|
24
|
+
"""Generic API resource with list/get/add operations.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
client : ICMD
|
|
29
|
+
The ICMD client instance.
|
|
30
|
+
list_url : Callable[[], str]
|
|
31
|
+
Callable that returns the list endpoint.
|
|
32
|
+
item_url : Callable[[str], str] | None
|
|
33
|
+
Callable that accepts a UUID and returns the detail endpoint.
|
|
34
|
+
If None, 'get()' raises 'NotImplementedError'.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
client: ICMD,
|
|
40
|
+
list_url: Callable[[], str],
|
|
41
|
+
item_url: Callable[[str], str] | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
self._client = client
|
|
44
|
+
self._list_url = list_url
|
|
45
|
+
self._item_url = item_url
|
|
46
|
+
|
|
47
|
+
def list(self, **params: Any) -> dict | list[dict]:
|
|
48
|
+
"""List resources, forwarding query parameters."""
|
|
49
|
+
return self._client._request_json("get", self._list_url(), params=params or None)
|
|
50
|
+
|
|
51
|
+
def get(self, uuid: str) -> dict | list[dict]:
|
|
52
|
+
"""Get a single resource by UUID."""
|
|
53
|
+
if self._item_url is None:
|
|
54
|
+
raise NotImplementedError("This resource does not support get()")
|
|
55
|
+
return self._client._request_json("get", self._item_url(uuid))
|
|
56
|
+
|
|
57
|
+
def add(self, data: Any) -> dict | list[dict]:
|
|
58
|
+
"""Create a new resource."""
|
|
59
|
+
return self._client._request_json("post", self._list_url(), json=data)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ReadOnlyResource:
|
|
63
|
+
"""Resource that only supports get-by-UUID (e.g. results).
|
|
64
|
+
|
|
65
|
+
Parameters
|
|
66
|
+
----------
|
|
67
|
+
client : ICMD
|
|
68
|
+
The ICMD client instance.
|
|
69
|
+
item_url : Callable[[str], str]
|
|
70
|
+
Callable that accepts a UUID and returns the detail endpoint.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, client: ICMD, item_url: Callable[[str], str]) -> None:
|
|
74
|
+
self._client = client
|
|
75
|
+
self._item_url = item_url
|
|
76
|
+
|
|
77
|
+
def get(self, uuid: str) -> dict | list[dict]:
|
|
78
|
+
"""Get a single resource by UUID."""
|
|
79
|
+
return self._client._request_json("get", self._item_url(uuid))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DesignResource(Resource):
|
|
83
|
+
"""Resource for toolkit designs — auto-attaches 'owner_group' on add().
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
client : ICMD
|
|
88
|
+
The ICMD client instance.
|
|
89
|
+
list_url : Callable[[], str]
|
|
90
|
+
Callable that returns the list endpoint.
|
|
91
|
+
item_url : Callable[[str], str] | None
|
|
92
|
+
Callable that accepts a UUID and returns the detail endpoint.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def add(
|
|
96
|
+
self,
|
|
97
|
+
data: dict | None = None,
|
|
98
|
+
design_name: str | None = None,
|
|
99
|
+
share_with_group: bool = True,
|
|
100
|
+
) -> dict | list[dict]:
|
|
101
|
+
"""Create a new design, auto-attaching owner_group.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
data : dict | None
|
|
106
|
+
Payload for the design. If None, an empty dict is used.
|
|
107
|
+
design_name : str | None
|
|
108
|
+
Optional name for the design (sets 'name' key).
|
|
109
|
+
share_with_group : bool
|
|
110
|
+
Whether to share with the owner group (default True).
|
|
111
|
+
"""
|
|
112
|
+
payload: dict[str, Any] = dict(data) if data else {}
|
|
113
|
+
payload["owner_group"] = self._client.owner_group
|
|
114
|
+
if design_name is not None:
|
|
115
|
+
payload["name"] = design_name
|
|
116
|
+
payload["share_with_group"] = share_with_group
|
|
117
|
+
return self._client._request_json("post", self._list_url(), json=payload)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class _ToolkitApp:
|
|
121
|
+
"""Private base for toolkit apps (Design, Fatigue).
|
|
122
|
+
|
|
123
|
+
Sets up 'job', 'design', 'model', and 'result' sub-resources
|
|
124
|
+
for the given toolkit name.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, client: ICMD, toolkit: str) -> None:
|
|
128
|
+
self.job = Resource(
|
|
129
|
+
client,
|
|
130
|
+
partial(urls.jobs, toolkit=toolkit),
|
|
131
|
+
partial(urls.job, toolkit=toolkit),
|
|
132
|
+
)
|
|
133
|
+
self.design = DesignResource(
|
|
134
|
+
client,
|
|
135
|
+
partial(urls.designs, toolkit=toolkit),
|
|
136
|
+
partial(urls.design, toolkit=toolkit),
|
|
137
|
+
)
|
|
138
|
+
self.model = Resource(
|
|
139
|
+
client,
|
|
140
|
+
partial(urls.models, toolkit=toolkit),
|
|
141
|
+
item_url=None,
|
|
142
|
+
)
|
|
143
|
+
self.result = ReadOnlyResource(
|
|
144
|
+
client,
|
|
145
|
+
partial(urls.results, toolkit=toolkit),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Design(_ToolkitApp):
|
|
150
|
+
"""Design Alloy toolkit resources."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, client: ICMD) -> None:
|
|
153
|
+
super().__init__(client, "design")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Fatigue(_ToolkitApp):
|
|
157
|
+
"""Fatigue Simulation toolkit resources."""
|
|
158
|
+
|
|
159
|
+
def __init__(self, client: ICMD) -> None:
|
|
160
|
+
super().__init__(client, "fatigue")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Workspace:
|
|
164
|
+
"""Workspace resources (datasets, ML models)."""
|
|
165
|
+
|
|
166
|
+
def __init__(self, client: ICMD) -> None:
|
|
167
|
+
self.dataset = Resource(client, urls.datasets, urls.dataset)
|
|
168
|
+
self.ml_model = Resource(client, urls.ml_models, urls.ml_model)
|
icmd/core/client.py
CHANGED
|
@@ -5,7 +5,8 @@ import webbrowser
|
|
|
5
5
|
from datetime import UTC, datetime, timedelta
|
|
6
6
|
from getpass import getpass
|
|
7
7
|
from http import HTTPStatus
|
|
8
|
-
from typing import TYPE_CHECKING, Any, ClassVar
|
|
8
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
|
9
|
+
from urllib.parse import quote
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
from pathlib import Path
|
|
@@ -13,6 +14,8 @@ if TYPE_CHECKING:
|
|
|
13
14
|
import requests
|
|
14
15
|
from requests.adapters import HTTPAdapter
|
|
15
16
|
|
|
17
|
+
from .apps import Design, Fatigue, Workspace
|
|
18
|
+
from .constants import GROUPS
|
|
16
19
|
from .credentials import CredentialManager
|
|
17
20
|
from .slim_packages import ensure_slim_packages
|
|
18
21
|
|
|
@@ -29,6 +32,26 @@ class ICMDValidationError(Exception):
|
|
|
29
32
|
"""Raised when input validation fails."""
|
|
30
33
|
|
|
31
34
|
|
|
35
|
+
class ICMDRequestError(Exception):
|
|
36
|
+
"""Raised when an API request fails.
|
|
37
|
+
|
|
38
|
+
Attributes
|
|
39
|
+
----------
|
|
40
|
+
status_code: HTTP status code from the failed request.
|
|
41
|
+
response_text: Raw response body text for debugging.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
message: str,
|
|
47
|
+
status_code: int | None = None,
|
|
48
|
+
response_text: str | None = None,
|
|
49
|
+
):
|
|
50
|
+
self.status_code = status_code
|
|
51
|
+
self.response_text = response_text
|
|
52
|
+
super().__init__(message)
|
|
53
|
+
|
|
54
|
+
|
|
32
55
|
class ICMD:
|
|
33
56
|
"""ICMD client - supports multiple domains per process."""
|
|
34
57
|
|
|
@@ -108,6 +131,11 @@ class ICMD:
|
|
|
108
131
|
self._server_config_cache = None
|
|
109
132
|
|
|
110
133
|
self._credentials = CredentialManager()
|
|
134
|
+
self._owner_group: int | None = None
|
|
135
|
+
|
|
136
|
+
self.design: Design = Design(self)
|
|
137
|
+
self.fatigue: Fatigue = Fatigue(self)
|
|
138
|
+
self.workspace: Workspace = Workspace(self)
|
|
111
139
|
|
|
112
140
|
# Clear cached credentials if requested
|
|
113
141
|
if clear_cache:
|
|
@@ -117,7 +145,9 @@ class ICMD:
|
|
|
117
145
|
if developer_token:
|
|
118
146
|
self._session.headers["Authorization"] = f"Bearer {developer_token}"
|
|
119
147
|
self._developer_token = developer_token
|
|
120
|
-
#
|
|
148
|
+
# Don't fail ICMD initialization if slim package management fails
|
|
149
|
+
with contextlib.suppress(Exception):
|
|
150
|
+
ensure_slim_packages(self)
|
|
121
151
|
return
|
|
122
152
|
|
|
123
153
|
self._developer_token = None
|
|
@@ -613,11 +643,8 @@ class ICMD:
|
|
|
613
643
|
|
|
614
644
|
except KeyboardInterrupt:
|
|
615
645
|
# Default to first provider on Ctrl+C
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
saml_providers[0].get('name', saml_providers[0]['id'])
|
|
619
|
-
}..."
|
|
620
|
-
)
|
|
646
|
+
provider_name = saml_providers[0].get("name", saml_providers[0]["id"])
|
|
647
|
+
print(f"\nDefaulting to {provider_name}...")
|
|
621
648
|
return saml_providers[0]
|
|
622
649
|
|
|
623
650
|
except EOFError:
|
|
@@ -631,8 +658,6 @@ class ICMD:
|
|
|
631
658
|
def _saml_authentication(self) -> None:
|
|
632
659
|
"""Authenticate with Django SAML SSO."""
|
|
633
660
|
try:
|
|
634
|
-
from urllib.parse import quote
|
|
635
|
-
|
|
636
661
|
# Get SAML provider configuration
|
|
637
662
|
provider = self._get_saml_provider()
|
|
638
663
|
|
|
@@ -783,6 +808,27 @@ class ICMD:
|
|
|
783
808
|
"""DELETE request."""
|
|
784
809
|
return self.request("DELETE", endpoint, **kwargs)
|
|
785
810
|
|
|
811
|
+
def upload(self, endpoint: str, data: Any = None, **kwargs) -> requests.Response:
|
|
812
|
+
"""Upload data to an endpoint.
|
|
813
|
+
|
|
814
|
+
Alias for post() - provides more intuitive naming for data submission.
|
|
815
|
+
|
|
816
|
+
Parameters
|
|
817
|
+
----------
|
|
818
|
+
endpoint : str
|
|
819
|
+
API endpoint to upload to.
|
|
820
|
+
data : Any
|
|
821
|
+
Data to upload (will be JSON serialized).
|
|
822
|
+
**kwargs : Any
|
|
823
|
+
Additional arguments passed to the request method.
|
|
824
|
+
|
|
825
|
+
Returns
|
|
826
|
+
-------
|
|
827
|
+
requests.Response
|
|
828
|
+
Response from the API.
|
|
829
|
+
"""
|
|
830
|
+
return self.post(endpoint, data, **kwargs)
|
|
831
|
+
|
|
786
832
|
def logout(self, clear_saved_credentials: bool = False) -> None:
|
|
787
833
|
"""Logout and clear session.
|
|
788
834
|
|
|
@@ -811,3 +857,153 @@ class ICMD:
|
|
|
811
857
|
self._credentials.clear_refresh_token(self.domain)
|
|
812
858
|
|
|
813
859
|
print("Logged out successfully.")
|
|
860
|
+
|
|
861
|
+
# -------------------------------------------------------------------------
|
|
862
|
+
# High-level API helper methods
|
|
863
|
+
# -------------------------------------------------------------------------
|
|
864
|
+
|
|
865
|
+
@staticmethod
|
|
866
|
+
def _check_response(resp: requests.Response, operation: str | None = None) -> None:
|
|
867
|
+
"""Determine if there was an error obtaining response and if so raise exception.
|
|
868
|
+
|
|
869
|
+
Parameters
|
|
870
|
+
----------
|
|
871
|
+
resp : Response
|
|
872
|
+
The response object to check.
|
|
873
|
+
operation : str | None
|
|
874
|
+
Description of the operation being performed, for error context.
|
|
875
|
+
"""
|
|
876
|
+
if not (HTTPStatus.OK <= resp.status_code < HTTPStatus.MULTIPLE_CHOICES):
|
|
877
|
+
if operation:
|
|
878
|
+
message = f"Failed to {operation}: status {resp.status_code}. Message: {resp.text}"
|
|
879
|
+
else:
|
|
880
|
+
message = f"Request failed with status: {resp.status_code}. Message: {resp.text}"
|
|
881
|
+
raise ICMDRequestError(
|
|
882
|
+
message,
|
|
883
|
+
status_code=resp.status_code,
|
|
884
|
+
response_text=resp.text,
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
def _request_json(
|
|
888
|
+
self,
|
|
889
|
+
method: Literal["get", "post", "put", "delete"],
|
|
890
|
+
endpoint: str,
|
|
891
|
+
operation: str | None = None,
|
|
892
|
+
**kwargs: Any,
|
|
893
|
+
) -> dict | list[dict]:
|
|
894
|
+
"""Make a request and return the JSON response.
|
|
895
|
+
|
|
896
|
+
Parameters
|
|
897
|
+
----------
|
|
898
|
+
method : Literal["get", "post", "post", "delete"]
|
|
899
|
+
HTTP method ('get', 'post', 'put', 'delete').
|
|
900
|
+
endpoint : str
|
|
901
|
+
API endpoint.
|
|
902
|
+
operation : str | None
|
|
903
|
+
Description of the operation being performed, for error context.
|
|
904
|
+
**kwargs : Any
|
|
905
|
+
Additional arguments passed to the request method.
|
|
906
|
+
|
|
907
|
+
Returns
|
|
908
|
+
-------
|
|
909
|
+
dict | list[dict]
|
|
910
|
+
Parsed JSON response as a dict or list of dicts.
|
|
911
|
+
|
|
912
|
+
Raises
|
|
913
|
+
------
|
|
914
|
+
ICMDRequestError
|
|
915
|
+
If the request fails.
|
|
916
|
+
"""
|
|
917
|
+
resp = self.request(method.upper(), endpoint, **kwargs)
|
|
918
|
+
self._check_response(resp, operation)
|
|
919
|
+
return resp.json()
|
|
920
|
+
|
|
921
|
+
def _get_owner_group_id(self, group_name: str | None = None) -> int:
|
|
922
|
+
"""Return the group ID of a specific user group.
|
|
923
|
+
|
|
924
|
+
Parameters
|
|
925
|
+
----------
|
|
926
|
+
group_name : str | None
|
|
927
|
+
Name of the group for which to obtain the ID.
|
|
928
|
+
If not specified, retrieves the ID of the first group listed in the user account.
|
|
929
|
+
|
|
930
|
+
Returns
|
|
931
|
+
-------
|
|
932
|
+
int
|
|
933
|
+
The group ID.
|
|
934
|
+
|
|
935
|
+
Raises
|
|
936
|
+
------
|
|
937
|
+
ICMDRequestError
|
|
938
|
+
If no groups are found for the user or group data is malformed.
|
|
939
|
+
ValueError
|
|
940
|
+
If group_name is specified but no group with that name exists.
|
|
941
|
+
"""
|
|
942
|
+
groups = self._request_json("get", GROUPS, operation="fetch owner group")
|
|
943
|
+
if not isinstance(groups, list) or not groups:
|
|
944
|
+
raise ICMDRequestError("No groups found for user")
|
|
945
|
+
if not isinstance(groups[0], dict) or "id" not in groups[0]:
|
|
946
|
+
raise ICMDRequestError("Group data missing 'id' field")
|
|
947
|
+
if group_name is not None:
|
|
948
|
+
groups_with_group_name = [group for group in groups if group["name"] == group_name]
|
|
949
|
+
if len(groups_with_group_name) == 0:
|
|
950
|
+
raise ValueError(f"No group found with name '{group_name}'") from None
|
|
951
|
+
return groups_with_group_name[0]["id"]
|
|
952
|
+
return groups[0]["id"]
|
|
953
|
+
|
|
954
|
+
def set_owner_group(self, group_name: str | None = None) -> None:
|
|
955
|
+
"""Set the owner group for subsequent API operations.
|
|
956
|
+
|
|
957
|
+
Parameters
|
|
958
|
+
----------
|
|
959
|
+
group_name : str | None
|
|
960
|
+
Name of the owner group. If not specified, uses the first group
|
|
961
|
+
listed in the user account.
|
|
962
|
+
|
|
963
|
+
Raises
|
|
964
|
+
------
|
|
965
|
+
ICMDRequestError
|
|
966
|
+
If no groups are found for the user or group data is malformed.
|
|
967
|
+
ValueError
|
|
968
|
+
If group_name is specified but no group with that name exists.
|
|
969
|
+
"""
|
|
970
|
+
self._owner_group = self._get_owner_group_id(group_name)
|
|
971
|
+
|
|
972
|
+
def get_owner_group_names(self) -> list[str]:
|
|
973
|
+
"""Get all available owner group names for the current user.
|
|
974
|
+
|
|
975
|
+
Returns
|
|
976
|
+
-------
|
|
977
|
+
list[str]
|
|
978
|
+
List of group names available to the user.
|
|
979
|
+
|
|
980
|
+
Raises
|
|
981
|
+
------
|
|
982
|
+
ICMDRequestError
|
|
983
|
+
If no groups are found for the user or group data is malformed.
|
|
984
|
+
"""
|
|
985
|
+
groups = self._request_json("get", GROUPS, operation="fetch owner groups")
|
|
986
|
+
if not isinstance(groups, list) or not groups:
|
|
987
|
+
raise ICMDRequestError("No groups found for user")
|
|
988
|
+
return [group["name"] for group in groups]
|
|
989
|
+
|
|
990
|
+
@property
|
|
991
|
+
def owner_group(self) -> int:
|
|
992
|
+
"""Return the owner group ID of the current user.
|
|
993
|
+
|
|
994
|
+
The owner group is fetched lazily on first access and cached
|
|
995
|
+
for subsequent calls.
|
|
996
|
+
|
|
997
|
+
Returns
|
|
998
|
+
-------
|
|
999
|
+
int
|
|
1000
|
+
The owner group ID.
|
|
1001
|
+
|
|
1002
|
+
Raises
|
|
1003
|
+
------
|
|
1004
|
+
ICMDRequestError
|
|
1005
|
+
If fetching the owner group fails.
|
|
1006
|
+
"""
|
|
1007
|
+
if self._owner_group is None:
|
|
1008
|
+
self._owner_group = self._get_owner_group_id()
|
|
1009
|
+
return self._owner_group
|
icmd/core/constants.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""API endpoint constants for ICMD client."""
|
|
2
|
+
|
|
3
|
+
# API endpoint constants
|
|
4
|
+
MACHINE_LEARNING_MODELS: str = "workspace/mlmodels"
|
|
5
|
+
DATASETS: str = "workspace/dataset"
|
|
6
|
+
DESIGN_TOOLKIT: str = "design_alloy"
|
|
7
|
+
FATIGUE_TOOLKIT: str = "simulation_fatigue"
|
|
8
|
+
GROUPS: str = "account/groups"
|
|
9
|
+
QT_MODEL: str = "qt_model"
|
|
10
|
+
JOB: str = "job"
|
|
11
|
+
RESULTS: str = "results"
|
|
12
|
+
DESIGN: str = "design"
|
icmd/core/display.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Display utilities for ICMD data.
|
|
2
|
+
|
|
3
|
+
This module provides interactive display functions for designs and jobs.
|
|
4
|
+
|
|
5
|
+
Example usage:
|
|
6
|
+
from icmd import ICMD
|
|
7
|
+
from icmd.core.display import display_designs, display_jobs
|
|
8
|
+
|
|
9
|
+
client = ICMD("icmd.questek.com")
|
|
10
|
+
display_designs(client, toolkit="design")
|
|
11
|
+
display_jobs(client, toolkit="fatigue")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING, Literal
|
|
17
|
+
|
|
18
|
+
from . import urls
|
|
19
|
+
from .pagination import handle_pagination_input, print_pagination_info, print_table
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .client import ICMD
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def display_designs(
|
|
26
|
+
client: ICMD,
|
|
27
|
+
toolkit: Literal["design", "fatigue"] = "design",
|
|
28
|
+
page_number: int = 1,
|
|
29
|
+
page_size: int = 10,
|
|
30
|
+
interactive: bool = True,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Display designs in a paginated table.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
client : ICMD
|
|
37
|
+
Authenticated ICMD client instance.
|
|
38
|
+
toolkit : Literal["design", "fatigue"]
|
|
39
|
+
Toolkit from which to display designs. Default is "design".
|
|
40
|
+
page_number : int
|
|
41
|
+
Starting page number. Default is 1.
|
|
42
|
+
page_size : int
|
|
43
|
+
Number of designs per page. Default is 10.
|
|
44
|
+
interactive : bool
|
|
45
|
+
If True, allows navigation between pages. Default is True.
|
|
46
|
+
"""
|
|
47
|
+
current_page = page_number
|
|
48
|
+
total_items = client.get(urls.designs(toolkit)).json()["count"]
|
|
49
|
+
total_pages = (total_items + page_size - 1) // page_size
|
|
50
|
+
toolkit_name = urls._toolkit(toolkit).replace("_", " ").title()
|
|
51
|
+
|
|
52
|
+
while True:
|
|
53
|
+
response = client.get(
|
|
54
|
+
urls.designs(toolkit),
|
|
55
|
+
params={"page": int(current_page), "page_size": int(page_size)},
|
|
56
|
+
).json()
|
|
57
|
+
designs = response["data"]
|
|
58
|
+
|
|
59
|
+
rows = [
|
|
60
|
+
[
|
|
61
|
+
d.get("uuid", "N/A"),
|
|
62
|
+
d.get("name", "Unnamed"),
|
|
63
|
+
d.get("last_updated", "N/A")[:19] if d.get("last_updated") else "N/A",
|
|
64
|
+
str(d.get("jobs_count", "N/A")),
|
|
65
|
+
]
|
|
66
|
+
for d in designs
|
|
67
|
+
]
|
|
68
|
+
print_table(
|
|
69
|
+
["UUID", "Name", "Last Updated", "Number of Jobs"],
|
|
70
|
+
rows,
|
|
71
|
+
[36, 30, 20, 15],
|
|
72
|
+
toolkit_name,
|
|
73
|
+
"designs",
|
|
74
|
+
)
|
|
75
|
+
print_pagination_info(current_page, page_size, total_items)
|
|
76
|
+
|
|
77
|
+
if not interactive:
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
current_page = handle_pagination_input(current_page, total_pages)
|
|
81
|
+
if current_page is None:
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def display_jobs(
|
|
86
|
+
client: ICMD,
|
|
87
|
+
toolkit: Literal["design", "fatigue"] = "design",
|
|
88
|
+
page_number: int = 1,
|
|
89
|
+
page_size: int = 10,
|
|
90
|
+
interactive: bool = True,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Display jobs in a paginated table.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
client : ICMD
|
|
97
|
+
Authenticated ICMD client instance.
|
|
98
|
+
toolkit : Literal["design", "fatigue"]
|
|
99
|
+
Toolkit from which to display jobs. Default is "design".
|
|
100
|
+
page_number : int
|
|
101
|
+
Starting page number. Default is 1.
|
|
102
|
+
page_size : int
|
|
103
|
+
Number of jobs per page. Default is 10.
|
|
104
|
+
interactive : bool
|
|
105
|
+
If True, allows navigation between pages. Default is True.
|
|
106
|
+
"""
|
|
107
|
+
current_page = page_number
|
|
108
|
+
total_items = client.get(urls.jobs(toolkit)).json()["count"]
|
|
109
|
+
total_pages = (total_items + page_size - 1) // page_size
|
|
110
|
+
toolkit_name = urls._toolkit(toolkit).replace("_", " ").title()
|
|
111
|
+
|
|
112
|
+
while True:
|
|
113
|
+
response = client.get(
|
|
114
|
+
urls.jobs(toolkit),
|
|
115
|
+
params={"page": int(current_page), "page_size": int(page_size)},
|
|
116
|
+
).json()
|
|
117
|
+
jobs = response["data"]
|
|
118
|
+
|
|
119
|
+
rows = [
|
|
120
|
+
[
|
|
121
|
+
j.get("uuid", "N/A"),
|
|
122
|
+
j.get("design_name", j.get("design", {}).get("name", "N/A"))[:30],
|
|
123
|
+
j.get("job_status", "N/A"),
|
|
124
|
+
j.get("created_at", "N/A")[:19] if j.get("created_at") else "N/A",
|
|
125
|
+
]
|
|
126
|
+
for j in jobs
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
print_table(
|
|
130
|
+
["UUID", "Design", "Status", "Created"],
|
|
131
|
+
rows,
|
|
132
|
+
[36, 30, 15, 20],
|
|
133
|
+
toolkit_name,
|
|
134
|
+
"jobs",
|
|
135
|
+
)
|
|
136
|
+
print_pagination_info(current_page, page_size, total_items)
|
|
137
|
+
|
|
138
|
+
if not interactive:
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
current_page = handle_pagination_input(current_page, total_pages)
|
|
142
|
+
if current_page is None:
|
|
143
|
+
break
|
icmd/core/pagination.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Pagination helpers for ICMD client."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def print_table(
|
|
5
|
+
headers: list[str],
|
|
6
|
+
rows: list[list[str]],
|
|
7
|
+
col_widths: list[int],
|
|
8
|
+
toolkit_name: str,
|
|
9
|
+
list_type: str,
|
|
10
|
+
) -> None:
|
|
11
|
+
"""Print a formatted table to the console.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
headers : list[str]
|
|
16
|
+
Column headers.
|
|
17
|
+
rows : list[list[str]]
|
|
18
|
+
Table rows.
|
|
19
|
+
col_widths : list[int]
|
|
20
|
+
Width for each column.
|
|
21
|
+
toolkit_name : str
|
|
22
|
+
Toolkit name (e.g., "Fatigue Simulation").
|
|
23
|
+
list_type : str
|
|
24
|
+
Type of items being listed (e.g., "jobs" or "designs"
|
|
25
|
+
"""
|
|
26
|
+
border_length = 80
|
|
27
|
+
print(f"\n{'=' * border_length}")
|
|
28
|
+
print(f"{list_type.title()} - {toolkit_name.title()} Toolkit")
|
|
29
|
+
print("=" * border_length)
|
|
30
|
+
header_row = " | ".join(h.ljust(w) for h, w in zip(headers, col_widths, strict=False))
|
|
31
|
+
separator = "-+-".join("-" * w for w in col_widths)
|
|
32
|
+
print(header_row)
|
|
33
|
+
print(separator)
|
|
34
|
+
|
|
35
|
+
for row in rows:
|
|
36
|
+
print(" | ".join(str(cell).ljust(w)[:w] for cell, w in zip(row, col_widths, strict=False)))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def print_pagination_info(page_number: int, page_size: int, total_items: int) -> None:
|
|
40
|
+
"""Print pagination information.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
page_number : int
|
|
45
|
+
Current page number.
|
|
46
|
+
page_size : int
|
|
47
|
+
Number of items per page.
|
|
48
|
+
total_items : int
|
|
49
|
+
Total number of items.
|
|
50
|
+
"""
|
|
51
|
+
total_pages = (total_items + page_size - 1) // page_size
|
|
52
|
+
start_item = (page_number - 1) * page_size + 1
|
|
53
|
+
end_item = min(page_number * page_size, total_items)
|
|
54
|
+
|
|
55
|
+
print(
|
|
56
|
+
f"\nPage {page_number} of {total_pages} | Items {start_item}-{end_item} of {total_items}"
|
|
57
|
+
)
|
|
58
|
+
print("Navigation: [n]ext, [p]revious, [g]o to page, [q]uit")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def handle_pagination_input(current_page: int, total_pages: int) -> int | None:
|
|
62
|
+
"""Handle pagination navigation input.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
int | None
|
|
67
|
+
The new page number, or None to exit.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
choice = input("\nEnter choice: ").strip().lower()
|
|
71
|
+
except (EOFError, KeyboardInterrupt):
|
|
72
|
+
print("\nExiting...")
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
if choice == "q":
|
|
76
|
+
return None
|
|
77
|
+
if choice == "n" and current_page < total_pages:
|
|
78
|
+
return current_page + 1
|
|
79
|
+
if choice == "p" and current_page > 1:
|
|
80
|
+
return current_page - 1
|
|
81
|
+
if choice == "g":
|
|
82
|
+
try:
|
|
83
|
+
target = int(input("Go to page: ").strip())
|
|
84
|
+
if 1 <= target <= total_pages:
|
|
85
|
+
return target
|
|
86
|
+
print(f"Invalid page. Please enter 1-{total_pages}.")
|
|
87
|
+
except ValueError:
|
|
88
|
+
print("Invalid input. Please enter a number.")
|
|
89
|
+
elif choice in ("n", "p"):
|
|
90
|
+
print("Already at the first/last page.")
|
|
91
|
+
return current_page
|
icmd/core/slim_packages.py
CHANGED
|
@@ -321,7 +321,7 @@ def _format_version_info(version_data: dict | None) -> str:
|
|
|
321
321
|
return "unknown"
|
|
322
322
|
|
|
323
323
|
parts = []
|
|
324
|
-
for repo in ["qt-models", "qt-calphad", "qt-base", "qt-optimizer"]:
|
|
324
|
+
for repo in ["qt-models", "qt-calphad", "qt-base", "qt-optimizer", "qt-designer"]:
|
|
325
325
|
if repo in version_data and version_data[repo] != "unknown":
|
|
326
326
|
version = version_data[repo]
|
|
327
327
|
parts.append(f"{repo}@{version}")
|
|
@@ -354,7 +354,11 @@ def ensure_slim_packages(icmd_client: "ICMD") -> None:
|
|
|
354
354
|
# Get remote version and download URL from backend (single API call)
|
|
355
355
|
remote_version, download_url = get_slim_packages_info(icmd_client)
|
|
356
356
|
if remote_version is None:
|
|
357
|
-
#
|
|
357
|
+
# Backend unavailable - print warning and try cached version
|
|
358
|
+
print(
|
|
359
|
+
"⚠ Warning: Could not check slim packages version from backend. "
|
|
360
|
+
"Using cached version if available."
|
|
361
|
+
)
|
|
358
362
|
# Try to inject cached version for this server
|
|
359
363
|
server_dir = QTPY_SLIM_BASE / get_server_slug(icmd_client.domain)
|
|
360
364
|
if server_dir.exists():
|
icmd/core/urls.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""URL builders for ICMD API endpoints.
|
|
2
|
+
|
|
3
|
+
This module provides helper functions to construct API endpoint URLs,
|
|
4
|
+
so users don't need to know the string patterns.
|
|
5
|
+
|
|
6
|
+
Example usage:
|
|
7
|
+
from icmd import ICMD
|
|
8
|
+
from icmd.core import urls
|
|
9
|
+
|
|
10
|
+
client = ICMD("icmd.questek.com")
|
|
11
|
+
dataset = client.get(urls.dataset("uuid-123")).json()
|
|
12
|
+
client.upload(urls.datasets(), data=payload)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .constants import (
|
|
16
|
+
DATASETS,
|
|
17
|
+
DESIGN,
|
|
18
|
+
DESIGN_TOOLKIT,
|
|
19
|
+
FATIGUE_TOOLKIT,
|
|
20
|
+
GROUPS,
|
|
21
|
+
JOB,
|
|
22
|
+
MACHINE_LEARNING_MODELS,
|
|
23
|
+
QT_MODEL,
|
|
24
|
+
RESULTS,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _toolkit(toolkit: str = "design") -> str:
|
|
29
|
+
"""Return toolkit endpoint.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
toolkit : str
|
|
34
|
+
Either "design" for Design Alloy or "fatigue" for Fatigue Simulation.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
str
|
|
39
|
+
The API endpoint string for the toolkit.
|
|
40
|
+
|
|
41
|
+
Raises
|
|
42
|
+
------
|
|
43
|
+
ValueError
|
|
44
|
+
If toolkit is not "design" or "fatigue".
|
|
45
|
+
"""
|
|
46
|
+
if toolkit == "design":
|
|
47
|
+
return DESIGN_TOOLKIT
|
|
48
|
+
if toolkit == "fatigue":
|
|
49
|
+
return FATIGUE_TOOLKIT
|
|
50
|
+
raise ValueError(f"Unknown toolkit: {toolkit}. Must be 'design' or 'fatigue'.")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Dataset URLs
|
|
54
|
+
def datasets() -> str:
|
|
55
|
+
"""Return the datasets endpoint."""
|
|
56
|
+
return DATASETS
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def dataset(uuid: str) -> str:
|
|
60
|
+
"""Return the endpoint for a specific dataset."""
|
|
61
|
+
return f"{DATASETS}/{uuid}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Design URLs
|
|
65
|
+
def designs(toolkit: str = "design") -> str:
|
|
66
|
+
"""Return the designs endpoint for a toolkit."""
|
|
67
|
+
return f"{_toolkit(toolkit)}/{DESIGN}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def design(uuid: str, toolkit: str = "design") -> str:
|
|
71
|
+
"""Return the endpoint for a specific design."""
|
|
72
|
+
return f"{_toolkit(toolkit)}/{DESIGN}/{uuid}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Job URLs
|
|
76
|
+
def jobs(toolkit: str = "design") -> str:
|
|
77
|
+
"""Return the jobs endpoint for a toolkit."""
|
|
78
|
+
return f"{_toolkit(toolkit)}/{JOB}"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def job(uuid: str, toolkit: str = "design") -> str:
|
|
82
|
+
"""Return the endpoint for a specific job."""
|
|
83
|
+
return f"{_toolkit(toolkit)}/{JOB}/{uuid}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Results URLs
|
|
87
|
+
def results(uuid: str, toolkit: str = "design") -> str:
|
|
88
|
+
"""Return the endpoint for specific results."""
|
|
89
|
+
return f"{_toolkit(toolkit)}/{RESULTS}/{uuid}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Model URLs
|
|
93
|
+
def models(toolkit: str = "design") -> str:
|
|
94
|
+
"""Return the models endpoint for a toolkit."""
|
|
95
|
+
return f"{_toolkit(toolkit)}/{QT_MODEL}"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def ml_models() -> str:
|
|
99
|
+
"""Return the ML models endpoint."""
|
|
100
|
+
return MACHINE_LEARNING_MODELS
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def ml_model(uuid: str) -> str:
|
|
104
|
+
"""Return the endpoint for a specific ML model."""
|
|
105
|
+
return f"{MACHINE_LEARNING_MODELS}/{uuid}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Groups
|
|
109
|
+
def groups() -> str:
|
|
110
|
+
"""Return the groups endpoint."""
|
|
111
|
+
return GROUPS
|
icmd/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.5
|
|
1
|
+
__version__ = "1.6.5"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: icmd-python
|
|
3
|
-
Version: 1.5
|
|
3
|
+
Version: 1.6.5
|
|
4
4
|
Summary: Python client app for ICMD®
|
|
5
5
|
Author: QuesTek Innovations LLC
|
|
6
6
|
Requires-Python: >=3.12
|
|
@@ -13,10 +13,9 @@ Requires-Dist: requests
|
|
|
13
13
|
Requires-Dist: scipy
|
|
14
14
|
Requires-Dist: xarray
|
|
15
15
|
Provides-Extra: dev
|
|
16
|
-
Requires-Dist:
|
|
16
|
+
Requires-Dist: pyright; extra == "dev"
|
|
17
17
|
Requires-Dist: pytest; extra == "dev"
|
|
18
18
|
Requires-Dist: pytest-xdist; extra == "dev"
|
|
19
19
|
Requires-Dist: ruff; extra == "dev"
|
|
20
|
-
Requires-Dist: sphinx; extra == "dev"
|
|
21
|
-
Requires-Dist: sphinx-rtd-theme; extra == "dev"
|
|
22
20
|
Requires-Dist: pre-commit; extra == "dev"
|
|
21
|
+
Requires-Dist: plotly; extra == "dev"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
icmd/__init__.py,sha256=qRlXs1lxfZ7zwpE8OhUYFUatAt6kztvCHK8Mc8qhQIk,310
|
|
2
|
+
icmd/version.py,sha256=LEtHSENWZLwKaCJasxjOIQSt5Y_OahmKZYacXCmyQtI,22
|
|
3
|
+
icmd/core/__init__.py,sha256=p92vCWcT0SMFNZRrIBz57NreZyNEKTJRS-tE3QbbBvI,614
|
|
4
|
+
icmd/core/apps.py,sha256=iWbK63Nv6MlyYOxIcu_HvenGI6nokzoUTCxU6xouxJk,5086
|
|
5
|
+
icmd/core/client.py,sha256=Ag7Se48Z6r28hcFmhA0DtQbesn0_uWcRiZtfoAzGxxo,38460
|
|
6
|
+
icmd/core/constants.py,sha256=DG8u7PKLiihCQOR_uftMmgZh5CLh3PRqVl2KF4atdoY,364
|
|
7
|
+
icmd/core/credentials.py,sha256=PwLstCLl11KlusLVE7oBkU1rEgvb9u0HpWUhfapfBQY,6690
|
|
8
|
+
icmd/core/display.py,sha256=F89Ihcr4mtEZJkV3_J7LkOPbF-vsTQIvJZ1vSuZOTtY,4265
|
|
9
|
+
icmd/core/pagination.py,sha256=Xv9vTY02FFIGBvndQsXoz1ruhlu148vtCWWFbL_5fwI,2735
|
|
10
|
+
icmd/core/slim_packages.py,sha256=MO4cMF5yGrYTfqUQnNWt16ajxneY-pzDJTejTNwC0JE,16081
|
|
11
|
+
icmd/core/urls.py,sha256=-K7rsGKLOqpcVKJhQnZJJArJWEd7y_uw-vwaJyryVXc,2631
|
|
12
|
+
icmd_python-1.6.5.dist-info/METADATA,sha256=GswTuSpG6uFnhvx1V6RBSlfI2pYsSshisCOiwf5QlnY,591
|
|
13
|
+
icmd_python-1.6.5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
icmd_python-1.6.5.dist-info/top_level.txt,sha256=olzM6Z_QEwg7RrFVOnkgO0I2Ps2m7vhSqYQZYeiawcw,5
|
|
15
|
+
icmd_python-1.6.5.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
icmd/__init__.py,sha256=Y3zT6WXeWkIuMsLjwXs1lTjiGSj-dzLvetVSmRu_u2w,84
|
|
2
|
-
icmd/version.py,sha256=oF7Y8496FUgsdoGzNFWSpl1UtgtgO43P0Tct09Fxbx0,22
|
|
3
|
-
icmd/core/__init__.py,sha256=4-zYFBsm52YklOAfbd0EF6wqAQS2bCIDeRAQFJRX9Lw,305
|
|
4
|
-
icmd/core/client.py,sha256=_5cQsWCraJsZoQH6-iTdnS6LwbA2cPvPb4FTQg4Sx0o,31766
|
|
5
|
-
icmd/core/credentials.py,sha256=PwLstCLl11KlusLVE7oBkU1rEgvb9u0HpWUhfapfBQY,6690
|
|
6
|
-
icmd/core/slim_packages.py,sha256=EdZTv8kMuqHKuHeNCbzdREkgvJZr_JnWcR1ytfbCYXU,15896
|
|
7
|
-
icmd_python-1.5.7.dist-info/METADATA,sha256=jj6l1A_bJXTuz09PjZ73hjG95zoEd0HnmC1gFJE1h2Q,639
|
|
8
|
-
icmd_python-1.5.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
icmd_python-1.5.7.dist-info/top_level.txt,sha256=olzM6Z_QEwg7RrFVOnkgO0I2Ps2m7vhSqYQZYeiawcw,5
|
|
10
|
-
icmd_python-1.5.7.dist-info/RECORD,,
|
|
File without changes
|