cgcsdk 1.0.5__py3-none-any.whl → 1.0.7__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.
cgc/.env CHANGED
@@ -6,4 +6,4 @@ CONFIG_FILE_NAME = cfg.json
6
6
  TMP_DIR = .tmp
7
7
  RELEASE = 1
8
8
  MAJOR_VERSION = 0
9
- MINOR_VERSION = 5
9
+ MINOR_VERSION = 7
cgc/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.0.7
4
+
5
+ Release on April 04, 2024
6
+
7
+ * speed of resource creation with volumes / shared memory, has been improved
8
+ * added jobs creation: cgc job create
9
+ * added jobs list: cgc job list
10
+ * added jobs delete: cgc job delete
11
+ * added logs (STDOUT, STDERR) reader for spawned applications: cgc logs
12
+ * added SDK for logs & jobs
13
+
14
+ ## 1.0.6
15
+
16
+ Release on March 13, 2024
17
+
18
+ * updated exception handling for SDK
19
+ * fix for cgc CLI command: compute create
20
+ * ensure encode("utf-8") for all payloads generated by CLI/SDK
21
+
3
22
  ## 1.0.5
4
23
 
5
24
  Release on March 7, 2024
@@ -11,7 +30,7 @@ Release on March 7, 2024
11
30
  * allows to mount volume with full path; works with one volume only
12
31
  * added support for adding for compute create config_map_data
13
32
  * added for compute create
14
- * --startup-command flag to set startup command (overrides default command in Dockerfile)
33
+ * startup_command as a STDIN on second argument
15
34
  * --repository-secret flag to set docker registry secret from namespace
16
35
  * --image flag to set docker image for custom template
17
36
  * managing compute ports, require user confirmation
@@ -21,8 +40,7 @@ Release on March 7, 2024
21
40
  * add compute and db **list** to cgc sdk
22
41
  * add resource ready command to cgc sdk
23
42
  * on-premises support for node_port_enabled
24
- * added new command: cgc context replace-env "path_to_env_file"
25
- * added new command: cgc context get-env "where_to_save_env_file"
43
+ * added new command: cgc context get-env-path
26
44
  * added new command: cgc compute list-mounts
27
45
 
28
46
  ## 1.0.4
cgc/cgc.py CHANGED
@@ -4,6 +4,7 @@ from cgc.commands.compute.compute_cmd import compute_group
4
4
  from cgc.commands.billing.billing_cmd import billing_group
5
5
  from cgc.commands.db.db_cmd import db_group
6
6
  from cgc.commands.resource.resource_cmd import resource_group
7
+ from cgc.commands.jobs.jobs_cmd import job_group
7
8
  from cgc.commands.auth.auth_cmd import (
8
9
  api_keys_group,
9
10
  auth_register,
@@ -13,10 +14,12 @@ from cgc.commands.debug.debug_cmd import debug_group
13
14
  from cgc.commands.cgc_cmd import (
14
15
  cgc_rm,
15
16
  cgc_status,
17
+ cgc_logs,
16
18
  sending_telemetry_permission,
17
19
  resource_events,
18
20
  context_group,
19
21
  )
22
+
20
23
  from cgc.utils.version_control import check_version, _get_version
21
24
  from cgc.utils.click_group import CustomGroup
22
25
 
@@ -41,7 +44,8 @@ cli.add_command(resource_group)
41
44
  cli.add_command(billing_group)
42
45
  cli.add_command(cgc_status)
43
46
  cli.add_command(sending_telemetry_permission)
44
-
47
+ cli.add_command(cgc_logs)
48
+ cli.add_command(job_group)
45
49
 
46
50
  if __name__ == "__main__" or __name__ == "cgc.cgc":
47
51
  cli()
cgc/commands/cgc_cmd.py CHANGED
@@ -1,7 +1,10 @@
1
1
  import click
2
2
  import json
3
3
 
4
- from cgc.commands.cgc_cmd_responses import cgc_status_response
4
+ from cgc.commands.cgc_cmd_responses import (
5
+ cgc_logs_response,
6
+ cgc_status_response,
7
+ )
5
8
  from cgc.commands.resource.resource_cmd import resource_delete
6
9
  from cgc.utils.requests_helper import call_api, EndpointTypes
7
10
  from cgc.utils.click_group import CustomCommand, CustomGroup
@@ -9,7 +12,7 @@ from cgc.utils.prepare_headers import get_api_url_and_prepare_headers
9
12
  from cgc.utils.response_utils import retrieve_and_validate_response_send_metric
10
13
  from cgc.telemetry.basic import telemetry_permission_set
11
14
  from cgc.commands.compute.compute_responses import compute_logs_response
12
- from cgc.commands.auth.auth_cmd import auth_register
15
+ # from cgc.commands.auth.auth_cmd import auth_register
13
16
  from cgc.utils import set_environment_data, check_if_config_exist, list_all_config_files
14
17
  from cgc.commands.cgc_helpers import table_of_user_context_files
15
18
  from cgc.utils.config_utils import config_path
@@ -37,7 +40,7 @@ def resource_events(app_name: str):
37
40
  request=EndpointTypes.get,
38
41
  url=url,
39
42
  headers=headers,
40
- data=json.dumps(__payload),
43
+ data=json.dumps(__payload).encode("utf-8"),
41
44
  )
