cgcsdk 1.2.5__py3-none-any.whl → 1.3.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.
cgc/.env CHANGED
@@ -5,5 +5,5 @@ API_SECURE_CONNECTION=yes
5
5
  CONFIG_FILE_NAME = cfg.json
6
6
  TMP_DIR = .tmp
7
7
  RELEASE = 1
8
- MAJOR_VERSION = 2
9
- MINOR_VERSION = 5
8
+ MAJOR_VERSION = 3
9
+ MINOR_VERSION = 0
cgc/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.3.0
4
+
5
+ Release on July 08, 2025
6
+
7
+ * stop events are not longer supported
8
+ * billing endpoints v1 are not longer supported
9
+ * added support for v2 billing endpoints
10
+ * added `cgc billing pricing` command
11
+ * error messages are now more descriptive
12
+
13
+ ## 1.2.6
14
+
15
+ Release on Apr 30, 2025
16
+
17
+ * `cgc resource scale up` scales up the resource replicas, with the given name
18
+ * `cgc resource scale down` scales down the resource replicas, with the given name
19
+ * modified error message displayed - uses custom_exceptions
20
+ * listing of `TurnedOff` resources is now available
21
+ * `TurnedOn` available if the resource is not running, but it is scaled up
22
+
3
23
  ## 1.2.5
4
24
 
5
25
  Release on Apr 16, 2025
@@ -2,12 +2,10 @@ import click
2
2
 
3
3
  from datetime import datetime
4
4
 
5
- from cgc.commands.compute.billing.billing_utils import verify_input_datetime
6
5
  from cgc.commands.compute.billing.billing_responses import (
6
+ billing_pricing_response,
7
7
  billing_status_response,
8
8
  billing_invoice_response,
9
- stop_events_resource_response,
10
- stop_events_volume_response,
11
9
  )
12
10
  from cgc.utils.prepare_headers import get_api_url_and_prepare_headers
13
11
  from cgc.utils.response_utils import retrieve_and_validate_response_send_metric
@@ -24,12 +22,22 @@ def billing_group():
24
22
 
25
23
 
26
24
  @billing_group.command("status", cls=CustomCommand)
27
- def billing_status():
25
+ @click.option(
26
+ "--detailed",
27
+ "-d",
28
+ "detailed",
29
+ prompt=True,
30
+ type=bool,
31
+ default=False,
32
+ help="If true, returns detailed invoice information",
33
+ is_flag=True,
34
+ )
35
+ def billing_status(detailed: bool):
28
36
  """
29
37
  Shows billing status for user namespace
30
38
  """
31
39
  api_url, headers = get_api_url_and_prepare_headers()
32
- url = f"{api_url}/v1/api/billing/status"
40
+ url = f"{api_url}/v2/api/billing/status?details={detailed}"
33
41
  metric = "billing.status"
34
42
  __res = call_api(request=EndpointTypes.get, url=url, headers=headers)
35
43
  click.echo(
@@ -64,12 +72,24 @@ def _get_previous_year_if_required():
64
72
  type=click.IntRange(1, 12),
65
73
  default=_get_previous_month(),
66
74
  )
67
- def billing_invoice(year: int, month: int):
75
+ @click.option(
76
+ "--detailed",
77
+ "-d",
78
+ "detailed",
79
+ prompt=True,
80
+ type=bool,
81
+ default=False,
82
+ help="If true, returns detailed invoice information",
83
+ is_flag=True,
84
+ )
85
+ def billing_invoice(year: int, month: int, detailed: bool):
68
86
  """
69
87
  Opens invoice from given year and month
70
88
  """
71
89
  api_url, headers = get_api_url_and_prepare_headers()
72
- url = f"{api_url}/v1/api/billing/invoice?year={year}&month={month}"
90
+ url = (
91
+ f"{api_url}/v2/api/billing/invoice?year={year}&month={month}&details={detailed}"
92
+ )
73
93
  metric = "billing.invoice"
74
94
  __res = call_api(request=EndpointTypes.get, url=url, headers=headers)
75
95
 
@@ -82,78 +102,17 @@ def billing_invoice(year: int, month: int):
82
102
  )
83
103
 
84
104
 
85
- @click.group("stop_events", cls=CustomGroup)
86
- def stop_events_group():
87
- """
88
- List stop events information.
89
- """
90
- pass
91
-
92
-
93
- @stop_events_group.command("resource")
94
- @click.option(
95
- "--date_from",
96
- "-f",
97
- "date_from",
98
- prompt="Date from (DD-MM-YYYY)",
99
- default=datetime.now().replace(day=1).strftime("%d-%m-%Y"),
100
- help="Start date for filtering stop events",
101
- )
102
- @click.option(
103
- "--date_to",
104
- "-t",
105
- "date_to",
106
- prompt="Date to (DD-MM-YYYY)",
107
- default=datetime.now().strftime("%d-%m-%Y"),
108
- help="End date for filtering stop events",
109
- )
110
- def stop_events_resource(date_from, date_to):
111
- """
112
- List resource stop events information for a given time period
113
- """
114
- verify_input_datetime(date_from, date_to)
115
- api_url, headers = get_api_url_and_prepare_headers()
116
- url = f"{api_url}/v1/api/billing/list_resource_stop_events?time_from={date_from}&time_till={date_to}"
117
- metric = "billing.stop_events.resource"
118
- __res = call_api(request=EndpointTypes.get, url=url, headers=headers)
119
- click.echo(
120
- stop_events_resource_response(
121
- retrieve_and_validate_response_send_metric(__res, metric)
122
- )
123
- )
124
-
125
-
126
- @stop_events_group.command("volume")
127
- @click.option(
128
- "--date_from",
129
- "-f",
130
- "date_from",
131
- prompt="Date from (DD-MM-YYYY)",
132
- default=datetime.now().replace(day=1).strftime("%d-%m-%Y"),
133
- help="Start date for filtering stop events",
134
- )
135
- @click.option(
136
- "--date_to",
137
- "-t",
138
- "date_to",
139
- prompt="Date to (DD-MM-YYYY)",
140
- default=datetime.now().strftime("%d-%m-%Y"),
141
- help="End date for filtering stop events",
142
- )
143
- def stop_events_volume(date_from, date_to):
105
+ @billing_group.command("pricing", cls=CustomCommand)
106
+ def billing_pricing():
144
107
  """
145
- List volume stop events information for a given time period
108
+ Shows billing pricing information for user
146
109
  """
147
- verify_input_datetime(date_from, date_to)
148
110
  api_url, headers = get_api_url_and_prepare_headers()
149
- url = f"{api_url}/v1/api/billing/list_storage_stop_events?time_from={date_from}&time_till={date_to}"
150
- metric = "billing.stop_events.volume"
111
+ url = f"{api_url}/v2/api/billing/user_pricing"
112
+ metric = "billing.pricing"
151
113
  __res = call_api(request=EndpointTypes.get, url=url, headers=headers)
