gmicloud 0.1.4__py3-none-any.whl → 0.1.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.
- gmicloud/__init__.py +12 -1
- gmicloud/_internal/_client/_artifact_client.py +126 -56
- gmicloud/_internal/_client/_http_client.py +5 -1
- gmicloud/_internal/_client/_iam_client.py +107 -42
- gmicloud/_internal/_client/_task_client.py +75 -30
- gmicloud/_internal/_enums.py +8 -0
- gmicloud/_internal/_manager/_artifact_manager.py +17 -5
- gmicloud/_internal/_manager/_iam_manager.py +36 -0
- gmicloud/_internal/_manager/_task_manager.py +19 -12
- gmicloud/_internal/_models.py +103 -10
- gmicloud/client.py +26 -5
- gmicloud/tests/test_artifacts.py +14 -15
- gmicloud/tests/test_tasks.py +1 -1
- {gmicloud-0.1.4.dist-info → gmicloud-0.1.5.dist-info}/METADATA +12 -16
- gmicloud-0.1.5.dist-info/RECORD +27 -0
- {gmicloud-0.1.4.dist-info → gmicloud-0.1.5.dist-info}/WHEEL +1 -1
- gmicloud-0.1.4.dist-info/RECORD +0 -26
- {gmicloud-0.1.4.dist-info → gmicloud-0.1.5.dist-info}/top_level.txt +0 -0
gmicloud/__init__.py
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
|
1
4
|
from ._internal._models import (
|
2
5
|
Artifact,
|
3
6
|
ArtifactData,
|
@@ -16,7 +19,8 @@ from ._internal._models import (
|
|
16
19
|
)
|
17
20
|
from ._internal._enums import (
|
18
21
|
BuildStatus,
|
19
|
-
TaskEndpointStatus
|
22
|
+
TaskEndpointStatus,
|
23
|
+
TaskStatus
|
20
24
|
)
|
21
25
|
from .client import Client
|
22
26
|
|
@@ -39,3 +43,10 @@ __all__ = [
|
|
39
43
|
"BuildStatus",
|
40
44
|
"TaskEndpointStatus",
|
41
45
|
]
|
46
|
+
|
47
|
+
# Configure logging
|
48
|
+
log_level = os.getenv("GMI_CLOUD_LOG_LEVEL", "INFO").upper()
|
49
|
+
logging.basicConfig(
|
50
|
+
level=log_level,
|
51
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
52
|
+
)
|
@@ -1,4 +1,6 @@
|
|
1
1
|
from typing import List
|
2
|
+
import logging
|
3
|
+
from requests.exceptions import RequestException
|
2
4
|
|
3
5
|
from ._http_client import HTTPClient
|
4
6
|
from ._iam_client import IAMClient
|
@@ -6,6 +8,8 @@ from ._decorator import handle_refresh_token
|
|
6
8
|
from .._models import *
|
7
9
|
from .._config import ARTIFACT_SERVICE_BASE_URL
|
8
10
|
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
9
13
|
|
10
14
|
class ArtifactClient:
|
11
15
|
"""
|
@@ -24,119 +28,185 @@ class ArtifactClient:
|
|
24
28
|
self.iam_client = iam_client
|
25
29
|
|
26
30
|
@handle_refresh_token
|
27
|
-
def get_artifact(self, artifact_id: str) -> Artifact:
|
31
|
+
def get_artifact(self, artifact_id: str) -> Optional[Artifact]:
|
28
32
|
"""
|
29
33
|
Fetches an artifact by its ID.
|
30
34
|
|
31
35
|
:param artifact_id: The ID of the artifact to fetch.
|
32
|
-
:return: The Artifact object.
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
36
|
+
:return: The Artifact object or None if an error occurs.
|
37
|
+
"""
|
38
|
+
try:
|
39
|
+
response = self.client.get(
|
40
|
+
"/get_artifact",
|
41
|
+
self.iam_client.get_custom_headers(),
|
42
|
+
{"artifact_id": artifact_id}
|
43
|
+
)
|
44
|
+
return Artifact.model_validate(response) if response else None
|
45
|
+
except (RequestException, ValueError) as e:
|
46
|
+
logger.error(f"Failed to fetch artifact {artifact_id}: {e}")
|
47
|
+
return None
|
38
48
|
|
39
49
|
@handle_refresh_token
|
40
50
|
def get_all_artifacts(self) -> List[Artifact]:
|
41
51
|
"""
|
42
52
|
Fetches all artifacts.
|
43
53
|
|
44
|
-
:return: A list of Artifact objects.
|
45
|
-
:rtype: List[Artifact]
|
54
|
+
:return: A list of Artifact objects. If an error occurs, returns an empty list.
|
46
55
|
"""
|
47
|
-
|
48
|
-
|
56
|
+
try:
|
57
|
+
response = self.client.get("/get_all_artifacts", self.iam_client.get_custom_headers())
|
58
|
+
if not response:
|
59
|
+
logger.error("Empty response from /get_all_artifacts")
|
60
|
+
return []
|
61
|
+
return [Artifact.model_validate(item) for item in response]
|
62
|
+
except (RequestException, ValueError) as e:
|
63
|
+
logger.error(f"Failed to fetch all artifacts: {e}")
|
49
64
|
return []
|
50
|
-
return [Artifact.model_validate(item) for item in result]
|
51
65
|
|
52
66
|
@handle_refresh_token
|
53
|
-
def create_artifact(self, request: CreateArtifactRequest) -> CreateArtifactResponse:
|
67
|
+
def create_artifact(self, request: CreateArtifactRequest) -> Optional[CreateArtifactResponse]:
|
54
68
|
"""
|
55
69
|
Creates a new artifact in the service.
|
56
70
|
|
57
71
|
:param request: The request object containing artifact details.
|
58
|
-
:return: The response object containing the created artifact details.
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
72
|
+
:return: The response object containing the created artifact details, or None on error.
|
73
|
+
"""
|
74
|
+
try:
|
75
|
+
response = self.client.post(
|
76
|
+
"/create_artifact",
|
77
|
+
self.iam_client.get_custom_headers(),
|
78
|
+
request.model_dump()
|
79
|
+
)
|
80
|
+
return CreateArtifactResponse.model_validate(response) if response else None
|
81
|
+
except (RequestException, ValueError) as e:
|
82
|
+
logger.error(f"Failed to create artifact: {e}")
|
83
|
+
return None
|
64
84
|
|
65
85
|
@handle_refresh_token
|
66
|
-
def create_artifact_from_template(self, artifact_template_id: str) -> CreateArtifactFromTemplateResponse:
|
86
|
+
def create_artifact_from_template(self, artifact_template_id: str) -> Optional[CreateArtifactFromTemplateResponse]:
|
67
87
|
"""
|
68
88
|
Creates a new artifact in the service.
|
69
89
|
|
70
90
|
:param artifact_template_id: The ID of the artifact template to use.
|
71
|
-
:return: The response object containing the created artifact details.
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
91
|
+
:return: The response object containing the created artifact details or None if an error occurs.
|
92
|
+
"""
|
93
|
+
try:
|
94
|
+
response = self.client.post(
|
95
|
+
"/create_artifact_from_template",
|
96
|
+
self.iam_client.get_custom_headers(),
|
97
|
+
{"artifact_template_id": artifact_template_id}
|
98
|
+
)
|
99
|
+
return CreateArtifactFromTemplateResponse.model_validate(response) if response else None
|
100
|
+
except (RequestException, ValueError) as e:
|
101
|
+
logger.error(f"Failed to create artifact from template {artifact_template_id}: {e}")
|
102
|
+
return None
|
78
103
|
|
79
104
|
@handle_refresh_token
|
80
|
-
def rebuild_artifact(self, artifact_id: str) -> RebuildArtifactResponse:
|
105
|
+
def rebuild_artifact(self, artifact_id: str) -> Optional[RebuildArtifactResponse]:
|
81
106
|
"""
|
82
107
|
Rebuilds an artifact in the service.
|
83
108
|
|
84
109
|
:param artifact_id: The ID of the artifact to rebuild.
|
85
|
-
:return: The response object containing the rebuilt artifact details.
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
110
|
+
:return: The response object containing the rebuilt artifact details or None if an error occurs.
|
111
|
+
"""
|
112
|
+
try:
|
113
|
+
response = self.client.post(
|
114
|
+
"/rebuild_artifact",
|
115
|
+
self.iam_client.get_custom_headers(),
|
116
|
+
{"artifact_id": artifact_id}
|
117
|
+
)
|
118
|
+
return RebuildArtifactResponse.model_validate(response) if response else None
|
119
|
+
except (RequestException, ValueError) as e:
|
120
|
+
logger.error(f"Failed to rebuild artifact {artifact_id}: {e}")
|
121
|
+
return None
|
92
122
|
|
93
123
|
@handle_refresh_token
|
94
|
-
def delete_artifact(self, artifact_id: str) -> DeleteArtifactResponse:
|
124
|
+
def delete_artifact(self, artifact_id: str) -> Optional[DeleteArtifactResponse]:
|
95
125
|
"""
|
96
126
|
Deletes an artifact by its ID.
|
97
127
|
|
98
128
|
:param artifact_id: The ID of the artifact to delete.
|
99
|
-
:return: The response object containing the deleted artifact details.
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
129
|
+
:return: The response object containing the deleted artifact details or None if an error occurs.
|
130
|
+
"""
|
131
|
+
try:
|
132
|
+
response = self.client.delete(
|
133
|
+
"/delete_artifact",
|
134
|
+
self.iam_client.get_custom_headers(),
|
135
|
+
{"artifact_id": artifact_id}
|
136
|
+
)
|
137
|
+
return DeleteArtifactResponse.model_validate(response) if response else None
|
138
|
+
except (RequestException, ValueError) as e:
|
139
|
+
logger.error(f"Failed to delete artifact {artifact_id}: {e}")
|
140
|
+
return None
|
106
141
|
|
107
142
|
@handle_refresh_token
|
108
|
-
def get_bigfile_upload_url(self, request: GetBigFileUploadUrlRequest) -> GetBigFileUploadUrlResponse:
|
143
|
+
def get_bigfile_upload_url(self, request: GetBigFileUploadUrlRequest) -> Optional[GetBigFileUploadUrlResponse]:
|
109
144
|
"""
|
110
145
|
Generates a pre-signed URL for uploading a large file.
|
111
146
|
|
112
147
|
:param request: The request object containing the artifact ID, file name, and file type.
|
113
|
-
:return: The response object containing the pre-signed URL and upload details.
|
114
|
-
:rtype: GetBigFileUploadUrlResponse
|
148
|
+
:return: The response object containing the pre-signed URL and upload details, or None if an error occurs.
|
115
149
|
"""
|
116
|
-
|
150
|
+
try:
|
151
|
+
response = self.client.post("/get_bigfile_upload_url",
|
152
|
+
self.iam_client.get_custom_headers(),
|
153
|
+
request.model_dump())
|
154
|
+
|
155
|
+
if not response:
|
156
|
+
logger.error("Empty response from /get_bigfile_upload_url")
|
157
|
+
return None
|
158
|
+
|
159
|
+
return GetBigFileUploadUrlResponse.model_validate(response)
|
117
160
|
|
118
|
-
|
161
|
+
except (RequestException, ValueError) as e:
|
162
|
+
logger.error(f"Failed to generate upload URL: {e}")
|
163
|
+
return None
|
119
164
|
|
120
165
|
@handle_refresh_token
|
121
|
-
def delete_bigfile(self, request: DeleteBigfileRequest) -> DeleteBigfileResponse:
|
166
|
+
def delete_bigfile(self, request: DeleteBigfileRequest) -> Optional[DeleteBigfileResponse]:
|
122
167
|
"""
|
123
168
|
Deletes a large file associated with an artifact.
|
124
169
|
|
125
170
|
:param request: The request object containing the artifact ID and file name.
|
126
|
-
:return: The response object containing the deletion status.
|
127
|
-
:rtype: DeleteBigfileResponse
|
171
|
+
:return: The response object containing the deletion status, or None if an error occurs.
|
128
172
|
"""
|
129
|
-
|
173
|
+
try:
|
174
|
+
response = self.client.delete("/delete_bigfile",
|
175
|
+
self.iam_client.get_custom_headers(),
|
176
|
+
request.model_dump())
|
177
|
+
|
178
|
+
if not response:
|
179
|
+
logger.error("Empty response from /delete_bigfile")
|
180
|
+
return None
|
181
|
+
|
182
|
+
return DeleteBigfileResponse.model_validate(response)
|
130
183
|
|
131
|
-
|
184
|
+
except (RequestException, ValueError) as e:
|
185
|
+
logger.error(f"Failed to delete big file: {e}")
|
186
|
+
return None
|
132
187
|
|
133
188
|
@handle_refresh_token
|
134
|
-
def
|
189
|
+
def get_public_templates(self) -> List[ArtifactTemplate]:
|
135
190
|
"""
|
136
191
|
Fetches all artifact templates.
|
137
192
|
|
138
193
|
:return: A list of ArtifactTemplate objects.
|
139
194
|
:rtype: List[ArtifactTemplate]
|
140
195
|
"""
|
141
|
-
|
142
|
-
|
196
|
+
try:
|
197
|
+
response = self.client.get("/get_public_templates", self.iam_client.get_custom_headers())
|
198
|
+
|
199
|
+
if not response:
|
200
|
+
logger.error("Empty response received from /get_public_templates API")
|
201
|
+
return []
|
202
|
+
|
203
|
+
try:
|
204
|
+
result = GetPublicTemplatesResponse.model_validate(response)
|
205
|
+
return result.artifact_templates
|
206
|
+
except ValueError as ve:
|
207
|
+
logger.error(f"Failed to validate response data: {ve}")
|
208
|
+
return []
|
209
|
+
|
210
|
+
except RequestException as e:
|
211
|
+
logger.error(f"Request to /get_public_templates failed: {e}")
|
212
|
+
return []
|
@@ -1,8 +1,12 @@
|
|
1
|
+
import logging
|
2
|
+
|
1
3
|
import requests
|
2
4
|
from .._exceptions import APIError
|
3
5
|
from .._exceptions import UnauthorizedError
|
4
6
|
from .._constants import *
|
5
7
|
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
6
10
|
|
7
11
|
class HTTPClient:
|
8
12
|
"""
|
@@ -51,6 +55,7 @@ class HTTPClient:
|
|
51
55
|
response = None
|
52
56
|
try:
|
53
57
|
response = requests.request(method, url, params=params, json=data, headers=headers)
|
58
|
+
logger.debug(response.text)
|
54
59
|
if response.status_code == 401:
|
55
60
|
raise UnauthorizedError("Access token expired or invalid.")
|
56
61
|
elif response.status_code != 200 and response.status_code != 201:
|
@@ -61,7 +66,6 @@ class HTTPClient:
|
|
61
66
|
raise APIError(f"HTTP Request failed: {error_message}")
|
62
67
|
# Raise for HTTP errors
|
63
68
|
response.raise_for_status()
|
64
|
-
print(response.text)
|
65
69
|
|
66
70
|
except requests.exceptions.RequestException as e:
|
67
71
|
raise APIError(f"HTTP Request failed: {str(e)}")
|
@@ -1,10 +1,14 @@
|
|
1
1
|
import jwt
|
2
|
+
import logging
|
3
|
+
from requests.exceptions import RequestException
|
2
4
|
|
3
5
|
from ._http_client import HTTPClient
|
4
6
|
from .._config import IAM_SERVICE_BASE_URL
|
5
7
|
from .._models import *
|
6
8
|
from .._constants import CLIENT_ID_HEADER, AUTHORIZATION_HEADER
|
7
9
|
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
8
12
|
|
9
13
|
class IAMClient:
|
10
14
|
"""
|
@@ -25,58 +29,113 @@ class IAMClient:
|
|
25
29
|
self._access_token = ""
|
26
30
|
self._refresh_token = ""
|
27
31
|
self._user_id = ""
|
32
|
+
self._organization_id = ""
|
28
33
|
self.client = HTTPClient(IAM_SERVICE_BASE_URL)
|
29
34
|
|
30
|
-
def login(self):
|
35
|
+
def login(self) -> bool:
|
31
36
|
"""
|
32
|
-
Logs in a user with the given
|
37
|
+
Logs in a user with the given email and password.
|
38
|
+
Returns True if login is successful, otherwise False.
|
33
39
|
"""
|
34
|
-
|
35
|
-
CLIENT_ID_HEADER: self._client_id
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
create_session_result = self.client.post(
|
52
|
-
"/me/sessions", custom_headers, create_session_req.model_dump()
|
40
|
+
try:
|
41
|
+
custom_headers = {CLIENT_ID_HEADER: self._client_id}
|
42
|
+
req = AuthTokenRequest(email=self._email, password=self._password)
|
43
|
+
auth_tokens_result = self.client.post("/me/auth-tokens", custom_headers, req.model_dump())
|
44
|
+
|
45
|
+
if not auth_tokens_result:
|
46
|
+
logger.error("Login failed: Received empty response from auth-tokens endpoint")
|
47
|
+
return False
|
48
|
+
|
49
|
+
auth_tokens_resp = AuthTokenResponse.model_validate(auth_tokens_result)
|
50
|
+
|
51
|
+
# Handle 2FA
|
52
|
+
if auth_tokens_resp.is2FARequired:
|
53
|
+
for attempt in range(3):
|
54
|
+
code = input(f"Attempt {attempt + 1}/3: Please enter the 2FA code: ")
|
55
|
+
create_session_req = CreateSessionRequest(
|
56
|
+
type="native", authToken=auth_tokens_resp.authToken, otpCode=code
|
53
57
|
)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
58
|
+
try:
|
59
|
+
session_result = self.client.post("/me/sessions", custom_headers,
|
60
|
+
create_session_req.model_dump())
|
61
|
+
if session_result:
|
62
|
+
break
|
63
|
+
except RequestException:
|
64
|
+
logger.warning("Invalid 2FA code, please try again.")
|
65
|
+
if attempt == 2:
|
66
|
+
logger.error("Failed to create session after 3 incorrect 2FA attempts.")
|
67
|
+
return False
|
68
|
+
else:
|
69
|
+
create_session_req = CreateSessionRequest(type="native", authToken=auth_tokens_resp.authToken,
|
70
|
+
otpCode=None)
|
71
|
+
session_result = self.client.post("/me/sessions", custom_headers, create_session_req.model_dump())
|
72
|
+
|
73
|
+
create_session_resp = CreateSessionResponse.model_validate(session_result)
|
62
74
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
self._user_id = self.parse_user_id()
|
75
|
+
self._access_token = create_session_resp.accessToken
|
76
|
+
self._refresh_token = create_session_resp.refreshToken
|
77
|
+
self._user_id = self.parse_user_id()
|
67
78
|
|
68
|
-
|
79
|
+
# Fetch profile to get organization ID
|
80
|
+
profile_result = self.client.get("/me/profile", self.get_custom_headers())
|
81
|
+
if not profile_result:
|
82
|
+
logger.error("Failed to fetch user profile data.")
|
83
|
+
return False
|
84
|
+
|
85
|
+
profile_resp = ProfileResponse.model_validate(profile_result)
|
86
|
+
self._organization_id = profile_resp.organization.id
|
87
|
+
|
88
|
+
return True
|
89
|
+
except (RequestException, ValueError, KeyError) as e:
|
90
|
+
logger.error(f"Login failed due to exception: {e}")
|
91
|
+
return False
|
92
|
+
|
93
|
+
def refresh_token(self) -> bool:
|
69
94
|
"""
|
70
|
-
Refreshes
|
95
|
+
Refreshes the access token. Returns True on success, False otherwise.
|
71
96
|
"""
|
72
|
-
|
73
|
-
CLIENT_ID_HEADER: self._client_id
|
74
|
-
|
75
|
-
|
97
|
+
try:
|
98
|
+
custom_headers = {CLIENT_ID_HEADER: self._client_id}
|
99
|
+
result = self.client.patch("/me/sessions", custom_headers, {"refreshToken": self._refresh_token})
|
100
|
+
|
101
|
+
if not result:
|
102
|
+
logger.error("Failed to refresh token: Empty response received")
|
103
|
+
return False
|
76
104
|
|
77
|
-
|
78
|
-
|
79
|
-
|
105
|
+
resp = CreateSessionResponse.model_validate(result)
|
106
|
+
self._access_token = resp.accessToken
|
107
|
+
self._refresh_token = resp.refreshToken
|
108
|
+
|
109
|
+
return True
|
110
|
+
except (RequestException, ValueError) as e:
|
111
|
+
logger.error(f"Token refresh failed: {e}")
|
112
|
+
return False
|
113
|
+
|
114
|
+
def create_org_api_key(self, request: CreateAPIKeyRequest) -> Optional[str]:
|
115
|
+
"""
|
116
|
+
Creates a new API key for the current user.
|
117
|
+
"""
|
118
|
+
try:
|
119
|
+
result = self.client.post(f"/organizations/{self.get_organization_id()}/api-keys",
|
120
|
+
self.get_custom_headers(), request.model_dump())
|
121
|
+
|
122
|
+
return CreateAPIKeyResponse.model_validate(result).key if result else None
|
123
|
+
except (RequestException, ValueError) as e:
|
124
|
+
logger.error(f"Failed to create API key: {e}")
|
125
|
+
return None
|
126
|
+
|
127
|
+
def get_org_api_keys(self) -> Optional[GetAPIKeysResponse]:
|
128
|
+
"""
|
129
|
+
Fetches all API keys for the current user.
|
130
|
+
"""
|
131
|
+
try:
|
132
|
+
result = self.client.get(f"/organizations/{self.get_organization_id()}/api-keys",
|
133
|
+
self.get_custom_headers())
|
134
|
+
|
135
|
+
return GetAPIKeysResponse.model_validate(result) if result else None
|
136
|
+
except (RequestException, ValueError) as e:
|
137
|
+
logger.error(f"Failed to retrieve organization API keys: {e}")
|
138
|
+
return None
|
80
139
|
|
81
140
|
def parse_user_id(self) -> str:
|
82
141
|
"""
|
@@ -114,6 +173,12 @@ class IAMClient:
|
|
114
173
|
"""
|
115
174
|
return self._client_id
|
116
175
|
|
176
|
+
def get_organization_id(self) -> str:
|
177
|
+
"""
|
178
|
+
Gets the current organization ID.
|
179
|
+
"""
|
180
|
+
return self._organization_id
|
181
|
+
|
117
182
|
def get_custom_headers(self) -> dict:
|
118
183
|
"""
|
119
184
|
Gets the custom headers for the IAM client.
|