day2 0.2.3__tar.gz → 0.2.4__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.3 → day2-0.2.4}/PKG-INFO +1 -75
- {day2-0.2.3 → day2-0.2.4}/README.md +0 -74
- {day2-0.2.3 → day2-0.2.4}/day2/auth/credentials.py +40 -42
- {day2-0.2.3 → day2-0.2.4}/day2/models/__init__.py +6 -1
- {day2-0.2.3 → day2-0.2.4}/day2/models/assessment.py +4 -2
- {day2-0.2.3 → day2-0.2.4}/day2/models/report.py +32 -0
- {day2-0.2.3 → day2-0.2.4}/day2/resources/assessment.py +4 -2
- {day2-0.2.3 → day2-0.2.4}/day2/resources/report.py +36 -1
- {day2-0.2.3 → day2-0.2.4}/day2/resources/tenant.py +1 -1
- {day2-0.2.3 → day2-0.2.4}/day2/session.py +3 -27
- day2-0.2.4/day2/utils/__init__.py +0 -0
- day2-0.2.4/day2/utils/helpers.py +27 -0
- {day2-0.2.3 → day2-0.2.4}/day2.egg-info/PKG-INFO +1 -75
- {day2-0.2.3 → day2-0.2.4}/day2.egg-info/SOURCES.txt +2 -0
- {day2-0.2.3 → day2-0.2.4}/day2_cli/commands/assessment.py +26 -26
- {day2-0.2.3 → day2-0.2.4}/day2_cli/commands/auth.py +34 -12
- {day2-0.2.3 → day2-0.2.4}/day2_cli/commands/profile.py +94 -54
- {day2-0.2.3 → day2-0.2.4}/day2_cli/commands/report.py +79 -8
- {day2-0.2.3 → day2-0.2.4}/pyproject.toml +1 -1
- {day2-0.2.3 → day2-0.2.4}/LICENSE +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/__init__.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/_version.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/auth/__init__.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/client/__init__.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/client/base.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/client/config.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/client/constants.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/client/profile.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/client/user_context.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/exceptions.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/models/cost.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/models/tenant.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/resources/__init__.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/resources/cost.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2/resources/user.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2.egg-info/dependency_links.txt +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2.egg-info/entry_points.txt +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2.egg-info/requires.txt +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2.egg-info/top_level.txt +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2_cli/__init__.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2_cli/cli.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2_cli/commands/__init__.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2_cli/commands/cost.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2_cli/commands/tenant.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2_cli/utils/__init__.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2_cli/utils/context.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2_cli/utils/exit_codes.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2_cli/utils/formatters.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/day2_cli/utils/output_formatter.py +0 -0
- {day2-0.2.3 → day2-0.2.4}/setup.cfg +0 -0
- {day2-0.2.3 → day2-0.2.4}/tests/test_exceptions.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.4
|
|
4
4
|
Summary: MontyCloud DAY2 SDK for Python
|
|
5
5
|
Author-email: MontyCloud <info@montycloud.com>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -220,34 +220,6 @@ default_session.set_tenant("org-123")
|
|
|
220
220
|
assessments = day2.assessment().list_assessments(tenant_id="tenant-123", status="PENDING")
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
## Running Examples
|
|
224
|
-
|
|
225
|
-
The SDK comes with example scripts in the `examples` directory that demonstrate various features. To run these examples:
|
|
226
|
-
|
|
227
|
-
1. Clone the repository:
|
|
228
|
-
```bash
|
|
229
|
-
git clone https://github.com/montycloud/day2-sdk.git
|
|
230
|
-
cd day2-sdk
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
2. Create a virtual environment:
|
|
234
|
-
```bash
|
|
235
|
-
python -m venv .venv
|
|
236
|
-
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
3. Install the package in development mode:
|
|
240
|
-
```bash
|
|
241
|
-
pip install -e .
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
4. Run an example:
|
|
245
|
-
```bash
|
|
246
|
-
python examples/basic_usage.py
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Note: Some examples may require you to have valid API credentials. Make sure to set up your authentication as described in the [Authentication](#authentication) section before running the examples.
|
|
250
|
-
|
|
251
223
|
## Error Handling
|
|
252
224
|
|
|
253
225
|
The SDK provides comprehensive error handling with custom exceptions:
|
|
@@ -276,52 +248,6 @@ except Exception as e:
|
|
|
276
248
|
print(f"Unexpected error: {e}")
|
|
277
249
|
```
|
|
278
250
|
|
|
279
|
-
## Development
|
|
280
|
-
|
|
281
|
-
### Documentation
|
|
282
|
-
|
|
283
|
-
The project uses MkDocs for documentation. Here are the available commands:
|
|
284
|
-
|
|
285
|
-
```bash
|
|
286
|
-
# Build documentation locally
|
|
287
|
-
make docs
|
|
288
|
-
|
|
289
|
-
# Serve documentation for local development
|
|
290
|
-
make serve-docs
|
|
291
|
-
|
|
292
|
-
# Deploy documentation to GitHub Pages (from main branch)
|
|
293
|
-
make deploy-docs
|
|
294
|
-
|
|
295
|
-
# Deploy documentation from any branch to GitHub Pages
|
|
296
|
-
make deploy-branch-docs
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
The `deploy-branch-docs` command is useful for:
|
|
300
|
-
- Testing documentation changes before merging to main
|
|
301
|
-
- Deploying documentation from feature branches
|
|
302
|
-
- Creating documentation previews for pull requests
|
|
303
|
-
|
|
304
|
-
**Note**: `deploy-branch-docs` will overwrite the current GitHub Pages content, so use with caution in production environments.
|
|
305
|
-
|
|
306
|
-
### Other Development Commands
|
|
307
|
-
|
|
308
|
-
```bash
|
|
309
|
-
# Run tests
|
|
310
|
-
make test
|
|
311
|
-
|
|
312
|
-
# Run linting
|
|
313
|
-
make lint
|
|
314
|
-
|
|
315
|
-
# Format code
|
|
316
|
-
make format
|
|
317
|
-
|
|
318
|
-
# Run type checking
|
|
319
|
-
make mypy
|
|
320
|
-
|
|
321
|
-
# Run all CI checks
|
|
322
|
-
make ci
|
|
323
|
-
```
|
|
324
|
-
|
|
325
251
|
## License
|
|
326
252
|
|
|
327
253
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -182,34 +182,6 @@ default_session.set_tenant("org-123")
|
|
|
182
182
|
assessments = day2.assessment().list_assessments(tenant_id="tenant-123", status="PENDING")
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
-
## Running Examples
|
|
186
|
-
|
|
187
|
-
The SDK comes with example scripts in the `examples` directory that demonstrate various features. To run these examples:
|
|
188
|
-
|
|
189
|
-
1. Clone the repository:
|
|
190
|
-
```bash
|
|
191
|
-
git clone https://github.com/montycloud/day2-sdk.git
|
|
192
|
-
cd day2-sdk
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
2. Create a virtual environment:
|
|
196
|
-
```bash
|
|
197
|
-
python -m venv .venv
|
|
198
|
-
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
3. Install the package in development mode:
|
|
202
|
-
```bash
|
|
203
|
-
pip install -e .
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
4. Run an example:
|
|
207
|
-
```bash
|
|
208
|
-
python examples/basic_usage.py
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
Note: Some examples may require you to have valid API credentials. Make sure to set up your authentication as described in the [Authentication](#authentication) section before running the examples.
|
|
212
|
-
|
|
213
185
|
## Error Handling
|
|
214
186
|
|
|
215
187
|
The SDK provides comprehensive error handling with custom exceptions:
|
|
@@ -238,52 +210,6 @@ except Exception as e:
|
|
|
238
210
|
print(f"Unexpected error: {e}")
|
|
239
211
|
```
|
|
240
212
|
|
|
241
|
-
## Development
|
|
242
|
-
|
|
243
|
-
### Documentation
|
|
244
|
-
|
|
245
|
-
The project uses MkDocs for documentation. Here are the available commands:
|
|
246
|
-
|
|
247
|
-
```bash
|
|
248
|
-
# Build documentation locally
|
|
249
|
-
make docs
|
|
250
|
-
|
|
251
|
-
# Serve documentation for local development
|
|
252
|
-
make serve-docs
|
|
253
|
-
|
|
254
|
-
# Deploy documentation to GitHub Pages (from main branch)
|
|
255
|
-
make deploy-docs
|
|
256
|
-
|
|
257
|
-
# Deploy documentation from any branch to GitHub Pages
|
|
258
|
-
make deploy-branch-docs
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
The `deploy-branch-docs` command is useful for:
|
|
262
|
-
- Testing documentation changes before merging to main
|
|
263
|
-
- Deploying documentation from feature branches
|
|
264
|
-
- Creating documentation previews for pull requests
|
|
265
|
-
|
|
266
|
-
**Note**: `deploy-branch-docs` will overwrite the current GitHub Pages content, so use with caution in production environments.
|
|
267
|
-
|
|
268
|
-
### Other Development Commands
|
|
269
|
-
|
|
270
|
-
```bash
|
|
271
|
-
# Run tests
|
|
272
|
-
make test
|
|
273
|
-
|
|
274
|
-
# Run linting
|
|
275
|
-
make lint
|
|
276
|
-
|
|
277
|
-
# Format code
|
|
278
|
-
make format
|
|
279
|
-
|
|
280
|
-
# Run type checking
|
|
281
|
-
make mypy
|
|
282
|
-
|
|
283
|
-
# Run all CI checks
|
|
284
|
-
make ci
|
|
285
|
-
```
|
|
286
|
-
|
|
287
213
|
## License
|
|
288
214
|
|
|
289
215
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -30,9 +30,7 @@ class Credentials:
|
|
|
30
30
|
self.base_url = base_url
|
|
31
31
|
|
|
32
32
|
# Determine which credential section to use
|
|
33
|
-
self.credential_section = self._determine_credential_section(
|
|
34
|
-
profile_name, base_url
|
|
35
|
-
)
|
|
33
|
+
self.credential_section = self._determine_credential_section(profile_name)
|
|
36
34
|
|
|
37
35
|
self.api_key = api_key or self._load_from_env() or self._load_from_config()
|
|
38
36
|
self.secret_key = (
|
|
@@ -65,52 +63,26 @@ class Credentials:
|
|
|
65
63
|
|
|
66
64
|
return config_dir / "credentials"
|
|
67
65
|
|
|
68
|
-
def _determine_credential_section(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
66
|
+
def _determine_credential_section(self, profile_name: Optional[str]) -> str:
|
|
67
|
+
"""Determine which credential section to use.
|
|
68
|
+
|
|
69
|
+
This now uses a simpler approach:
|
|
70
|
+
- If no profile name, use DEFAULT
|
|
71
|
+
- Otherwise, try the profile's own section first
|
|
72
|
+
- The actual loading logic will handle fallback to DEFAULT
|
|
72
73
|
|
|
73
74
|
Args:
|
|
74
75
|
profile_name: Profile name
|
|
75
|
-
base_url: Base URL for the profile
|
|
76
76
|
|
|
77
77
|
Returns:
|
|
78
|
-
Credential section name to use ("DEFAULT"
|
|
78
|
+
Credential section name to use (profile name or "DEFAULT")
|
|
79
79
|
"""
|
|
80
80
|
if not profile_name:
|
|
81
81
|
return "DEFAULT"
|
|
82
82
|
|
|
83
|
-
#
|
|
84
|
-
if
|
|
85
|
-
|
|
86
|
-
return "DEFAULT"
|
|
87
|
-
|
|
88
|
-
def _should_use_profile_credentials(self, base_url: Optional[str]) -> bool:
|
|
89
|
-
"""Determine if a profile should use profile-specific credentials.
|
|
90
|
-
|
|
91
|
-
Logic:
|
|
92
|
-
- If base_url is different from the default base_url, use profile-specific credentials
|
|
93
|
-
- Otherwise, use shared credentials for the common MSP case
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
base_url: Base URL for this profile (or None to use default)
|
|
97
|
-
|
|
98
|
-
Returns:
|
|
99
|
-
True if profile should use profile-specific credentials, False for shared
|
|
100
|
-
"""
|
|
101
|
-
# Default base URL
|
|
102
|
-
default_base_url = "https://api.montycloud.com/day2/api"
|
|
103
|
-
|
|
104
|
-
# If no base_url provided, use default
|
|
105
|
-
if not base_url:
|
|
106
|
-
base_url = default_base_url
|
|
107
|
-
|
|
108
|
-
# Check if this base_url differs from default (different region/environment)
|
|
109
|
-
if base_url != default_base_url:
|
|
110
|
-
return True
|
|
111
|
-
|
|
112
|
-
# Default to shared credentials for the common MSP case
|
|
113
|
-
return False
|
|
83
|
+
# Return the profile name - the loading logic will handle checking
|
|
84
|
+
# if credentials exist there and falling back to DEFAULT if needed
|
|
85
|
+
return profile_name
|
|
114
86
|
|
|
115
87
|
def _load_from_env(self) -> Optional[str]:
|
|
116
88
|
"""Load API key from environment variables.
|
|
@@ -188,7 +160,21 @@ class Credentials:
|
|
|
188
160
|
config_parser = configparser.ConfigParser()
|
|
189
161
|
config_parser.read(credentials_file)
|
|
190
162
|
|
|
191
|
-
#
|
|
163
|
+
# First, always check if the profile has its own credentials
|
|
164
|
+
if self.profile_name:
|
|
165
|
+
profile_section = self._find_section_case_insensitive(
|
|
166
|
+
config_parser, self.profile_name
|
|
167
|
+
)
|
|
168
|
+
if profile_section:
|
|
169
|
+
api_key = config_parser.get(
|
|
170
|
+
profile_section, "api_key", fallback=None
|
|
171
|
+
)
|
|
172
|
+
if api_key:
|
|
173
|
+
# Update credential_section to reflect what we actually used
|
|
174
|
+
self.credential_section = profile_section
|
|
175
|
+
return api_key
|
|
176
|
+
|
|
177
|
+
# Then try the determined section (case-insensitive)
|
|
192
178
|
actual_section = self._find_section_case_insensitive(
|
|
193
179
|
config_parser, self.credential_section
|
|
194
180
|
)
|
|
@@ -220,7 +206,19 @@ class Credentials:
|
|
|
220
206
|
config_parser = configparser.ConfigParser()
|
|
221
207
|
config_parser.read(credentials_file)
|
|
222
208
|
|
|
223
|
-
#
|
|
209
|
+
# First, always check if the profile has its own credentials
|
|
210
|
+
if self.profile_name:
|
|
211
|
+
profile_section = self._find_section_case_insensitive(
|
|
212
|
+
config_parser, self.profile_name
|
|
213
|
+
)
|
|
214
|
+
if profile_section:
|
|
215
|
+
secret_key = config_parser.get(
|
|
216
|
+
profile_section, "api_secret_key", fallback=None
|
|
217
|
+
)
|
|
218
|
+
if secret_key:
|
|
219
|
+
return secret_key
|
|
220
|
+
|
|
221
|
+
# Then try the determined section (case-insensitive)
|
|
224
222
|
actual_section = self._find_section_case_insensitive(
|
|
225
223
|
config_parser, self.credential_section
|
|
226
224
|
)
|
|
@@ -12,7 +12,11 @@ from day2.models.assessment import (
|
|
|
12
12
|
RunAssessmentOutput,
|
|
13
13
|
)
|
|
14
14
|
from day2.models.cost import GetCostByChargeTypeOutput
|
|
15
|
-
from day2.models.report import
|
|
15
|
+
from day2.models.report import (
|
|
16
|
+
GetReportDetailsOutput,
|
|
17
|
+
GetReportOutput,
|
|
18
|
+
ListReportsOutput,
|
|
19
|
+
)
|
|
16
20
|
from day2.models.tenant import ListTenantsOutput, TenantDetails
|
|
17
21
|
|
|
18
22
|
__all__ = [
|
|
@@ -30,4 +34,5 @@ __all__ = [
|
|
|
30
34
|
"GenerateAssessmentReportOutput",
|
|
31
35
|
"GetReportDetailsOutput",
|
|
32
36
|
"GetReportOutput",
|
|
37
|
+
"ListReportsOutput",
|
|
33
38
|
]
|
|
@@ -507,7 +507,8 @@ class RunAssessmentInput(BaseModel):
|
|
|
507
507
|
|
|
508
508
|
Attributes:
|
|
509
509
|
lens_name: Name of the lens (default: "AWS Well-Architected Framework").
|
|
510
|
-
pillar_id: Optional ID of the pillar.
|
|
510
|
+
pillar_id: Optional ID of the pillar. Allowed values: security, costOptimization,
|
|
511
|
+
reliability, performance, operationalExcellence, sustainability.
|
|
511
512
|
question_id: Optional ID of the specific question.
|
|
512
513
|
best_practice_id: Optional ID of the best practice.
|
|
513
514
|
"""
|
|
@@ -558,7 +559,8 @@ class GenerateAssessmentReportInput(BaseModel):
|
|
|
558
559
|
|
|
559
560
|
Attributes:
|
|
560
561
|
cloud_provider: Cloud provider name (default: AWS).
|
|
561
|
-
pillar_ids: List of pillar IDs to filter findings.
|
|
562
|
+
pillar_ids: List of pillar IDs to filter findings. Allowed values: security, costOptimization,
|
|
563
|
+
reliability, performance, operationalExcellence, sustainability.
|
|
562
564
|
severity: List of severity levels to filter findings.
|
|
563
565
|
status: List of statuses to filter findings.
|
|
564
566
|
account_number: List of AWS account numbers to filter findings.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
from typing import List
|
|
2
3
|
|
|
3
4
|
from pydantic import BaseModel, ConfigDict, Field
|
|
4
5
|
|
|
@@ -39,3 +40,34 @@ class GetReportOutput(BaseModel):
|
|
|
39
40
|
|
|
40
41
|
# Allow extra fields
|
|
41
42
|
model_config = ConfigDict(extra="allow")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Models for handling listing report
|
|
46
|
+
class ReportItem(BaseModel):
|
|
47
|
+
"""Details of individual report item.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
report_id: Unique identifier for the report.
|
|
51
|
+
report_name: Name of the report.
|
|
52
|
+
report_type: Type of the report.
|
|
53
|
+
status: Current status of the report generation (e.g., "Pending", "Success", "Failed").
|
|
54
|
+
export_format: Format of the report (e.g., "PDF", "CSV", "JSON").
|
|
55
|
+
created_at: Timestamp when the report was created.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
report_id: str = Field(alias="ReportId")
|
|
59
|
+
report_name: str = Field(alias="ReportName")
|
|
60
|
+
report_type: str = Field(alias="ReportType")
|
|
61
|
+
status: str = Field(alias="Status")
|
|
62
|
+
export_format: str = Field(alias="ExportFormat")
|
|
63
|
+
created_at: datetime = Field(alias="CreatedAt")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ListReportsOutput(BaseModel):
|
|
67
|
+
"""Output model for the ListReports API.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
reports: List of report items.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
reports: List[ReportItem] = Field(alias="Records", default=[])
|
|
@@ -186,7 +186,8 @@ class AssessmentClient(BaseClient):
|
|
|
186
186
|
Args:
|
|
187
187
|
tenant_id: ID of the tenant the assessment belongs to.
|
|
188
188
|
assessment_id: ID of the assessment to get questions for.
|
|
189
|
-
pillar_id: ID of the pillar to get questions for.
|
|
189
|
+
pillar_id: ID of the pillar to get questions for. Allowed values: security, costOptimization,
|
|
190
|
+
reliability, performance, operationalExcellence, sustainability.
|
|
190
191
|
|
|
191
192
|
Returns:
|
|
192
193
|
ListQuestionsOutput: Object containing list of questions for the specified pillar.
|
|
@@ -346,7 +347,8 @@ class AssessmentClient(BaseClient):
|
|
|
346
347
|
resource_type: Optional list of resource types to filter findings by.
|
|
347
348
|
question_ids: Optional list of question IDs to filter findings by.
|
|
348
349
|
resource_ids: Optional list of resource IDs to filter findings by.
|
|
349
|
-
pillar_ids: Optional list of pillar IDs to filter findings by.
|
|
350
|
+
pillar_ids: Optional list of pillar IDs to filter findings by. Allowed values: security, costOptimization,
|
|
351
|
+
reliability, performance, operationalExcellence, sustainability.
|
|
350
352
|
page_size: The number of findings per page (default: 10).
|
|
351
353
|
page_token: Token for pagination.
|
|
352
354
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"""Reports resource implementation for the MontyCloud DAY2 SDK."""
|
|
2
2
|
|
|
3
3
|
from day2.client.base import BaseClient
|
|
4
|
-
from day2.models.report import
|
|
4
|
+
from day2.models.report import (
|
|
5
|
+
GetReportDetailsOutput,
|
|
6
|
+
GetReportOutput,
|
|
7
|
+
ListReportsOutput,
|
|
8
|
+
)
|
|
5
9
|
from day2.session import Session
|
|
6
10
|
|
|
7
11
|
|
|
@@ -78,3 +82,34 @@ class ReportClient(BaseClient):
|
|
|
78
82
|
params=params,
|
|
79
83
|
)
|
|
80
84
|
return GetReportOutput.model_validate(response)
|
|
85
|
+
|
|
86
|
+
def list_reports(
|
|
87
|
+
self, tenant_id: str, cloud_provider: str = "AWS"
|
|
88
|
+
) -> ListReportsOutput:
|
|
89
|
+
"""Retrieves up to 100 of the most recent reports for a tenant, ordered by creation time (newest first).
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
tenant_id: ID of the tenant that owns the reports.
|
|
93
|
+
cloud_provider: Cloud provider for which to list reports (default is "AWS").
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
ListReportsOutput: Object containing a list of report items (maximum 100).
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
ResourceNotFoundError: If no reports are found.
|
|
100
|
+
ClientError: If the request is invalid.
|
|
101
|
+
ServerError: If an internal server error occurs.
|
|
102
|
+
AuthenticationError: If authentication fails.
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
>>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
|
|
106
|
+
>>> client = session.report
|
|
107
|
+
>>> response = client.list_reports(tenant_id="tenant-123", cloud_provider="AWS")
|
|
108
|
+
>>> for report in response.reports:
|
|
109
|
+
>>> print(f"Report Name: {report.report_name}, Status: {report.status}")
|
|
110
|
+
"""
|
|
111
|
+
params = {"CloudProvider": cloud_provider}
|
|
112
|
+
response = self._make_request(
|
|
113
|
+
"GET", f"tenants/{tenant_id}/reports/", params=params
|
|
114
|
+
)
|
|
115
|
+
return ListReportsOutput.model_validate(response)
|
|
@@ -52,7 +52,7 @@ class TenantClient(BaseClient):
|
|
|
52
52
|
if page_token:
|
|
53
53
|
params["PageToken"] = page_token
|
|
54
54
|
|
|
55
|
-
response = self._make_request("GET", "tenants", params=params)
|
|
55
|
+
response = self._make_request("GET", "tenants/", params=params)
|
|
56
56
|
return ListTenantsOutput.model_validate(response)
|
|
57
57
|
|
|
58
58
|
def get_tenant(self, tenant_id: str) -> GetTenantOutput:
|
|
@@ -165,36 +165,12 @@ class Session:
|
|
|
165
165
|
if user_context and user_context.tenant_id:
|
|
166
166
|
self.tenant_id = user_context.tenant_id
|
|
167
167
|
logger.debug("Using tenant_id from user context: %s", self.tenant_id)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
# Note: We do NOT fetch user context from API during Session init anymore
|
|
169
|
+
# This prevents file creation issues in Lambda environments
|
|
170
|
+
# User context should only be fetched and saved during auth login
|
|
171
171
|
|
|
172
172
|
logger.debug("Initialized session with tenant_id=%s", self.tenant_id)
|
|
173
173
|
|
|
174
|
-
def _fetch_and_store_user_context(self, config_dir: Optional[Path] = None) -> None:
|
|
175
|
-
"""Fetch user context from API and store it locally.
|
|
176
|
-
|
|
177
|
-
Args:
|
|
178
|
-
config_dir: Directory to store the user context file.
|
|
179
|
-
"""
|
|
180
|
-
try:
|
|
181
|
-
# Import here to avoid circular imports during initialization
|
|
182
|
-
from day2.resources.user import UserClient
|
|
183
|
-
|
|
184
|
-
# Create a temporary user client without tenant context
|
|
185
|
-
user_client = UserClient(self)
|
|
186
|
-
user_context = user_client.get_user()
|
|
187
|
-
|
|
188
|
-
if user_context and user_context.tenant_id:
|
|
189
|
-
# Store the user context for future use
|
|
190
|
-
user_context.save(config_dir)
|
|
191
|
-
self.tenant_id = user_context.tenant_id
|
|
192
|
-
logger.debug(
|
|
193
|
-
"Fetched and stored user context with tenant_id: %s", self.tenant_id
|
|
194
|
-
)
|
|
195
|
-
except (ImportError, AttributeError, ValueError) as e:
|
|
196
|
-
logger.debug("Could not fetch user context: %s", e)
|
|
197
|
-
|
|
198
174
|
def _save_tenant_to_config(self) -> None:
|
|
199
175
|
"""Save tenant ID to configuration file."""
|
|
200
176
|
if not self.tenant_id:
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Helper utilities for the MontyCloud DAY2 SDK."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def format_datetime(
|
|
8
|
+
dt: Optional[Union[datetime, str]] = None,
|
|
9
|
+
format_str: str = "%Y-%m-%d %H:%M:%S",
|
|
10
|
+
default: str = "N/A",
|
|
11
|
+
) -> str:
|
|
12
|
+
"""Format a datetime object or string to a standard format.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
dt: Datetime object or string to format. If None, returns the default value.
|
|
16
|
+
format_str: Format string to use for datetime formatting.
|
|
17
|
+
default: Default value to return if dt is None.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Formatted datetime string or default value if dt is None.
|
|
21
|
+
"""
|
|
22
|
+
if dt is None:
|
|
23
|
+
return default
|
|
24
|
+
|
|
25
|
+
if isinstance(dt, datetime):
|
|
26
|
+
return dt.strftime(format_str)
|
|
27
|
+
return str(dt)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: day2
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: MontyCloud DAY2 SDK for Python
|
|
5
5
|
Author-email: MontyCloud <info@montycloud.com>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -220,34 +220,6 @@ default_session.set_tenant("org-123")
|
|
|
220
220
|
assessments = day2.assessment().list_assessments(tenant_id="tenant-123", status="PENDING")
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
## Running Examples
|
|
224
|
-
|
|
225
|
-
The SDK comes with example scripts in the `examples` directory that demonstrate various features. To run these examples:
|
|
226
|
-
|
|
227
|
-
1. Clone the repository:
|
|
228
|
-
```bash
|
|
229
|
-
git clone https://github.com/montycloud/day2-sdk.git
|
|
230
|
-
cd day2-sdk
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
2. Create a virtual environment:
|
|
234
|
-
```bash
|
|
235
|
-
python -m venv .venv
|
|
236
|
-
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
3. Install the package in development mode:
|
|
240
|
-
```bash
|
|
241
|
-
pip install -e .
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
4. Run an example:
|
|
245
|
-
```bash
|
|
246
|
-
python examples/basic_usage.py
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Note: Some examples may require you to have valid API credentials. Make sure to set up your authentication as described in the [Authentication](#authentication) section before running the examples.
|
|
250
|
-
|
|
251
223
|
## Error Handling
|
|
252
224
|
|
|
253
225
|
The SDK provides comprehensive error handling with custom exceptions:
|
|
@@ -276,52 +248,6 @@ except Exception as e:
|
|
|
276
248
|
print(f"Unexpected error: {e}")
|
|
277
249
|
```
|
|
278
250
|
|
|
279
|
-
## Development
|
|
280
|
-
|
|
281
|
-
### Documentation
|
|
282
|
-
|
|
283
|
-
The project uses MkDocs for documentation. Here are the available commands:
|
|
284
|
-
|
|
285
|
-
```bash
|
|
286
|
-
# Build documentation locally
|
|
287
|
-
make docs
|
|
288
|
-
|
|
289
|
-
# Serve documentation for local development
|
|
290
|
-
make serve-docs
|
|
291
|
-
|
|
292
|
-
# Deploy documentation to GitHub Pages (from main branch)
|
|
293
|
-
make deploy-docs
|
|
294
|
-
|
|
295
|
-
# Deploy documentation from any branch to GitHub Pages
|
|
296
|
-
make deploy-branch-docs
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
The `deploy-branch-docs` command is useful for:
|
|
300
|
-
- Testing documentation changes before merging to main
|
|
301
|
-
- Deploying documentation from feature branches
|
|
302
|
-
- Creating documentation previews for pull requests
|
|
303
|
-
|
|
304
|
-
**Note**: `deploy-branch-docs` will overwrite the current GitHub Pages content, so use with caution in production environments.
|
|
305
|
-
|
|
306
|
-
### Other Development Commands
|
|
307
|
-
|
|
308
|
-
```bash
|
|
309
|
-
# Run tests
|
|
310
|
-
make test
|
|
311
|
-
|
|
312
|
-
# Run linting
|
|
313
|
-
make lint
|
|
314
|
-
|
|
315
|
-
# Format code
|
|
316
|
-
make format
|
|
317
|
-
|
|
318
|
-
# Run type checking
|
|
319
|
-
make mypy
|
|
320
|
-
|
|
321
|
-
# Run all CI checks
|
|
322
|
-
make ci
|
|
323
|
-
```
|
|
324
|
-
|
|
325
251
|
## License
|
|
326
252
|
|
|
327
253
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -335,7 +335,7 @@ def list_questions(
|
|
|
335
335
|
If --tenant-id is not provided, uses the default tenant configured with 'day2 profile create' or 'day2 auth login'.
|
|
336
336
|
|
|
337
337
|
ASSESSMENT-ID: ID of the assessment to list questions for.
|
|
338
|
-
PILLAR-ID: ID of the pillar to list questions for (required).
|
|
338
|
+
PILLAR-ID: ID of the pillar to list questions for (required). Allowed values: security, costOptimization, reliability, performance, operationalExcellence, sustainability.
|
|
339
339
|
"""
|
|
340
340
|
try:
|
|
341
341
|
# Get enhanced context with tenant resolution
|
|
@@ -788,7 +788,7 @@ def answer_question(
|
|
|
788
788
|
@click.option(
|
|
789
789
|
"--pillar-id",
|
|
790
790
|
multiple=True,
|
|
791
|
-
help="Filter by pillar ID (can be used multiple times)",
|
|
791
|
+
help="Filter by pillar ID (can be used multiple times). Allowed values: security, costOptimization, reliability, performance, operationalExcellence, sustainability.",
|
|
792
792
|
)
|
|
793
793
|
@click.option("--page-token", help="Page token for pagination")
|
|
794
794
|
@click.option("--page-size", type=int, default=10, help="Page size")
|
|
@@ -912,19 +912,19 @@ def list_findings(
|
|
|
912
912
|
@click.option(
|
|
913
913
|
"--lens-name",
|
|
914
914
|
default="AWS Well-Architected Framework",
|
|
915
|
-
help="
|
|
915
|
+
help="The name of the lens to use for the assessment (default: AWS Well-Architected Framework).",
|
|
916
916
|
)
|
|
917
917
|
@click.option(
|
|
918
918
|
"--pillar-id",
|
|
919
|
-
help="
|
|
919
|
+
help="The ID of the pillar to assess. Allowed values: security, costOptimization, reliability, performance, operationalExcellence, sustainability.",
|
|
920
920
|
)
|
|
921
921
|
@click.option(
|
|
922
922
|
"--question-id",
|
|
923
|
-
help="
|
|
923
|
+
help="The ID of a specific question to assess within the selected pillar.",
|
|
924
924
|
)
|
|
925
925
|
@click.option(
|
|
926
926
|
"--best-practice-id",
|
|
927
|
-
help="
|
|
927
|
+
help="The ID of a best practice to evaluate for the selected question ID.",
|
|
928
928
|
)
|
|
929
929
|
@with_common_options(include_tenant_id=True)
|
|
930
930
|
def run_assessment(
|
|
@@ -969,16 +969,16 @@ def run_assessment(
|
|
|
969
969
|
|
|
970
970
|
# Format and output the result
|
|
971
971
|
run_data = {
|
|
972
|
-
"
|
|
973
|
-
"
|
|
974
|
-
"
|
|
975
|
-
"
|
|
976
|
-
"
|
|
972
|
+
"id": result.run_id,
|
|
973
|
+
"lens_name": lens_name,
|
|
974
|
+
"pillar_id": pillar_id or "N/A",
|
|
975
|
+
"question_id": question_id or "N/A",
|
|
976
|
+
"best_practice_id": best_practice_id or "N/A",
|
|
977
977
|
}
|
|
978
978
|
|
|
979
979
|
format_item_output(
|
|
980
980
|
item=run_data,
|
|
981
|
-
title=f"Assessment Run Details: {
|
|
981
|
+
title=f"Assessment Run Details: {result.run_id}",
|
|
982
982
|
format_override=output_format,
|
|
983
983
|
)
|
|
984
984
|
|
|
@@ -994,9 +994,9 @@ def run_assessment(
|
|
|
994
994
|
help="Cloud provider name (default: AWS)",
|
|
995
995
|
)
|
|
996
996
|
@click.option(
|
|
997
|
-
"--pillar-
|
|
997
|
+
"--pillar-id",
|
|
998
998
|
multiple=True,
|
|
999
|
-
help="Filter findings by pillar
|
|
999
|
+
help="Filter findings by pillar ID (Can be used multiple times). Allowed values: security, costOptimization, reliability, performance, operationalExcellence, sustainability.",
|
|
1000
1000
|
)
|
|
1001
1001
|
@click.option(
|
|
1002
1002
|
"--severity",
|
|
@@ -1032,7 +1032,7 @@ def run_assessment(
|
|
|
1032
1032
|
def generate_report(
|
|
1033
1033
|
assessment_id: str,
|
|
1034
1034
|
cloud_provider: str,
|
|
1035
|
-
|
|
1035
|
+
pillar_id: Tuple[str, ...],
|
|
1036
1036
|
severity: Tuple[str, ...],
|
|
1037
1037
|
status: Tuple[str, ...],
|
|
1038
1038
|
account_number: Tuple[str, ...],
|
|
@@ -1064,7 +1064,7 @@ def generate_report(
|
|
|
1064
1064
|
# Create the input model
|
|
1065
1065
|
report_input = GenerateAssessmentReportInput(
|
|
1066
1066
|
CloudProvider=cloud_provider,
|
|
1067
|
-
PillarIds=list(
|
|
1067
|
+
PillarIds=list(pillar_id) if pillar_id else None,
|
|
1068
1068
|
Severity=list(severity) if severity else None,
|
|
1069
1069
|
Status=list(status) if status else None,
|
|
1070
1070
|
AccountNumber=list(account_number) if account_number else None,
|
|
@@ -1082,20 +1082,20 @@ def generate_report(
|
|
|
1082
1082
|
|
|
1083
1083
|
# Format and output the result
|
|
1084
1084
|
report_data = {
|
|
1085
|
-
"
|
|
1086
|
-
"
|
|
1087
|
-
"
|
|
1088
|
-
"
|
|
1089
|
-
"
|
|
1090
|
-
"
|
|
1091
|
-
"
|
|
1092
|
-
"
|
|
1093
|
-
"
|
|
1085
|
+
"id": result.report_id,
|
|
1086
|
+
"cloud_provider": cloud_provider,
|
|
1087
|
+
"pillar_id": ", ".join(pillar_id) if pillar_id else "N/A",
|
|
1088
|
+
"severity": ", ".join(severity) if severity else "N/A",
|
|
1089
|
+
"status": ", ".join(status) if status else "N/A",
|
|
1090
|
+
"account_number": ", ".join(account_number) if account_number else "N/A",
|
|
1091
|
+
"region_code": ", ".join(region_code) if region_code else "N/A",
|
|
1092
|
+
"unique_report": unique_report,
|
|
1093
|
+
"lenses": ", ".join(lenses_list),
|
|
1094
1094
|
}
|
|
1095
1095
|
|
|
1096
1096
|
format_item_output(
|
|
1097
1097
|
item=report_data,
|
|
1098
|
-
title=f"Generated Report
|
|
1098
|
+
title=f"Generated Report successfully: {result.report_id}",
|
|
1099
1099
|
format_override=output_format,
|
|
1100
1100
|
)
|
|
1101
1101
|
|
|
@@ -557,9 +557,24 @@ def login(
|
|
|
557
557
|
# Determine credential storage scope
|
|
558
558
|
credential_profile_name = profile # Default to the determined profile
|
|
559
559
|
|
|
560
|
+
# Check if DEFAULT credentials already exist
|
|
561
|
+
credentials_file = Path.home() / ".day2" / "credentials"
|
|
562
|
+
has_default_credentials = False
|
|
563
|
+
if credentials_file.exists():
|
|
564
|
+
cred_parser = configparser.ConfigParser()
|
|
565
|
+
cred_parser.read(credentials_file)
|
|
566
|
+
# Check if DEFAULT section exists with credentials
|
|
567
|
+
if "DEFAULT" in cred_parser and cred_parser["DEFAULT"].get("api_key"):
|
|
568
|
+
has_default_credentials = True
|
|
569
|
+
|
|
560
570
|
# Check if we should prompt for credential scope
|
|
571
|
+
# Don't prompt if:
|
|
572
|
+
# 1. This is first time setup (no DEFAULT credentials exist)
|
|
573
|
+
# 2. User explicitly provided --profile
|
|
574
|
+
# 3. Current profile is "default"
|
|
561
575
|
should_prompt = (
|
|
562
|
-
|
|
576
|
+
has_default_credentials # Only prompt if DEFAULT credentials already exist
|
|
577
|
+
and not profile_explicitly_provided # User didn't specify --profile
|
|
563
578
|
and profile != "default" # User has an active non-default profile
|
|
564
579
|
and (
|
|
565
580
|
os.environ.get("DAY2_PROFILE") or profile_manager.get_default_profile()
|
|
@@ -585,9 +600,6 @@ def login(
|
|
|
585
600
|
credential_profile_name = "default"
|
|
586
601
|
console.print(" ✓ Will store shared credentials for all profiles")
|
|
587
602
|
|
|
588
|
-
# Determine credentials file path
|
|
589
|
-
credentials_file = Path.home() / ".day2" / "credentials"
|
|
590
|
-
|
|
591
603
|
# Determine credential section based on user choice
|
|
592
604
|
if should_prompt:
|
|
593
605
|
# User was prompted and made an explicit choice
|
|
@@ -598,14 +610,24 @@ def login(
|
|
|
598
610
|
# User chose specific profile - use profile name
|
|
599
611
|
credential_section = credential_profile_name
|
|
600
612
|
else:
|
|
601
|
-
# No
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
613
|
+
# No prompt was shown
|
|
614
|
+
if not has_default_credentials:
|
|
615
|
+
# First time setup
|
|
616
|
+
if profile_explicitly_provided and profile != "default":
|
|
617
|
+
# User explicitly specified a non-default profile - respect their choice
|
|
618
|
+
credential_section = credential_profile_name
|
|
619
|
+
console.print(
|
|
620
|
+
f"\n[dim]First time setup - storing credentials for profile {profile}[/dim]"
|
|
621
|
+
)
|
|
622
|
+
else:
|
|
623
|
+
# No explicit profile or profile is "default" - use DEFAULT section
|
|
624
|
+
credential_section = "DEFAULT"
|
|
625
|
+
console.print(
|
|
626
|
+
"\n[dim]First time setup - storing credentials for all profiles[/dim]"
|
|
627
|
+
)
|
|
628
|
+
else:
|
|
629
|
+
# Use the profile name (which will check profile first, then DEFAULT)
|
|
630
|
+
credential_section = credential_profile_name
|
|
609
631
|
|
|
610
632
|
if verbose:
|
|
611
633
|
if credential_section == "DEFAULT":
|
|
@@ -23,6 +23,45 @@ def get_profile_manager() -> ProfileManager:
|
|
|
23
23
|
return ProfileManager()
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def show_no_profiles_message(output: Optional[str]) -> None:
|
|
27
|
+
"""Display a consistent 'no profiles configured' message.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
output: Output format ("json" or None for table format)
|
|
31
|
+
"""
|
|
32
|
+
if output and output.lower() == "json":
|
|
33
|
+
console.print(json.dumps({"error": "No profiles configured"}, indent=2))
|
|
34
|
+
else:
|
|
35
|
+
console.print("[yellow]No profiles configured.[/yellow]")
|
|
36
|
+
console.print("\n[dim]To get started:[/dim]")
|
|
37
|
+
console.print(" 1. Run [cyan]day2 auth login[/cyan] to set up authentication")
|
|
38
|
+
console.print(
|
|
39
|
+
" 2. Or create a profile with [cyan]day2 profile create <name>[/cyan]"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def determine_current_profile() -> Optional[str]:
|
|
44
|
+
"""Determine the currently active profile.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The name of the current profile, or None if no profiles are configured.
|
|
48
|
+
"""
|
|
49
|
+
env_profile = os.environ.get("DAY2_PROFILE")
|
|
50
|
+
profile_manager = get_profile_manager()
|
|
51
|
+
default_profile = profile_manager.get_default_profile()
|
|
52
|
+
existing_profiles = profile_manager.list_profiles()
|
|
53
|
+
|
|
54
|
+
# Determine current profile
|
|
55
|
+
if env_profile:
|
|
56
|
+
return env_profile
|
|
57
|
+
if default_profile:
|
|
58
|
+
return default_profile
|
|
59
|
+
if existing_profiles:
|
|
60
|
+
return "default"
|
|
61
|
+
# No profiles configured at all
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
26
65
|
def ensure_default_profile() -> Optional[str]:
|
|
27
66
|
"""Ensure that a default profile exists.
|
|
28
67
|
|
|
@@ -637,10 +676,13 @@ def list_profiles(output: Optional[str] = None, detailed: bool = False) -> None:
|
|
|
637
676
|
profile_details = get_profile_manager().list_profiles_with_details()
|
|
638
677
|
|
|
639
678
|
if output and output.lower() == "json":
|
|
640
|
-
|
|
679
|
+
if not profile_details:
|
|
680
|
+
console.print(json.dumps({"error": "No profiles configured"}, indent=2))
|
|
681
|
+
else:
|
|
682
|
+
console.print(json.dumps({"profiles": profile_details}, indent=2))
|
|
641
683
|
else:
|
|
642
684
|
if not profile_details:
|
|
643
|
-
|
|
685
|
+
show_no_profiles_message(output)
|
|
644
686
|
return
|
|
645
687
|
|
|
646
688
|
table = Table(title="Available Profiles")
|
|
@@ -670,11 +712,17 @@ def list_profiles(output: Optional[str] = None, detailed: bool = False) -> None:
|
|
|
670
712
|
default_profile = get_profile_manager().get_default_profile()
|
|
671
713
|
|
|
672
714
|
if output and output.lower() == "json":
|
|
673
|
-
|
|
674
|
-
|
|
715
|
+
if not profiles:
|
|
716
|
+
console.print(json.dumps({"error": "No profiles configured"}, indent=2))
|
|
717
|
+
else:
|
|
718
|
+
profile_data = {
|
|
719
|
+
"profiles": profiles,
|
|
720
|
+
"default_profile": default_profile,
|
|
721
|
+
}
|
|
722
|
+
console.print(json.dumps(profile_data, indent=2))
|
|
675
723
|
else:
|
|
676
724
|
if not profiles:
|
|
677
|
-
|
|
725
|
+
show_no_profiles_message(output)
|
|
678
726
|
return
|
|
679
727
|
|
|
680
728
|
table = Table(title="Available Profiles")
|
|
@@ -697,27 +745,33 @@ def list_profiles(output: Optional[str] = None, detailed: bool = False) -> None:
|
|
|
697
745
|
def current_profile(output: Optional[str] = None) -> None:
|
|
698
746
|
"""Show the currently active profile name and essential configuration."""
|
|
699
747
|
try:
|
|
700
|
-
|
|
748
|
+
# Determine current profile using shared logic
|
|
749
|
+
current = determine_current_profile()
|
|
701
750
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
current = env_profile or default_profile or "default"
|
|
751
|
+
# Handle case where no profiles are configured
|
|
752
|
+
if current is None:
|
|
753
|
+
show_no_profiles_message(output)
|
|
754
|
+
return
|
|
707
755
|
|
|
708
|
-
# Get
|
|
756
|
+
# Get profile configuration without creating a session (no credentials needed)
|
|
709
757
|
try:
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
758
|
+
profile_manager = get_profile_manager()
|
|
759
|
+
|
|
760
|
+
# Get profile config
|
|
761
|
+
if current == "default":
|
|
762
|
+
config = profile_manager.get_default_config()
|
|
763
|
+
else:
|
|
764
|
+
config = profile_manager.get_profile_config(current)
|
|
765
|
+
|
|
766
|
+
# Determine output format - use provided output or profile's configured format
|
|
767
|
+
output_format = output if output else config.output_format
|
|
714
768
|
|
|
715
769
|
# Essential profile information only
|
|
716
770
|
profile_data = {
|
|
717
771
|
"name": current,
|
|
718
|
-
"base_url":
|
|
719
|
-
"tenant_id":
|
|
720
|
-
"output_format":
|
|
772
|
+
"base_url": config.base_url,
|
|
773
|
+
"tenant_id": config.tenant_id or None,
|
|
774
|
+
"output_format": config.output_format,
|
|
721
775
|
}
|
|
722
776
|
|
|
723
777
|
if output_format and output_format.lower() == "json":
|
|
@@ -730,9 +784,9 @@ def current_profile(output: Optional[str] = None) -> None:
|
|
|
730
784
|
|
|
731
785
|
# Add essential information
|
|
732
786
|
table.add_row("Profile Name", current)
|
|
733
|
-
table.add_row("Base URL",
|
|
734
|
-
table.add_row("Tenant ID",
|
|
735
|
-
table.add_row("Output Format",
|
|
787
|
+
table.add_row("Base URL", config.base_url)
|
|
788
|
+
table.add_row("Tenant ID", config.tenant_id or "Not set")
|
|
789
|
+
table.add_row("Output Format", config.output_format)
|
|
736
790
|
|
|
737
791
|
console.print(table)
|
|
738
792
|
|
|
@@ -809,13 +863,15 @@ def show_profile(
|
|
|
809
863
|
PROFILE-NAME: Name of the profile to show. If not provided, shows the current/default profile.
|
|
810
864
|
"""
|
|
811
865
|
try:
|
|
812
|
-
from day2_cli.utils.context import get_enhanced_context
|
|
813
|
-
|
|
814
866
|
# Determine which profile to show
|
|
815
867
|
if not profile_name:
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
868
|
+
# Use shared logic to determine current profile
|
|
869
|
+
profile_name = determine_current_profile()
|
|
870
|
+
|
|
871
|
+
# Handle case where no profiles are configured
|
|
872
|
+
if profile_name is None:
|
|
873
|
+
show_no_profiles_message(output)
|
|
874
|
+
return
|
|
819
875
|
|
|
820
876
|
# Check if profile exists
|
|
821
877
|
if profile_name != "default" and not get_profile_manager().profile_exists(
|
|
@@ -826,38 +882,22 @@ def show_profile(
|
|
|
826
882
|
)
|
|
827
883
|
return
|
|
828
884
|
|
|
829
|
-
#
|
|
830
|
-
|
|
831
|
-
try:
|
|
832
|
-
context = get_enhanced_context(
|
|
833
|
-
output=output
|
|
834
|
-
) # Use current profile for formatting
|
|
835
|
-
output_format = context["output_format"]
|
|
836
|
-
# Try to get runtime session data for tenant_id from target profile
|
|
837
|
-
try:
|
|
838
|
-
target_context = get_enhanced_context(
|
|
839
|
-
output=output, profile=profile_name
|
|
840
|
-
)
|
|
841
|
-
target_session = target_context["session"]
|
|
842
|
-
runtime_tenant_id = target_session.tenant_id
|
|
843
|
-
except (ImportError, AttributeError, ValueError):
|
|
844
|
-
runtime_tenant_id = None
|
|
845
|
-
except (ImportError, AttributeError, ValueError):
|
|
846
|
-
# Fall back to profile config only
|
|
847
|
-
output_format = output if output else "table"
|
|
848
|
-
runtime_tenant_id = None
|
|
849
|
-
|
|
850
|
-
# Get profile config
|
|
885
|
+
# Get profile config without creating a session
|
|
886
|
+
profile_manager = get_profile_manager()
|
|
851
887
|
if profile_name == "default":
|
|
852
|
-
config =
|
|
888
|
+
config = profile_manager.get_default_config()
|
|
853
889
|
is_default = True
|
|
854
890
|
else:
|
|
855
|
-
config =
|
|
856
|
-
default_profile =
|
|
891
|
+
config = profile_manager.get_profile_config(profile_name)
|
|
892
|
+
default_profile = profile_manager.get_default_profile()
|
|
857
893
|
is_default = profile_name == default_profile
|
|
858
894
|
|
|
859
|
-
#
|
|
860
|
-
|
|
895
|
+
# Determine output format - use provided output or profile's configured format
|
|
896
|
+
output_format = output if output else config.output_format
|
|
897
|
+
|
|
898
|
+
# For profile show, we just display the configured tenant_id
|
|
899
|
+
# No need to fetch runtime data which requires credentials
|
|
900
|
+
display_tenant_id = config.tenant_id
|
|
861
901
|
|
|
862
902
|
# Prepare profile data
|
|
863
903
|
profile_data = {
|
|
@@ -7,9 +7,10 @@ from rich.console import Console
|
|
|
7
7
|
from rich.table import Table
|
|
8
8
|
|
|
9
9
|
from day2.exceptions import Day2Error
|
|
10
|
+
from day2.utils.helpers import format_datetime
|
|
10
11
|
from day2_cli.utils.context import get_enhanced_context, with_common_options
|
|
11
12
|
from day2_cli.utils.exit_codes import handle_error_with_exit
|
|
12
|
-
from day2_cli.utils.output_formatter import format_item_output
|
|
13
|
+
from day2_cli.utils.output_formatter import format_item_output, format_list_output
|
|
13
14
|
|
|
14
15
|
console = Console()
|
|
15
16
|
|
|
@@ -19,6 +20,76 @@ def report() -> None:
|
|
|
19
20
|
"""Reports commands."""
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
@report.command("list")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--cloud-provider",
|
|
26
|
+
default="AWS",
|
|
27
|
+
help="Cloud provider for which to list reports (default is 'AWS').",
|
|
28
|
+
)
|
|
29
|
+
@with_common_options(include_tenant_id=True)
|
|
30
|
+
def list_reports(
|
|
31
|
+
cloud_provider: str,
|
|
32
|
+
output: Optional[str] = None,
|
|
33
|
+
profile: Optional[str] = None,
|
|
34
|
+
tenant_id: Optional[str] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Retrieves up to 100 of the most recent reports for a tenant, ordered by creation time (newest first).
|
|
37
|
+
|
|
38
|
+
If --tenant-id is not provided, uses the default tenant configured with 'day2 profile create' or 'day2 auth login'.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
# Get enhanced context with tenant resolution
|
|
42
|
+
context = get_enhanced_context(
|
|
43
|
+
output=output, profile=profile, tenant_id=tenant_id, require_tenant=True
|
|
44
|
+
)
|
|
45
|
+
session = context["session"]
|
|
46
|
+
output_format = context["output_format"]
|
|
47
|
+
resolved_tenant_id = context["tenant_id"]
|
|
48
|
+
|
|
49
|
+
# Call the client method
|
|
50
|
+
result = session.report.list_reports(resolved_tenant_id, cloud_provider)
|
|
51
|
+
|
|
52
|
+
# Convert report objects to dictionaries for the formatter
|
|
53
|
+
reports_data = []
|
|
54
|
+
for report_item in result.reports:
|
|
55
|
+
created_at = (
|
|
56
|
+
format_datetime(report_item.created_at)
|
|
57
|
+
if report_item.created_at
|
|
58
|
+
else "N/A"
|
|
59
|
+
)
|
|
60
|
+
reports_data.append(
|
|
61
|
+
{
|
|
62
|
+
"id": report_item.report_id,
|
|
63
|
+
"name": report_item.report_name,
|
|
64
|
+
"type": report_item.report_type,
|
|
65
|
+
"status": report_item.status,
|
|
66
|
+
"export_format": report_item.export_format,
|
|
67
|
+
"created_at": created_at,
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Define column mapping for the formatter
|
|
72
|
+
columns = {
|
|
73
|
+
"id": "ID",
|
|
74
|
+
"name": "Name",
|
|
75
|
+
"type": "Type",
|
|
76
|
+
"status": "Status",
|
|
77
|
+
"export_format": "Export Format",
|
|
78
|
+
"created_at": "Created At",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Format and output the results
|
|
82
|
+
format_list_output(
|
|
83
|
+
items=reports_data,
|
|
84
|
+
title=f"Reports for Tenant: {resolved_tenant_id}",
|
|
85
|
+
columns=columns,
|
|
86
|
+
format_override=output_format,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
except Day2Error as e:
|
|
90
|
+
handle_error_with_exit(e)
|
|
91
|
+
|
|
92
|
+
|
|
22
93
|
@report.command("get-details")
|
|
23
94
|
@click.argument("report-id")
|
|
24
95
|
@with_common_options(include_tenant_id=True)
|
|
@@ -60,13 +131,13 @@ def get_report_details(
|
|
|
60
131
|
|
|
61
132
|
# Format and output the result
|
|
62
133
|
report_data = {
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
134
|
+
"id": result.report_id,
|
|
135
|
+
"type": result.report_type,
|
|
136
|
+
"status": result.status,
|
|
137
|
+
"export_format": result.export_format,
|
|
138
|
+
"created_at": created_at,
|
|
139
|
+
"updated_at": updated_at,
|
|
140
|
+
"created_by": result.created_by,
|
|
70
141
|
}
|
|
71
142
|
|
|
72
143
|
format_item_output(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|