152
114
  click.echo(
153
- stop_events_volume_response(
115
+ billing_pricing_response(
154
116
  retrieve_and_validate_response_send_metric(__res, metric)
155
117
  )
156
118
  )
157
-
158
-
159
- billing_group.add_command(stop_events_group)
@@ -1,15 +1,12 @@
1
1
  import calendar
2
+ import click
3
+ from decimal import Decimal
4
+ from tabulate import tabulate
2
5
  from cgc.commands.compute.billing import (
3
6
  NoCostsFound,
4
7
  NoInvoiceFoundForSelectedMonth,
5
- NoResourceStopEvents,
6
- NoVolumeStopEvents,
7
- )
8
- from cgc.commands.compute.billing.billing_utils import (
9
- get_billing_status_message,
10
- get_table_compute_stop_events_message,
11
- get_table_volume_stop_events_message,
12
8
  )
9
+ from cgc.commands.compute.billing.billing_utils import get_billing_status_message
13
10
  from cgc.utils.message_utils import key_error_decorator_for_helpers
14
11
 
15
12
 
@@ -17,10 +14,11 @@ from cgc.utils.message_utils import key_error_decorator_for_helpers
17
14
  def billing_status_response(data: dict) -> str:
18
15
  total_cost = data["details"]["cost_total"]
19
16
  namespace = data["details"]["namespace"]
20
- user_list = data["details"]["details"]
21
- if not user_list:
17
+ billing_records = data["details"]["billing_records"]
18
+ details = data["details"].get("details", [])
19
+ if not billing_records:
22
20
  raise NoCostsFound()
23
- message = get_billing_status_message(user_list)
21
+ message = get_billing_status_message(billing_records, details)
24
22
  message += f"Total cost for namespace {namespace}: {total_cost:.2f} pln"
25
23
  return message
26
24
 
@@ -29,27 +27,39 @@ def billing_status_response(data: dict) -> str:
29
27
  def billing_invoice_response(year: int, month: int, data: dict) -> str:
30
28
  total_cost = float(data["details"]["cost_total"])
31
29
  namespace = data["details"]["namespace"]
32
- users_invoices_list = data["details"]["invoice"]
30
+ billing_records = data["details"]["billing_records"]
31
+ details = data["details"].get("details", [])
33
32
  if (
34
- not users_invoices_list or total_cost == 0
33
+ not billing_records or total_cost == 0
35
34
  ): # TODO: total_cost == 0 is it correct thinking?
36
35
  raise NoInvoiceFoundForSelectedMonth(year, month)
37
- message = get_billing_status_message(users_invoices_list)
36
+ message = get_billing_status_message(billing_records, details)
38
37
  message += f"Total cost for namespace {namespace} in {calendar.month_name[month]} {year}: {total_cost:.2f} pln"
39
38
  return message
40
39
 
41
40
 
42
41
  @key_error_decorator_for_helpers
43
- def stop_events_resource_response(data: dict) -> str:
44
- event_list = data["details"]["event_list"]
45
- if not event_list:
46
- raise NoResourceStopEvents()
47
- return get_table_compute_stop_events_message(event_list)
42
+ def billing_pricing_response(data: dict) -> str:
43
+ """Create response string for billing pricing command.
48
44
 
45
+ :return: Response string.
46
+ :rtype: str
47
+ """
48
+ pricing_details = data["details"]["pricing_details"]
49
+ if not pricing_details:
50
+ return "No pricing details available."
51
+ if pricing_details.get("tier"):
52
+ tier = pricing_details["tier"] or "DEFAULT"
53
+ click.echo(f"Current pricing tier: {tier}")
54
+ if not pricing_details.get("resources"):
55
+ return "No resources costs available."
56
+ headers = ["Resource", "Price per unit (pln) / (second OR token)"]
57
+ pricing_data = [
58
+ (resource, f"{Decimal(str(price)):.7f}")
59
+ for resource, price in pricing_details.get("resources").items()
60
+ ]
61
+ click.echo(
62
+ "Pricing values displayed are approximated due to float representation. For exact values, refer to the billing system dashboard."
63
+ )
49
64
 
50
- @key_error_decorator_for_helpers
51
- def stop_events_volume_response(data: dict) -> str:
52
- event_list = data["details"]["event_list"]
53
- if not event_list:
54
- raise NoVolumeStopEvents()
55
- return get_table_volume_stop_events_message(event_list)
65
+ return tabulate(pricing_data, headers=headers)
@@ -17,7 +17,7 @@ def verify_input_datetime(*args):
17
17
 
18
18
 
19
19
  def _get_costs_list_for_user(costs_list: list):
20
- """Method to format data in costs list to be displayed in a table and calculate user cost
20
+ """Format data in costs list to be displayed in a table and calculate user cost
21
21
 
22
22
  :param costs_list: list of costs for user
23
23
  :type costs_list: list
@@ -25,83 +25,142 @@ def _get_costs_list_for_user(costs_list: list):
25
25
  :rtype: user_costs_list_to_print: list, total_user_cost: float
26
26
  """
27
27
  user_costs_list_to_print = []
28
- oneoff_aggregated = {}
29
28
  total_user_cost = 0
30
29
 
31
30
  for cost in costs_list:
32
- if cost["type"] == "events_resource":
33
- resources = cost["resources"]
34
- user_costs_list_to_print.append([resources["entity"],
35
- resources["name"], cost["rent_start_cost"],
36
- cost["rent_end_cost"], f"{int(cost['rent_time_cost'])} s",
37
- f"{float(cost['cost_total']):.2f} pln"])
38
- elif cost["type"] == "events_volume":
39
- resources = cost["resources"]
40
- user_costs_list_to_print.append(["volume", resources["name"],
41
- cost["rent_start_cost"], cost["rent_end_cost"],
42
- f"{int(cost['rent_time_cost'])} s",
43
- f"{float(cost['cost_total']):.2f} pln"])
44
- elif cost["type"] == "events_oneoff":
45
- product = cost["product"]
46
- if product not in oneoff_aggregated:
47
- oneoff_aggregated[product] = {
48
- "quantity": 0,
49
- "cost_total": 0
50
- }
51
- oneoff_aggregated[product]["quantity"] += cost["quantity"]
52
- oneoff_aggregated[product]["cost_total"] += cost["cost_total"]
31
+ if "resource_cost" in cost:
32
+ if cost.get("type", "") == "oneoff":
33
+ for resource, value in cost["resource_cost"].items():
34
+ user_costs_list_to_print.append(
35
+ [
36
+ cost.get("type", ""),
37
+ cost.get("namespace", ""),
38
+ f"{resource}: {float(value):.2f} pln",
39
+ "-",
40
+ "-",
41
+ "<<-",
42
+ ]
43
+ )
44
+ total_user_cost += float(cost.get("cost_total", 0))
45
+ else:
46
+ resource_cost_str = ", ".join(
47
+ f"{k}: {float(v):.2f}" for k, v in cost["resource_cost"].items()
48
+ )
49
+ start, end = "-", "-"
50
+ if "datetime_range" in cost and len(cost["datetime_range"]) == 2:
51
+ start, end = cost["datetime_range"]
52
+ user_costs_list_to_print.append(
53
+ [
54
+ cost.get("type", ""),
55
+ cost.get("namespace", ""),
56
+ resource_cost_str,
57
+ start,
58
+ end,
59
+ f"{float(cost.get('cost_total', 0)):.2f} pln",
60
+ ]
61
+ )
62
+ total_user_cost += float(cost.get("cost_total", 0))
63
+
64
+ if costs_list:
65
+ user_costs_list_to_print.sort(key=lambda d: f"{d[0]} {d[1]}")
66
+ return user_costs_list_to_print, total_user_cost
67
+
68
+
69
+ def _get_costs_list_for_user_with_details(costs_list: list):
70
+ """Format data in costs list to be displayed in a table and calculate user cost with details
71
+
72
+ :param costs_list: list of costs for user
73
+ :type costs_list: list
74
+ :return: formatted list of costs and total cost for user
75
+ :rtype: user_costs_list_to_print: list, total_user_cost: float
76
+ """
77
+ user_costs_list_to_print = []
78
+ total_user_cost = 0
79
+
80
+ for cost in costs_list:
81
+ # Support both old and new structure
82
+ record = cost.get("record", {})
83
+ name = record.get("name", cost.get("name", ""))
84
+ # id_ = record.get("id", cost.get("id", ""))
85
+ user_id = record.get("user_id", cost.get("user_id", ""))
86
+ namespace = record.get("namespace", cost.get("namespace", ""))
87
+ type_ = record.get("type", cost.get("type", ""))
88
+ resource_cost = cost.get("resource_cost", {})
89
+ if type_ == "oneoff":
90
+ for resource, value in resource_cost.items():
91
+ user_costs_list_to_print.append(
92
+ [
93
+ name,
94
+ user_id,
95
+ namespace,
96
+ type_,
97
+ f"{resource}: {float(value):.2f} pln",
98
+ "-",
99
+ "-",
100
+ "<<-",
101
+ ]
102
+ )
103
+ total_user_cost += float(cost.get("cost_total", 0))
53
104
  else:
54
- user_costs_list_to_print.append(["ERROR", "", "", "", "", ""])
55
- total_user_cost += cost["cost_total"]
56
-
57
- for product, data in oneoff_aggregated.items():
58
- user_costs_list_to_print.append([
59
- "oneoff",
60
- product,
61
- "-",
62
- "-",
63
- f"{data['quantity']:d}",
64
- f"{data['cost_total']:.8f} pln"
65
- ])
105
+ resource_cost_str = ", ".join(
106
+ f"{k}: {float(v):.2f}" for k, v in resource_cost.items()
107
+ )
108
+ start = cost.get("calculation_start_time", "-")
109
+ end = cost.get("calculation_end_time", "-")
110
+ cost_total = cost.get("cost_total", 0)
111
+
112
+ user_costs_list_to_print.append(
113
+ [
114
+ name,
115
+ # id_,
116
+ user_id,
117
+ namespace,
118
+ type_,
119
+ resource_cost_str,
120
+ start,
121
+ end,
122
+ f"{float(cost_total):.2f} pln",
123
+ ]
124
+ )
125
+ total_user_cost += float(cost_total)
66
126
 
67
127
  if costs_list:
68
- user_costs_list_to_print.sort(key=lambda d: f"{d[0]} {d[2]}")
128
+ user_costs_list_to_print.sort(key=lambda d: f"{d[0]} {d[1]}")
69
129
  return user_costs_list_to_print, total_user_cost
70
130
 
71
131
 
72
- def get_billing_status_message(user_list: list):
132
+ def get_billing_status_message(billing_records: list, details: list = []):
73
133
  """Prints billing status for all users in a pretty table
74
134
 
75
135
  :param user_list: list of users with costs
76
136
  :type user_list: list
77
137
  """
78
138
  message = ""
79
- for user in user_list:
80
- user_id = user["user_id"]
81
- costs_list = user["details"]
82
- costs_list_to_print, _user_cost = _get_costs_list_for_user(costs_list)
139
+ users = set(
140
+ record.get("user_id", "") for record in billing_records
141
+ ) # Get unique user IDs
142
+ if not users:
143
+ return "No billing records found."
144
+ for user in users:
145
+ user_records = [
146
+ record for record in billing_records if record["user_id"] == user
147
+ ]
148
+ costs_list_to_print, _user_cost = _get_costs_list_for_user(user_records)
83
149
  list_headers = _get_status_list_headers()
84
- message += f"Billing status for user: {user_id}\n"
150
+ message += f"Billing status for user: {user}\n"
85
151
  message += tabulate(costs_list_to_print, headers=list_headers)
86
152
  message += f"\n\nSummary user cost: {float(_user_cost):.2f} pln\n\n"
153
+ if details:
154
+ message += "Detailed billing records:\n"
155
+ costs_list_to_print, _ = _get_costs_list_for_user_with_details(details)
156
+ if not costs_list_to_print:
157
+ message += "No detailed billing records found.\n"
158
+ else:
159
+ list_headers = _get_billing_details_list_headers()
160
+ message += tabulate(costs_list_to_print, headers=list_headers)
161
+ message += "\n\n"
87
162
  return message
88
163
 
89
- # "status": "Success",
90
- # "reason": "BILLING_INVOICE",
91
- # "details": {
92
- # "namespace": "pytest",
93
- # "cost_total": 0,
94
- # "invoice": [
95
- # {
96
- # "user_id": "e57c8668-6bc3-47c7-85de-903bfc3772b7",
97
- # "month": 11,
98
- # "year": 2022,
99
- # "cost_total": 0,
100
- # "details": [],
101
- # }
102
- # ],
103
- # "date_requested": {"year": 2022, "month": 11},
104
-
105
164
 
106
165
  def _get_status_list_headers():
107
166
  """Generates headers for billing status command
@@ -109,7 +168,26 @@ def _get_status_list_headers():
109
168
  :return: list of headers
110
169
  :rtype: list
111
170
  """
112
- return ["entity", "name", "start", "end", "quantity", "cost"]
171
+ return ["type", "namespace", "resource breakdown", "start", "end", "cost"]
172
+
173
+
174
+ def _get_billing_details_list_headers():
175
+ """Generates headers for billing details command
176
+
177
+ :return: list of headers
178
+ :rtype: list
179
+ """
180
+ return [
181
+ "name",
182
+ # "id",
183
+ "user_id",
184
+ "namespace",
185
+ "type",
186
+ "resource breakdown",
187
+ "start",
188
+ "end",
189
+ "cost_total",
190
+ ]
113
191
 
114
192
 
115
193
  # TODO: refactor to use: tabulate_a_response(data: list) -> str:
@@ -62,6 +62,10 @@ def compute_list_response(detailed: bool, data: dict) -> str:
62
62
  pod_list = data["details"]["pods_list"]
63
63
  setup_gauge(f"{get_namespace()}.compute.count", len(pod_list))
64
64
 
65
+ # disabled resources pod list
66
+ other_pods_list = data["details"].get("other_pods_list", [])
67
+ pod_list.extend(other_pods_list)
68
+
65
69
  if not pod_list:
66
70
  raise NoAppsToList()
67
71
 
@@ -156,6 +160,20 @@ def compute_delete_response(data: dict) -> str:
156
160
  return f"App {name} and its service successfully deleted."
157
161
 
158
162
 
163
+ @key_error_decorator_for_helpers
164
+ def compute_scale_response(data: dict) -> str:
165
+ """Create response string for compute scale command.
166
+
167
+ :param response: dict object from API response.
168
+ :type response: requests.Response
169
+ :return: Response string.
170
+ :rtype: str
171
+ """
172
+ name = data["details"]["template_name"]
173
+ replicas = data["details"]["replicas"]
174
+ return f"App {name} has been successfully scaled to {replicas} replicas."
175
+
176
+
159
177
  @key_error_decorator_for_helpers
160
178
  def compute_restart_response(data: dict) -> str:
161
179
  """Create response string for compute restart command.
@@ -83,6 +83,13 @@ def get_app_list(pod_list: list, detailed: bool) -> list:
83
83
  """
84
84
  output_data = []
85
85
 
86
+ def update_output_data_count(pod_name):
87
+ for pod in output_data:
88
+ if pod["name"] == pod_name:
89
+ pod["count"] += 1
90
+ return True
91
+ return False
92
+
86
93
  for pod in pod_list:
87
94
  try:
88
95
  main_container_name = pod["labels"]["entity"]
@@ -94,7 +101,7 @@ def get_app_list(pod_list: list, detailed: bool) -> list:
94
101
  raise Exception(
95
102
  "Parser was unable to find main container in server output in container list"
96
103
  )
97
- volumes_mounted = list_get_mounted_volumes(main_container["mounts"])
104
+ volumes_mounted = list_get_mounted_volumes(main_container.get("mounts", []))
98
105
  limits = main_container["resources"].get("limits")
99
106
  cpu = limits.get("cpu") if limits is not None else 0
100
107
  ram = limits.get("memory") if limits is not None else "0Gi"
@@ -106,6 +113,7 @@ def get_app_list(pod_list: list, detailed: bool) -> list:
106
113
  "volumes_mounted": volumes_mounted,
107
114
  "cpu": cpu,
108
115
  "ram": ram,
116
+ "count": pod.get("replicas", 1), # first in the list are always pods
109
117
  }
110
118
  # getting rid of unwanted and used values
111
119
  if "pod-template-hash" in pod["labels"].keys():
@@ -139,7 +147,9 @@ def get_app_list(pod_list: list, detailed: bool) -> list:
139
147
 
140
148
  # appending the rest of labels
141
149
  pod_data.update(pod["labels"])
142
- output_data.append(pod_data)
150
+
151
+ if not update_output_data_count(pod_data["name"]):
152
+ output_data.append(pod_data)
143
153
  except KeyError:
144
154
  pass
145
155
 
@@ -1,18 +1,18 @@
1
- import click
2
- from cgc.commands.auth.auth_utils import (
3
- _get_jwt_from_server,
4
- )
5
- from cgc.utils.click_group import CustomCommand, CustomGroup
6
-
7
-
8
- @click.group("debug", cls=CustomGroup, hidden=True)
9
- def debug_group():
10
- """
11
- Debug commands for testing.
12
- """
13
- pass
14
-
15
-
16
- @debug_group.command("get-jwt", cls=CustomCommand)
17
- def get_jwt_from_server():
18
- click.echo(_get_jwt_from_server())
1
+ import click
2
+ from cgc.commands.auth.auth_utils import (
3
+ _get_jwt_from_server,
4
+ )
5
+ from cgc.utils.click_group import CustomCommand, CustomGroup
6
+
7
+
8
+ @click.group("debug", cls=CustomGroup, hidden=True)
9
+ def debug_group():
10
+ """
11
+ Debug commands for testing.
12
+ """
13
+ pass
14
+
15
+
16
+ @debug_group.command("get-jwt", cls=CustomCommand)
17
+ def get_jwt_from_server():
18
+ click.echo(_get_jwt_from_server())
@@ -4,6 +4,7 @@ import json
4
4
  from cgc.commands.db.db_models import DatabasesList
5
5
  from cgc.commands.compute.compute_models import ComputesList
6
6
  from cgc.commands.compute.compute_responses import (
7
+ compute_scale_response,
7
8
  template_list_response,
8
9
  template_get_start_path_response,
9
10
  compute_restart_response,
@@ -82,6 +83,63 @@ def compute_restart(name: str):
82
83
  )
83
84
 
84
85
 
86
+ @resource_group.group(name="scale", cls=CustomGroup, hidden=False)
87
+ def scale_group():
88
+ """
89
+ Management of scaling resources replicas.
90
+ """
91
+
92
+
93
+ @scale_group.command("up", cls=CustomCommand)
94
+ @click.argument("name", type=click.STRING)
95
+ # @click.option(
96
+ # "-s",
97
+ # "--scale",
98
+ # "replicas",
99
+ # type=NonNegativeInteger(),
100
+ # required=True,
101
+ # help="Scale factor - number of replicas (must be 0 or greater)",
102
+ # )
103
+ def compute_scale_up(name: str):
104
+ """Scales the specified app up"""
105
+ api_url, headers = get_api_url_and_prepare_headers()
106
+ url = f"{api_url}/v1/api/resource/scale?replicas=1"
107
+ metric = "resource.scale"
108
+ __payload = {"name": name}
109
+ __res = call_api(
110
+ request=EndpointTypes.post,
111
+ url=url,
112
+ headers=headers,
113
+ data=json.dumps(__payload).encode("utf-8"),
114
+ )
115
+ click.echo(
116
+ compute_scale_response(
117
+ retrieve_and_validate_response_send_metric(__res, metric)
118
+ )
119
+ )
120
+
121
+
122
+ @scale_group.command("down", cls=CustomCommand)
123
+ @click.argument("name", type=click.STRING)
124
+ def compute_scale_down(name: str):
125
+ """Scales the specified app down"""
126
+ api_url, headers = get_api_url_and_prepare_headers()
127
+ url = f"{api_url}/v1/api/resource/scale?replicas=0"
128
+ metric = "resource.scale"
129
+ __payload = {"name": name}
130
+ __res = call_api(
131
+ request=EndpointTypes.post,
132
+ url=url,
133
+ headers=headers,
134
+ data=json.dumps(__payload).encode("utf-8"),
135
+ )
136
+ click.echo(
137
+ compute_scale_response(
138
+ retrieve_and_validate_response_send_metric(__res, metric)
139
+ )
140
+ )
141
+
142
+
85
143
  @resource_group.command("ingress", cls=CustomCommand)
86
144
  @click.argument("name", type=click.STRING)
87
145
  def get_resource_ingress(name: str):
@@ -41,7 +41,7 @@ def volume_storage_class_details_response(data: dict) -> str:
41
41
  :rtype: str
42
42
  """
43
43
  # ["details"]["storage_class"] -> storage class name
44
- # ["details"]["storage_class_info"] -> billing_cost, storage_type, reclaim_policy, volume_binding_mode
44
+ # ["details"]["storage_class_info"] -> storage_type, reclaim_policy, volume_binding_mode
45
45
 
46
46
  storage_class_headers = [
47
47
  "name",
cgc/tests/test.py ADDED
@@ -0,0 +1,110 @@
1
+ def list_get_mounted_volumes(volume_list: list) -> str:
2
+ """Formats and returns list of PVC volumes mounted to an app.
3
+
4
+ :param volume_list: list of all volumes mounted to an app
5
+ :type volume_list: list
6
+ :return: list of PVC volumes
7
+ :rtype: str
8
+ """
9
+ volume_name_list = []
10
+ for volume in volume_list:
11
+ volume_type = volume.get("type")
12
+ if volume_type == "PVC":
13
+ volume_name = volume.get("name")
14
+ volume_name_list.append(volume_name)
15
+ volumes_mounted = (
16
+ ", ".join(volume_name_list) if len(volume_name_list) != 0 else None
17
+ )
18
+ return volumes_mounted
19
+
20
+
21
+ def get_job_json_data(job_list: list):
22
+ """Formats and returns list of jobs to print.
23
+
24
+ :param job_list: list of jobs
25
+ :type job_list: list
26
+ :return: formatted list of jobs
27
+ :rtype: list
28
+ """
29
+ output_data = []
30
+
31
+ for job in job_list:
32
+ try:
33
+ main_container_name = "custom-job"
34
+ try:
35
+ main_container = [
36
+ x
37
+ for x in job.get("containers", [])
38
+ if x.get("name") == main_container_name
39
+ ][0]
40
+ except IndexError:
41
+ raise Exception(
42
+ "Parser was unable to find main container in server output in container list"
43
+ )
44
+ volumes_mounted = list_get_mounted_volumes(main_container.get("mounts", []))
45
+ limits = main_container.get("resources", {}).get("limits")
46
+ cpu = limits.get("cpu") if limits is not None else 0
47
+ ram = limits.get("memory") if limits is not None else "0Gi"
48
+
49
+ job_data = {
50
+ "name": job.get("labels", {}).get("app-name"),
51
+ "status": job.get("status", {}).get("phase", "Unknown"),
52
+ "volumes_mounted": volumes_mounted,
53
+ "cpu": cpu,
54
+ "ram": ram,
55
+ }
56
+ # getting rid of unwanted and used values
57
+ if "pod-template-hash" in job["labels"].keys():
58
+ job["labels"].pop("pod-template-hash")
59
+ job["labels"].pop("entity")
60
+
61
+ # appending the rest of labels
62
+ job_data.update(job["labels"])
63
+ output_data.append(job_data)
64
+ except KeyError:
65
+ pass
66
+
67
+ return output_data
68
+
69
+
70
+ def get_job_list(job_list: list, job_pod_list: list):
71
+ list_of_json_job_data = get_job_json_data(job_list)
72
+
73
+ for i, job_data in enumerate(job_list):
74
+ list_of_json_job_data[i]["name"] = job_data.get("name", "")
75
+ list_of_json_job_data[i]["ttl"] = job_data.get(
76
+ "ttl_seconds_after_finished", "N/A"
77
+ )
78
+ list_of_json_job_data[i]["ads"] = job_data.get("active_deadline_seconds", "N/A")
79
+ for job in list_of_json_job_data:
80
+ for job_pod in job_pod_list:
81
+ job_pod_labels: dict = job_pod.get("labels", {})
82
+ print(job_pod.get("labels"))
83
+ if job_pod_labels.get("app-name", "") == job.get("name"):
84
+ if job["status"] is not None and job["status"] == "Unknown":
85
+ job["status"] = job_pod["status"] # try to get status from pod
86
+ elif job["status"] is None: # support older server versions
87
+ job["status"] = job_pod["status"]
88
+ job["gpu-count"] = job_pod_labels.get("gpu-count", 0)
89
+ job["gpu-label"] = job_pod_labels.get("gpu-label", "N/A")
90
+ break
91
+
92
+ return list_of_json_job_data
93
+
94
+
95
+ def main():
96
+ import json
97
+
98
+ with open("test.json") as f:
99
+ response_data = json.load(f)
100
+
101
+ job_list = response_data.get("details", {}).get("job_list", [])
102
+ job_pod_list = response_data.get("details", {}).get("job_pod_list", [])
103
+
104
+ _list_of_json_job_data = get_job_list(job_list, job_pod_list)
105
+
106
+ # print(list_of_json_job_data)
107
+
108
+
109
+ if __name__ == "__main__":
110
+ main()
cgc/utils/click_group.py CHANGED
@@ -17,3 +17,16 @@ class CustomCommand(click.Command):
17
17
  pieces = self.collect_usage_pieces(ctx)
18
18
  cmd_path = ctx.command_path.removeprefix("python -m ")
19
19
  formatter.write_usage(cmd_path, " ".join(pieces))
20
+
21
+
22
+ class NonNegativeInteger(click.types.IntParamType):
23
+ """A parameter that only accepts non-negative integers."""
24
+
25
+ name = "non-negative-integer"
26
+
27
+ def convert(self, value, param, ctx):
28
+ # First convert using the parent class method
29
+ result = super().convert(value, param, ctx)
30
+ if result < 0:
31
+ self.fail(f"{value} is not a non-negative integer", param, ctx)
32
+ return result
@@ -3,29 +3,33 @@
3
3
  # every warning has its message that will be returned to print
4
4
  CUSTOM_EXCEPTIONS = {
5
5
  500: {
6
- "UNDEFINED": "undefined exception",
7
- "USER_KEY_CREATE_ERROR": "Error while creating key",
6
+ "UNDEFINED": "Undefined exception.",
7
+ "USER_KEY_CREATE_ERROR": "Error while creating key.",
8
+ "RESOURCE_SCALE_TEMPLATE_ERROR": "Error while scaling resource - resource not fully compatible with CGC.",
8
9
  },
9
10
  413: {
10
- "PVC_CREATE_STORAGE_LIMIT_EXCEEDED": "This request exceeds your storage limits",
11
- "PVC_CREATE_NOT_ENOUGH_STORAGE_IN_CLUSTER": "No more storage available",
12
- "REQUEST_RESOURCE_LIMIT_EXCEEDED": "This request exceeds your resources limit",
13
- "RESOURCES_NOT_AVAILABLE_IN_CLUSTER": "Requested resources not available",
11
+ "PVC_CREATE_STORAGE_LIMIT_EXCEEDED": "This request exceeds your storage limits.",
12
+ "PVC_CREATE_NOT_ENOUGH_STORAGE_IN_CLUSTER": "No more storage available.",
13
+ "REQUEST_RESOURCE_LIMIT_EXCEEDED": "This request exceeds your resource limits.",
14
+ "RESOURCES_NOT_AVAILABLE_IN_CLUSTER": "Requested resources are not available.",
15
+ "RESOURCE_SCALE_RESOURCES_LIMIT_EXCEEDED": "This request exceeds your resource limits.",
16
+ "PVC_CREATE_STORAGE_LIMIT_EXCEEDED": "Storage limit exceeded.",
14
17
  },
15
18
  409: {
16
- "PVC_NAME_ALREADY_EXISTS": "Volume with this name already exists.",
17
- "PVC_DELETE_EXCEPTION": "Can't delete mounted volume, try with force",
18
- "RESOURCE_PORTS_ALREADY_EXISTS": "Port with this name already exists.",
19
- "RESOURCE_TEMPLATE_NAME_ALREADY_EXISTS": "Resource with this name already exists.",
20
- "JOB_CREATE_ALREADY_EXISTS": "Job with this name already exists.",
21
- "USER_KEY_ALREADY_EXISTS": "Key with these data already exists.",
19
+ "PVC_NAME_ALREADY_EXISTS": "A volume with this name already exists.",
20
+ "PVC_DELETE_EXCEPTION": "Can't delete mounted volume. Try with force.",
21
+ "RESOURCE_PORTS_ALREADY_EXISTS": "A port with this name already exists.",
22
+ "RESOURCE_TEMPLATE_NAME_ALREADY_EXISTS": "A resource with this name already exists.",
23
+ "JOB_CREATE_ALREADY_EXISTS": "A job with this name already exists.",
24
+ "USER_KEY_ALREADY_EXISTS": "A key with this data already exists.",
25
+ "AlreadyExists": "An object with this name already exists.",
22
26
  },
23
27
  404: {
24
- "PVC_CREATE_NO_SC": "Selected disk type and access mode unavailable",
28
+ "PVC_CREATE_NO_SC": "Selected disk type and access mode are unavailable.",
25
29
  "BILLING_STATUS_NO_DATA": "No data to print.",
26
30
  "NOT_DELETED_ANYTHING_IN_COMPUTE_DELETE": "No app with this name to delete.",
27
- "API_KEY_DELETE_ERROR": "No api key with this id to delete",
28
- "PVC_MOUNT_NOT_FOUND_TEMPLATE": "No app with this name to mount.",
31
+ "API_KEY_DELETE_ERROR": "No API key with this ID to delete.",
32
+ "PVC_MOUNT_NOT_FOUND_TEMPLATE": "App resource with this name not found.",
29
33
  "PVC_UNMOUNT_NOT_MOUNTED": "Volume with this name is not mounted.",
30
34
  "PVC_NOT_FOUND": "Volume with this name not found.",
31
35
  "PVC_DELETE_NOT_FOUND": "App with this name not found.",
@@ -35,13 +39,21 @@ CUSTOM_EXCEPTIONS = {
35
39
  "COMPUTE_RESOURCE_QUOTA_NOT_FOUND": "You do not have enforced limits on your namespace.",
36
40
  "JOB_NOT_FOUND": "Job with this name not found.",
37
41
  "RESOURCE_NOT_FOUND": "Resource with this name not found.",
38
- "USER_KEY_NOT_FOUND": "Key with this id not found.",
42
+ "USER_KEY_NOT_FOUND": "Key with this ID not found.",
43
+ "RESOURCE_SCALE_TEMPLATE_NOT_FOUND": "No app with this name.",
44
+ "NotFound": "Resource not found.",
45
+ "USER_SECRET_UPDATE_NOT_FOUND": "Secret not found.",
46
+ "USER_SECRET_DELETE_NOT_FOUND": "Secret not found.",
47
+ "PVC_SC_NOT_FOUND": "Storage class with this name is not defined.",
48
+ "RESOURCE_RESTART_TEMPLATE_NOT_FOUND": "Resource with this name not found.",
49
+ "NOT_DELETED_ANYTHING_IN_RESOURCE_DELETE": "There is nothing to delete with this name.",
39
50
  },
40
51
  400: {
41
52
  "WRONG_DATE_FORMAT": "Wrong date format.",
42
53
  "ENTITY_NOT_ALLOWED": "You can't create this entity.",
43
54
  "PVC_MOUNT_ALREADY_MOUNTED": "This volume is already mounted.",
44
- "TEMPLATE_NAME_SYSTEM_RESERVED": "You can't create app with this name.",
45
- "JOB_LACKS_REQUIRED_PARAMETER": "Job requires container image parameter.",
55
+ "TEMPLATE_NAME_SYSTEM_RESERVED": "You can't create an app with this name.",
56
+ "JOB_LACKS_REQUIRED_PARAMETER": "Job requires a container image parameter.",
57
+ "RESOURCE_PORTS_EXCEPTION": "Resource with this name not found.",
46
58
  },
47
59
  }
@@ -63,5 +63,9 @@ def key_error_decorator_for_helpers(func):
63
63
  return prepare_error_message(UNKNOWN_ERROR)
64
64
  except (ResponseException, click.ClickException) as err:
65
65
  return prepare_warning_message(err)
66
+ except KeyboardInterrupt:
67
+ increment_metric(metric="client.interrupted", is_error=True)
68
+ # silently exit
69
+ exit(0)
66
70
 
67
71
  return wrapper
@@ -20,7 +20,7 @@ def get_api_url_and_prepare_headers():
20
20
  }
21
21
  return get_headers_data.load_user_api_url(), headers
22
22
 
23
-
23
+ @key_error_decorator_for_helpers
24
24
  def get_url_and_prepare_headers_register(
25
25
  user_id: str, access_key: str, url: str = None, secret: str = None
26
26
  ):
@@ -39,7 +39,7 @@ def get_url_and_prepare_headers_register(
39
39
  }
40
40
  return url, headers
41
41
 
42
-
42
+ @key_error_decorator_for_helpers
43
43
  def get_url_and_headers_jwt_token():
44
44
  url = f"{get_headers_data.load_user_api_url()}/v1/api/user/create/token"
45
45
  headers = {
@@ -63,7 +63,7 @@ def prepare_headers_api_key(user_id: str = None, password: str = None):
63
63
  }
64
64
  return headers
65
65
 
66
-
66
+ @key_error_decorator_for_helpers
67
67
  def get_api_url_and_prepare_headers_version_control():
68
68
  """Prepares headers for version control request.
69
69
 
@@ -59,6 +59,10 @@ def call_api(request: EndpointTypes, **kwargs):
59
59
  except OSError:
60
60
  increment_metric(metric="client.certificate", is_error=True)
61
61
  click.echo(prepare_error_message(CERTIFICATE_ERROR))
62
+ except KeyboardInterrupt:
63
+ increment_metric(metric="client.interrupted", is_error=True)
64
+ # silently exit
65
+ sys.exit()
62
66
  except:
63
67
  increment_metric(metric="client.unhandled", is_error=True)
64
68
  click.echo(prepare_error_message(UNKNOWN_ERROR))
@@ -118,9 +118,12 @@ def retrieve_and_validate_response_send_metric(
118
118
  else:
119
119
  try:
120
120
  response_json = response.json()
121
- if "details" in response_json:
122
- click.echo(prepare_error_message(response_json["details"]))
123
- else:
121
+ status_code = response.status_code
122
+ reason = response_json.get("reason", "")
123
+ if (
124
+ status_code in CUSTOM_EXCEPTIONS
125
+ and reason in CUSTOM_EXCEPTIONS[status_code]
126
+ ):
124
127
  click.echo(
125
128
  prepare_error_message(
126
129
  CUSTOM_EXCEPTIONS[response.status_code][
@@ -128,6 +131,10 @@ def retrieve_and_validate_response_send_metric(
128
131
  ]
129
132
  )
130
133
  )
134
+ elif "details" in response_json:
135
+ click.echo(prepare_error_message(response_json["details"]))
136
+ else:
137
+ raise KeyError("No specific error message or details field found.")
131
138
  except KeyError:
132
139
  error_message = _get_response_json_error_message(response_json)
133
140
  if isinstance(error_message, str):
@@ -38,7 +38,7 @@ def get_server_version():
38
38
 
39
39
 
40
40
  def print_compare_versions(server_version: str, client_version: str):
41
- click.echo(f"Available version: {server_version}")
41
+ click.echo(f"Server version: {server_version}")
42
42
  click.echo(f"Installed version: {client_version}")
43
43
 
44
44
 
@@ -1,76 +1,76 @@
1
- Metadata-Version: 2.4
2
- Name: cgcsdk
3
- Version: 1.2.5
4
- Summary: CGC Core REST API client
5
- Home-page: https://cgc.comtegra.cloud/
6
- Author: Comtegra AI Team
7
- Author-email: ai@comtegra.pl
8
- License: BSD 2-clause
9
- Project-URL: Documentation, https://docs.cgc.comtegra.cloud/
10
- Project-URL: GitHub, https://git.comtegra.pl/k8s/cgc-client-k8s-cloud
11
- Project-URL: Changelog, https://git.comtegra.pl/k8s/cgc-client-k8s-cloud/-/blob/main/cgc/CHANGELOG.md
12
- Keywords: cloud,sdk,orchestrator,kubernetes,jupyter-notebook,cgc-core
13
- Classifier: Development Status :: 5 - Production/Stable
14
- Classifier: Intended Audience :: Science/Research
15
- Classifier: License :: OSI Approved :: BSD License
16
- Classifier: Operating System :: POSIX :: Linux
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Description-Content-Type: text/markdown
21
- License-File: LICENSE
22
- Requires-Dist: click
23
- Requires-Dist: python-dotenv
24
- Requires-Dist: tabulate
25
- Requires-Dist: pycryptodomex
26
- Requires-Dist: paramiko>=2.11
27
- Requires-Dist: statsd
28
- Requires-Dist: requests
29
- Requires-Dist: setuptools
30
- Requires-Dist: colorama
31
- Requires-Dist: psycopg2-binary
32
- Dynamic: author
33
- Dynamic: author-email
34
- Dynamic: classifier
35
- Dynamic: description
36
- Dynamic: description-content-type
37
- Dynamic: home-page
38
- Dynamic: keywords
39
- Dynamic: license
40
- Dynamic: license-file
41
- Dynamic: project-url
42
- Dynamic: requires-dist
43
- Dynamic: summary
44
-
45
- # Comtegra GPU Cloud CLI Client
46
-
47
- ## Basic info
48
-
49
- CGC Clinet is complete solution to create and manage your compute resources through CLI interface and python code. It incorporates CLI and SDK in one package.
50
-
51
- CGC CLI is a command line interface for Comtegra GPU Cloud. CGC CLI enables management of your Comtegra GPU Cloud resources. Current version of the app provides support for compute, storage and network resurces to be created, listed and deleted. Every compute resource is given to you as an URL, which is accessible from open Internet.
52
-
53
- To enable better access to your storage resources, every account has the ability to spawn free of charge filebrowser which is local implementation of dropbox. Remember to mount newely created volumes to it.
54
-
55
- For now, we provide the ability to spawn compute resources like:
56
-
57
- 1. [Jupyter notebook](https://jupyter.org/) with tensorflow or pytorch installed as default
58
- 2. [Triton inferencing server](https://docs.nvidia.com/deeplearning/triton-inference-server/) for large scale inferencing
59
- 3. [Label studio](https://labelstud.io/) for easy management of your data annotation tasks with variety of modes
60
- 4. [Rapids](https://rapids.ai/) suite of accelerated libraries for data processing
61
-
62
- Notebooks are equiped with all CUDA libraries and GPU drivers which enables the usage of GPU for accelerated computations.
63
- Apart from compute resources, we provide the database engines accessible from within your namespace:
64
-
65
- 1. [PostgreSQL](https://www.postgresql.org/)
66
- 2. [Weaviate](https://weaviate.io/)
67
-
68
- More are coming!
69
- Please follow instructions to get started.
70
-
71
- ## More info
72
-
73
- If you'd like to know more visit:
74
-
75
- - [Comtegra GPU Website](https://cgc.comtegra.cloud)
76
- - [Docs](https://docs.cgc.comtegra.cloud)
1
+ Metadata-Version: 2.4
2
+ Name: cgcsdk
3
+ Version: 1.3.0
4
+ Summary: CGC Core REST API client
5
+ Home-page: https://cgc.comtegra.cloud/
6
+ Author: Comtegra AI Team
7
+ Author-email: ai@comtegra.pl
8
+ License: BSD 2-clause
9
+ Project-URL: Documentation, https://docs.cgc.comtegra.cloud/
10
+ Project-URL: GitHub, https://git.comtegra.pl/k8s/cgc-client-k8s-cloud
11
+ Project-URL: Changelog, https://git.comtegra.pl/k8s/cgc-client-k8s-cloud/-/blob/main/cgc/CHANGELOG.md
12
+ Keywords: cloud,sdk,orchestrator,kubernetes,jupyter-notebook,cgc-core
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: BSD License
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: click
23
+ Requires-Dist: python-dotenv
24
+ Requires-Dist: tabulate
25
+ Requires-Dist: pycryptodomex
26
+ Requires-Dist: paramiko>=2.11
27
+ Requires-Dist: statsd
28
+ Requires-Dist: requests
29
+ Requires-Dist: setuptools
30
+ Requires-Dist: colorama
31
+ Requires-Dist: psycopg2-binary
32
+ Dynamic: author
33
+ Dynamic: author-email
34
+ Dynamic: classifier
35
+ Dynamic: description
36
+ Dynamic: description-content-type
37
+ Dynamic: home-page
38
+ Dynamic: keywords
39
+ Dynamic: license
40
+ Dynamic: license-file
41
+ Dynamic: project-url
42
+ Dynamic: requires-dist
43
+ Dynamic: summary
44
+
45
+ # Comtegra GPU Cloud CLI Client
46
+
47
+ ## Basic info
48
+
49
+ CGC Clinet is complete solution to create and manage your compute resources through CLI interface and python code. It incorporates CLI and SDK in one package.
50
+
51
+ CGC CLI is a command line interface for Comtegra GPU Cloud. CGC CLI enables management of your Comtegra GPU Cloud resources. Current version of the app provides support for compute, storage and network resurces to be created, listed and deleted. Every compute resource is given to you as an URL, which is accessible from open Internet.
52
+
53
+ To enable better access to your storage resources, every account has the ability to spawn free of charge filebrowser which is local implementation of dropbox. Remember to mount newely created volumes to it.
54
+
55
+ For now, we provide the ability to spawn compute resources like:
56
+
57
+ 1. [Jupyter notebook](https://jupyter.org/) with tensorflow or pytorch installed as default
58
+ 2. [Triton inferencing server](https://docs.nvidia.com/deeplearning/triton-inference-server/) for large scale inferencing
59
+ 3. [Label studio](https://labelstud.io/) for easy management of your data annotation tasks with variety of modes
60
+ 4. [Rapids](https://rapids.ai/) suite of accelerated libraries for data processing
61
+
62
+ Notebooks are equiped with all CUDA libraries and GPU drivers which enables the usage of GPU for accelerated computations.
63
+ Apart from compute resources, we provide the database engines accessible from within your namespace:
64
+
65
+ 1. [PostgreSQL](https://www.postgresql.org/)
66
+ 2. [Weaviate](https://weaviate.io/)
67
+
68
+ More are coming!
69
+ Please follow instructions to get started.
70
+
71
+ ## More info
72
+
73
+ If you'd like to know more visit:
74
+
75
+ - [Comtegra GPU Website](https://cgc.comtegra.cloud)
76
+ - [Docs](https://docs.cgc.comtegra.cloud)
@@ -1,5 +1,5 @@
1
- cgc/.env,sha256=__9LESwdxS-0FzmB6h-HdVRL8IND1O4rCf7Gt3RKhyk,209
2
- cgc/CHANGELOG.md,sha256=ioh9EniDUXJD71PbXHcQQaVM4-9iwl24gXKNRGfUDCk,11489
1
+ cgc/.env,sha256=HL_aZZeVlSqO2b4XjXFvcFow-X89HtNK4yfZn3bFQeg,209
2
+ cgc/CHANGELOG.md,sha256=DtPZAzR5ZNARX9TRnasZVbY7Tk9ddn913KQw3_smdlo,12119
3
3
  cgc/__init__.py,sha256=d03Xv8Pw4ktNyUHfmicP6XfxYPXnVYLaCZPyUlg_RNQ,326
4
4
  cgc/cgc.py,sha256=3I_Ef0ggX9caaJKJkhfGYSe8XwkHzSWxwGAClMHDnUs,1663
5
5
  cgc/config.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -17,23 +17,23 @@ cgc/commands/auth/auth_utils.py,sha256=3RSBAR_V5DANhJ_R0cN4poP2uCNdx2tU1VXxP17yX
17
17
  cgc/commands/compute/__init__.py,sha256=lCdLfZ0ECSHtXEUSwq5YRHH85yXHchSsz8ZJvmClPtI,239
18
18
  cgc/commands/compute/compute_cmd.py,sha256=1i4B_tXjSBtRNDnimgilz4a6BacZTHjFnMsf9EUodOU,16433
19
19
  cgc/commands/compute/compute_models.py,sha256=5do3kSrzoZSDX1ElSZMmmW3AI9A1oeOGtrjQYePf4O0,884
20
- cgc/commands/compute/compute_responses.py,sha256=X1ExOKDIxSFZbBRbWyfWSCeRAe3_cFGa3jOgq6CfjHw,6439
21
- cgc/commands/compute/compute_utils.py,sha256=wtSgzaoqvvwfR2dHvt8Eup0LH8Fm8pec4Hf52rU_TvI,9734
20
+ cgc/commands/compute/compute_responses.py,sha256=R_BcEPxh57JEK_Er83qN22RpuYCDNctlKY6m-5aPx-c,7025
21
+ cgc/commands/compute/compute_utils.py,sha256=VneJYv8OUiDLYH7u-Gea1vBIx4Tayv3_SRzqfI_i1Sg,10095
22
22
  cgc/commands/compute/billing/__init__.py,sha256=ccjz-AzBCROjuR11qZRM4_62slI9ErmLi27xPUoRPHM,752
23
- cgc/commands/compute/billing/billing_cmd.py,sha256=cUOPQyf5d-vfUVMoetdBxoJx1dqxeQfyke5v-QP88wM,4508
24
- cgc/commands/compute/billing/billing_responses.py,sha256=5vQSR_d41uizengzfXlHXL7XivO_73PpWdKmoUgqYNw,1965
25
- cgc/commands/compute/billing/billing_utils.py,sha256=wIiDm7n4bOuKLO4-kLy0AnpPk9G_T14Y5DiFcXOtNrI,5553
23
+ cgc/commands/compute/billing/billing_cmd.py,sha256=d6TzvpbLEetEZCLOz3epk29leIJSofRCeCQ0hCW8jvM,3096
24
+ cgc/commands/compute/billing/billing_responses.py,sha256=R_FQXcU2YhPP80Q0euBsZUc4iVUurJxXY6hLLwazfKY,2514
25
+ cgc/commands/compute/billing/billing_utils.py,sha256=VQTgZd6pSjwROHb_BpwaATo8LowPyUPdrdbdLbI802Y,8313
26
26
  cgc/commands/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  cgc/commands/db/db_cmd.py,sha256=iXwJzWu4xiaUIfyKSixpnK7-GtoplvfbUN8HHly1YAY,3981
28
28
  cgc/commands/db/db_models.py,sha256=zpMcrDBanLx7YQJ_-PWrQCbh3B-C0qWn1Pt5SnDwsh0,1351
29
29
  cgc/commands/debug/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- cgc/commands/debug/debug_cmd.py,sha256=kuAuh5YOqzGFjoiYZwfM9FJ1z5OeSpC0JAIUEzS83lM,412
30
+ cgc/commands/debug/debug_cmd.py,sha256=HU6d1kSRhLVJUxw5Np2wrOREM1GWXQY0WIvFOe9brEo,394
31
31
  cgc/commands/jobs/__init__.py,sha256=E-438wgIzlnGmXs5jgmWAhJ1KNV6UXF2gz8SXu3UxA0,231
32
32
  cgc/commands/jobs/job_utils.py,sha256=-r5YNt0Wr_LRL2bOp9RJ3OKSej827UYew2ds4qFy5_U,7135
33
33
  cgc/commands/jobs/jobs_cmd.py,sha256=4zHZtT2y_FoBrGNR5nfSJ0gi-MX1rUP68KwpvuZ8m0Q,6922
34
34
  cgc/commands/jobs/jobs_responses.py,sha256=QXFXA4zwQOo5Gvq5rEc7J_cxxsYqkdU19X9MCcZetUM,1771
35
35
  cgc/commands/resource/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- cgc/commands/resource/resource_cmd.py,sha256=y2R6ZF0jFhTppD9JUpwLIcEdFRXg82Xw_5yrNIfG4mI,4089
36
+ cgc/commands/resource/resource_cmd.py,sha256=UfhaNoiNvqLfYmcBskKD7QHzfY_LPvU0a4zlPdvzxQk,5722
37
37
  cgc/commands/resource/resource_responses.py,sha256=sES7mAi_Cv5B6Z3I_6eUOqVwOr2HgMO45cz8MiNZetQ,197
38
38
  cgc/commands/user/__init__.py,sha256=Rhaa2SDXQsJVJnTX0oFyZ90tAuiAUBeRy3Bd415S4os,382
39
39
  cgc/commands/user/keys_cmd.py,sha256=dfHnhHmEwRH1xl_wRNQcpCT-THJTBYPHMTJSI2AsDXE,4509
@@ -48,7 +48,7 @@ cgc/commands/volume/__init__.py,sha256=Ou3kyb72aaXkrVCfQCVdniA65R2xHsRFgebooG1gf
48
48
  cgc/commands/volume/data_model.py,sha256=lLHuKRMe-5mgHL3i48QJn8_S3tJHFMCwu8cJAxXe-PU,1267
49
49
  cgc/commands/volume/volume_cmd.py,sha256=Eylo_V8Tex8OF5IQcAlpo6ggC4FXYfnQunGtat6WwSs,8279
50
50
  cgc/commands/volume/volume_models.py,sha256=eKHYLcAUezoJ1X2ENE-GE2CgE8lynlfT3Hs2PI8zAnY,779
51
- cgc/commands/volume/volume_responses.py,sha256=lO6NiUFJkNTTKLerFihe3CeQwdo56WyUN5bk01dtfMs,4027
51
+ cgc/commands/volume/volume_responses.py,sha256=tuFws-4yMWj3fcjxwclc7FaJbvGK5n3WzK1SwPNGx7Q,4013
52
52
  cgc/commands/volume/volume_utils.py,sha256=6IuDCNT-KAvUUF_EDg5cL9JewTGsbBsZlYd_zKHowCU,1973
53
53
  cgc/sdk/__init__.py,sha256=m8uAD2e_ADbHC4_kaOpLrUk_bHy7wC56rPjhcttclCs,177
54
54
  cgc/sdk/exceptions.py,sha256=99XIzDO6LYKjex715troH-MkGUN7hi2Bit4KHfSHDis,214
@@ -59,6 +59,7 @@ cgc/telemetry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  cgc/telemetry/basic.py,sha256=h0f2yAanaflkEnetLjQvs_IR43KL0JmhJ30yrl6tOas,3174
60
60
  cgc/tests/__init__.py,sha256=8aI3MVpkzaj0_UX02kZCtY5vmGO0rnq0mw2H04-OHf8,102743
61
61
  cgc/tests/responses_tests.py,sha256=9vLOaUICeUXoDObeFwuu0FBTmC-hnJNZfzvolT96dGM,9188
62
+ cgc/tests/test.py,sha256=pFPo8SkE17Si2UhbyLaDhlBxcUMk445M3ZxLlVfhqR8,3877
62
63
  cgc/tests/desired_responses/test_billing_invoice.txt,sha256=KR5m2gamn_bgfBdBmWDH2sPRJIPOw1u8kdH-gYE8jow,1579
63
64
  cgc/tests/desired_responses/test_billing_status.txt,sha256=2KSUixFOrhXI5ON6qtsIUmzFye5LBZB1xneY8ST0MqE,2381
64
65
  cgc/tests/desired_responses/test_billing_stop_events_compute.txt,sha256=nHdixgLhAXDMeoDvjk9Mwv6b0JIAsW8j7Z6SS5scjSs,234
@@ -68,16 +69,16 @@ cgc/tests/desired_responses/test_compute_list_no_labels.txt,sha256=-OeQIaEHHsHZ8
68
69
  cgc/tests/desired_responses/test_tabulate_response.txt,sha256=beNyCTS9fwrHn4ueEOVk2BpOeSYZWumIa3H5EUGnW1I,789
69
70
  cgc/tests/desired_responses/test_volume_list.txt,sha256=vYB1p50BBHD801q7LUdDc_aca4ezQ8CFLWw7I-b4Uao,309
70
71
  cgc/utils/__init__.py,sha256=JOvbqyGdFtswXE1TntOqM7XuIg5-t042WzAzvmX7Xtk,3661
71
- cgc/utils/click_group.py,sha256=Scfw8eMIyt2dE1ezUq2JuiI-E_LklqXQXJEr7L-EG6A,633
72
+ cgc/utils/click_group.py,sha256=57jEtLJV6944p6gIfuCWmAd_kXfY3rneRngoglYZ9u8,1055
72
73
  cgc/utils/config_utils.py,sha256=rbiA2ZNP2SslJ7zA-my7SB0ZLILBMxGjnzP1JhhGc2g,7525
73
- cgc/utils/custom_exceptions.py,sha256=qvHdzaunZswZgN96iOHZIfLjehlJ79mcjqoMoW-tqEM,2628
74
+ cgc/utils/custom_exceptions.py,sha256=lNpt6Oacd4RqxOQicExUHprLd_a_VtfWS0qVGS3TYtI,3587
74
75
  cgc/utils/get_headers_data.py,sha256=JdEg5vrAHcWfsSJ7poYk3sNIY10OxX7YGVcmua-37lY,413
75
- cgc/utils/message_utils.py,sha256=Lg9G86MRKhQyl1Ito38clGBK-REfgp5fT5aeMqSfhW4,1961
76
- cgc/utils/prepare_headers.py,sha256=Hi3WNqtqydW56tNTLZmpfMTpu4aKCoDrLx4OcCGH9_U,2656
77
- cgc/utils/requests_helper.py,sha256=Z89dTOTbSSi1xmtNPmAdJpduR9-DC12WEQsHYeuM9a0,2046
78
- cgc/utils/response_utils.py,sha256=C0we250lDVxpadtMlx7vUQj0u_gff0zieuVqeuqS2x8,7378
76
+ cgc/utils/message_utils.py,sha256=ebU-omCBTEx3Vvq4kF3vG3x0MPv2fotnvxAFX4mR03c,2116
77
+ cgc/utils/prepare_headers.py,sha256=crjwBvyejtgEQGaL9kJxU4v8Hk6tLn9zaLbMzNIGR6s,2752
78
+ cgc/utils/requests_helper.py,sha256=L0h_u5PkgSJvNY4Q8eraRebhdLghtDtSeBveCWFzNFQ,2188
79
+ cgc/utils/response_utils.py,sha256=LCuUFBmkBHmLX5sQ9-WlPShe_yLPXzs8tDS4RUNYJTk,7734
79
80
  cgc/utils/update.py,sha256=AsQwhcBqsjgNPKn6AN6ojt0Ew5otvJXyshys6bjr7DQ,413
80
- cgc/utils/version_control.py,sha256=rnHeo8_y-8HJfEKLqpiMM26QR8b-CAl3zfgcv0a3y8I,4163
81
+ cgc/utils/version_control.py,sha256=Ssd01sYYp79Y8Bl7_N4BDZLvfrmJ23YoWIBRs4VxHCM,4160
81
82
  cgc/utils/consts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
83
  cgc/utils/consts/env_consts.py,sha256=_yHjhXDBwYfLfUWfHOPlhGdzC0j9VFlkjKuSrvblBsU,1154
83
84
  cgc/utils/consts/message_consts.py,sha256=KMajQ5yGxQ45ZPTWVLQj9ajy1KbMeCkHvSW-871A1rs,1355
@@ -85,9 +86,9 @@ cgc/utils/cryptography/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
85
86
  cgc/utils/cryptography/aes_crypto.py,sha256=S0rKg38oy7rM5lTrP6DDpjLA-XRxuZggAXyxMFHtyzY,3333
86
87
  cgc/utils/cryptography/encryption_module.py,sha256=rbblBBorHYPGl-iKblyZX3_NuPEvUTpnH1l_RgNGCbA,1958
87
88
  cgc/utils/cryptography/rsa_crypto.py,sha256=h3jU5qPpj9uVjP1rTqZJTdYB5yjhD9HZpr_nD439h9Q,4180
88
- cgcsdk-1.2.5.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
- cgcsdk-1.2.5.dist-info/METADATA,sha256=wMQvJPI6XdJ5Qgy88ukNP1cd6GkGaT6xfqNlYZwDMPw,3247
90
- cgcsdk-1.2.5.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
91
- cgcsdk-1.2.5.dist-info/entry_points.txt,sha256=bdfIHeJ6Y-BBr5yupCVoK7SUrJj1yNdew8OtIOg_3No,36
92
- cgcsdk-1.2.5.dist-info/top_level.txt,sha256=nqW9tqcIcCXFigQT69AuOk7XHKc4pCuv4HGJQGXb6iA,12
93
- cgcsdk-1.2.5.dist-info/RECORD,,
89
+ cgcsdk-1.3.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
+ cgcsdk-1.3.0.dist-info/METADATA,sha256=ShijHWs9C9oy1NabyeD32sZ8B61dyLrCDANHfIkRYXk,3171
91
+ cgcsdk-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
92
+ cgcsdk-1.3.0.dist-info/entry_points.txt,sha256=bdfIHeJ6Y-BBr5yupCVoK7SUrJj1yNdew8OtIOg_3No,36
93
+ cgcsdk-1.3.0.dist-info/top_level.txt,sha256=nqW9tqcIcCXFigQT69AuOk7XHKc4pCuv4HGJQGXb6iA,12
94
+ cgcsdk-1.3.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5