gmicloud 0.1.6__py3-none-any.whl → 0.1.9__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 CHANGED
@@ -15,7 +15,7 @@ from ._internal._models import (
15
15
  OneOffScheduling,
16
16
  DailyScheduling,
17
17
  DailyTrigger,
18
- ArtifactTemplate,
18
+ Template,
19
19
  )
20
20
  from ._internal._enums import (
21
21
  BuildStatus,
@@ -39,7 +39,7 @@ __all__ = [
39
39
  "OneOffScheduling",
40
40
  "DailyScheduling",
41
41
  "DailyTrigger",
42
- "ArtifactTemplate",
42
+ "Template",
43
43
  "BuildStatus",
44
44
  "TaskEndpointStatus",
45
45
  ]
@@ -1,7 +1,7 @@
1
1
  from typing import List
2
2
  import logging
3
3
  from requests.exceptions import RequestException
4
-
4
+ import json
5
5
  from ._http_client import HTTPClient
6
6
  from ._iam_client import IAMClient
7
7
  from ._decorator import handle_refresh_token
@@ -120,6 +120,39 @@ class ArtifactClient:
120
120
  logger.error(f"Failed to rebuild artifact {artifact_id}: {e}")
121
121
  return None
122
122
 
123
+ @handle_refresh_token
124
+ def add_env_parameters_to_artifact(self, artifact_id: str, env_parameters: dict[str, str]) -> None:
125
+ """
126
+ Updates an artifact by its ID.
127
+
128
+ :param artifact_id: The ID of the artifact to update.
129
+ :param request: The request object containing the updated artifact details.
130
+ """
131
+ try:
132
+ old_artifact = self.get_artifact(artifact_id)
133
+ if not old_artifact:
134
+ logger.error(f"Artifact {artifact_id} not found")
135
+ return
136
+ request = UpdateArtifactRequestBody(
137
+ artifact_description=old_artifact.artifact_metadata.artifact_description,
138
+ artifact_name=old_artifact.artifact_metadata.artifact_name,
139
+ artifact_tags=old_artifact.artifact_metadata.artifact_tags,
140
+ env_parameters=old_artifact.artifact_parameters.env_parameters,
141
+ model_parameters=old_artifact.artifact_parameters.model_parameters
142
+ )
143
+ new_env_parameters = [EnvParameter(key=k, value=v) for k, v in env_parameters.items()]
144
+ if not request.env_parameters:
145
+ request.env_parameters = []
146
+ request.env_parameters.extend(new_env_parameters)
147
+ response = self.client.put(
148
+ f"/update_artifact?artifact_id={artifact_id}",
149
+ self.iam_client.get_custom_headers(),
150
+ request.model_dump()
151
+ )
152
+ except (RequestException, ValueError) as e:
153
+ logger.error(f"Failed to add env parameters to artifact {artifact_id}: {e}")
154
+ return
155
+
123
156
  @handle_refresh_token
124
157
  def delete_artifact(self, artifact_id: str) -> Optional[DeleteArtifactResponse]:
125
158
  """
@@ -140,7 +173,7 @@ class ArtifactClient:
140
173
  return None
141
174
 
142
175
  @handle_refresh_token
143
- def get_bigfile_upload_url(self, request: GetBigFileUploadUrlRequest) -> Optional[GetBigFileUploadUrlResponse]:
176
+ def get_bigfile_upload_url(self, request: ResumableUploadLinkRequest) -> Optional[ResumableUploadLinkResponse]:
144
177
  """
145
178
  Generates a pre-signed URL for uploading a large file.
146
179
 
@@ -156,7 +189,7 @@ class ArtifactClient:
156
189
  logger.error("Empty response from /get_bigfile_upload_url")
157
190
  return None
158
191
 
159
- return GetBigFileUploadUrlResponse.model_validate(response)
192
+ return ResumableUploadLinkResponse.model_validate(response)
160
193
 
161
194
  except (RequestException, ValueError) as e:
162
195
  logger.error(f"Failed to generate upload URL: {e}")
@@ -186,12 +219,12 @@ class ArtifactClient:
186
219
  return None
187
220
 
188
221
  @handle_refresh_token
189
- def get_public_templates(self) -> List[ArtifactTemplate]:
222
+ def get_public_templates(self) -> List[Template]:
190
223
  """
191
224
  Fetches all artifact templates.
192
225
 
193
- :return: A list of ArtifactTemplate objects.
194
- :rtype: List[ArtifactTemplate]
226
+ :return: A list of Template objects.
227
+ :rtype: List[Template]
195
228
  """
196
229
  try:
197
230
  response = self.client.get("/get_public_templates", self.iam_client.get_custom_headers())
@@ -201,7 +234,7 @@ class ArtifactClient:
201
234
  return []
202
235
 
203
236
  try:
204
- result = GetPublicTemplatesResponse.model_validate(response)
237
+ result = GetTemplatesResponse.model_validate(response)
205
238
  return result.artifact_templates
206
239
  except ValueError as ve:
207
240
  logger.error(f"Failed to validate response data: {ve}")
@@ -0,0 +1,78 @@
1
+ import os
2
+ import jwt
3
+ import time
4
+ import json
5
+ import logging
6
+ import threading
7
+ from pathlib import Path
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ CONFIG_FILE_NAME = ".gmicloud.config.json"
12
+
13
+ # create the thread lock object56
14
+ lock = threading.Lock()
15
+
16
+ def _read_config_file()->dict|None:
17
+ """Read the config file."""
18
+ base_dir = Path.home()
19
+ config_file_path =os.path.join(base_dir,CONFIG_FILE_NAME)
20
+ if not os.path.exists(config_file_path):
21
+ return None
22
+ with lock:
23
+ # open the config file, read mode with lock
24
+ with open(config_file_path,"r") as fr:
25
+ return json.loads(fr.read())
26
+
27
+
28
+ def _write_config_file(config_file_path:str,config_dic:dict)->None:
29
+ """Write the config file."""
30
+ with lock:
31
+ # open the config file, write mode with lock
32
+ with open(config_file_path,"w") as fw:
33
+ # transform the config dictionary to JSON format and write it to the file
34
+ fw.write(json.dumps(config_dic))
35
+
36
+
37
+ def write_user_refresh_token_to_system_config(email:str,refresh_token:str)->bool:
38
+ """Write the user refresh token to the system config file."""
39
+ base_dir = Path.home()
40
+ config_file_path = os.path.join(base_dir,CONFIG_FILE_NAME)
41
+ try:
42
+ # check the config file is exists. if not, create it, if yes, update the refresh token
43
+ if not os.path.exists(config_file_path):
44
+ config_dic = { email : {"refresh_token": refresh_token} }
45
+ _write_config_file(config_file_path,config_dic)
46
+ else:
47
+ config_dic = _read_config_file()
48
+ if not config_dic.get(email):
49
+ config_dic[email] = dict()
50
+ config_dic[email] = {"refresh_token": refresh_token}
51
+ _write_config_file(config_file_path,config_dic)
52
+ except Exception as e:
53
+ logger.error("write file wrong :", e)
54
+ return False
55
+ return True
56
+
57
+
58
+ def get_user_refresh_token_from_system_config(email:str)->str|None:
59
+ """Get the user refresh token from the system config file."""
60
+ config_dic = _read_config_file()
61
+ if not config_dic or not config_dic.get(email):
62
+ return None
63
+ return config_dic[email]["refresh_token"]
64
+
65
+
66
+ def _parese_refresh_token(refresh_token:str)->dict:
67
+ """Parse the refresh token."""
68
+ return jwt.decode(refresh_token, options={"verify_signature": False})
69
+
70
+
71
+ def is_refresh_token_expired(refresh_token:str)->bool:
72
+ """Check the refresh token is expired. if expired, return True, else return False."""
73
+ try:
74
+ refresh_token_time = _parese_refresh_token(refresh_token)['exp']
75
+ except Exception as e:
76
+ logger.error("parse refresh token wrong :", e)
77
+ return True
78
+ return refresh_token_time < time.time()
@@ -1,8 +1,10 @@
1
1
  import os
2
2
  import requests
3
+ import logging
3
4
 
4
5
  from .._exceptions import UploadFileError
5
6
 
7
+ logger = logging.getLogger()
6
8
 
7
9
  class FileUploadClient:
8
10
  CHUNK_SIZE = 10 * 1024 * 1024 # 10MB Default Chunk Size
@@ -45,13 +47,13 @@ class FileUploadClient:
45
47
  """
46
48
  try:
47
49
  file_size = os.path.getsize(file_path)
48
- print(f"File Size: {file_size} bytes")
50
+ logger.info(f"File {file_path} size: {file_size} bytes")
49
51
 
50
52
  start_byte = 0
51
53
  uploaded_range = FileUploadClient._check_file_status(upload_url, file_size)
52
54
  if uploaded_range:
53
55
  start_byte = int(uploaded_range.split("-")[1]) + 1
54
- print(f"Resuming upload from {start_byte} bytes")
56
+ logger.info(f"Resuming uploading {file_path} from {start_byte} bytes")
55
57
 
56
58
  with open(file_path, "rb") as file:
57
59
  while start_byte < file_size:
@@ -74,14 +76,15 @@ class FileUploadClient:
74
76
  # Ensure upload is successful for this chunk
75
77
  if resp.status_code not in (200, 201, 308):
76
78
  raise UploadFileError(
77
- f"Failed to upload file, code:{resp.status_code} ,message: {resp.text}")
79
+ f"Failed to upload file {file_path}, code:{resp.status_code} ,message: {resp.text}")
78
80
 
79
81
  start_byte = end_byte + 1
80
- print(f"Uploaded {end_byte + 1}/{file_size} bytes")
82
+ percentage = (start_byte / file_size) * 100
83
+ logger.info(f"File {file_path} uploaded {end_byte + 1:,}/{file_size:,} bytes ({percentage:.2f}%)")
81
84
 
82
- print("Upload completed successfully.")
85
+ logger.info(f"File {file_path} uploaded successfully.")
83
86
  except Exception as e:
84
- raise UploadFileError(f"Failed to upload file: {str(e)}")
87
+ raise UploadFileError(f"Failed to upload file {file_path}, got error: {str(e)}")
85
88
 
86
89
  @staticmethod
87
90
  def _check_file_status(upload_url: str, file_size: int) -> str:
@@ -104,7 +107,7 @@ class FileUploadClient:
104
107
  if resp.status_code == 308:
105
108
  range_header = resp.headers.get("Range")
106
109
  if range_header:
107
- print(f"Server reports partial upload range: {range_header}")
110
+ logger.info(f"Server reports partial upload range: {range_header}")
108
111
  return range_header
109
112
 
110
113
  if resp.status_code in (200, 201):
@@ -6,7 +6,11 @@ from ._http_client import HTTPClient
6
6
  from .._config import IAM_SERVICE_BASE_URL
7
7
  from .._models import *
8
8
  from .._constants import CLIENT_ID_HEADER, AUTHORIZATION_HEADER
9
-
9
+ from ._auth_config import (
10
+ get_user_refresh_token_from_system_config,
11
+ write_user_refresh_token_to_system_config,
12
+ is_refresh_token_expired
13
+ )
10
14
  logger = logging.getLogger(__name__)
11
15
 
12
16
 
@@ -38,42 +42,50 @@ class IAMClient:
38
42
  Returns True if login is successful, otherwise False.
39
43
  """
40
44
  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
57
- )
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
45
+ # Check config refresh token is available and is not expired, if yes ,refresh it
46
+ temp_refresh_token = get_user_refresh_token_from_system_config(self._email)
47
+ if temp_refresh_token and not is_refresh_token_expired(temp_refresh_token):
48
+ self._refresh_token = temp_refresh_token
49
+ self.refresh_token()
68
50
  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)
