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.
Files changed (104) hide show
  1. {cgcsdk-1.2.6/cgcsdk.egg-info → cgcsdk-1.3.0}/PKG-INFO +1 -1
  2. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/.env +2 -2
  3. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/CHANGELOG.md +10 -0
  4. cgcsdk-1.3.0/cgc/commands/compute/billing/billing_cmd.py +118 -0
  5. cgcsdk-1.3.0/cgc/commands/compute/billing/billing_responses.py +65 -0
  6. cgcsdk-1.3.0/cgc/commands/compute/billing/billing_utils.py +241 -0
  7. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/volume_responses.py +1 -1
  8. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/custom_exceptions.py +26 -26
  9. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/message_utils.py +4 -0
  10. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/prepare_headers.py +3 -3
  11. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/requests_helper.py +4 -0
  12. {cgcsdk-1.2.6 → cgcsdk-1.3.0/cgcsdk.egg-info}/PKG-INFO +1 -1
  13. cgcsdk-1.2.6/cgc/commands/compute/billing/billing_cmd.py +0 -159
  14. cgcsdk-1.2.6/cgc/commands/compute/billing/billing_responses.py +0 -55
  15. cgcsdk-1.2.6/cgc/commands/compute/billing/billing_utils.py +0 -163
  16. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/LICENSE +0 -0
  17. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/MANIFEST.in +0 -0
  18. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/README.md +0 -0
  19. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/__init__.py +0 -0
  20. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/cgc.py +0 -0
  21. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/__init__.py +0 -0
  22. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/auth/__init__.py +0 -0
  23. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/auth/auth_cmd.py +0 -0
  24. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/auth/auth_logic.py +0 -0
  25. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/auth/auth_responses.py +0 -0
  26. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/auth/auth_utils.py +0 -0
  27. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/cgc_cmd.py +0 -0
  28. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/cgc_cmd_responses.py +0 -0
  29. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/cgc_helpers.py +0 -0
  30. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/cgc_models.py +0 -0
  31. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/__init__.py +0 -0
  32. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/billing/__init__.py +0 -0
  33. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/compute_cmd.py +0 -0
  34. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/compute_models.py +0 -0
  35. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/compute_responses.py +0 -0
  36. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/compute/compute_utils.py +0 -0
  37. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/db/__init__.py +0 -0
  38. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/db/db_cmd.py +0 -0
  39. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/db/db_models.py +0 -0
  40. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/debug/__init__.py +0 -0
  41. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/debug/debug_cmd.py +0 -0
  42. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/exceptions.py +0 -0
  43. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/jobs/__init__.py +0 -0
  44. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/jobs/job_utils.py +0 -0
  45. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/jobs/jobs_cmd.py +0 -0
  46. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/jobs/jobs_responses.py +0 -0
  47. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/resource/__init__.py +0 -0
  48. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/resource/resource_cmd.py +0 -0
  49. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/resource/resource_responses.py +0 -0
  50. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/__init__.py +0 -0
  51. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/keys_cmd.py +0 -0
  52. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/keys_models.py +0 -0
  53. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/keys_responses.py +0 -0
  54. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/keys_utils.py +0 -0
  55. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/secret_cmd.py +0 -0
  56. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/secret_models.py +0 -0
  57. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/secret_responses.py +0 -0
  58. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/user/secret_utils.py +0 -0
  59. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/__init__.py +0 -0
  60. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/data_model.py +0 -0
  61. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/volume_cmd.py +0 -0
  62. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/volume_models.py +0 -0
  63. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/commands/volume/volume_utils.py +0 -0
  64. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/config.py +0 -0
  65. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/sdk/__init__.py +0 -0
  66. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/sdk/exceptions.py +0 -0
  67. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/sdk/job.py +0 -0
  68. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/sdk/postgresql.py +0 -0
  69. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/sdk/resource.py +0 -0
  70. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/telemetry/__init__.py +0 -0
  71. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/telemetry/basic.py +0 -0
  72. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/__init__.py +0 -0
  73. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_billing_invoice.txt +0 -0
  74. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_billing_status.txt +0 -0
  75. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_billing_stop_events_compute.txt +0 -0
  76. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_billing_stop_events_volume.txt +0 -0
  77. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_compute_list.txt +0 -0
  78. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_compute_list_no_labels.txt +0 -0
  79. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_tabulate_response.txt +0 -0
  80. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/desired_responses/test_volume_list.txt +0 -0
  81. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/responses_tests.py +0 -0
  82. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/tests/test.py +0 -0
  83. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/__init__.py +0 -0
  84. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/click_group.py +0 -0
  85. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/config_utils.py +0 -0
  86. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/consts/__init__.py +0 -0
  87. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/consts/env_consts.py +0 -0
  88. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/consts/message_consts.py +0 -0
  89. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/cryptography/__init__.py +0 -0
  90. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/cryptography/aes_crypto.py +0 -0
  91. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/cryptography/encryption_module.py +0 -0
  92. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/cryptography/rsa_crypto.py +0 -0
  93. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/get_headers_data.py +0 -0
  94. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/response_utils.py +0 -0
  95. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/update.py +0 -0
  96. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgc/utils/version_control.py +0 -0
  97. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgcsdk.egg-info/SOURCES.txt +0 -0
  98. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgcsdk.egg-info/dependency_links.txt +0 -0
  99. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgcsdk.egg-info/entry_points.txt +0 -0
  100. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgcsdk.egg-info/requires.txt +0 -0
  101. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/cgcsdk.egg-info/top_level.txt +0 -0
  102. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/pyproject.toml +0 -0
  103. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/setup.cfg +0 -0
  104. {cgcsdk-1.2.6 → cgcsdk-1.3.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cgcsdk
3
- Version: 1.2.6
3
+ Version: 1.3.0
4
4
  Summary: CGC Core REST API client
5
5
  Home-page: https://cgc.comtegra.cloud/
6
6
  Author: Comtegra AI Team
@@ -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 = 6
8
+ MAJOR_VERSION = 3
9
+ MINOR_VERSION = 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"] -> 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",
@@ -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": "undefined exception",
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 resources limit",
14
- "RESOURCES_NOT_AVAILABLE_IN_CLUSTER": "Requested resources not available",
15
- "RESOURCE_SCALE_RESOURCES_LIMIT_EXCEEDED": "This request exceeds your resources limit",
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": "Volume with this name already exists.",
20
- "PVC_DELETE_EXCEPTION": "Can't delete mounted volume, try with force",
21
- "RESOURCE_PORTS_ALREADY_EXISTS": "Port with this name already exists.",
22
- "RESOURCE_TEMPLATE_NAME_ALREADY_EXISTS": "Resource with this name already exists.",
23
- "JOB_CREATE_ALREADY_EXISTS": "Job with this name already exists.",
24
- "USER_KEY_ALREADY_EXISTS": "Key with these data already exists.",
25
- "AlreadyExists": "Object with this name 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.",
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 api key with this id to delete",
32
- "PVC_MOUNT_NOT_FOUND_TEMPLATE": "No volume 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.",
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 id not found.",
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))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cgcsdk
3
- Version: 1.2.6
3
+ Version: 1.3.0
4
4
  Summary: CGC Core REST API client
5
5
  Home-page: https://cgc.comtegra.cloud/
6
6
  Author: Comtegra AI Team