gmicloud 0.1.0__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.
@@ -0,0 +1,221 @@
1
+ import os
2
+
3
+ from .._client._iam_client import IAMClient
4
+ from .._client._task_client import TaskClient
5
+ from .._models import *
6
+
7
+
8
+ class TaskManager:
9
+ """
10
+ TaskManager handles operations related to tasks, including creation, scheduling, and stopping tasks.
11
+ """
12
+
13
+ def __init__(self, iam_client: IAMClient):
14
+ """
15
+ Initialize the TaskManager instance and the associated TaskClient.
16
+
17
+ :param iam_client: The IAMClient instance used for authentication
18
+ """
19
+ self.iam_client = iam_client
20
+ self.task_client = TaskClient(iam_client)
21
+
22
+ def get_task(self, task_id: str) -> Task:
23
+ """
24
+ Retrieve a task by its ID.
25
+
26
+ :param task_id: The ID of the task to retrieve.
27
+ :return: A `Task` object containing the details of the task.
28
+ :raises ValueError: If `task_id` is invalid (None or empty string).
29
+ """
30
+ self._validate_not_empty(task_id, "Task ID")
31
+
32
+ return self.task_client.get_task(task_id)
33
+
34
+ def get_all_tasks(self) -> List[Task]:
35
+ """
36
+ Retrieve a list of all tasks available in the system.
37
+
38
+ :return: A list of `Task` objects.
39
+ """
40
+ return self.task_client.get_all_tasks(self.iam_client.get_user_id()).tasks
41
+
42
+ def create_task(self, task: Task) -> Task:
43
+ """
44
+ Create a new task.
45
+
46
+ :param task: A `Task` object containing the details of the task to be created.
47
+ :return: A `Task` object containing the details of the created task.
48
+ :rtype: Task
49
+ :raises ValueError: If `task` is None.
50
+ """
51
+ self._validate_task(task)
52
+ if not task.owner:
53
+ task.owner = TaskOwner(user_id=self.iam_client.get_user_id())
54
+
55
+ return self.task_client.create_task(task).task
56
+
57
+ def create_task_from_file(self, artifact_id: str, config_file_path: str, trigger_timestamp: int = None) -> Task:
58
+ """
59
+ Create a new task using the configuration data from a file.
60
+
61
+ :param artifact_id: The ID of the artifact to be used in the task.
62
+ :param config_file_path: The path to the file containing the task configuration data.
63
+ :param trigger_timestamp: Optional, for one-off scheduling.
64
+ :return: A `Task` object containing the details of the created task.
65
+ :rtype: Task
66
+ :raises ValueError: If the `file_path` is invalid or the file cannot be read.
67
+ """
68
+ self._validate_not_empty(artifact_id, "Artifact ID")
69
+ self._validate_file_path(config_file_path)
70
+
71
+ task = self._read_file_and_parse_task(config_file_path)
72
+ task.config.ray_task_config.artifact_id = artifact_id
73
+
74
+ if trigger_timestamp:
75
+ task.config.task_scheduling.scheduling_oneoff.trigger_timestamp = trigger_timestamp
76
+
77
+ return self.create_task(task)
78
+
79
+ def update_task_schedule(self, task: Task):
80
+ """
81
+ Update the schedule of an existing task.
82
+
83
+ :param task: A `Task` object containing the updated schedule details.
84
+ :return: None
85
+ :raises ValueError: If `task` is None.
86
+ """
87
+ self._validate_task(task)
88
+ self._validate_not_empty(task.task_id, "Task ID")
89
+
90
+ self.task_client.update_task_schedule(task)
91
+
92
+ def update_task_schedule_from_file(self, artifact_id: str, task_id: str, config_file_path: str,
93
+ trigger_timestamp: int = None):
94
+ """
95
+ Update the schedule of an existing task using data from a file. The file should contain a valid task definition.
96
+
97
+ :param artifact_id: The ID of the artifact to be used in the task.
98
+ :param task_id: The ID of the task to update.
99
+ :param config_file_path: The path to the file containing the task configuration data.
100
+ :param trigger_timestamp: Optional, for one-off scheduling.
101
+ :return: None
102
+ :raises ValueError: If the `file_path` is invalid or the file cannot be read.
103
+ """
104
+ self._validate_not_empty(artifact_id, "Artifact ID")
105
+ self._validate_not_empty(task_id, "Task ID")
106
+ self._validate_file_path(config_file_path)
107
+
108
+ task = self._read_file_and_parse_task(config_file_path)
109
+ print("================", task)
110
+ task.task_id = task_id
111
+ task.config.ray_task_config.artifact_id = artifact_id
112
+
113
+ if trigger_timestamp:
114
+ task.config.task_scheduling.scheduling_oneoff.trigger_timestamp = trigger_timestamp
115
+
116
+ self.update_task_schedule(task)
117
+
118
+ def start_task(self, task_id: str):
119
+ """
120
+ Start a task by its ID.
121
+
122
+ :param task_id: The ID of the task to be started.
123
+ :return: None
124
+ :raises ValueError: If `task_id` is invalid (None or empty string).
125
+ """
126
+ self._validate_not_empty(task_id, "Task ID")
127
+
128
+ self.task_client.start_task(task_id)
129
+
130
+ def stop_task(self, task_id: str):
131
+ """
132
+ Stop a task by its ID.
133
+
134
+ :param task_id: The ID of the task to be stopped.
135
+ :return: None
136
+ :raises ValueError: If `task_id` is invalid (None or empty string).
137
+ """
138
+ self._validate_not_empty(task_id, "Task ID")
139
+
140
+ self.task_client.stop_task(task_id)
141
+
142
+ def get_usage_data(self, start_timestamp: str, end_timestamp: str) -> GetUsageDataResponse:
143
+ """
144
+ Retrieve the usage data of a task within a given time range.
145
+
146
+ :param start_timestamp: The start timestamp of the usage data.
147
+ :param end_timestamp: The end timestamp of the usage data.
148
+ :return: A `GetUsageDataResponse` object containing the usage data.
149
+ """
150
+ self._validate_not_empty(start_timestamp, "Start timestamp")
151
+ self._validate_not_empty(end_timestamp, "End timestamp")
152
+
153
+ return self.task_client.get_usage_data(start_timestamp, end_timestamp)
154
+
155
+ def archive_task(self, task_id: str):
156
+ """
157
+ Archive a task by its ID.
158
+
159
+ :param task_id: The ID of the task to be archived.
160
+ :return: None
161
+ :raises ValueError: If `task_id` is invalid (None or empty string).
162
+ """
163
+ self._validate_not_empty(task_id, "Task ID")
164
+
165
+ self.task_client.archive_task(task_id)
166
+
167
+ @staticmethod
168
+ def _validate_not_empty(value: str, name: str):
169
+ """
170
+ Validate a string is neither None nor empty.
171
+
172
+ :param value: The string to validate.
173
+ :param name: The name of the value for error reporting.
174
+ """
175
+ if not value or not value.strip():
176
+ raise ValueError(f"{name} is required and cannot be empty.")
177
+
178
+ @staticmethod
179
+ def _validate_task(task: Task) -> None:
180
+ """
181
+ Validate a Task object.
182
+
183
+ :param task: The Task object to validate.
184
+ :raises ValueError: If `task` is None.
185
+ """
186
+ if task is None:
187
+ raise ValueError("Task object is required and cannot be None.")
188
+
189
+ @staticmethod
190
+ def _validate_file_path(file_path: str) -> None:
191
+ """
192
+ Validate the file path.
193
+
194
+ :param file_path: The file path to validate.
195
+ :raises ValueError: If `file_path` is None, empty, or does not exist.
196
+ """
197
+ if not file_path or not file_path.strip():
198
+ raise ValueError("File path is required and cannot be empty.")
199
+ if not os.path.exists(file_path):
200
+ raise FileNotFoundError(f"File not found: {file_path}")
201
+
202
+ def _read_file_and_parse_task(self, file_path: str) -> Task:
203
+ """
204
+ Read a file and parse it into a Task object.
205
+
206
+ :param file_path: The path to the file to be read.
207
+ :return: A `Task` object parsed from the file content.
208
+ :raises ValueError: If the file is invalid or cannot be parsed.
209
+ """
210
+ self._validate_file_path(file_path)
211
+
212
+ with open(file_path, "rb") as file:
213
+ file_data = file.read()
214
+
215
+ try:
216
+ print("!!!!!!!!!!!1", file_data)
217
+ task = Task.model_validate_json(file_data) # Ensure Task has a static method for model validation.
218
+ except Exception as e:
219
+ raise ValueError(f"Failed to parse Task from file: {file_path}. Error: {str(e)}")
220
+
221
+ return task
@@ -0,0 +1,336 @@
1
+ from typing import Optional, List
2
+ from datetime import datetime
3
+
4
+ from pydantic import BaseModel
5
+ from gmicloud._internal._enums import BuildStatus, TaskEndpointStatus
6
+
7
+
8
+ class BigFileMetadata(BaseModel):
9
+ """
10
+ Metadata about a large file stored in a GCS bucket.
11
+ """
12
+ gcs_link: Optional[str] = "" # Link to the file stored in Google Cloud Storage.
13
+ file_name: Optional[str] = "" # Name of the uploaded file.
14
+ bucket_name: Optional[str] = "" # Name of the bucket where the file is stored.
15
+ upload_time: Optional[datetime] # Time when the file was uploaded.
16
+
17
+
18
+ class ArtifactMetadata(BaseModel):
19
+ """
20
+ Metadata information for an artifact.
21
+ """
22
+ user_id: Optional[str] = "" # The user ID associated with this artifact.
23
+ artifact_name: Optional[str] = "" # Name of the artifact.
24
+ artifact_description: Optional[str] = "" # Description of the artifact.
25
+ artifact_tags: Optional[List[str]] = "" # Comma-separated tags for categorizing the artifact.
26
+ artifact_volume_path: Optional[str] = "" # Path to the volume where the artifact is stored.
27
+
28
+
29
+ class ArtifactData(BaseModel):
30
+ """
31
+ Data related to the artifact's creation, upload, and status.
32
+ """
33
+ artifact_type: Optional[str] = "" # The type of the artifact (e.g., model, DockerImage, etc.).
34
+ artifact_link: Optional[str] = "" # Link to access the artifact.
35
+ artifact_resource: Optional[str] = "" # Resource associated with the artifact (e.g., GCS link, S3 link).
36
+ build_status: BuildStatus # Status of the artifact build (e.g., in progress, succeeded, failed).
37
+ build_error: Optional[str] = "" # Error message if the build failed.
38
+ build_file_name: Optional[str] = "" # Name of the file used for the build.
39
+ status: Optional[str] = "" # Status of the artifact (e.g., active, inactive).
40
+ build_id: Optional[str] = "" # ID of the build process associated with the artifact.
41
+ create_at: Optional[datetime] # Timestamp when the artifact was created.
42
+ update_at: Optional[datetime] # Timestamp when the artifact was last updated.
43
+
44
+
45
+ class Artifact(BaseModel):
46
+ """
47
+ Representation of an artifact, including its data and metadata.
48
+ """
49
+ artifact_id: str # Unique identifier for the artifact.
50
+ artifact_link: Optional[str] = "" # Link to access the artifact.
51
+ build_file_name: Optional[str] = "" # Name of the file used for the build.
52
+ build_status: Optional[BuildStatus] = None # Status of the artifact build (e.g., in progress, succeeded, failed).
53
+ artifact_data: Optional[ArtifactData] = None # Data associated with the artifact.
54
+ artifact_metadata: Optional[ArtifactMetadata] = None # Metadata describing the artifact.
55
+ big_files_metadata: Optional[List[BigFileMetadata]] = None # Metadata for large files associated with the artifact.
56
+
57
+
58
+ class GetAllArtifactsResponse(BaseModel):
59
+ """
60
+ Response containing a list of all artifacts for a user.
61
+ """
62
+ artifacts: list[Artifact] # List of Artifact objects.
63
+
64
+
65
+ class CreateArtifactRequest(BaseModel):
66
+ """
67
+ Request object to create a new artifact.
68
+ """
69
+ user_id: str # The user ID creating the artifact.
70
+ artifact_name: str # The name of the artifact to create.
71
+ artifact_description: Optional[str] = "" # Description of the artifact.
72
+ artifact_tags: Optional[List[str]] = None # Tags for the artifact, separated by commas.
73
+
74
+
75
+ class CreateArtifactResponse(BaseModel):
76
+ """
77
+ Response object after creating an artifact.
78
+ """
79
+ artifact_id: str # ID of the newly created artifact.
80
+ upload_link: str # URL to upload the artifact data.
81
+
82
+
83
+ class GetBigFileUploadUrlRequest(BaseModel):
84
+ """
85
+ Request to generate a pre-signed URL for uploading large files.
86
+ """
87
+ artifact_id: Optional[str] = "" # ID of the artifact for which the upload URL is requested.
88
+ file_name: Optional[str] = "" # Name of the file to upload.
89
+ file_type: Optional[str] = "" # MIME type of the file.
90
+
91
+
92
+ class GetBigFileUploadUrlResponse(BaseModel):
93
+ """
94
+ Response containing a pre-signed upload URL for large files.
95
+ """
96
+ artifact_id: str # ID of the artifact.
97
+ upload_link: str # Pre-signed upload URL for the file.
98
+
99
+
100
+ class RebuildArtifactResponse(BaseModel):
101
+ """
102
+ Response object after rebuilding an artifact.
103
+ """
104
+ artifact_id: str # ID of the rebuilt artifact.
105
+ build_status: BuildStatus # Status of the artifact build (e.g., in progress, succeeded, failed).
106
+
107
+
108
+ class DeleteArtifactResponse(BaseModel):
109
+ """
110
+ Response object after deleting an artifact.
111
+ """
112
+ artifact_id: str # ID of the deleted artifact.
113
+ delete_at: Optional[datetime] = None # Timestamp when the artifact was deleted.
114
+ status: Optional[str] = "" # Status of the deletion process.
115
+
116
+
117
+ class DeleteBigfileRequest(BaseModel):
118
+ """
119
+ Request to delete a large file associated with an artifact.
120
+ """
121
+ artifact_id: str # ID of the artifact for which the large file is to be deleted.
122
+ file_name: str # Name of the large file to delete.
123
+
124
+
125
+ class DeleteBigfileResponse(BaseModel):
126
+ """
127
+ Response object after deleting a large file.
128
+ """
129
+ artifact_id: str # ID of the artifact.
130
+ file_name: str # Name of the deleted file.
131
+ status: Optional[str] = "" # Status of the deletion process.
132
+
133
+
134
+ class GetArtifactTemplatesResponse(BaseModel):
135
+ """
136
+ Response containing a list of artifact templates.
137
+ """
138
+ artifact_templates: list["ArtifactTemplate"] # List of artifact templates.
139
+
140
+
141
+ class ArtifactTemplate(BaseModel):
142
+ """
143
+ Template for creating an artifact.
144
+ """
145
+ artifact_template_id: str # Unique identifier for the artifact template.
146
+ artifact_description: Optional[str] = "" # Description of the artifact template.
147
+ artifact_name: Optional[str] = "" # Name of the artifact template.
148
+ artifact_tags: Optional[List[str]] = None # Tags associated with the artifact template.
149
+
150
+
151
+ class CreateArtifactFromTemplateRequest(BaseModel):
152
+ """
153
+ Request object to create a new artifact from a template.
154
+ """
155
+ user_id: str # The user ID creating the artifact.
156
+ artifact_template_id: str # The ID of the artifact template to use.
157
+
158
+
159
+ class CreateArtifactFromTemplateResponse(BaseModel):
160
+ """
161
+ Response object after creating an artifact from a template
162
+ """
163
+ artifact_id: str # ID of the newly created artifact.
164
+ status: str # Status of the creation process.
165
+
166
+
167
+ class TaskOwner(BaseModel):
168
+ """
169
+ Ownership information of a task.
170
+ """
171
+ user_id: Optional[str] = "" # ID of the user owning the task.
172
+ group_id: Optional[str] = "" # ID of the group the user belongs to.
173
+ service_account_id: Optional[str] = "" # ID of the service account used to execute the task.
174
+
175
+
176
+ class ReplicaResource(BaseModel):
177
+ """
178
+ Resources allocated for task replicas.
179
+ """
180
+ cpu: Optional[int] = 0 # Number of CPU cores allocated.
181
+ ram_gb: Optional[int] = 0 # Amount of RAM (in GB) allocated.
182
+ gpu: Optional[int] = 0 # Number of GPUs allocated.
183
+ gpu_name: Optional[str] = "" # Type or model of the GPU allocated.
184
+
185
+
186
+ class VolumeMount(BaseModel):
187
+ """
188
+ Configuration for mounting volumes in a container.
189
+ """
190
+ access_mode: Optional[str] = "" # Access mode for the volume (e.g., read-only, read-write).
191
+ capacity_GB: Optional[int] = "" # Capacity of the volume in GB.
192
+ host_path: Optional[str] = "" # Path on the host machine where the volume is mounted.
193
+ mount_path: Optional[str] = "" # Path where the volume is mounted in the container.
194
+
195
+
196
+ class RayTaskConfig(BaseModel):
197
+ """
198
+ Configuration settings for Ray tasks.
199
+ """
200
+ artifact_id: Optional[str] = "" # Associated artifact ID.
201
+ ray_version: Optional[str] = "" # Version of Ray used.
202
+ ray_cluster_image: Optional[str] = "" # Docker image for the Ray cluster.
203
+ file_path: Optional[str] = "" # Path to the task file in storage.
204
+ deployment_name: Optional[str] = "" # Name of the deployment.
205
+ replica_resource: Optional[ReplicaResource] = None # Resources allocated for task replicas.
206
+ volume_mounts: Optional[VolumeMount] = None # Configuration for mounted volumes.
207
+
208
+
209
+ class OneOffScheduling(BaseModel):
210
+ """
211
+ Scheduling configuration for a one-time trigger.
212
+ """
213
+ trigger_timestamp: Optional[int] = 0 # Timestamp when the task should start.
214
+ min_replicas: Optional[int] = 0 # Minimum number of replicas to deploy.
215
+ max_replicas: Optional[int] = 0 # Maximum number of replicas to deploy.
216
+
217
+
218
+ class DailyTrigger(BaseModel):
219
+ """
220
+ Scheduling configuration for daily task triggers.
221
+ """
222
+ timezone: Optional[str] = "" # Timezone for the trigger (e.g., "UTC").
223
+ Hour: Optional[int] = 0 # Hour of the day the task should start (0-23).
224
+ minute: Optional[int] = 0 # Minute of the hour the task should start (0-59).
225
+ second: Optional[int] = 0 # Second of the minute the task should start (0-59).
226
+ min_replicas: Optional[int] = 0 # Minimum number of replicas for this daily trigger.
227
+ max_replicas: Optional[int] = 0 # Maximum number of replicas for this daily trigger.
228
+
229
+
230
+ class DailyScheduling(BaseModel):
231
+ """
232
+ Configuration for daily scheduling triggers.
233
+ """
234
+ triggers: Optional[list[DailyTrigger]] = None # List of daily triggers.
235
+
236
+
237
+ class TaskScheduling(BaseModel):
238
+ """
239
+ Complete scheduling configuration for a task.
240
+ """
241
+ scheduling_oneoff: Optional[OneOffScheduling] = None # One-time scheduling configuration.
242
+ scheduling_daily: Optional[DailyScheduling] = None # Daily scheduling configuration.
243
+
244
+
245
+ class TaskConfig(BaseModel):
246
+ """
247
+ Configuration data for a task.
248
+ """
249
+ ray_task_config: Optional[RayTaskConfig] = None # Configuration for a Ray-based task.
250
+ task_scheduling: Optional[TaskScheduling] = None # Scheduling configuration for the task.
251
+ create_timestamp: Optional[int] = 0 # Timestamp when the task was created.
252
+ last_update_timestamp: Optional[int] = 0 # Timestamp when the task was last updated.
253
+
254
+
255
+ class TaskInfo(BaseModel):
256
+ """
257
+ Additional information about a task.
258
+ """
259
+ endpoint_status: Optional[TaskEndpointStatus] = None # Current status of the task (e.g., running, stopped).
260
+ endpoint: Optional[str] = "" # API endpoint exposed by the task, if applicable.
261
+
262
+
263
+ class UserPreference(BaseModel):
264
+ """
265
+ User preference for a task.
266
+ """
267
+ block_list: Optional[List[str]] = None # List of tasks to exclude.
268
+ preference_scale: Optional[int] = 0 # Scale of user preference.
269
+
270
+
271
+ class Task(BaseModel):
272
+ """
273
+ Representation of a task.
274
+ """
275
+ task_id: Optional[str] = None # Unique identifier for the task.
276
+ owner: Optional[TaskOwner] = None # Ownership information of the task.
277
+ config: Optional[TaskConfig] = None # Configuration data for the task.
278
+ info: Optional[TaskInfo] = None # Additional information about the task.
279
+ task_status: Optional[str] = "" # Status of the task.
280
+ readiness_status: Optional[str] = "" # Readiness status of the task.
281
+ user_preference: Optional[UserPreference] = None # User preference for the task.
282
+
283
+
284
+ class GetAllTasksResponse(BaseModel):
285
+ """
286
+ Response containing a list of all tasks.
287
+ """
288
+ tasks: Optional[list[Task]] = None # List of tasks.
289
+
290
+
291
+ class CreateTaskResponse(BaseModel):
292
+ task: Task # The created task.
293
+ upload_link: str # URL to upload the task data.
294
+
295
+
296
+ class LoginResponse(BaseModel):
297
+ """
298
+ Response object for user login.
299
+ """
300
+ accessToken: str # Access token for the user session.
301
+ refreshToken: str # Refresh token for the user session.
302
+
303
+
304
+ class GPUUsage(BaseModel):
305
+ """
306
+ GPU usage data for a task.
307
+ """
308
+ geo_location: Optional[str] = "" # Location of the GPU.
309
+ gpu_count: Optional[int] = "" # Number of GPUs.
310
+ gpu_type: Optional[str] = "" # Type of GPU.
311
+
312
+
313
+ class Usage(BaseModel):
314
+ """
315
+ Usage data for a task.
316
+ """
317
+ user_id: Optional[str] = "" # ID of the user.
318
+ task_id: Optional[str] = "" # ID of the task.
319
+ gpu_usage_list: Optional[List[GPUUsage]] = None # List of GPU usage data.
320
+ replica_count: Optional[int] = 0 # Number of replicas.
321
+ timestamp: Optional[int] = 0 # Timestamp of the usage data.
322
+
323
+
324
+ class GetUsageDataResponse(BaseModel):
325
+ """
326
+ Response containing the usage data of a task.
327
+ """
328
+ usage_data: list[Usage] = None # List of usage data for the task.
329
+
330
+
331
+ class LoginRequest(BaseModel):
332
+ """
333
+ Request object for user login.
334
+ """
335
+ email: str # User email.
336
+ password: str # User password.
gmicloud/client.py ADDED
@@ -0,0 +1,51 @@
1
+ import os
2
+
3
+ from typing import Optional
4
+
5
+ from ._internal._client._iam_client import IAMClient
6
+ from ._internal._manager._artifact_manager import ArtifactManager
7
+ from ._internal._manager._task_manager import TaskManager
8
+
9
+
10
+ class Client:
11
+ def __init__(self, client_id: Optional[str] = "", email: Optional[str] = "", password: Optional[str] = ""):
12
+ if not client_id or not client_id.strip():
13
+ client_id = os.getenv("GMI_CLOUD_CLIENT_ID")
14
+ if not email or not email.strip():
15
+ email = os.getenv("GMI_CLOUD_EMAIL")
16
+ if not password or not password.strip():
17
+ password = os.getenv("GMI_CLOUD_PASSWORD")
18
+
19
+ if not client_id:
20
+ raise ValueError("Client ID must be provided.")
21
+ if not email:
22
+ raise ValueError("Email must be provided.")
23
+ if not password:
24
+ raise ValueError("Password must be provided.")
25
+
26
+ self.iam_client = IAMClient(client_id, email, password)
27
+ self.iam_client.login()
28
+
29
+ # Managers are lazily initialized through private attributes
30
+ self._artifact_manager = None
31
+ self._task_manager = None
32
+
33
+ @property
34
+ def artifact_manager(self):
35
+ """
36
+ Lazy initialization for ArtifactManager.
37
+ Ensures the Client instance controls its lifecycle.
38
+ """
39
+ if self._artifact_manager is None:
40
+ self._artifact_manager = ArtifactManager(self.iam_client)
41
+ return self._artifact_manager
42
+
43
+ @property
44
+ def task_manager(self):
45
+ """
46
+ Lazy initialization for TaskManager.
47
+ Ensures the Client instance controls its lifecycle.
48
+ """
49
+ if self._task_manager is None:
50
+ self._task_manager = TaskManager(self.iam_client)
51
+ return self._task_manager
File without changes