74
-
75
- self._access_token = create_session_resp.accessToken
76
- self._refresh_token = create_session_resp.refreshToken
51
+ custom_headers = {CLIENT_ID_HEADER: self._client_id}
52
+ req = AuthTokenRequest(email=self._email, password=self._password)
53
+ auth_tokens_result = self.client.post("/me/auth-tokens", custom_headers, req.model_dump())
54
+
55
+ if not auth_tokens_result:
56
+ logger.error("Login failed: Received empty response from auth-tokens endpoint")
57
+ return False
58
+
59
+ auth_tokens_resp = AuthTokenResponse.model_validate(auth_tokens_result)
60
+
61
+ # Handle 2FA
62
+ if auth_tokens_resp.is2FARequired:
63
+ for attempt in range(3):
64
+ code = input(f"Attempt {attempt + 1}/3: Please enter the 2FA code: ")
65
+ create_session_req = CreateSessionRequest(
66
+ type="native", authToken=auth_tokens_resp.authToken, otpCode=code
67
+ )
68
+ try:
69
+ session_result = self.client.post("/me/sessions", custom_headers,
70
+ create_session_req.model_dump())
71
+ if session_result:
72
+ break
73
+ except RequestException:
74
+ logger.warning("Invalid 2FA code, please try again.")
75
+ if attempt == 2:
76
+ logger.error("Failed to create session after 3 incorrect 2FA attempts.")
77
+ return False
78
+ else:
79
+ create_session_req = CreateSessionRequest(type="native", authToken=auth_tokens_resp.authToken,
80
+ otpCode=None)
81
+ session_result = self.client.post("/me/sessions", custom_headers, create_session_req.model_dump())
82
+
83
+ create_session_resp = CreateSessionResponse.model_validate(session_result)
84
+
85
+ self._access_token = create_session_resp.accessToken
86
+ self._refresh_token = create_session_resp.refreshToken
87
+ # first login write refresh token to system config
88
+ write_user_refresh_token_to_system_config(self._email,self._refresh_token)
77
89
  self._user_id = self.parse_user_id()