42
45
  click.echo(
43
46
  compute_logs_response(retrieve_and_validate_response_send_metric(__res, metric))
@@ -122,3 +125,25 @@ def folder_of_contexts():
122
125
  def get_env_path():
123
126
  """Displays current environment file path"""
124
127
  click.echo(f"Current environment file path: {ENV_FILE_PATH}")
128
+
129
+
130
+ @click.command("logs", cls=CustomCommand)
131
+ @click.argument("app_name", type=click.STRING)
132
+ def cgc_logs(app_name):
133
+ """Displays logs of a given app"""
134
+
135
+ if not app_name:
136
+ raise click.ClickException("Please provide a non-empty name")
137
+
138
+ api_url, headers = get_api_url_and_prepare_headers()
139
+ url = f"{api_url}/v1/api/resource/logs/{app_name}"
140
+ metric = "logs.get"
141
+ __res = call_api(
142
+ request=EndpointTypes.get,
143
+ url=url,
144
+ headers=headers,
145
+ )
146
+
147
+ click.echo(
148
+ cgc_logs_response(retrieve_and_validate_response_send_metric(__res, metric))
149
+ )
@@ -72,3 +72,11 @@ def cgc_status_response(data: dict):
72
72
  ),
73
73
  headers=list_headers,
74
74
  )
75
+
76
+
77
+ def cgc_logs_response(data: dict):
78
+ return "\n".join(
79
+ "==> %s/%s <==\n%s" % (pod, cont, log)
80
+ for pod, containers in data["details"]["logs"].items()
81
+ for cont, log in containers.items()
82
+ )
@@ -31,15 +31,3 @@ def table_of_user_context_files(config_files: List[str]):
31
31
  contexts_sorted.append(contexts[contexts_nrs.index(context)])
32
32
 
33
33
  return tabulate(contexts_sorted, headers=headers)
34
-
35
- def process_stdin(input_file: str):
36
- full_stdin = []
37
- try:
38
- for line in input_file:
39
- line = line.strip()
40
- if line and line != '|':
41
- full_stdin.append(line.rstrip('\n')) # Remove newline characters
42
- except TypeError:
43
- return None
44
- return " ".join(full_stdin)
45
-
@@ -1,7 +1,6 @@
1
1
  import json
2
2
  import sys
3
3
  import click
4
- from cgc.commands.cgc_helpers import process_stdin
5
4
 
6
5
  from cgc.commands.compute.compute_models import ComputesList, GPUsList
7
6
  from cgc.commands.compute.compute_responses import (
@@ -61,7 +60,7 @@ def compute_filebrowser_create(puid: int, pgid: int):
61
60
  request=EndpointTypes.post,
62
61
  url=url,
63
62
  headers=headers,
64
- data=json.dumps(__payload),
63
+ data=json.dumps(__payload).encode("utf-8"),
65
64
  )
