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.
@@ -1,9 +1,14 @@
1
+ import logging
2
+ from requests.exceptions import RequestException
3
+
1
4
  from ._http_client import HTTPClient
2
5
  from ._decorator import handle_refresh_token
3
6
  from ._iam_client import IAMClient
4
7
  from .._config import TASK_SERVICE_BASE_URL
5
8
  from .._models import *
6
9
 
10
+ logger = logging.getLogger(__name__)
11
+
7
12
 
8
13
  class TaskClient:
9
14
  """
@@ -21,17 +26,19 @@ class TaskClient:
21
26
  self.iam_client = iam_client
22
27
 
23
28
  @handle_refresh_token
24
- def get_task(self, task_id: str) -> Task:
29
+ def get_task(self, task_id: str) -> Optional[Task]:
25
30
  """
26
31
  Retrieves a task from the task service using the given task ID.
27
32
 
28
33
  :param task_id: The ID of the task to be retrieved.
29
- :return: An instance of Task containing the details of the retrieved task.
30
- :rtype: Task
34
+ :return: An instance of Task containing the details of the retrieved task, or None if an error occurs.
31
35
  """
32
- result = self.client.get("/get_task", self.iam_client.get_custom_headers(), {"task_id": task_id})
33
-
34
- return Task.model_validate(result)
36
+ try:
37
+ response = self.client.get("/get_task", self.iam_client.get_custom_headers(), {"task_id": task_id})
38
+ return Task.model_validate(response) if response else None
39
+ except (RequestException, ValueError) as e:
40
+ logger.error(f"Failed to retrieve task {task_id}: {e}")
41
+ return None
35
42
 
36
43
  @handle_refresh_token
37
44
  def get_all_tasks(self) -> GetAllTasksResponse:
@@ -39,70 +46,108 @@ class TaskClient:
39
46
  Retrieves all tasks from the task service.
40
47
 
41
48
  :return: An instance of GetAllTasksResponse containing the retrieved tasks.
42
- :rtype: GetAllTasksResponse
43
49
  """
44
- result = self.client.get("/get_tasks", self.iam_client.get_custom_headers())
45
- if not result:
50
+ try:
51
+ response = self.client.get("/get_tasks", self.iam_client.get_custom_headers())
52
+ if not response:
53
+ logger.error("Empty response from /get_tasks")
54
+ return GetAllTasksResponse(tasks=[])
55
+ return GetAllTasksResponse.model_validate(response)
56
+ except (RequestException, ValueError) as e:
57
+ logger.error(f"Failed to retrieve all tasks: {e}")
46
58
  return GetAllTasksResponse(tasks=[])
47
59
 
48
- return GetAllTasksResponse.model_validate(result)
49
-
50
60
  @handle_refresh_token
51
- def create_task(self, task: Task) -> CreateTaskResponse:
61
+ def create_task(self, task: Task) -> Optional[CreateTaskResponse]:
52
62
  """
53
63
  Creates a new task using the provided task object.
54
64
 
55
65
  :param task: The Task object containing the details of the task to be created.
66
+ :return: The response object containing created task details, or None if an error occurs.
56
67
  """
57
- result = self.client.post("/create_task", self.iam_client.get_custom_headers(), task.model_dump())
58
-
59
- return CreateTaskResponse.model_validate(result)
68
+ try:
69
+ response = self.client.post("/create_task", self.iam_client.get_custom_headers(), task.model_dump())
70
+ return CreateTaskResponse.model_validate(response) if response else None
71
+ except (RequestException, ValueError) as e:
72
+ logger.error(f"Failed to create task: {e}")
73
+ return None
60
74
 
61
75
  @handle_refresh_token
62
- def update_task_schedule(self, task: Task):
76
+ def update_task_schedule(self, task: Task) -> bool:
63
77
  """
64
78
  Updates the schedule of an existing task.
65
79
 
66
80
  :param task: The Task object containing the updated task details.
81
+ :return: True if update is successful, False otherwise.
67
82
  """
