day2 0.2.0.dev1__tar.gz → 0.2.1__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.
- {day2-0.2.0.dev1 → day2-0.2.1}/PKG-INFO +1 -9
- {day2-0.2.0.dev1 → day2-0.2.1}/README.md +0 -8
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/client/base.py +56 -50
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/client/config.py +2 -33
- day2-0.2.1/day2/models/cost.py +54 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/models/tenant.py +25 -5
- day2-0.2.1/day2/resources/cost.py +62 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/resources/tenant.py +37 -1
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/session.py +12 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2.egg-info/PKG-INFO +1 -9
- {day2-0.2.0.dev1 → day2-0.2.1}/day2.egg-info/SOURCES.txt +6 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2_cli/cli.py +2 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2_cli/commands/assessment.py +7 -4
- day2-0.2.1/day2_cli/commands/cost.py +129 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2_cli/commands/tenant.py +83 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/pyproject.toml +1 -1
- {day2-0.2.0.dev1 → day2-0.2.1}/tests/test_cli_config.py +1 -1
- day2-0.2.1/tests/test_client_base.py +231 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/tests/test_config.py +3 -3
- day2-0.2.1/tests/test_models_cost.py +112 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/tests/test_models_tenant.py +79 -15
- day2-0.2.1/tests/test_resources_cost.py +90 -0
- day2-0.2.1/tests/test_resources_tenant.py +230 -0
- day2-0.2.0.dev1/tests/test_resources_tenant.py +0 -100
- {day2-0.2.0.dev1 → day2-0.2.1}/LICENSE +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/__init__.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/_version.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/auth/__init__.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/auth/credentials.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/client/__init__.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/exceptions.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/models/__init__.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/models/assessment.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/resources/__init__.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2/resources/assessment.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2.egg-info/dependency_links.txt +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2.egg-info/entry_points.txt +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2.egg-info/requires.txt +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2.egg-info/top_level.txt +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2_cli/__init__.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2_cli/commands/__init__.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2_cli/commands/auth.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2_cli/commands/config.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2_cli/utils/__init__.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2_cli/utils/config.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2_cli/utils/formatters.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/day2_cli/utils/output_formatter.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/setup.cfg +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/tests/test_cli_auth.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/tests/test_models_assessment.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/tests/test_resources_assessment.py +0 -0
- {day2-0.2.0.dev1 → day2-0.2.1}/tests/test_session.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: day2
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: MontyCloud DAY2 SDK for Python
|
|
5
5
|
Author-email: MontyCloud <info@montycloud.com>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -276,14 +276,6 @@ except Exception as e:
|
|
|
276
276
|
print(f"Unexpected error: {e}")
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
-
## Development
|
|
280
|
-
|
|
281
|
-
Refer to [DEVELOPER.md](DEVELOPER.md) for development instructions.
|
|
282
|
-
|
|
283
|
-
## Release
|
|
284
|
-
|
|
285
|
-
Refer to [RELEASE.md](RELEASE.md) for release instructions.
|
|
286
|
-
|
|
287
279
|
## License
|
|
288
280
|
|
|
289
281
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -238,14 +238,6 @@ except Exception as e:
|
|
|
238
238
|
print(f"Unexpected error: {e}")
|
|
239
239
|
```
|
|
240
240
|
|
|
241
|
-
## Development
|
|
242
|
-
|
|
243
|
-
Refer to [DEVELOPER.md](DEVELOPER.md) for development instructions.
|
|
244
|
-
|
|
245
|
-
## Release
|
|
246
|
-
|
|
247
|
-
Refer to [RELEASE.md](RELEASE.md) for release instructions.
|
|
248
|
-
|
|
249
241
|
## License
|
|
250
242
|
|
|
251
243
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -84,12 +84,6 @@ class BaseClient:
|
|
|
84
84
|
if self.session.tenant_id:
|
|
85
85
|
headers["x-tenant-id"] = str(self.session.tenant_id)
|
|
86
86
|
|
|
87
|
-
# Add any additional headers from config
|
|
88
|
-
if self._config.additional_headers:
|
|
89
|
-
# Ensure all header values are strings
|
|
90
|
-
for key, value in self._config.additional_headers.items():
|
|
91
|
-
headers[key] = str(value)
|
|
92
|
-
|
|
93
87
|
return headers
|
|
94
88
|
|
|
95
89
|
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
|
|
@@ -112,10 +106,10 @@ class BaseClient:
|
|
|
112
106
|
|
|
113
107
|
try:
|
|
114
108
|
data: Dict[str, Any] = response.json()
|
|
115
|
-
logger.
|
|
109
|
+
logger.debug("Response data: %s", json.dumps(data, indent=2)[:1000])
|
|
116
110
|
except ValueError:
|
|
117
111
|
data = {"Message": response.text}
|
|
118
|
-
logger.
|
|
112
|
+
logger.debug("Response text: %s", response.text[:1000])
|
|
119
113
|
|
|
120
114
|
if 400 <= response.status_code < 500:
|
|
121
115
|
message = data.get("Message", "Client error")
|
|
@@ -134,63 +128,75 @@ class BaseClient:
|
|
|
134
128
|
|
|
135
129
|
return dict(data)
|
|
136
130
|
|
|
137
|
-
@retry(
|
|
138
|
-
stop=stop_after_attempt(3),
|
|
139
|
-
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
140
|
-
retry=retry_if_exception_type(ServerError),
|
|
141
|
-
reraise=True,
|
|
142
|
-
)
|
|
143
131
|
def _request_with_retry(
|
|
144
132
|
self, method: str, url: str, **kwargs: Any
|
|
145
133
|
) -> Dict[str, Any]:
|
|
146
|
-
"""Make an API request with retry logic.
|
|
134
|
+
"""Make an API request with retry logic based on configuration.
|
|
147
135
|
|
|
148
136
|
Args:
|
|
149
|
-
method: HTTP method (GET, POST,
|
|
150
|
-
url:
|
|
151
|
-
**kwargs: Additional arguments to pass to requests.
|
|
137
|
+
method: HTTP method (GET, POST, etc.)
|
|
138
|
+
url: URL to request
|
|
139
|
+
**kwargs: Additional arguments to pass to requests.request
|
|
152
140
|
|
|
153
141
|
Returns:
|
|
154
|
-
|
|
142
|
+
Dict containing the parsed response
|
|
155
143
|
|
|
156
144
|
Raises:
|
|
157
|
-
ClientError: For client errors (4xx)
|
|
158
|
-
ServerError: For server errors (5xx)
|
|
145
|
+
ClientError: For client errors (4xx)
|
|
146
|
+
ServerError: For server errors (5xx)
|
|
159
147
|
"""
|
|
160
|
-
headers = self._get_headers()
|
|
161
|
-
|
|
162
|
-
# Update headers with any provided in kwargs
|
|
163
|
-
if "headers" in kwargs:
|
|
164
|
-
headers.update(kwargs.pop("headers"))
|
|
165
|
-
|
|
166
|
-
# Log request details
|
|
167
|
-
logger.debug(
|
|
168
|
-
"Making %s request to %s with headers %s and kwargs %s",
|
|
169
|
-
method,
|
|
170
|
-
url,
|
|
171
|
-
headers,
|
|
172
|
-
kwargs,
|
|
173
|
-
)
|
|
174
148
|
|
|
175
|
-
|
|
176
|
-
|
|
149
|
+
# Define the actual request function to retry
|
|
150
|
+
def _do_request() -> Dict[str, Any]:
|
|
151
|
+
headers = self._get_headers()
|
|
152
|
+
|
|
153
|
+
# Update headers with any provided in kwargs
|
|
154
|
+
request_kwargs = kwargs.copy()
|
|
155
|
+
if "headers" in request_kwargs:
|
|
156
|
+
headers.update(request_kwargs.pop("headers"))
|
|
157
|
+
|
|
158
|
+
# Log request details
|
|
159
|
+
logger.debug(
|
|
160
|
+
"Making %s request to %s with headers %s and kwargs %s",
|
|
177
161
|
method,
|
|
178
162
|
url,
|
|
179
|
-
headers
|
|
180
|
-
|
|
181
|
-
**kwargs,
|
|
163
|
+
headers,
|
|
164
|
+
request_kwargs,
|
|
182
165
|
)
|
|
183
166
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
167
|
+
try:
|
|
168
|
+
response = requests.request(
|
|
169
|
+
method,
|
|
170
|
+
url,
|
|
171
|
+
headers=headers,
|
|
172
|
+
timeout=self._config.timeout,
|
|
173
|
+
**request_kwargs,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Log response details
|
|
177
|
+
logger.debug("Received response with status %s", response.status_code)
|
|
178
|
+
logger.debug("Response headers: %s", response.headers)
|
|
179
|
+
logger.debug("Response content: %s", response.text[:1000])
|
|
180
|
+
|
|
181
|
+
return self._handle_response(response)
|
|
182
|
+
except requests.RequestException as e:
|
|
183
|
+
logger.error("Request failed: %s", str(e))
|
|
184
|
+
raise ServerError(str(e), 0, None) from e
|
|
185
|
+
|
|
186
|
+
# Create a retry object with config values
|
|
187
|
+
r = retry(
|
|
188
|
+
stop=stop_after_attempt(self._config.max_retries),
|
|
189
|
+
wait=wait_exponential(
|
|
190
|
+
multiplier=self._config.retry_backoff_factor,
|
|
191
|
+
min=self._config.retry_min_delay,
|
|
192
|
+
max=self._config.retry_max_delay,
|
|
193
|
+
),
|
|
194
|
+
retry=retry_if_exception_type(ServerError),
|
|
195
|
+
reraise=True,
|
|
196
|
+
)
|
|
190
197
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
raise ServerError(str(e), 0) from e
|
|
198
|
+
# Apply retry to our request function and call it
|
|
199
|
+
return r(_do_request)()
|
|
194
200
|
|
|
195
201
|
def _make_request(
|
|
196
202
|
self, method: str, endpoint: str, **kwargs: Any
|
|
@@ -210,6 +216,6 @@ class BaseClient:
|
|
|
210
216
|
kwargs["json"] = kwargs.pop("json_data")
|
|
211
217
|
|
|
212
218
|
url = self._get_endpoint_url(endpoint)
|
|
213
|
-
logger.
|
|
219
|
+
logger.debug("Making request to %s %s", method, url)
|
|
214
220
|
# Pass the full URL to the request method
|
|
215
221
|
return self._request_with_retry(method, url, **kwargs)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Configuration for the MontyCloud SDK."""
|
|
2
2
|
|
|
3
3
|
import configparser
|
|
4
|
-
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, Dict, Optional
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ class Config:
|
|
|
11
11
|
"""Configuration for the MontyCloud SDK."""
|
|
12
12
|
|
|
13
13
|
# Base URL for the MontyCloud API
|
|
14
|
-
base_url: str = "https://
|
|
14
|
+
base_url: str = "https://api.montycloud.com/day2/api"
|
|
15
15
|
|
|
16
16
|
# API version
|
|
17
17
|
api_version: str = "v1"
|
|
@@ -34,12 +34,6 @@ class Config:
|
|
|
34
34
|
# Maximum retry delay in seconds
|
|
35
35
|
retry_max_delay: float = 10.0
|
|
36
36
|
|
|
37
|
-
# Status codes to retry on
|
|
38
|
-
retry_status_codes: tuple = (500, 502, 503, 504)
|
|
39
|
-
|
|
40
|
-
# Additional headers to include in all requests
|
|
41
|
-
additional_headers: Dict[str, str] = field(default_factory=dict)
|
|
42
|
-
|
|
43
37
|
# CLI output format (table or json)
|
|
44
38
|
output_format: str = "table"
|
|
45
39
|
|
|
@@ -67,10 +61,6 @@ class Config:
|
|
|
67
61
|
"tenant_id": self.tenant_id,
|
|
68
62
|
"retry_min_delay": self.retry_min_delay,
|
|
69
63
|
"retry_max_delay": self.retry_max_delay,
|
|
70
|
-
"retry_status_codes": ",".join(
|
|
71
|
-
str(code) for code in self.retry_status_codes
|
|
72
|
-
),
|
|
73
|
-
"additional_headers": self.additional_headers,
|
|
74
64
|
"output_format": self.output_format,
|
|
75
65
|
}
|
|
76
66
|
|
|
@@ -84,16 +74,6 @@ class Config:
|
|
|
84
74
|
Returns:
|
|
85
75
|
Config object initialized with values from dictionary.
|
|
86
76
|
"""
|
|
87
|
-
# Handle special case for retry_status_codes if it's a string
|
|
88
|
-
if "retry_status_codes" in config_dict and isinstance(
|
|
89
|
-
config_dict["retry_status_codes"], str
|
|
90
|
-
):
|
|
91
|
-
config_dict["retry_status_codes"] = tuple(
|
|
92
|
-
int(code)
|
|
93
|
-
for code in config_dict["retry_status_codes"].split(",")
|
|
94
|
-
if code.strip().isdigit()
|
|
95
|
-
)
|
|
96
|
-
|
|
97
77
|
return cls(**config_dict)
|
|
98
78
|
|
|
99
79
|
@classmethod
|
|
@@ -150,17 +130,6 @@ class Config:
|
|
|
150
130
|
fallback=str(cls.retry_max_delay),
|
|
151
131
|
)
|
|
152
132
|
),
|
|
153
|
-
# Status codes need special handling as they're a tuple in code
|
|
154
|
-
retry_status_codes=tuple(
|
|
155
|
-
int(code)
|
|
156
|
-
for code in config_parser.get(
|
|
157
|
-
"DEFAULT", "retry_status_codes", fallback=""
|
|
158
|
-
).split(",")
|
|
159
|
-
if code.strip().isdigit()
|
|
160
|
-
)
|
|
161
|
-
or cls.retry_status_codes,
|
|
162
|
-
# Headers would need to be serialized/deserialized specially
|
|
163
|
-
additional_headers={},
|
|
164
133
|
# Output format
|
|
165
134
|
output_format=config_parser.get(
|
|
166
135
|
"DEFAULT", "output_format", fallback=cls.output_format
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field, root_validator
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Utility function to convert camelCase or PascalCase to snake_case
|
|
8
|
+
def to_snake_case(name: str) -> str:
|
|
9
|
+
return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GetCostByChargeTypeOutput(BaseModel):
|
|
13
|
+
"""Details of cost breakdown by charge type."""
|
|
14
|
+
|
|
15
|
+
total_cost: Optional[float] = Field(None, alias="TotalCost")
|
|
16
|
+
usage: Optional[float] = Field(None, alias="Usage")
|
|
17
|
+
bundled_discount: Optional[float] = Field(None, alias="BundledDiscount")
|
|
18
|
+
credit: Optional[float] = Field(None, alias="Credit")
|
|
19
|
+
discount: Optional[float] = Field(None, alias="Discount")
|
|
20
|
+
discounted_usage: Optional[float] = Field(None, alias="DiscountedUsage")
|
|
21
|
+
fee: Optional[float] = Field(None, alias="Fee")
|
|
22
|
+
refund: Optional[float] = Field(None, alias="Refund")
|
|
23
|
+
ri_fee: Optional[float] = Field(None, alias="RIFee")
|
|
24
|
+
tax: Optional[float] = Field(None, alias="Tax")
|
|
25
|
+
savings_plan_upfront_fee: Optional[float] = Field(
|
|
26
|
+
None, alias="SavingsPlanUpfrontFee"
|
|
27
|
+
)
|
|
28
|
+
savings_plan_recurring_fee: Optional[float] = Field(
|
|
29
|
+
None, alias="SavingsPlanRecurringFee"
|
|
30
|
+
)
|
|
31
|
+
savings_plan_covered_usage: Optional[float] = Field(
|
|
32
|
+
None, alias="SavingsPlanCoveredUsage"
|
|
33
|
+
)
|
|
34
|
+
savings_plan_negation: Optional[float] = Field(None, alias="SavingsPlanNegation")
|
|
35
|
+
spp_discount: Optional[float] = Field(None, alias="SPPDiscount")
|
|
36
|
+
distributor_discount: Optional[float] = Field(None, alias="DistributorDiscount")
|
|
37
|
+
|
|
38
|
+
# Allow extra fields
|
|
39
|
+
model_config = ConfigDict(extra="allow")
|
|
40
|
+
|
|
41
|
+
@root_validator(pre=True)
|
|
42
|
+
@classmethod
|
|
43
|
+
def handle_dynamic_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
44
|
+
"""Handle dynamic extra fields by converting them to snake_case."""
|
|
45
|
+
known_fields = {
|
|
46
|
+
field.alias or field_name for field_name, field in cls.model_fields.items()
|
|
47
|
+
}
|
|
48
|
+
# Dynamically handle all extra fields
|
|
49
|
+
for key in list(values.keys()):
|
|
50
|
+
if key not in known_fields:
|
|
51
|
+
# Convert the extra field to snake_case and map it
|
|
52
|
+
snake_case_key = to_snake_case(key)
|
|
53
|
+
values[snake_case_key] = values.pop(key)
|
|
54
|
+
return values
|
|
@@ -31,11 +31,6 @@ class ListTenantsOutput(BaseModel):
|
|
|
31
31
|
|
|
32
32
|
tenants: List[TenantDetails] = Field(alias="Tenants", default=[])
|
|
33
33
|
next_page_token: Optional[str] = Field(None, alias="NextPageToken")
|
|
34
|
-
# Keep these fields for backward compatibility
|
|
35
|
-
has_more: Optional[bool] = Field(None, alias="HasMore")
|
|
36
|
-
page_number: Optional[int] = Field(None, alias="PageNumber")
|
|
37
|
-
page_size: Optional[int] = Field(None, alias="PageSize")
|
|
38
|
-
total_count: Optional[int] = Field(None, alias="TotalCount")
|
|
39
34
|
|
|
40
35
|
# Allow extra fields
|
|
41
36
|
model_config = ConfigDict(extra="allow")
|
|
@@ -43,3 +38,28 @@ class ListTenantsOutput(BaseModel):
|
|
|
43
38
|
|
|
44
39
|
class GetTenantOutput(TenantDetails):
|
|
45
40
|
"""Output of get_tenant operation."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Account(BaseModel):
|
|
44
|
+
"""Details of an account."""
|
|
45
|
+
|
|
46
|
+
number: str = Field(alias="Number")
|
|
47
|
+
name: str = Field(alias="Name")
|
|
48
|
+
status: str = Field(alias="Status")
|
|
49
|
+
type: str = Field(alias="Type")
|
|
50
|
+
permission_model: str = Field(alias="PermissionModel")
|
|
51
|
+
onboarded_date: str = Field(alias="OnboardedDate")
|
|
52
|
+
|
|
53
|
+
# Allow extra fields
|
|
54
|
+
model_config = ConfigDict(extra="allow")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ListAccountsOutput(BaseModel):
|
|
58
|
+
"""Output of list_accounts operation."""
|
|
59
|
+
|
|
60
|
+
accounts: List[Account] = Field(alias="Accounts", default=[])
|
|
61
|
+
has_more: bool = Field(alias="HasMore", default=False)
|
|
62
|
+
page_number: int = Field(alias="PageNumber")
|
|
63
|
+
|
|
64
|
+
# Allow extra fields
|
|
65
|
+
model_config = ConfigDict(extra="allow")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from day2.client.base import BaseClient
|
|
4
|
+
from day2.models.cost import GetCostByChargeTypeOutput
|
|
5
|
+
from day2.session import Session
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CostClient(BaseClient):
|
|
9
|
+
"""Client for interacting with the Cost service."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, session: Session) -> None:
|
|
12
|
+
"""Initialize a new CostClient.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
session: MontyCloud session.
|
|
16
|
+
"""
|
|
17
|
+
super().__init__(session, "cost")
|
|
18
|
+
|
|
19
|
+
def get_cost_by_charge_type(
|
|
20
|
+
self,
|
|
21
|
+
tenant_id: str,
|
|
22
|
+
cloud_provider: str,
|
|
23
|
+
start_date: str,
|
|
24
|
+
end_date: str,
|
|
25
|
+
) -> GetCostByChargeTypeOutput:
|
|
26
|
+
"""Get cost breakdown by charge type for a tenant.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
tenant_id: The ID of the tenant to fetch cost data for.
|
|
30
|
+
cloud_provider: The cloud provider (e.g., "AWS", "Azure").
|
|
31
|
+
start_date: Start date in YYYY-MM-DD format.
|
|
32
|
+
end_date: End date in YYYY-MM-DD format.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
GetCostByChargeTypeOutput: Object containing cost breakdown by charge type.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ClientError: If the request is invalid.
|
|
39
|
+
ServerError: If an internal server error occurs.
|
|
40
|
+
AuthenticationError: If authentication fails.
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
>>> session = Session(api_key="your-api-key")
|
|
44
|
+
>>> client = session.cost
|
|
45
|
+
>>> response = client.get_cost_by_charge_type(
|
|
46
|
+
... tenant_id="tenant-123",
|
|
47
|
+
... cloud_provider="AWS",
|
|
48
|
+
... start_date="2023-01-01",
|
|
49
|
+
... end_date="2023-01-31"
|
|
50
|
+
... )
|
|
51
|
+
>>> print(f"Total Cost: {response.total_cost}")
|
|
52
|
+
>>> print(f"Bundled Discount: {response.bundled_discount}")
|
|
53
|
+
"""
|
|
54
|
+
params: Dict[str, str] = {
|
|
55
|
+
"CloudProvider": cloud_provider,
|
|
56
|
+
"StartDate": start_date,
|
|
57
|
+
"EndDate": end_date,
|
|
58
|
+
}
|
|
59
|
+
response = self._make_request(
|
|
60
|
+
"GET", f"tenants/{tenant_id}/cost/cost-by-charge-type", params=params
|
|
61
|
+
)
|
|
62
|
+
return GetCostByChargeTypeOutput.model_validate(response)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
5
|
from day2.client.base import BaseClient
|
|
6
|
-
from day2.models.tenant import GetTenantOutput, ListTenantsOutput
|
|
6
|
+
from day2.models.tenant import GetTenantOutput, ListAccountsOutput, ListTenantsOutput
|
|
7
7
|
from day2.session import Session
|
|
8
8
|
|
|
9
9
|
|
|
@@ -79,3 +79,39 @@ class TenantClient(BaseClient):
|
|
|
79
79
|
# The endpoint for getting tenant details is directly using the tenant ID in the path
|
|
80
80
|
response = self._make_request("GET", f"tenants/{tenant_id}")
|
|
81
81
|
return GetTenantOutput.model_validate(response)
|
|
82
|
+
|
|
83
|
+
def list_accounts(
|
|
84
|
+
self, tenant_id: str, page_size: int = 10, page_number: int = 1
|
|
85
|
+
) -> ListAccountsOutput:
|
|
86
|
+
"""List accounts associated with a specific tenant.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
tenant_id: ID of the tenant to list accounts for.
|
|
90
|
+
page_size: Number of accounts to be fetched in a page (default: 10).
|
|
91
|
+
page_number: Page number for pagination (default: 1).
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
ListAccountsOutput: Object containing list of accounts and pagination info.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ResourceNotFoundError: If the tenant does not exist.
|
|
98
|
+
ClientError: If the request is invalid.
|
|
99
|
+
ServerError: If an internal server error occurs.
|
|
100
|
+
AuthenticationError: If authentication fails.
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
>>> session = Session(api_key="your-api-key")
|
|
104
|
+
>>> client = session.tenant
|
|
105
|
+
>>> response = client.list_accounts("tenant-123", page_size=10)
|
|
106
|
+
>>> for account in response.accounts:
|
|
107
|
+
... print(f"{account.number}: {account.name}")
|
|
108
|
+
"""
|
|
109
|
+
params: dict[str, object] = {
|
|
110
|
+
"PageSize": page_size,
|
|
111
|
+
"PageNumber": page_number,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
response = self._make_request(
|
|
115
|
+
"GET", f"tenants/{tenant_id}/accounts", params=params
|
|
116
|
+
)
|
|
117
|
+
return ListAccountsOutput.model_validate(response)
|
|
@@ -8,6 +8,7 @@ from day2.client.config import Config
|
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from day2.resources.assessment import AssessmentClient
|
|
11
|
+
from day2.resources.cost import CostClient
|
|
11
12
|
from day2.resources.tenant import TenantClient
|
|
12
13
|
|
|
13
14
|
T = TypeVar("T")
|
|
@@ -101,6 +102,15 @@ class Session:
|
|
|
101
102
|
"""
|
|
102
103
|
return cast("AssessmentClient", self.client("assessment"))
|
|
103
104
|
|
|
105
|
+
@property
|
|
106
|
+
def cost(self) -> "CostClient":
|
|
107
|
+
"""Get the cost client.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
CostClient: The cost client.
|
|
111
|
+
"""
|
|
112
|
+
return cast("CostClient", self.client("cost"))
|
|
113
|
+
|
|
104
114
|
def _create_client(self, service_name: str) -> Any:
|
|
105
115
|
"""Create a client for the specified service.
|
|
106
116
|
|
|
@@ -115,11 +125,13 @@ class Session:
|
|
|
115
125
|
"""
|
|
116
126
|
# Import the actual classes at runtime to avoid circular imports
|
|
117
127
|
from day2.resources.assessment import AssessmentClient
|
|
128
|
+
from day2.resources.cost import CostClient
|
|
118
129
|
from day2.resources.tenant import TenantClient
|
|
119
130
|
|
|
120
131
|
service_map = {
|
|
121
132
|
"tenant": TenantClient,
|
|
122
133
|
"assessment": AssessmentClient,
|
|
134
|
+
"cost": CostClient,
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
if service_name not in service_map:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: day2
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: MontyCloud DAY2 SDK for Python
|
|
5
5
|
Author-email: MontyCloud <info@montycloud.com>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -276,14 +276,6 @@ except Exception as e:
|
|
|
276
276
|
print(f"Unexpected error: {e}")
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
-
## Development
|
|
280
|
-
|
|
281
|
-
Refer to [DEVELOPER.md](DEVELOPER.md) for development instructions.
|
|
282
|
-
|
|
283
|
-
## Release
|
|
284
|
-
|
|
285
|
-
Refer to [RELEASE.md](RELEASE.md) for release instructions.
|
|
286
|
-
|
|
287
279
|
## License
|
|
288
280
|
|
|
289
281
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -18,9 +18,11 @@ day2/client/base.py
|
|
|
18
18
|
day2/client/config.py
|
|
19
19
|
day2/models/__init__.py
|
|
20
20
|
day2/models/assessment.py
|
|
21
|
+
day2/models/cost.py
|
|
21
22
|
day2/models/tenant.py
|
|
22
23
|
day2/resources/__init__.py
|
|
23
24
|
day2/resources/assessment.py
|
|
25
|
+
day2/resources/cost.py
|
|
24
26
|
day2/resources/tenant.py
|
|
25
27
|
day2_cli/__init__.py
|
|
26
28
|
day2_cli/cli.py
|
|
@@ -28,6 +30,7 @@ day2_cli/commands/__init__.py
|
|
|
28
30
|
day2_cli/commands/assessment.py
|
|
29
31
|
day2_cli/commands/auth.py
|
|
30
32
|
day2_cli/commands/config.py
|
|
33
|
+
day2_cli/commands/cost.py
|
|
31
34
|
day2_cli/commands/tenant.py
|
|
32
35
|
day2_cli/utils/__init__.py
|
|
33
36
|
day2_cli/utils/config.py
|
|
@@ -35,9 +38,12 @@ day2_cli/utils/formatters.py
|
|
|
35
38
|
day2_cli/utils/output_formatter.py
|
|
36
39
|
tests/test_cli_auth.py
|
|
37
40
|
tests/test_cli_config.py
|
|
41
|
+
tests/test_client_base.py
|
|
38
42
|
tests/test_config.py
|
|
39
43
|
tests/test_models_assessment.py
|
|
44
|
+
tests/test_models_cost.py
|
|
40
45
|
tests/test_models_tenant.py
|
|
41
46
|
tests/test_resources_assessment.py
|
|
47
|
+
tests/test_resources_cost.py
|
|
42
48
|
tests/test_resources_tenant.py
|
|
43
49
|
tests/test_session.py
|
|
@@ -13,6 +13,7 @@ from day2_cli.commands.assessment import assessment
|
|
|
13
13
|
# Import from day2_cli package
|
|
14
14
|
from day2_cli.commands.auth import auth
|
|
15
15
|
from day2_cli.commands.config import config
|
|
16
|
+
from day2_cli.commands.cost import cost
|
|
16
17
|
from day2_cli.commands.tenant import tenant
|
|
17
18
|
from day2_cli.utils.formatters import format_error
|
|
18
19
|
|
|
@@ -41,6 +42,7 @@ cli.add_command(auth)
|
|
|
41
42
|
cli.add_command(config)
|
|
42
43
|
cli.add_command(tenant)
|
|
43
44
|
cli.add_command(assessment)
|
|
45
|
+
cli.add_command(cost)
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
def main() -> None:
|
|
@@ -38,7 +38,7 @@ def get_tenant_id(session: Session, tenant_id: Optional[str]) -> Optional[str]:
|
|
|
38
38
|
"[red]Error: No tenant ID provided and no default tenant configured.[/red]"
|
|
39
39
|
)
|
|
40
40
|
console.print(
|
|
41
|
-
"[yellow]Tip: Configure a default tenant with 'day2
|
|
41
|
+
"[yellow]Tip: Configure a default tenant with 'day2 config set tenant-id YOUR_TENANT_ID'[/yellow]"
|
|
42
42
|
)
|
|
43
43
|
return None
|
|
44
44
|
|
|
@@ -71,7 +71,7 @@ def list_assessments(
|
|
|
71
71
|
) -> None:
|
|
72
72
|
"""List assessments for a tenant.
|
|
73
73
|
|
|
74
|
-
If --tenant-id is not provided, uses the default tenant configured with '
|
|
74
|
+
If --tenant-id is not provided, uses the default tenant configured with 'day2 config set tenant-id YOUR_TENANT_ID'.
|
|
75
75
|
"""
|
|
76
76
|
try:
|
|
77
77
|
# Get output format from context if set via global option
|
|
@@ -267,8 +267,11 @@ def create_assessment(
|
|
|
267
267
|
|
|
268
268
|
try:
|
|
269
269
|
scope_data = json.loads(scope)
|
|
270
|
-
if not isinstance(scope_data,
|
|
271
|
-
|
|
270
|
+
if not isinstance(scope_data, dict):
|
|
271
|
+
console.print(
|
|
272
|
+
"[red]Error: Scope must be a JSON object (dictionary), not a list or primitive value[/red]"
|
|
273
|
+
)
|
|
274
|
+
return
|
|
272
275
|
except json.JSONDecodeError:
|
|
273
276
|
console.print("[red]Error: Scope must be a valid JSON string[/red]")
|
|
274
277
|
return
|