66
65
  click.echo(
67
66
  compute_create_filebrowser_response(
@@ -119,7 +118,10 @@ def compute_port_add(app_name: str, port_name: str, port: int, ingress: bool):
119
118
  app_name=app_name,
120
119
  )
121
120
  __res = call_api(
122
- request=EndpointTypes.post, url=url, headers=headers, data=json.dumps(__payload)
121
+ request=EndpointTypes.post,
122
+ url=url,
123
+ headers=headers,
124
+ data=json.dumps(__payload).encode("utf-8"),
123
125
  )
124
126
 
125
127
  __res_list = get_compute_port_list(
@@ -181,7 +183,10 @@ def compute_port_update(app_name: str, port_name: str, port: int, ingress: bool)
181
183
  app_name=app_name,
182
184
  )
183
185
  __res = call_api(
184
- request=EndpointTypes.post, url=url, headers=headers, data=json.dumps(__payload)
186
+ request=EndpointTypes.post,
187
+ url=url,
188
+ headers=headers,
189
+ data=json.dumps(__payload).encode("utf-8"),
185
190
  )
186
191
 
187
192
  retrieve_and_validate_response_send_metric(__res, metric)
@@ -219,7 +224,10 @@ def compute_port_delete(app_name: str, port_name: str):
219
224
  app_name=app_name,
220
225
  )
221
226
  __res = call_api(
222
- request=EndpointTypes.post, url=url, headers=headers, data=json.dumps(__payload)
227
+ request=EndpointTypes.post,
228
+ url=url,
229
+ headers=headers,
230
+ data=json.dumps(__payload).encode("utf-8"),
223
231
  )
224
232
 
225
233
  retrieve_and_validate_response_send_metric(__res, metric)
@@ -251,7 +259,7 @@ def compute_port_list(app_name: str):
251
259
 
252
260
  @compute_group.command("create", cls=CustomCommand)
253
261
  @click.argument("entity", type=click.Choice(ComputesList.get_list()))
254
- @click.argument("startup_command", type=click.File("r"), default="-", required=False)
262
+ @click.argument("startup_command", required=False)
255
263
  @click.option(
256
264
  "-n", "--name", "name", type=click.STRING, required=True, help="Desired app name"
257
265
  )
@@ -393,8 +401,13 @@ def compute_create(
393
401
  """
394
402
  api_url, headers = get_api_url_and_prepare_headers()
395
403
  url = f"{api_url}/v1/api/resource/create"
396
- if startup_command != "-":
397
- startup_command = process_stdin(startup_command)
404
+ cleaned_data = ""
405
+ if not sys.stdin.isatty():
406
+ input_data = sys.stdin.read()
407
+ cleaned_data = input_data.replace("|", "")
408
+ startup_command = cleaned_data
409
+ elif startup_command:
410
+ cleaned_data = startup_command
398
411
  metric = "compute.create"
399
412
  __payload = compute_create_payload(
400
413
  name=name,
@@ -409,7 +422,7 @@ def compute_create(
409
422
  gpu_type=gpu_type,
410
423
  shm_size=shm_size,
411
424
  image_name=image_name,
412
- startup_command=startup_command,
425
+ startup_command=cleaned_data,
413
426
  repository_secret=repository_secret,
414
427
  node_port_enabled=node_port_enabled,
415
428
  )
@@ -443,7 +456,7 @@ def compute_create(
443
456
  request=EndpointTypes.post,
444
457
  url=url,
445
458
  headers=headers,
446
- data=json.dumps(__payload),
459
+ data=json.dumps(__payload).encode("utf-8"),
447
460
  )
448
461
  click.echo(
449
462
  compute_create_response(
@@ -54,3 +54,5 @@ class GPUsList(CGCEntityList):
54
54
  A100 = "A100"
55
55
  A5000 = "A5000"
56
56
  H100 = "H100"
57
+ P40 = "P40"
58
+ P100 = "P100"
@@ -1,6 +1,6 @@
1
- from ast import main
2
1
  import cgc.utils.consts.env_consts as env_consts
3
2
 
3
+
4
4
  def list_get_mounted_volumes_paths(volume_list: list) -> str:
5
5
  """Formats and returns list of PVC volumes mounted to an app.
6
6
 
@@ -20,6 +20,7 @@ def list_get_mounted_volumes_paths(volume_list: list) -> str:
20
20
  )
21
21
  return volumes_mounted
22
22
 
23
+
23
24
  def list_get_mounted_volumes(volume_list: list) -> str:
24
25
  """Formats and returns list of PVC volumes mounted to an app.
25
26
 
@@ -39,16 +40,21 @@ def list_get_mounted_volumes(volume_list: list) -> str:
39
40
  )
40
41
  return volumes_mounted
41
42
 
42
- def get_app_mounts(pod_list:list) -> list:
43
+
44
+ def get_app_mounts(pod_list: list) -> list:
43
45
  output_data = []
44
-
46
+
45
47
  for pod in pod_list:
46
48
  try:
47
- main_container_name = pod["labels"]["entity"]
49
+ main_container_name = pod["labels"]["entity"]
48
50
  try:
49
- main_container = [x for x in pod["containers"] if x["name"] == main_container_name][0]
51
+ main_container = [
52
+ x for x in pod["containers"] if x["name"] == main_container_name
53
+ ][0]
50
54
  except IndexError:
51
- raise Exception("Parser was unable to find main container in server output in container list")
55
+ raise Exception(
56
+ "Parser was unable to find main container in server output in container list"
57
+ )
52
58
  volumes_mounted = list_get_mounted_volumes(main_container["mounts"])
53
59
  volumes_paths = list_get_mounted_volumes_paths(main_container["mounts"])
54
60
  pod_data = {
@@ -63,6 +69,7 @@ def get_app_mounts(pod_list:list) -> list:
63
69
  pass
64
70
  return output_data
65
71
 
72
+
66
73
  def get_app_list(pod_list: list, detailed: bool) -> list:
67
74
  """Formats and returns list of apps to print.
68
75
 
@@ -110,6 +117,9 @@ def get_app_list(pod_list: list, detailed: bool) -> list:
110
117
  pod["labels"]["url"] = pod["labels"]["pod_url"]
111
118
  pod["labels"].pop("app-token")
112
119
  pod["labels"].pop("pod_url")
120
+ pod["labels"].pop("resource-type")
121
+ pod["labels"].pop("api-key-id", None)
122
+ pod["labels"].pop("user-id", None)
113
123
 
114
124
  # appending the rest of labels
115
125
  pod_data.update(pod["labels"])
cgc/commands/db/db_cmd.py CHANGED
@@ -80,7 +80,7 @@ def db_create(
80
80
  request=EndpointTypes.post,
81
81
  url=url,
82
82
  headers=headers,
83
- data=json.dumps(__payload),
83
+ data=json.dumps(__payload).encode("utf-8"),
84
84
  )