68
- self.client.put("/update_schedule", self.iam_client.get_custom_headers(), task.model_dump())
83
+ try:
84
+ response = self.client.put("/update_schedule", self.iam_client.get_custom_headers(), task.model_dump())
85
+ return response is not None
86
+ except RequestException as e:
87
+ logger.error(f"Failed to update schedule for task {task.task_id}: {e}")
88
+ return False
69
89
 
70
90
  @handle_refresh_token
71
- def start_task(self, task_id: str):
91
+ def start_task(self, task_id: str) -> bool:
72
92
  """
73
93
  Starts a task using the given task ID.
74
94
 
75
95
  :param task_id: The ID of the task to be started.
96
+ :return: True if start is successful, False otherwise.
76
97
  """
77
- self.client.post("/start_task", self.iam_client.get_custom_headers(), {"task_id": task_id})
98
+ try:
99
+ response = self.client.post("/start_task", self.iam_client.get_custom_headers(), {"task_id": task_id})
100
+ return response is not None
101
+ except RequestException as e:
102
+ logger.error(f"Failed to start task {task_id}: {e}")
103
+ return False
78
104
 
79
105
  @handle_refresh_token
80
- def stop_task(self, task_id: str):
106
+ def stop_task(self, task_id: str) -> bool:
81
107
  """
82
108
  Stops a running task using the given task ID.
83
109
 
84
110
  :param task_id: The ID of the task to be stopped.
111
+ :return: True if stop is successful, False otherwise.
85
112
  """
86
- self.client.post("/stop_task", self.iam_client.get_custom_headers(), {"task_id": task_id})
113
+ try:
114
+ response = self.client.post("/stop_task", self.iam_client.get_custom_headers(), {"task_id": task_id})
115
+ return response is not None
116
+ except RequestException as e:
117
+ logger.error(f"Failed to stop task {task_id}: {e}")
118
+ return False
87
119
 
88
120
  @handle_refresh_token
89
- def get_usage_data(self, start_timestamp: str, end_timestamp: str) -> GetUsageDataResponse:
121
+ def get_usage_data(self, start_timestamp: str, end_timestamp: str) -> Optional[GetUsageDataResponse]:
90
122
  """
91
123
  Retrieves the usage data of a task using the given task ID.
92
124
 
93
125
  :param start_timestamp: The start timestamp of the usage data.
94
126
  :param end_timestamp: The end timestamp of the usage data.
95
- """
96
- result = self.client.get("/get_usage_data", self.iam_client.get_custom_headers(),
97
- {"start_timestamp": start_timestamp, "end_timestamp": end_timestamp})
98
-
99
- return result
127
+ :return: An instance of GetUsageDataResponse, or None if an error occurs.
128
+ """
129
+ try:
130
+ response = self.client.get(
131
+ "/get_usage_data",
132
+ self.iam_client.get_custom_headers(),
133
+ {"start_timestamp": start_timestamp, "end_timestamp": end_timestamp}
134
+ )
135
+ return GetUsageDataResponse.model_validate(response) if response else None
136
+ except (RequestException, ValueError) as e:
137
+ logger.error(f"Failed to retrieve usage data from {start_timestamp} to {end_timestamp}: {e}")
138
+ return None
100
139
 
101
140
  @handle_refresh_token
102
- def archive_task(self, task_id: str):
141
+ def archive_task(self, task_id: str) -> bool:
103
142
  """
104
143
  Archives a task using the given task ID.
105
144
 
106
145
  :param task_id: The ID of the task to be archived.
107
- """
108
- self.client.post("/archive_task", self.iam_client.get_custom_headers(), {"task_id": task_id})
146
+ :return: True if archiving is successful, False otherwise.
147
+ """
148
+ try:
149
+ response = self.client.post("/archive_task", self.iam_client.get_custom_headers(), {"task_id": task_id})
150
+ return response is not None
151
+ except RequestException as e:
152
+ logger.error(f"Failed to archive task {task_id}: {e}")
153
+ return False
@@ -23,3 +23,11 @@ class TaskEndpointStatus(str, Enum):
23
23
  READY = "ready"
