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 +2 -2
- cgc/CHANGELOG.md +20 -0
- cgc/commands/compute/billing/billing_cmd.py +33 -74
- cgc/commands/compute/billing/billing_responses.py +34 -24
- cgc/commands/compute/billing/billing_utils.py +137 -59
- cgc/commands/compute/compute_responses.py +18 -0
- cgc/commands/compute/compute_utils.py +12 -2
- cgc/commands/debug/debug_cmd.py +18 -18
- cgc/commands/resource/resource_cmd.py +58 -0
- cgc/commands/volume/volume_responses.py +1 -1
- cgc/tests/test.py +110 -0
- cgc/utils/click_group.py +13 -0
- cgc/utils/custom_exceptions.py +30 -18
- cgc/utils/message_utils.py +4 -0
- cgc/utils/prepare_headers.py +3 -3
- cgc/utils/requests_helper.py +4 -0
- cgc/utils/response_utils.py +10 -3
- cgc/utils/version_control.py +1 -1
- {cgcsdk-1.2.5.dist-info → cgcsdk-1.3.0.dist-info}/METADATA +76 -76
- {cgcsdk-1.2.5.dist-info → cgcsdk-1.3.0.dist-info}/RECORD +24 -23
- {cgcsdk-1.2.5.dist-info → cgcsdk-1.3.0.dist-info}/WHEEL +1 -1
- {cgcsdk-1.2.5.dist-info → cgcsdk-1.3.0.dist-info}/entry_points.txt +0 -0
- {cgcsdk-1.2.5.dist-info → cgcsdk-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {cgcsdk-1.2.5.dist-info → cgcsdk-1.3.0.dist-info}/top_level.txt +0 -0
cgc/.env
CHANGED
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
|
-
|
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}/
|
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
|
-
|
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 =
|
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
|
-
@
|
86
|
-
def
|
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
|
-
|
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}/
|
150
|
-
metric = "billing.
|
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
|
-
|
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
|
-
|
21
|
-
|
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(
|
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
|
-
|
30
|
+
billing_records = data["details"]["billing_records"]
|
31
|
+
details = data["details"].get("details", [])
|
33
32
|
if (
|
34
|
-
not
|
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(
|
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
|
44
|
-
|
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
|
-
|
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
|
-
"""
|
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
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
"
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
"
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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[
|
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(
|
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
|
-
|
80
|
-
user_id
|
81
|
-
|
82
|
-
|
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: {
|
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 ["
|
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
|
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
|
-
|
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
|
|
cgc/commands/debug/debug_cmd.py
CHANGED
@@ -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"] ->
|
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
|
cgc/utils/custom_exceptions.py
CHANGED
@@ -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": "
|
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
|
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": "
|
17
|
-
"PVC_DELETE_EXCEPTION": "Can't delete mounted volume
|
18
|
-
"RESOURCE_PORTS_ALREADY_EXISTS": "
|
19
|
-
"RESOURCE_TEMPLATE_NAME_ALREADY_EXISTS": "
|
20
|
-
"JOB_CREATE_ALREADY_EXISTS": "
|
21
|
-
"USER_KEY_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
|
28
|
-
"PVC_MOUNT_NOT_FOUND_TEMPLATE": "
|
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
|
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
|
}
|
cgc/utils/message_utils.py
CHANGED
@@ -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
|
cgc/utils/prepare_headers.py
CHANGED
@@ -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
|
|
cgc/utils/requests_helper.py
CHANGED
@@ -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))
|
cgc/utils/response_utils.py
CHANGED
@@ -118,9 +118,12 @@ def retrieve_and_validate_response_send_metric(
|
|
118
118
|
else:
|
119
119
|
try:
|
120
120
|
response_json = response.json()
|
121
|
-
|
122
|
-
|
123
|
-
|
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):
|
cgc/utils/version_control.py
CHANGED
@@ -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"
|
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.
|
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=
|
2
|
-
cgc/CHANGELOG.md,sha256=
|
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=
|
21
|
-
cgc/commands/compute/compute_utils.py,sha256=
|
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=
|
24
|
-
cgc/commands/compute/billing/billing_responses.py,sha256=
|
25
|
-
cgc/commands/compute/billing/billing_utils.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
76
|
-
cgc/utils/prepare_headers.py,sha256=
|
77
|
-
cgc/utils/requests_helper.py,sha256=
|
78
|
-
cgc/utils/response_utils.py,sha256=
|
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=
|
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.
|
89
|
-
cgcsdk-1.
|
90
|
-
cgcsdk-1.
|
91
|
-
cgcsdk-1.
|
92
|
-
cgcsdk-1.
|
93
|
-
cgcsdk-1.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|