cgcsdk 1.2.6__tar.gz → 1.3.0__tar.gz
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.
- {cgcsdk-1.2.6/cgcsdk.egg-info → cgcsdk-1.3.0}/PKG-INFO +1 -1
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/.env +2 -2
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/CHANGELOG.md +10 -0
- cgcsdk-1.3.0/cgc/commands/compute/billing/billing_cmd.py +118 -0
- cgcsdk-1.3.0/cgc/commands/compute/billing/billing_responses.py +65 -0
- cgcsdk-1.3.0/cgc/commands/compute/billing/billing_utils.py +241 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/volume_responses.py +1 -1
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/custom_exceptions.py +26 -26
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/message_utils.py +4 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/prepare_headers.py +3 -3
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/requests_helper.py +4 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0/cgcsdk.egg-info}/PKG-INFO +1 -1
- cgcsdk-1.2.6/cgc/commands/compute/billing/billing_cmd.py +0 -159
- cgcsdk-1.2.6/cgc/commands/compute/billing/billing_responses.py +0 -55
- cgcsdk-1.2.6/cgc/commands/compute/billing/billing_utils.py +0 -163
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/LICENSE +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/MANIFEST.in +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/README.md +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/cgc.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/auth/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/auth/auth_cmd.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/auth/auth_logic.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/auth/auth_responses.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/auth/auth_utils.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/cgc_cmd.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/cgc_cmd_responses.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/cgc_helpers.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/cgc_models.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/billing/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/compute_cmd.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/compute_models.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/compute_responses.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/compute_utils.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/db/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/db/db_cmd.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/db/db_models.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/debug/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/debug/debug_cmd.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/exceptions.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/jobs/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/jobs/job_utils.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/jobs/jobs_cmd.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/jobs/jobs_responses.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/resource/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/resource/resource_cmd.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/resource/resource_responses.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/keys_cmd.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/keys_models.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/keys_responses.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/keys_utils.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/secret_cmd.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/secret_models.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/secret_responses.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/secret_utils.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/data_model.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/volume_cmd.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/volume_models.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/volume_utils.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/config.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/sdk/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/sdk/exceptions.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/sdk/job.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/sdk/postgresql.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/sdk/resource.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/telemetry/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/telemetry/basic.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_billing_invoice.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_billing_status.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_billing_stop_events_compute.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_billing_stop_events_volume.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_compute_list.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_compute_list_no_labels.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_tabulate_response.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_volume_list.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/responses_tests.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/test.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/click_group.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/config_utils.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/consts/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/consts/env_consts.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/consts/message_consts.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/cryptography/__init__.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/cryptography/aes_crypto.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/cryptography/encryption_module.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/cryptography/rsa_crypto.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/get_headers_data.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/response_utils.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/update.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/version_control.py +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgcsdk.egg-info/SOURCES.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgcsdk.egg-info/dependency_links.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgcsdk.egg-info/entry_points.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgcsdk.egg-info/requires.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgcsdk.egg-info/top_level.txt +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/pyproject.toml +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/setup.cfg +0 -0
- {cgcsdk-1.2.6 → cgcsdk-1.3.0}/setup.py +0 -0
@@ -1,5 +1,15 @@
|
|
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
|
+
|
3
13
|
## 1.2.6
|
4
14
|
|
5
15
|
Release on Apr 30, 2025
|
@@ -0,0 +1,118 @@
|
|
1
|
+
import click
|
2
|
+
|
3
|
+
from datetime import datetime
|
4
|
+
|
5
|
+
from cgc.commands.compute.billing.billing_responses import (
|
6
|
+
billing_pricing_response,
|
7
|
+
billing_status_response,
|
8
|
+
billing_invoice_response,
|
9
|
+
)
|
10
|
+
from cgc.utils.prepare_headers import get_api_url_and_prepare_headers
|
11
|
+
from cgc.utils.response_utils import retrieve_and_validate_response_send_metric
|
12
|
+
from cgc.utils.click_group import CustomGroup, CustomCommand
|
13
|
+
from cgc.utils.requests_helper import call_api, EndpointTypes
|
14
|
+
|
15
|
+
|
16
|
+
@click.group("billing", cls=CustomGroup)
|
17
|
+
def billing_group():
|
18
|
+
"""
|
19
|
+
Access and manage billing information.
|
20
|
+
"""
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
24
|
+
@billing_group.command("status", cls=CustomCommand)
|
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):
|
36
|
+
"""
|
37
|
+
Shows billing status for user namespace
|
38
|
+
"""
|
39
|
+
api_url, headers = get_api_url_and_prepare_headers()
|
40
|
+
url = f"{api_url}/v2/api/billing/status?details={detailed}"
|
41
|
+
metric = "billing.status"
|
42
|
+
__res = call_api(request=EndpointTypes.get, url=url, headers=headers)
|
43
|
+
click.echo(
|
44
|
+
billing_status_response(
|
45
|
+
retrieve_and_validate_response_send_metric(__res, metric)
|
46
|
+
)
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
def _get_previous_month():
|
51
|
+
return datetime.now().month - 1 if datetime.now().month > 1 else 12
|
52
|
+
|
53
|
+
|
54
|
+
def _get_previous_year_if_required():
|
55
|
+
return datetime.now().year - 1 if datetime.now().month == 1 else datetime.now().year
|
56
|
+
|
57
|
+
|
58
|
+
@billing_group.command("invoice", cls=CustomCommand)
|
59
|
+
@click.option(
|
60
|
+
"--year",
|
61
|
+
"-y",
|
62
|
+
"year",
|
63
|
+
prompt=True,
|
64
|
+
type=int,
|
65
|
+
default=_get_previous_year_if_required(),
|
66
|
+
)
|
67
|
+
@click.option(
|
68
|
+
"--month",
|
69
|
+
"-m",
|
70
|
+
"month",
|
71
|
+
prompt=True,
|
72
|
+
type=click.IntRange(1, 12),
|
73
|
+
default=_get_previous_month(),
|
74
|
+
)
|
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):
|
86
|
+
"""
|
87
|
+
Opens invoice from given year and month
|
88
|
+
"""
|
89
|
+
api_url, headers = get_api_url_and_prepare_headers()
|
90
|
+
url = (
|
91
|
+
f"{api_url}/v2/api/billing/invoice?year={year}&month={month}&details={detailed}"
|
92
|
+
)
|
93
|
+
metric = "billing.invoice"
|
94
|
+
__res = call_api(request=EndpointTypes.get, url=url, headers=headers)
|
95
|
+
|
96
|
+
click.echo(
|
97
|
+
billing_invoice_response(
|
98
|
+
year,
|
99
|
+
month,
|
100
|
+
retrieve_and_validate_response_send_metric(__res, metric),
|
101
|
+
)
|
102
|
+
)
|
103
|
+
|
104
|
+
|
105
|
+
@billing_group.command("pricing", cls=CustomCommand)
|
106
|
+
def billing_pricing():
|
107
|
+
"""
|
108
|
+
Shows billing pricing information for user
|
109
|
+
"""
|
110
|
+
api_url, headers = get_api_url_and_prepare_headers()
|
111
|
+
url = f"{api_url}/v2/api/billing/user_pricing"
|
112
|
+
metric = "billing.pricing"
|
113
|
+
__res = call_api(request=EndpointTypes.get, url=url, headers=headers)
|
114
|
+
click.echo(
|
115
|
+
billing_pricing_response(
|
116
|
+
retrieve_and_validate_response_send_metric(__res, metric)
|
117
|
+
)
|
118
|
+
)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
import calendar
|
2
|
+
import click
|
3
|
+
from decimal import Decimal
|
4
|
+
from tabulate import tabulate
|
5
|
+
from cgc.commands.compute.billing import (
|
6
|
+
NoCostsFound,
|
7
|
+
NoInvoiceFoundForSelectedMonth,
|
8
|
+
)
|
9
|
+
from cgc.commands.compute.billing.billing_utils import get_billing_status_message
|
10
|
+
from cgc.utils.message_utils import key_error_decorator_for_helpers
|
11
|
+
|
12
|
+
|
13
|
+
@key_error_decorator_for_helpers
|
14
|
+
def billing_status_response(data: dict) -> str:
|
15
|
+
total_cost = data["details"]["cost_total"]
|
16
|
+
namespace = data["details"]["namespace"]
|
17
|
+
billing_records = data["details"]["billing_records"]
|
18
|
+
details = data["details"].get("details", [])
|
19
|
+
if not billing_records:
|
20
|
+
raise NoCostsFound()
|
21
|
+
message = get_billing_status_message(billing_records, details)
|
22
|
+
message += f"Total cost for namespace {namespace}: {total_cost:.2f} pln"
|
23
|
+
return message
|
24
|
+
|
25
|
+
|
26
|
+
@key_error_decorator_for_helpers
|
27
|
+
def billing_invoice_response(year: int, month: int, data: dict) -> str:
|
28
|
+
total_cost = float(data["details"]["cost_total"])
|
29
|
+
namespace = data["details"]["namespace"]
|
30
|
+
billing_records = data["details"]["billing_records"]
|
31
|
+
details = data["details"].get("details", [])
|
32
|
+
if (
|
33
|
+
not billing_records or total_cost == 0
|
34
|
+
): # TODO: total_cost == 0 is it correct thinking?
|
35
|
+
raise NoInvoiceFoundForSelectedMonth(year, month)
|
36
|
+
message = get_billing_status_message(billing_records, details)
|
37
|
+
message += f"Total cost for namespace {namespace} in {calendar.month_name[month]} {year}: {total_cost:.2f} pln"
|
38
|
+
return message
|
39
|
+
|
40
|
+
|
41
|
+
@key_error_decorator_for_helpers
|
42
|
+
def billing_pricing_response(data: dict) -> str:
|
43
|
+
"""Create response string for billing pricing command.
|
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
|
+
)
|
64
|
+
|
65
|
+
return tabulate(pricing_data, headers=headers)
|
@@ -0,0 +1,241 @@
|
|
1
|
+
import click
|
2
|
+
import datetime
|
3
|
+
import sys
|
4
|
+
from tabulate import tabulate
|
5
|
+
from cgc.utils.message_utils import (
|
6
|
+
prepare_error_message,
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
def verify_input_datetime(*args):
|
11
|
+
try:
|
12
|
+
for arg in args:
|
13
|
+
datetime.datetime.strptime(arg, "%d-%m-%Y")
|
14
|
+
except ValueError:
|
15
|
+
click.echo(prepare_error_message("Incorrect date format, should be DD-MM-YYYY"))
|
16
|
+
sys.exit()
|
17
|
+
|
18
|
+
|
19
|
+
def _get_costs_list_for_user(costs_list: list):
|
20
|
+
"""Format data in costs list to be displayed in a table and calculate user cost
|
21
|
+
|
22
|
+
:param costs_list: list of costs for user
|
23
|
+
:type costs_list: list
|
24
|
+
:return: formatted list of costs and total cost for user
|
25
|
+
:rtype: user_costs_list_to_print: list, total_user_cost: float
|
26
|
+
"""
|
27
|
+
user_costs_list_to_print = []
|
28
|
+
total_user_cost = 0
|
29
|
+
|
30
|
+
for cost in costs_list:
|
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))
|
104
|
+
else:
|
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)
|
126
|
+
|
127
|
+
if costs_list:
|
128
|
+
user_costs_list_to_print.sort(key=lambda d: f"{d[0]} {d[1]}")
|
129
|
+
return user_costs_list_to_print, total_user_cost
|
130
|
+
|
131
|
+
|
132
|
+
def get_billing_status_message(billing_records: list, details: list = []):
|
133
|
+
"""Prints billing status for all users in a pretty table
|
134
|
+
|
135
|
+
:param user_list: list of users with costs
|
136
|
+
:type user_list: list
|
137
|
+
"""
|
138
|
+
message = ""
|
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)
|
149
|
+
list_headers = _get_status_list_headers()
|
150
|
+
message += f"Billing status for user: {user}\n"
|
151
|
+
message += tabulate(costs_list_to_print, headers=list_headers)
|
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"
|
162
|
+
return message
|
163
|
+
|
164
|
+
|
165
|
+
def _get_status_list_headers():
|
166
|
+
"""Generates headers for billing status command
|
167
|
+
|
168
|
+
:return: list of headers
|
169
|
+
:rtype: list
|
170
|
+
"""
|
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
|
+
]
|
191
|
+
|
192
|
+
|
193
|
+
# TODO: refactor to use: tabulate_a_response(data: list) -> str:
|
194
|
+
def get_table_compute_stop_events_message(event_list: list):
|
195
|
+
"""Prints compute stop events info
|
196
|
+
|
197
|
+
:param event_list: raw list of events
|
198
|
+
:type event_list: list
|
199
|
+
"""
|
200
|
+
message = "Compute stop events:"
|
201
|
+
event_list_headers = ["id", "name", "entity", "date created"]
|
202
|
+
event_list_to_print = []
|
203
|
+
for event in event_list:
|
204
|
+
event_id = event["event_id"]
|
205
|
+
event_name = event["event_name"]
|
206
|
+
event_date = event["date_created"]
|
207
|
+
event_entity = event["entity"]
|
208
|
+
row_list = [event_id, event_name, event_entity, event_date]
|
209
|
+
event_list_to_print.append(row_list)
|
210
|
+
message += tabulate(event_list_to_print, headers=event_list_headers)
|
211
|
+
return message
|
212
|
+
|
213
|
+
|
214
|
+
# TODO: refactor to use: tabulate_a_response(data: list) -> str:
|
215
|
+
def get_table_volume_stop_events_message(event_list: list):
|
216
|
+
"""Prints volume stop events info
|
217
|
+
|
218
|
+
:param event_list: raw list of events
|
219
|
+
:type event_list: list
|
220
|
+
"""
|
221
|
+
message = "Volume stop events:"
|
222
|
+
event_list_headers = [
|
223
|
+
"id",
|
224
|
+
"name",
|
225
|
+
"disks type",
|
226
|
+
"access type",
|
227
|
+
"size",
|
228
|
+
"date created",
|
229
|
+
]
|
230
|
+
event_list_to_print = []
|
231
|
+
for event in event_list:
|
232
|
+
event_id = event["event_id"]
|
233
|
+
volume_name = event["volume_name"]
|
234
|
+
event_date = event["date_created"]
|
235
|
+
disks_type = event["disks_type"]
|
236
|
+
access_type = event["access_type"]
|
237
|
+
size = event["size"]
|
238
|
+
row_list = [event_id, volume_name, disks_type, access_type, size, event_date]
|
239
|
+
event_list_to_print.append(row_list)
|
240
|
+
message += tabulate(event_list_to_print, headers=event_list_headers)
|
241
|
+
return message
|
@@ -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",
|
@@ -3,33 +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",
|
8
|
-
"RESOURCE_SCALE_TEMPLATE_ERROR": "Error while scaling resource - resource not fully compatible CGC",
|
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.",
|
9
9
|
},
|
10
10
|
413: {
|
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
|
14
|
-
"RESOURCES_NOT_AVAILABLE_IN_CLUSTER": "Requested resources not available",
|
15
|
-
"RESOURCE_SCALE_RESOURCES_LIMIT_EXCEEDED": "This request exceeds your
|
16
|
-
"PVC_CREATE_STORAGE_LIMIT_EXCEEDED": "Storage limit exceeded",
|
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.",
|
17
17
|
},
|
18
18
|
409: {
|
19
|
-
"PVC_NAME_ALREADY_EXISTS": "
|
20
|
-
"PVC_DELETE_EXCEPTION": "Can't delete mounted volume
|
21
|
-
"RESOURCE_PORTS_ALREADY_EXISTS": "
|
22
|
-
"RESOURCE_TEMPLATE_NAME_ALREADY_EXISTS": "
|
23
|
-
"JOB_CREATE_ALREADY_EXISTS": "
|
24
|
-
"USER_KEY_ALREADY_EXISTS": "
|
25
|
-
"AlreadyExists": "
|
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.",
|
26
26
|
},
|
27
27
|
404: {
|
28
|
-
"PVC_CREATE_NO_SC": "Selected disk type and access mode unavailable",
|
28
|
+
"PVC_CREATE_NO_SC": "Selected disk type and access mode are unavailable.",
|
29
29
|
"BILLING_STATUS_NO_DATA": "No data to print.",
|
30
30
|
"NOT_DELETED_ANYTHING_IN_COMPUTE_DELETE": "No app with this name to delete.",
|
31
|
-
"API_KEY_DELETE_ERROR": "No
|
32
|
-
"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.",
|
33
33
|
"PVC_UNMOUNT_NOT_MOUNTED": "Volume with this name is not mounted.",
|
34
34
|
"PVC_NOT_FOUND": "Volume with this name not found.",
|
35
35
|
"PVC_DELETE_NOT_FOUND": "App with this name not found.",
|
@@ -39,21 +39,21 @@ CUSTOM_EXCEPTIONS = {
|
|
39
39
|
"COMPUTE_RESOURCE_QUOTA_NOT_FOUND": "You do not have enforced limits on your namespace.",
|
40
40
|
"JOB_NOT_FOUND": "Job with this name not found.",
|
41
41
|
"RESOURCE_NOT_FOUND": "Resource with this name not found.",
|
42
|
-
"USER_KEY_NOT_FOUND": "Key with this
|
42
|
+
"USER_KEY_NOT_FOUND": "Key with this ID not found.",
|
43
43
|
"RESOURCE_SCALE_TEMPLATE_NOT_FOUND": "No app with this name.",
|
44
|
-
"NotFound": "Resource not found",
|
44
|
+
"NotFound": "Resource not found.",
|
45
45
|
"USER_SECRET_UPDATE_NOT_FOUND": "Secret not found.",
|
46
46
|
"USER_SECRET_DELETE_NOT_FOUND": "Secret not found.",
|
47
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",
|
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.",
|
50
50
|
},
|
51
51
|
400: {
|
52
52
|
"WRONG_DATE_FORMAT": "Wrong date format.",
|
53
53
|
"ENTITY_NOT_ALLOWED": "You can't create this entity.",
|
54
54
|
"PVC_MOUNT_ALREADY_MOUNTED": "This volume is already mounted.",
|
55
|
-
"TEMPLATE_NAME_SYSTEM_RESERVED": "You can't create app with this name.",
|
56
|
-
"JOB_LACKS_REQUIRED_PARAMETER": "Job requires container image parameter.",
|
57
|
-
"RESOURCE_PORTS_EXCEPTION": "Resource with this name not found",
|
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.",
|
58
58
|
},
|
59
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))
|