24
24
  UNREADY = "unready"
25
25
  NEW = "new"
26
+
27
+ class TaskStatus(str, Enum):
28
+ IDLE = "idle"
29
+ STARTING = "starting"
30
+ IN_QUEUE = "in-queue"
31
+ RUNNING = "running"
32
+ NEEDSTOP = "needstop"
33
+ ARCHIVED = "archived"
@@ -81,7 +81,11 @@ class ArtifactManager:
81
81
  if not artifact_template_id or not artifact_template_id.strip():
82
82
  raise ValueError("Artifact template ID is required and cannot be empty.")
83
83
 
84
- return self.artifact_client.create_artifact_from_template(artifact_template_id).artifact_id
84
+ resp = self.artifact_client.create_artifact_from_template(artifact_template_id)
85
+ if not resp or not resp.artifact_id:
86
+ raise ValueError("Failed to create artifact from template.")
87
+
88
+ return resp.artifact_id
85
89
 
86
90
  def rebuild_artifact(self, artifact_id: str) -> RebuildArtifactResponse:
87
91
  """
@@ -170,7 +174,11 @@ class ArtifactManager:
170
174
 
171
175
  req = GetBigFileUploadUrlRequest(artifact_id=artifact_id, file_name=model_file_name, file_type=model_file_type)
172
176
 
173
- return self.artifact_client.get_bigfile_upload_url(req).upload_link
177
+ resp = self.artifact_client.get_bigfile_upload_url(req)
178
+ if not resp or not resp.upload_link:
179
+ raise ValueError("Failed to get bigfile upload URL.")
180
+
181
+ return resp.upload_link
174
182
 
175
183
  def delete_bigfile(self, artifact_id: str, file_name: str) -> str:
176
184
  """
@@ -182,7 +190,11 @@ class ArtifactManager:
182
190
  self._validate_artifact_id(artifact_id)
183
191
  self._validate_file_name(file_name)
184
192
 
185
- return self.artifact_client.delete_bigfile(artifact_id, file_name).status
193
+ resp = self.artifact_client.delete_bigfile(artifact_id, file_name)
194
+ if not resp or not resp.status:
195
+ raise ValueError("Failed to delete bigfile.")
196
+
197
+ return resp.status
186
198
 
187
199
  def upload_large_file(self, upload_link: str, file_path: str) -> None:
188
200
  """
@@ -229,14 +241,14 @@ class ArtifactManager:
229
241
 
230
242
  return artifact_id
231
243
 
232
- def get_artifact_templates(self) -> List[ArtifactTemplate]:
244
+ def get_public_templates(self) -> List[ArtifactTemplate]:
233
245
  """
234
246
  Fetch all artifact templates.
235
247
 
236
248
  :return: A list of ArtifactTemplate objects.
237
249
  :rtype: List[ArtifactTemplate]
238
250
  """
239
- return self.artifact_client.get_artifact_templates().artifact_templates
251
+ return self.artifact_client.get_public_templates()
240
252
 
241
253
  @staticmethod
242
254
  def _validate_file_name(file_name: str) -> None:
@@ -0,0 +1,36 @@
1
+ from datetime import datetime
2
+ from .._client._iam_client import IAMClient
3
+ from .._models import *
4
+
5
+
6
+ class IAMManager:
7
+ """
8
+ IamManager handles operations related to IAM, including user authentication and authorization.
9
+ """
10
+
11
+ def __init__(self, iam_client: IAMClient):
12
+ """
13
+ Initialize the IAMManager instance and the associated IAMClient.
14
+ """
15
+ self.iam_client = iam_client
16
+
17
+ def create_org_api_key(self, name: str, expires_at: Optional[int] = None) -> str:
18
+ """
19
+ Creates a new API key for the current user.
20
+ """
21
+
22
+ if not name:
23
+ raise ValueError("API key name cannot be empty")
24
+ if not expires_at:
25
+ # Set the expiration date to 30 days from now
26
+ expires_at = int(datetime.now().timestamp()) + 30 * 24 * 60 * 60
27
+ print(expires_at)
28
+
29
+ return self.iam_client.create_org_api_key(
30
+ CreateAPIKeyRequest(name=name, type="ie_model", expiresAt=expires_at))
31
+
32
+ def get_org_api_keys(self) -> List[APIKey]:
33
+ """
34
+ Fetches all API keys for the current user.
35
+ """
36
+ return self.iam_client.get_org_api_keys().keys
@@ -37,7 +37,11 @@ class TaskManager:
37
37
 
38
38
  :return: A list of `Task` objects.
39
39
  """