85
85
  click.echo(
86
86
  compute_create_response(
@@ -0,0 +1,10 @@
1
+ from cgc.commands.exceptions import ResponseException
2
+
3
+
4
+ class JobCommandException(ResponseException):
5
+ pass
6
+
7
+
8
+ class NoJobsToList(JobCommandException):
9
+ def __init__(self) -> None:
10
+ super().__init__("No jobs to list.")
@@ -0,0 +1,175 @@
1
+ from typing import Optional
2
+ from cgc.commands.compute.compute_utills import list_get_mounted_volumes
3
+ import cgc.utils.consts.env_consts as env_consts
4
+
5
+
6
+ def job_delete_payload(name):
7
+ """
8
+ Create payload for job delete.
9
+ """
10
+ payload = {
11
+ "name": name,
12
+ }
13
+ return payload
14
+
15
+
16
+ def job_create_payload(
17
+ name,
18
+ cpu,
19
+ memory,
20
+ volumes: list,
21
+ volume_full_path: str,
22
+ resource_data: list = [],
23
+ config_maps_data: list = [],
24
+ gpu: int = 0,
25
+ gpu_type: str = None,
26
+ shm_size: int = 0,
27
+ image_name: str = "",
28
+ startup_command: str = "",
29
+ repository_secret: str = "",
30
+ ttl_seconds_after_finished: Optional[int] = None,
31
+ ):
32
+ """
33
+ Create payload for app creation.
34
+ """
35
+ extra_payload = {}
36
+ if shm_size is not None and shm_size != 0:
37
+ extra_payload["shared_memory"] = shm_size
38
+
39
+ if ttl_seconds_after_finished is not None:
40
+ extra_payload["ttl_seconds_after_finished"] = ttl_seconds_after_finished
41
+
42
+ payload = {
43
+ "resource_data": {
44
+ "name": name,
45
+ "cpu": cpu,
46
+ "gpu": gpu,
47
+ "memory": memory,
48
+ "gpu_type": gpu_type,
49
+ "full_mount_path": volume_full_path,
50
+ **extra_payload,
51
+ }
52
+ }
53
+ try:
54
+ if len(volumes) != 0:
55
+ if not volume_full_path:
56
+ payload["resource_data"]["pv_volume"] = volumes
57
+ elif volume_full_path and len(volumes) != 1:
58
+ raise Exception(
59
+ "Volume full path can only be used with a single volume"
60
+ )
61
+ else:
62
+ payload["resource_data"]["pv_volume"] = volumes
63
+ except TypeError:
64
+ pass
65
+ try:
66
+ resource_data_dict = {"resource_data": {}}
67
+ if len(resource_data) != 0:
68
+ for resource in resource_data:
69
+ try:
70
+ key, value = resource.split("=")
71
+ resource_data_dict["resource_data"][key] = value
72
+ except ValueError:
73
+ raise Exception(
74
+ "Invalid resource data format. Use key=value format"
75
+ )
76
+ if image_name:
77
+ resource_data_dict["resource_data"]["custom_image"] = image_name
78
+ if startup_command:
79
+ resource_data_dict["resource_data"]["custom_command"] = startup_command
80
+ if repository_secret:
81
+ resource_data_dict["resource_data"][
82
+ "image_pull_secret_name"
83
+ ] = repository_secret
84
+ if resource_data_dict["resource_data"] != {}:
85
+ payload["template_specific_data"] = resource_data_dict
86
+ except TypeError:
87
+ pass
88
+ try:
89
+ if len(config_maps_data) != 0:
90
+ config_maps_data_dict = {}
91
+ for config_map in config_maps_data:
92
+ try:
93
+ key, value = config_map.split(
94
+ "="
95
+ ) # where key is name of config map and value is data
96
+ config_maps_data_dict[key] = (
97
+ value # value is dict, ex.: {"key": "value"}
98
+ )
99
+ except ValueError:
100
+ raise Exception(
101
+ "Invalid config map data format. Use key=value format"
102
+ )
103
+ payload["config_maps_data"] = config_maps_data_dict
104
+ except TypeError:
105
+ pass
106
+ return payload
107
+
108
+
109
+ def get_job_list(job_pod_list: list, job_list: list):
110
+ list_of_json_data = get_job_pod_list(job_pod_list)
111
+ for json_data in list_of_json_data:
112
+ for job in job_list:
113
+ if job.get("name") == json_data.get("name"):
114
+ json_data["ttl_seconds_after_finished"] = job.get(
115
+ "ttl_seconds_after_finished", "N/A"
116
+ )
117
+ break
118
+ return list_of_json_data
119
+
120
+
121
+ def get_job_pod_list(job_pod_list: list) -> list:
122
+ """Formats and returns list of jobs to print.
123
+
124
+ :param pod_list: list of pods
125
+ :type pod_list: list
126
+ :return: formatted list of apps
127
+ :rtype: list
128
+ """
129
+ output_data = []
130
+
131
+ for pod in job_pod_list:
132
+ try:
133
+ main_container_name = "custom-job"
134
+ try:
135
+ main_container = [
136
+ x
137
+ for x in pod.get("containers", [])
138
+ if x.get("name") == main_container_name
139
+ ][0]
140
+ except IndexError:
141
+ raise Exception(
142
+ "Parser was unable to find main container in server output in container list"
143
+ )
144
+ volumes_mounted = list_get_mounted_volumes(main_container.get("mounts", []))
145
+ limits = main_container.get("resources", {}).get("limits")
146
+ cpu = limits.get("cpu") if limits is not None else 0
147
+ ram = limits.get("memory") if limits is not None else "0Gi"
148
+
149
+ pod_data = {
150
+ "name": pod.get("labels", {}).get("app-name"),
151
+ "status": pod.get("status", {}),
152
+ "volumes_mounted": volumes_mounted,
153
+ "cpu": cpu,
154
+ "ram": ram,
155
+ "gpu-count": pod.get("labels", {}).get("gpu-count", 0),
156
+ "gpu-label": pod.get("labels", {}).get("gpu-label", "N/A"),
157
+ }
158
+ # getting rid of unwanted and used values
159
+ if "pod-template-hash" in pod["labels"].keys():
160
+ pod["labels"].pop("pod-template-hash")
161
+ pod["labels"].pop("app-name")
162
+ pod["labels"].pop("entity")
163
+ pod["labels"].pop("resource-type")
164
+ pod["labels"].pop("job-name")
165
+ pod["labels"].pop("controller-uid")
166
+ pod["labels"].pop("api-key-id", None)
167
+ pod["labels"].pop("user-id", None)
168
+
169
+ # appending the rest of labels
170
+ pod_data.update(pod["labels"])
171
+ output_data.append(pod_data)
172
+ except KeyError:
173
+ pass
174
+
175
+ return output_data