78
90
 
79
91
  # Fetch profile to get organization ID
@@ -96,7 +108,12 @@ class IAMClient:
96
108
  """
97
109
  try:
98
110
  custom_headers = {CLIENT_ID_HEADER: self._client_id}
99
- result = self.client.patch("/me/sessions", custom_headers, {"refreshToken": self._refresh_token})
111
+ try:
112
+ result = self.client.patch("/me/sessions", custom_headers, {"refreshToken": self._refresh_token})
113
+ except Exception as err:
114
+ logger.error(f"{str(err)}, please re-login.")
115
+ write_user_refresh_token_to_system_config(self._email,"")
116
+ return False
100
117
 
101
118
  if not result:
102
119
  logger.error("Failed to refresh token: Empty response received")
@@ -105,7 +122,9 @@ class IAMClient:
105
122
  resp = CreateSessionResponse.model_validate(result)
106
123
  self._access_token = resp.accessToken
107
124
  self._refresh_token = resp.refreshToken
108
-
125
+ # the _refresh_token will be updated when call this function
126
+ # so write it to system config file for update the _refresh_token expired time
127
+ write_user_refresh_token_to_system_config(self._email,self._refresh_token)
109
128
  return True
110
129
  except (RequestException, ValueError) as e:
111
130
  logger.error(f"Token refresh failed: {e}")
@@ -0,0 +1,111 @@
1
+
2
+ import logging
3
+ from requests.exceptions import RequestException
4
+
5
+ from ._http_client import HTTPClient
6
+ from ._decorator import handle_refresh_token
7
+ from ._iam_client import IAMClient
8
+ from .._config import IAM_SERVICE_BASE_URL
9
+ from .._models import *
10
+
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class VideoClient:
16
+ """
17
+ A client for interacting with the video service API.
18
+
19
+ This client provides methods to retrieve, create, update, and stop video tasks
20
+ through HTTP calls to the video service.
21
+ """
22
+
23
+ def __init__(self, iam_client: IAMClient):
24
+ """
25
+ Initializes the VideoClient with the given base URL for the video service.
26
+ """
27
+ self.client = HTTPClient(IAM_SERVICE_BASE_URL+ "/ie/requestqueue")
28
+ self.iam_client = iam_client
29
+
30
+
31
+ @handle_refresh_token
32
+ def get_request_detail(self, request_id: str) -> GetRequestResponse:
33
+ """
34
+ Retrieves detailed information about a specific request by its ID. This endpoint requires authentication with a bearer token and only returns requests belonging to the authenticated organization.
35
+
36
+ :param request_id: The ID of the request to be retrieved.
37
+ :return: Details of the GetRequestResponse successfully retrieved
38
+ """
39
+ try:
40
+ response = self.client.get(f"/requests/{request_id}", self.iam_client.get_custom_headers())
41
+ return GetRequestResponse.model_validate(response) if response else None
42
+ except (RequestException, ValueError) as e:
43
+ logger.error(f"Failed to retrieve request details for {request_id}: {e}")
44
+ return None
45
+
46
+
47
+ @handle_refresh_token
48
+ def get_requests(self, model_id: str) -> List[GetRequestResponse]:
49
+ """
50
+ Retrieves a list of requests submitted by the authenticated user for a specific model. This endpoint requires authentication with a bearer token and filters results by the authenticated organization.
51
+
52
+ :param model_id: The ID of the model to be retrieved.
53
+ :return: List of GetRequestResponse successfully retrieved
54
+ """
55
+ try:
56
+ response = self.client.get("/requests", self.iam_client.get_custom_headers(), {"model_id": model_id})
57
+ requests = response.get('requests', []) if response else []
58
+ return [GetRequestResponse.model_validate(req) for req in requests] if requests else None
59
+ except (RequestException, ValueError) as e:
60
+ logger.error(f"Failed to retrieve requests for model {model_id}: {e}")
61
+ return None
62
+
63
+
64
+ @handle_refresh_token
65
+ def create_request(self, request: SubmitRequestRequest) -> SubmitRequestResponse:
66
+ """
67
+ Submits a new asynchronous request to process a specified model with provided parameters. This endpoint requires authentication with a bearer token.
68
+
69
+ :param request: The request data to be created by SubmitRequestRequest model.
70
+ :return: The created request data as SubmitRequestResponse model.
71
+ """
72
+ try:
73
+ response = self.client.post("/requests", self.iam_client.get_custom_headers(), request.model_dump())
74
+ return SubmitRequestResponse.model_validate(response) if response else None
75
+ except (RequestException, ValueError) as e:
76
+ logger.error(f"Failed to create request: {e}")
77
+ return None
78
+
79
+
80
+ @handle_refresh_token
81
+ def get_model_detail(self, model_id: str) -> GetModelResponse:
82
+ """
83
+ Retrieves detailed information about a specific model by its ID.
84
+
85
+ :param model_id: The ID of the model to be retrieved.
86
+ :return: Details of the GetModelResponse model successfully retrieved.
87
+ """
88
+ try:
89
+ response = self.client.get(f"/models/{model_id}", self.iam_client.get_custom_headers())
90
+ return GetModelResponse.model_validate(response) if response else None
91
+ except (RequestException, ValueError) as e:
92
+ logger.error(f"Failed to retrieve model details for {model_id}: {e}")
93
+ return None
94
+
95
+
96
+ @handle_refresh_token
97
+ def get_models(self) -> List[GetModelResponse]:
98
+ """
99
+ Retrieves a list of available models from the video service.
100
+
101
+ :return: A list of GetModelResponse model successfully retrieved.
102
+ """
103
+ try:
104
+ response = self.client.get("/models", self.iam_client.get_custom_headers())
105
+ models = response.get('models', []) if response else []
106
+ return [GetModelResponse.model_validate(model) for model in models] if models else None
107
+ except (RequestException, ValueError) as e:
108
+ logger.error(f"Failed to retrieve models: {e}")
109
+ return None
110
+
111
+
@@ -1,3 +1,9 @@
1
- ARTIFACT_SERVICE_BASE_URL = "https://ce-tot.gmicloud-dev.com/api/v1/ie/artifact"
2
- TASK_SERVICE_BASE_URL = "https://ce-tot.gmicloud-dev.com/api/v1/ie/task"
3
- IAM_SERVICE_BASE_URL = "https://ce-tot.gmicloud-dev.com/api/v1"
1
+ # Dev environment
2
+ # ARTIFACT_SERVICE_BASE_URL = "https://ce-tot.gmicloud-dev.com/api/v1/ie/artifact"
3
+ # TASK_SERVICE_BASE_URL = "https://ce-tot.gmicloud-dev.com/api/v1/ie/task"
4
+ # IAM_SERVICE_BASE_URL = "https://ce-tot.gmicloud-dev.com/api/v1"
5
+
6
+ # Prod environment
7
+ ARTIFACT_SERVICE_BASE_URL = "https://inference-engine.gmicloud.ai/api/v1/ie/artifact"
8
+ TASK_SERVICE_BASE_URL = "https://inference-engine.gmicloud.ai/api/v1/ie/task"
9
+ IAM_SERVICE_BASE_URL = "https://inference-engine.gmicloud.ai/api/v1"
@@ -24,6 +24,7 @@ class TaskEndpointStatus(str, Enum):
24
24
  UNREADY = "unready"
25
25
  NEW = "new"
26
26
 
27
+
27
28
  class TaskStatus(str, Enum):
28
29
  IDLE = "idle"
29
30
  STARTING = "starting"
@@ -32,7 +33,24 @@ class TaskStatus(str, Enum):
32
33
  NEEDSTOP = "needstop"
33
34
  ARCHIVED = "archived"
34
35
 
36
+
35
37
  class ModelParameterType(str, Enum):
36
38
  NUMERIC = "numeric"
37
39
  TEXT = "text"
38
- BOOLEAN = "boolean"
40
+ BOOLEAN = "boolean"
41
+
42
+
43
+ class RequestStatus(Enum):
44
+ CREATED = "created"
45
+ QUEUED = "queued"
46
+ DISPATCHED = "dispatched"
47
+ PROCESSING = "processing"
48
+ SUCCESS = "success"
49
+ FAILED = "failed"
50
+ CANCELLED = "cancelled"
51
+
52
+
53
+ class HostType(Enum):
54
+ DEFAULT = ""
55
+ INTERNAL = "internal"
56
+ EXTERNAL = "external"