40
- return self.task_client.get_all_tasks(self.iam_client.get_user_id()).tasks
40
+ resp = self.task_client.get_all_tasks(self.iam_client.get_user_id())
41
+ if not resp or not resp.tasks:
42
+ return []
43
+
44
+ return resp.tasks
41
45
 
42
46
  def create_task(self, task: Task) -> Task:
43
47
  """
@@ -51,8 +55,11 @@ class TaskManager:
51
55
  self._validate_task(task)
52
56
  if not task.owner:
53
57
  task.owner = TaskOwner(user_id=self.iam_client.get_user_id())
58
+ resp = self.task_client.create_task(task)
59
+ if not resp or not resp.task:
60
+ raise ValueError("Failed to create task.")
54
61
 
55
- return self.task_client.create_task(task).task
62
+ return resp.task
56
63
 
57
64
  def create_task_from_file(self, artifact_id: str, config_file_path: str, trigger_timestamp: int = None) -> Task:
58
65
  """
@@ -76,7 +83,7 @@ class TaskManager:
76
83
 
77
84
  return self.create_task(task)
78
85
 
79
- def update_task_schedule(self, task: Task):
86
+ def update_task_schedule(self, task: Task) -> bool:
80
87
  """
81
88
  Update the schedule of an existing task.
82
89
 
@@ -87,10 +94,10 @@ class TaskManager:
87
94
  self._validate_task(task)
88
95
  self._validate_not_empty(task.task_id, "Task ID")
89
96
 
90
- self.task_client.update_task_schedule(task)
97
+ return self.task_client.update_task_schedule(task)
91
98
 
92
99
  def update_task_schedule_from_file(self, artifact_id: str, task_id: str, config_file_path: str,
93
- trigger_timestamp: int = None):
100
+ trigger_timestamp: int = None) -> bool:
94
101
  """
95
102
  Update the schedule of an existing task using data from a file. The file should contain a valid task definition.
96
103
 
@@ -112,9 +119,9 @@ class TaskManager:
112
119
  if trigger_timestamp:
113
120
  task.config.task_scheduling.scheduling_oneoff.trigger_timestamp = trigger_timestamp
114
121
 
115
- self.update_task_schedule(task)
122
+ return self.update_task_schedule(task)
116
123
 
117
- def start_task(self, task_id: str):
124
+ def start_task(self, task_id: str) -> bool:
118
125
  """
119
126
  Start a task by its ID.
120
127
 
@@ -124,9 +131,9 @@ class TaskManager:
124
131
  """
125
132
  self._validate_not_empty(task_id, "Task ID")
126
133
 
127
- self.task_client.start_task(task_id)
134
+ return self.task_client.start_task(task_id)
128
135
 
129
- def stop_task(self, task_id: str):
136
+ def stop_task(self, task_id: str) -> bool:
130
137
  """
131
138
  Stop a task by its ID.
132
139
 
@@ -136,7 +143,7 @@ class TaskManager:
136
143
  """
137
144
  self._validate_not_empty(task_id, "Task ID")
138
145
 
139
- self.task_client.stop_task(task_id)
146
+ return self.task_client.stop_task(task_id)
140
147
 
141
148
  def get_usage_data(self, start_timestamp: str, end_timestamp: str) -> GetUsageDataResponse:
142
149
  """
@@ -151,7 +158,7 @@ class TaskManager:
151
158
 
152
159
  return self.task_client.get_usage_data(start_timestamp, end_timestamp)
153
160
 
154
- def archive_task(self, task_id: str):
161
+ def archive_task(self, task_id: str) -> bool:
155
162
  """
156
163
  Archive a task by its ID.
157
164
 
@@ -161,7 +168,7 @@ class TaskManager:
161
168
  """
162
169
  self._validate_not_empty(task_id, "Task ID")
163
170
 
164
- self.task_client.archive_task(task_id)
171
+ return self.task_client.archive_task(task_id)
165
172
 
166
173
  @staticmethod
167
174
  def _validate_not_empty(value: str, name: str):
@@ -2,7 +2,7 @@ from typing import Optional, List
2
2
  from datetime import datetime
3
3
 
4
4
  from pydantic import BaseModel
5
- from gmicloud._internal._enums import BuildStatus, TaskEndpointStatus
5
+ from gmicloud._internal._enums import BuildStatus, TaskStatus, TaskEndpointStatus
6
6
 
7
7
 
8
8
  class BigFileMetadata(BaseModel):
@@ -24,6 +24,7 @@ class ArtifactMetadata(BaseModel):
24
24
  artifact_description: Optional[str] = "" # Description of the artifact.
25
25
  artifact_tags: Optional[List[str]] = "" # Comma-separated tags for categorizing the artifact.
26
26
  artifact_volume_path: Optional[str] = "" # Path to the volume where the artifact is stored.
27
+ artifact_template_id: Optional[str] = "" # The template ID used to create this artifact.
27
28
 
28
29
 
29
30
  class ArtifactData(BaseModel):
@@ -130,7 +131,7 @@ class DeleteBigfileResponse(BaseModel):
130
131
  status: Optional[str] = "" # Status of the deletion process.
131
132
 
132
133
 
133
- class GetArtifactTemplatesResponse(BaseModel):
134
+ class GetPublicTemplatesResponse(BaseModel):
134
135
  """
135
136
  Response containing a list of artifact templates.
136
137
  """
@@ -141,24 +142,46 @@ class ArtifactTemplate(BaseModel):
141
142
  """
142
143
  Template for creating an artifact.
143
144
  """
144
- artifact_template_id: str # Unique identifier for the artifact template.
145
- artifact_description: Optional[str] = "" # Description of the artifact template.
146
- artifact_name: Optional[str] = "" # Name of the artifact template.
147
- artifact_tags: Optional[List[str]] = None # Tags associated with the artifact template.
148
- ray: Optional["RayTemplate"] = None # Template for Ray-based artifacts.
145
+ template_id: str # Unique identifier for the artifact template.
146
+ template_data: Optional["TemplateData"] = None # Data for the artifact template.
147
+ template_metadata: Optional["TemplateMetadata"] = None # Metadata for the artifact template.
148
+
149
+
150
+ class TemplateMetadata(BaseModel):
151
+ """
152
+ Metadata for an artifact template.
153
+ """
154
+ create_at: Optional[str] = None # Timestamp when the template was created.
155
+ create_by: Optional[str] = "" # ID of the user who created the template.
156
+ create_by_org_id: Optional[str] = "" # ID of the organization to which the user belongs.
157
+ is_public: Optional[bool] = False # Indicates if the template is public.
158
+ update_at: Optional[str] = None # Timestamp when the template was last updated.
159
+ update_by: Optional[str] = "" # ID of the user who last updated the template.
160
+
161
+ class TemplateData(BaseModel):
162
+ """
163
+ Data for an artifact template.
164
+ """
165
+ description: Optional[str] = "" # Description of the artifact template.
166
+ icon_link: Optional[str] = "" # Link to the icon for the artifact template.
167
+ image_link: Optional[str] = "" # Link to the image for the artifact template.
168
+ name: Optional[str] = "" # Name of the artifact template.
169
+ ray: Optional["RayContent"] = None # Template for Ray-based artifacts.
149
170
  resources: Optional["ResourcesTemplate"] = None # Resource allocation template.
171
+ tags: Optional[List[str]] = None # Tags associated with the artifact template.
172
+ volume_path: Optional[str] = "" # Path to the volume where the artifact is stored.
150
173
 
151
174
 
152
- class RayTemplate(BaseModel):
175
+ class RayContent(BaseModel):
153
176
  deployment_name: Optional[str] = "" # Name of the deployment.
154
177
  file_path: Optional[str] = "" # Path to the task file in storage.
155
- version: Optional[str] = "" # Version of Ray used.
156
178
 
157
179
 
158
180
  class ResourcesTemplate(BaseModel):
159
181
  cpu: Optional[int] = 0 # Number of CPU cores allocated.
160
182
  memory: Optional[int] = 0 # Amount of RAM (in GB) allocated.
161
183
  gpu: Optional[int] = 0 # Number of GPUs allocated.
184
+ gpu_name: Optional[str] = "" # Type the GPU allocated.
162
185
 
163
186
 
164
187
  class CreateArtifactFromTemplateRequest(BaseModel):
@@ -290,7 +313,7 @@ class Task(BaseModel):
290
313
  config: Optional[TaskConfig] = None # Configuration data for the task.
291
314
  endpoint_info: Optional[EndpointInfo] = None # Additional information about the task endpoint.
292
315
  cluster_endpoints: Optional[List[EndpointInfo]] = None # Endpoints for the task cluster.
293
- task_status: Optional[str] = "" # Status of the task.
316
+ task_status: Optional[TaskStatus] = "" # Status of the task.
294
317
  readiness_status: Optional[str] = "" # Readiness status of the task.
295
318
  user_preference: Optional[UserPreference] = None # User preference for the task.
296
319
 
@@ -373,3 +396,73 @@ class LoginRequest(BaseModel):
373
396
  """
374
397
  email: str # User email.
375
398
  password: str # User password.
399
+
400
+
401
+ class User(BaseModel):
402
+ """
403
+ User information.
404
+ """
405
+ id: Optional[str] = "" # User ID.
406
+ email: Optional[str] = "" # User email.
407
+ firstName: Optional[str] = "" # User first name.
408
+ lastName: Optional[str] = "" # User last name.
409
+
410
+
411
+ class Organization(BaseModel):
412
+ """
413
+ Organization information.
414
+ """
415
+ id: Optional[str] = "" # Organization ID.
416
+ role: Optional[str] = "" # Organization role.
417
+
418
+
419
+ class ProfileResponse(BaseModel):
420
+ """
421
+ Response object for user profile.
422
+ """
423
+ user: User # User information.
424
+ organization: Organization # Organization information.
425
+
426
+
427
+ class CreateAPIKeyRequest(BaseModel):
428
+ """
429
+ Request object for creating an API key.
430
+ """
431
+ name: str # Name of the API key.
432
+ type: Optional[str] = "" # Type of the API key.
433
+ expiresAt: Optional[int] = 0 # Expiration timestamp for the API key.
434
+
435
+
436
+ class CreateAPIKeyResponse(BaseModel):
437
+ """
438
+ Response object for creating an API key.
439
+ """
440
+ key: str # The created API key.
441
+
442
+
443
+ class APIKey(BaseModel):
444
+ """
445
+ API key information.
446
+ """
447
+ id: Optional[str] = "" # API key ID.
448
+ name: Optional[str] = "" # API key name.
449
+ type: Optional[str] = "" # API key type.
450
+ partialKey: Optional[str] = "" # Partial key for the API key.
451
+ expiresAt: Optional[int] = 0 # Expiration timestamp for the API key.
452
+ createdAt: Optional[int] = 0 # Creation timestamp for the API key.
453
+ owner: Optional[User] = None # Owner of the API key.
454
+
455
+
456
+ class GetAPIKeysResponse(BaseModel):
457
+ """
458
+ Response object for getting a list of API keys.
459
+ """
460
+ keys: list[APIKey] # List of API keys.
461
+
462
+
463
+ class GetSelfAPIKeyResponse(BaseModel):
464
+ """
465
+ Response object for getting the API key of the current user.
466
+ """
467
+ key: APIKey # The API key of the current user.
468
+ organization: Optional[Organization] = None # Organization information.
gmicloud/client.py CHANGED
@@ -1,14 +1,18 @@
1
1
  import os
2
2
  import time
3
+ import logging
3
4
 
4
5
  from typing import Optional
5
6
 
6
7
  from ._internal._client._iam_client import IAMClient
7
8
  from ._internal._manager._artifact_manager import ArtifactManager
8
9
  from ._internal._manager._task_manager import TaskManager
10
+ from ._internal._manager._iam_manager import IAMManager
9
11
  from ._internal._enums import BuildStatus
10
12
  from ._internal._models import Task, TaskConfig, RayTaskConfig, TaskScheduling, ReplicaResource
11
13
 
14
+ logger = logging.getLogger(__name__)
15
+
12
16
 
13
17
  class Client:
14
18
  def __init__(self, client_id: Optional[str] = "", email: Optional[str] = "", password: Optional[str] = ""):
@@ -32,6 +36,7 @@ class Client:
32
36
  # Managers are lazily initialized through private attributes
33
37
  self._artifact_manager = None
34
38
  self._task_manager = None
39
+ self._iam_manager = None
35
40
 
36
41
  def create_task_from_artifact_template(self, artifact_template_id: str, task_scheduling: TaskScheduling) -> Task:
37
42
  """
@@ -50,24 +55,28 @@ class Client:
50
55
  artifact_manager = self.artifact_manager
51
56
  task_manager = self.task_manager
52
57
 
53
- templates = artifact_manager.get_artifact_templates()
58
+ templates = artifact_manager.get_public_templates()
54
59
  template = None
55
60
  for v in templates:
56
- if v.artifact_template_id == artifact_template_id:
61
+ if v.template_id == artifact_template_id:
57
62
  template = v
58
63
  if not template:
59
64
  raise ValueError(f"Template with ID {artifact_template_id} not found.")
60
- if not template.ray:
65
+ if not template.template_data:
66
+ raise ValueError("Template does not contain template data.")
67
+ if not template.template_data.ray:
61
68
  raise ValueError("Template does not contain Ray configuration.")
62
- if not template.resources:
69
+ if not template.template_data.resources:
63
70
  raise ValueError("Template does not contain resource configuration.")
64
71
 
65
72
  artifact_id = artifact_manager.create_artifact_from_template(artifact_template_id)
73
+
74
+ logger.info(f"Successfully created artifact from template, artifact_id: {artifact_id}")
66
75
  # Wait for the artifact to be ready
67
76
  while True:
68
77
  try:
69
78
  artifact = artifact_manager.get_artifact(artifact_id)
70
- print(f"Artifact status: {artifact.build_status}")
79
+ logger.info(f"Successfully got artifact info, artifact status: {artifact.build_status}")
71
80
  # Wait until the artifact is ready
72
81
  if artifact.build_status == BuildStatus.SUCCESS:
73
82
  break
@@ -94,8 +103,10 @@ class Client:
94
103
  ),
95
104
  ))
96
105
 
106
+ logger.info(f"Successfully created task, task_id: {task.task_id}")
97
107
  # Start the task
98
108
  task_manager.start_task(task.task_id)
109
+ logger.info(f"Successfully started task, task_id: {task.task_id}")
99
110
  except Exception as e:
100
111
  raise e
101
112
 
@@ -120,3 +131,13 @@ class Client:
120
131
  if self._task_manager is None:
121
132
  self._task_manager = TaskManager(self.iam_client)
122
133
  return self._task_manager
134
+
135
+ @property
136
+ def iam_manager(self):
137
+ """
138
+ Lazy initialization for IAMManager.
139
+ Ensures the Client instance controls its lifecycle.
140
+ """
141
+ if self._iam_manager is None:
142
+ self._iam_manager = IAMManager(self.iam_client)
143
+ return self._iam_manager
@@ -251,24 +251,23 @@ class TestArtifactManager(unittest.TestCase):
251
251
  self.artifact_manager.delete_bigfile("nonexistent_id", "file.txt")
252
252
  self.assertTrue("Artifact not found" in str(context.exception))
253
253
 
254
- @patch('gmicloud._internal._client._artifact_client.ArtifactClient.get_artifact_templates')
255
- def test_get_artifact_templates_returns_templates(self, mock_get_artifact_templates):
256
- mock_get_artifact_templates.return_value = GetArtifactTemplatesResponse(
257
- artifact_templates=[ArtifactTemplate(artifact_template_id="1", artifact_name="Template1")])
258
- templates = self.artifact_manager.get_artifact_templates()
254
+ @patch('gmicloud._internal._client._artifact_client.ArtifactClient.get_public_templates')
255
+ def test_get_artifact_templates_returns_templates(self, mock_get_public_templates):
256
+ mock_get_public_templates.return_value = [ArtifactTemplate(template_id="1", template_data=TemplateData(name="Template1"))]
257
+ templates = self.artifact_manager.get_public_templates()
259
258
  self.assertEqual(len(templates), 1)
260
- self.assertEqual(templates[0].artifact_template_id, "1")
261
- self.assertEqual(templates[0].artifact_name, "Template1")
259
+ self.assertEqual(templates[0].template_id, "1")
260
+ self.assertEqual(templates[0].template_data.name, "Template1")
262
261
 
263
- @patch('gmicloud._internal._client._artifact_client.ArtifactClient.get_artifact_templates')
264
- def test_get_artifact_templates_returns_empty_list_when_no_templates(self, mock_get_artifact_templates):
265
- mock_get_artifact_templates.return_value = GetArtifactTemplatesResponse(artifact_templates=[])
266
- templates = self.artifact_manager.get_artifact_templates()
262
+ @patch('gmicloud._internal._client._artifact_client.ArtifactClient.get_public_templates')
263
+ def test_get_artifact_templates_returns_empty_list_when_no_templates(self, mock_get_public_templates):
264
+ mock_get_public_templates.return_value = []
265
+ templates = self.artifact_manager.get_public_templates()
267
266
  self.assertEqual(len(templates), 0)
268
267
 
269
- @patch('gmicloud._internal._client._artifact_client.ArtifactClient.get_artifact_templates')
270
- def test_get_artifact_templates_raises_error_on_failure(self, mock_get_artifact_templates):
271
- mock_get_artifact_templates.side_effect = Exception("Failed to fetch templates")
268
+ @patch('gmicloud._internal._client._artifact_client.ArtifactClient.get_public_templates')
269
+ def test_get_artifact_templates_raises_error_on_failure(self, mock_get_public_templates):
270
+ mock_get_public_templates.side_effect = Exception("Failed to fetch templates")
272
271
  with self.assertRaises(Exception) as context:
273
- self.artifact_manager.get_artifact_templates()
272
+ self.artifact_manager.get_public_templates()
274
273
  self.assertTrue("Failed to fetch templates" in str(context.exception))
@@ -72,7 +72,7 @@ class TestTaskManager(unittest.TestCase):
72
72
  'create_timestamp': 1734951094,
73
73
  'last_update_timestamp': 1734975798
74
74
  },
75
- 'task_status': '',
75
+ 'task_status': 'running',
76
76
  'readiness_status': '',
77
77
  'info': {
78
78
  'endpoint_status': 'ready',