day2 0.2.1__tar.gz → 0.2.2__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 (51) hide show
  1. {day2-0.2.1 → day2-0.2.2}/PKG-INFO +3 -3
  2. {day2-0.2.1 → day2-0.2.2}/README.md +2 -2
  3. {day2-0.2.1 → day2-0.2.2}/day2/__init__.py +18 -1
  4. {day2-0.2.1 → day2-0.2.2}/day2/client/config.py +13 -1
  5. {day2-0.2.1 → day2-0.2.2}/day2/models/__init__.py +2 -0
  6. {day2-0.2.1 → day2-0.2.2}/day2/models/assessment.py +196 -19
  7. {day2-0.2.1 → day2-0.2.2}/day2/models/cost.py +30 -3
  8. {day2-0.2.1 → day2-0.2.2}/day2/models/tenant.py +44 -5
  9. {day2-0.2.1 → day2-0.2.2}/day2/resources/__init__.py +2 -1
  10. {day2-0.2.1 → day2-0.2.2}/day2/resources/assessment.py +8 -9
  11. {day2-0.2.1 → day2-0.2.2}/day2/resources/cost.py +1 -1
  12. {day2-0.2.1 → day2-0.2.2}/day2/resources/tenant.py +3 -3
  13. {day2-0.2.1 → day2-0.2.2}/day2/session.py +26 -5
  14. {day2-0.2.1 → day2-0.2.2}/day2.egg-info/PKG-INFO +3 -3
  15. {day2-0.2.1 → day2-0.2.2}/day2_cli/commands/assessment.py +25 -2
  16. {day2-0.2.1 → day2-0.2.2}/pyproject.toml +7 -1
  17. {day2-0.2.1 → day2-0.2.2}/tests/test_models_assessment.py +12 -11
  18. {day2-0.2.1 → day2-0.2.2}/tests/test_resources_assessment.py +2 -0
  19. {day2-0.2.1 → day2-0.2.2}/LICENSE +0 -0
  20. {day2-0.2.1 → day2-0.2.2}/day2/_version.py +0 -0
  21. {day2-0.2.1 → day2-0.2.2}/day2/auth/__init__.py +0 -0
  22. {day2-0.2.1 → day2-0.2.2}/day2/auth/credentials.py +0 -0
  23. {day2-0.2.1 → day2-0.2.2}/day2/client/__init__.py +0 -0
  24. {day2-0.2.1 → day2-0.2.2}/day2/client/base.py +0 -0
  25. {day2-0.2.1 → day2-0.2.2}/day2/exceptions.py +0 -0
  26. {day2-0.2.1 → day2-0.2.2}/day2.egg-info/SOURCES.txt +0 -0
  27. {day2-0.2.1 → day2-0.2.2}/day2.egg-info/dependency_links.txt +0 -0
  28. {day2-0.2.1 → day2-0.2.2}/day2.egg-info/entry_points.txt +0 -0
  29. {day2-0.2.1 → day2-0.2.2}/day2.egg-info/requires.txt +0 -0
  30. {day2-0.2.1 → day2-0.2.2}/day2.egg-info/top_level.txt +0 -0
  31. {day2-0.2.1 → day2-0.2.2}/day2_cli/__init__.py +0 -0
  32. {day2-0.2.1 → day2-0.2.2}/day2_cli/cli.py +0 -0
  33. {day2-0.2.1 → day2-0.2.2}/day2_cli/commands/__init__.py +0 -0
  34. {day2-0.2.1 → day2-0.2.2}/day2_cli/commands/auth.py +0 -0
  35. {day2-0.2.1 → day2-0.2.2}/day2_cli/commands/config.py +0 -0
  36. {day2-0.2.1 → day2-0.2.2}/day2_cli/commands/cost.py +0 -0
  37. {day2-0.2.1 → day2-0.2.2}/day2_cli/commands/tenant.py +0 -0
  38. {day2-0.2.1 → day2-0.2.2}/day2_cli/utils/__init__.py +0 -0
  39. {day2-0.2.1 → day2-0.2.2}/day2_cli/utils/config.py +0 -0
  40. {day2-0.2.1 → day2-0.2.2}/day2_cli/utils/formatters.py +0 -0
  41. {day2-0.2.1 → day2-0.2.2}/day2_cli/utils/output_formatter.py +0 -0
  42. {day2-0.2.1 → day2-0.2.2}/setup.cfg +0 -0
  43. {day2-0.2.1 → day2-0.2.2}/tests/test_cli_auth.py +0 -0
  44. {day2-0.2.1 → day2-0.2.2}/tests/test_cli_config.py +0 -0
  45. {day2-0.2.1 → day2-0.2.2}/tests/test_client_base.py +0 -0
  46. {day2-0.2.1 → day2-0.2.2}/tests/test_config.py +0 -0
  47. {day2-0.2.1 → day2-0.2.2}/tests/test_models_cost.py +0 -0
  48. {day2-0.2.1 → day2-0.2.2}/tests/test_models_tenant.py +0 -0
  49. {day2-0.2.1 → day2-0.2.2}/tests/test_resources_cost.py +0 -0
  50. {day2-0.2.1 → day2-0.2.2}/tests/test_resources_tenant.py +0 -0
  51. {day2-0.2.1 → day2-0.2.2}/tests/test_session.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: day2
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: MontyCloud DAY2 SDK for Python
5
5
  Author-email: MontyCloud <info@montycloud.com>
6
6
  Requires-Python: >=3.11
@@ -152,7 +152,7 @@ The SDK supports authentication with both an API key and an API secret key. You
152
152
  1. Directly in code:
153
153
  ```python
154
154
  # Simplified API
155
- session = day2.Session(api_key="your-api-key")
155
+ session = day2.Session(api_key="your-api-key", api_secret_key="your-secret-key")
156
156
 
157
157
  # With API secret key
158
158
  session = day2.Session(
@@ -191,7 +191,7 @@ The SDK supports multi-tenant operations through session-based tenant context:
191
191
 
192
192
  ```python
193
193
  # Create a session
194
- session = day2.Session(api_key="your-api-key")
194
+ session = day2.Session(api_key="your-api-key", api_secret_key="your-secret-key")
195
195
 
196
196
  # Set tenant context
197
197
  session.set_tenant("org-123")
@@ -114,7 +114,7 @@ The SDK supports authentication with both an API key and an API secret key. You
114
114
  1. Directly in code:
115
115
  ```python
116
116
  # Simplified API
117
- session = day2.Session(api_key="your-api-key")
117
+ session = day2.Session(api_key="your-api-key", api_secret_key="your-secret-key")
118
118
 
119
119
  # With API secret key
120
120
  session = day2.Session(
@@ -153,7 +153,7 @@ The SDK supports multi-tenant operations through session-based tenant context:
153
153
 
154
154
  ```python
155
155
  # Create a session
156
- session = day2.Session(api_key="your-api-key")
156
+ session = day2.Session(api_key="your-api-key", api_secret_key="your-secret-key")
157
157
 
158
158
  # Set tenant context
159
159
  session.set_tenant("org-123")
@@ -12,6 +12,7 @@ from day2.session import Session
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from day2.resources.assessment import AssessmentClient
15
+ from day2.resources.cost import CostClient
15
16
  from day2.resources.tenant import TenantClient
16
17
 
17
18
 
@@ -54,4 +55,20 @@ def assessment() -> "AssessmentClient":
54
55
  return get_default_session().assessment
55
56
 
56
57
 
57
- __all__ = ["Session", "__version__", "tenant", "assessment", "get_default_session"]
58
+ def cost() -> "CostClient":
59
+ """Get a cost client using the default session.
60
+
61
+ Returns:
62
+ CostClient: A client for interacting with cost resources
63
+ """
64
+ return get_default_session().cost
65
+
66
+
67
+ __all__ = [
68
+ "Session",
69
+ "__version__",
70
+ "tenant",
71
+ "assessment",
72
+ "cost",
73
+ "get_default_session",
74
+ ]
@@ -8,7 +8,19 @@ from typing import Any, Dict, Optional
8
8
 
9
9
  @dataclass
10
10
  class Config:
11
- """Configuration for the MontyCloud SDK."""
11
+ """Configuration for the MontyCloud SDK.
12
+
13
+ Attributes:
14
+ base_url: Base URL for the MontyCloud API
15
+ api_version: API version to use
16
+ timeout: Request timeout in seconds
17
+ max_retries: Maximum number of retries for failed requests
18
+ retry_backoff_factor: Exponential backoff factor for retries
19
+ tenant_id: Default tenant ID for operations
20
+ retry_min_delay: Minimum delay between retries in seconds
21
+ retry_max_delay: Maximum delay between retries in seconds
22
+ output_format: Output format for CLI commands (table or json)
23
+ """
12
24
 
13
25
  # Base URL for the MontyCloud API
14
26
  base_url: str = "https://api.montycloud.com/day2/api"
@@ -7,6 +7,7 @@ from day2.models.assessment import (
7
7
  GetAssessmentOutput,
8
8
  ListAssessmentsOutput,
9
9
  )
10
+ from day2.models.cost import GetCostByChargeTypeOutput
10
11
  from day2.models.tenant import ListTenantsOutput, TenantDetails
11
12
 
12
13
  __all__ = [
@@ -17,4 +18,5 @@ __all__ = [
17
18
  "CreateAssessmentInput",
18
19
  "CreateAssessmentOutput",
19
20
  "GetAssessmentOutput",
21
+ "GetCostByChargeTypeOutput",
20
22
  ]
@@ -5,7 +5,20 @@ from pydantic import BaseModel, ConfigDict, Field
5
5
 
6
6
 
7
7
  class Assessment(BaseModel):
8
- """Details of an assessment."""
8
+ """Details of an assessment.
9
+
10
+ Attributes:
11
+ id: Assessment identifier
12
+ name: Name of the assessment
13
+ description: Description of the assessment
14
+ status: Current status of the assessment
15
+ assessment_arn: AWS ARN of the assessment
16
+ lenses: List of lens identifiers used in the assessment
17
+ total_questions: Total number of questions in the assessment
18
+ answered_questions: Number of questions that have been answered
19
+ created_at: Timestamp when the assessment was created
20
+ updated_at: Timestamp when the assessment was last updated
21
+ """
9
22
 
10
23
  id: str = Field(alias="AssessmentId")
11
24
  name: str = Field(alias="AssessmentName")
@@ -23,13 +36,15 @@ class Assessment(BaseModel):
23
36
 
24
37
 
25
38
  class ListAssessmentsOutput(BaseModel):
26
- """Output of list_assessments operation."""
39
+ """Output of list_assessments operation.
40
+
41
+ Attributes:
42
+ assessments: List of assessment details
43
+ next_page_token: Token for fetching the next page of results
44
+ """
27
45
 
28
46
  assessments: List[Assessment] = Field(alias="Assessments")
29
47
  next_page_token: Optional[str] = Field(None, alias="NextPageToken")
30
- # Keep these fields for backward compatibility
31
- page_number: Optional[int] = Field(None, alias="PageNumber")
32
- has_more: Optional[bool] = Field(None, alias="HasMore")
33
48
 
34
49
  # Allow extra fields
35
50
  model_config = ConfigDict(extra="allow")
@@ -38,16 +53,48 @@ class ListAssessmentsOutput(BaseModel):
38
53
  class CreateAssessmentInput(BaseModel):
39
54
  """Input for creating an assessment.
40
55
 
41
- According to the server-side schema, Scope can be one of two formats:
42
- 1. ScopeAcc: {"Project": {}, "Accounts": [{"AccountNumber": "123", "Regions": ["us-east-1"]}]}
43
- 2. ScopeProj: {"Project": {"ProjectId": "123", "Applications": ["app1"]}, "Accounts": ["123"]}
56
+ The scope parameter can be in one of two formats:
57
+
58
+ 1. Account-based scope:
59
+ ```python
60
+ {
61
+ "Project": {},
62
+ "Accounts": [
63
+ {"AccountNumber": "123456789012", "Regions": ["us-east-1", "us-west-2"]}
64
+ ]
65
+ }
66
+ ```
67
+
68
+ 2. Project-based scope:
69
+ ```python
70
+ {
71
+ "Project": {"ProjectId": "proj-123", "Applications": ["app1", "app2"]},
72
+ "Accounts": ["123456789012"]
73
+ }
74
+ ```
75
+
76
+ Attributes:
77
+ name: Name of the assessment
78
+ description: Description of the assessment
79
+ review_owner: Email or identifier of the review owner
80
+ scope: Scope configuration (see formats above)
81
+ lenses: List of lens aliases to use (defaults to ["AWS Well-Architected Framework"] only "AWS Well-Architected Framework" is supported)
82
+ tags: Key-value pairs for tagging the assessment
83
+ region_code: AWS region code where the assessment is created
84
+ environment: Environment type (e.g., PRODUCTION, DEVELOPMENT)
85
+ hosted_account_number: Optional AWS account number for hosting
86
+ diagram_url: Optional URL to architecture diagram
87
+ industry_type: Optional industry type classification
88
+ industry: Optional industry specification
44
89
  """
45
90
 
46
91
  name: str = Field(alias="AssessmentName")
47
92
  description: str = Field(alias="Description")
48
93
  review_owner: str = Field(alias="ReviewOwner")
49
94
  scope: Dict[str, Any] = Field(alias="Scope")
50
- lenses: Optional[List[str]] = Field(default=[], alias="Lenses")
95
+ lenses: List[str] = Field(
96
+ default=["AWS Well-Architected Framework"], alias="Lenses"
97
+ )
51
98
  tags: Optional[Dict[str, Any]] = Field(default={}, alias="Tags")
52
99
  region_code: str = Field(alias="RegionCode")
53
100
  environment: str = Field(alias="Environment")
@@ -63,7 +110,38 @@ class CreateAssessmentInput(BaseModel):
63
110
 
64
111
 
65
112
  class GetAssessmentOutput(BaseModel):
66
- """Output of get_assessment operation."""
113
+ """Output of get_assessment operation.
114
+
115
+ Attributes:
116
+ id: Assessment identifier
117
+ name: Name of the assessment
118
+ description: Description of the assessment
119
+ status: Current status of the assessment
120
+ assessment_arn: AWS ARN of the assessment
121
+ created_at: Timestamp when the assessment was created
122
+ updated_at: Timestamp when the assessment was last updated
123
+ answered_questions: Number of questions that have been answered
124
+ total_questions: Total number of questions in the assessment
125
+ lenses: List of lens identifiers used in the assessment
126
+ owner: Owner of the assessment
127
+ diagram_url: URL to architecture diagram (if provided)
128
+ environment: Environment type
129
+ improvement_status: Status of improvement plan
130
+ in_sync: Synchronization status indicator
131
+ industry: Industry specification
132
+ industry_type: Industry type classification
133
+ region_code: AWS region code
134
+ scope: Scope configuration
135
+ risk_counts: Dictionary of risk counts by severity
136
+ lens_alias: Primary lens alias
137
+ lens_arn: AWS ARN of the lens
138
+ lens_version: Version of the lens
139
+ lens_name: Name of the lens
140
+ lens_status: Status of the lens
141
+ aws_updated_at: Timestamp of the last AWS update
142
+ last_run_at: Timestamp of the last execution run (if completed)
143
+ execution_id: ID of the last execution run (if completed)
144
+ """
67
145
 
68
146
  id: str = Field(alias="AssessmentId")
69
147
  name: str = Field(alias="AssessmentName")
@@ -93,6 +171,12 @@ class GetAssessmentOutput(BaseModel):
93
171
  aws_updated_at: str = Field(
94
172
  alias="AWSUpdatedAt"
95
173
  ) # Changed to str to handle timezone format
174
+ last_run_at: Optional[datetime] = Field(
175
+ None, alias="LastRunAt"
176
+ ) # Optional since it will be present only when the execution run is completed for the assessment
177
+ execution_id: Optional[str] = Field(
178
+ None, alias="ExecutionId"
179
+ ) # Optional since it will be present only when the execution run is completed for the assessment
96
180
 
97
181
  # Allow extra fields
98
182
  model_config = ConfigDict(extra="allow")
@@ -101,7 +185,12 @@ class GetAssessmentOutput(BaseModel):
101
185
  class CreateAssessmentOutput(BaseModel):
102
186
  """Output of create_assessment operation.
103
187
 
104
- According to the OpenAPI specification, this only returns the AssessmentId and AssessmentArn.
188
+ Note:
189
+ According to the OpenAPI specification, this only returns the AssessmentId and AssessmentArn.
190
+
191
+ Attributes:
192
+ id: Newly created assessment identifier
193
+ assessment_arn: AWS ARN of the newly created assessment
105
194
  """
106
195
 
107
196
  # Fields from the OpenAPI spec
@@ -113,7 +202,21 @@ class CreateAssessmentOutput(BaseModel):
113
202
 
114
203
 
115
204
  class Question(BaseModel):
116
- """Details of an assessment question."""
205
+ """Details of an assessment question.
206
+
207
+ Attributes:
208
+ id: Question identifier
209
+ title: Title of the question
210
+ pillar_id: ID of the pillar this question belongs to
211
+ risk: Current risk level (HIGH, MEDIUM, LOW, or UNANSWERED)
212
+ reason: Reason for the current answer (if applicable)
213
+ description: Detailed description of the question
214
+ pillar_name: Name of the pillar this question belongs to
215
+ is_answered: Whether the question has been answered
216
+ choices: Available choices for the question
217
+ selected_choices: Currently selected choice IDs
218
+ notes: Additional notes for the answer
219
+ """
117
220
 
118
221
  id: str = Field(alias="QuestionId")
119
222
  title: str = Field(alias="QuestionTitle")
@@ -135,7 +238,15 @@ class Question(BaseModel):
135
238
 
136
239
 
137
240
  class ListQuestionsOutput(BaseModel):
138
- """Output of list_questions operation."""
241
+ """Output of list_questions operation.
242
+
243
+ Attributes:
244
+ questions: List of question details
245
+ total_questions: Total number of questions
246
+ answered_questions: Number of questions that have been answered
247
+ pillar_id: ID of the pillar (if filtered by pillar)
248
+ pillar_name: Name of the pillar (if filtered by pillar)
249
+ """
139
250
 
140
251
  questions: List[Question] = Field(alias="Questions")
141
252
 
@@ -171,7 +282,25 @@ class ListQuestionsOutput(BaseModel):
171
282
 
172
283
 
173
284
  class GetQuestionOutput(BaseModel):
174
- """Output of get_question operation."""
285
+ """Output of get_question operation.
286
+
287
+ Attributes:
288
+ title: Title of the question
289
+ description: Detailed description of the question
290
+ pillar_id: ID of the pillar this question belongs to
291
+ choices: Available choices for answering the question
292
+ is_applicable: Whether the question is applicable
293
+ risk: Current risk level (if answered)
294
+ reason: Reason for marking as not applicable
295
+ helpful_resource_url: URL to helpful resources
296
+ improvement_plan_url: URL to improvement plan
297
+ choice_answers: Currently selected choice answers
298
+ id: Question identifier
299
+ pillar_name: Name of the pillar
300
+ notes: Additional notes
301
+ is_answered: Whether the question has been answered
302
+ selected_choices: List of selected choice IDs
303
+ """
175
304
 
176
305
  # Fields directly from the API response
177
306
  title: str = Field(alias="QuestionTitle")
@@ -228,7 +357,11 @@ class GetQuestionOutput(BaseModel):
228
357
 
229
358
 
230
359
  class ChoiceStatus(BaseModel):
231
- """Status of a choice in an answer."""
360
+ """Status of a choice in an answer.
361
+
362
+ Attributes:
363
+ status: Status of the choice (SELECTED or UNSELECTED)
364
+ """
232
365
 
233
366
  status: str = Field("SELECTED", alias="Status")
234
367
 
@@ -236,7 +369,18 @@ class ChoiceStatus(BaseModel):
236
369
 
237
370
 
238
371
  class AnswerQuestionInput(BaseModel):
239
- """Input for answering a question."""
372
+ """Input for answering a question.
373
+
374
+ Attributes:
375
+ lens_alias: Alias of the lens (defaults to 'wellarchitected')
376
+ choice_updates: Dictionary mapping choice IDs to their status
377
+ notes: Additional notes about the answer
378
+ is_applicable: Whether the question is applicable (defaults to True)
379
+ reason: Reason if not applicable (OUT_OF_SCOPE, BUSINESS_PRIORITIES,
380
+ ARCHITECTURE_CONSTRAINTS, OTHER, or NONE)
381
+ selected_choices: List of selected choice IDs (deprecated, use choice_updates)
382
+ risk: Risk level (deprecated, automatically calculated)
383
+ """
240
384
 
241
385
  lens_alias: str = Field("wellarchitected", alias="LensAlias")
242
386
  choice_updates: Dict[str, ChoiceStatus] = Field(
@@ -275,7 +419,13 @@ class AnswerQuestionInput(BaseModel):
275
419
  class AnswerQuestionOutput(BaseModel):
276
420
  """Output of answer_question operation.
277
421
 
278
- The API returns a success message with status and message fields.
422
+ Note:
423
+ The API returns a success message with status and message fields.
424
+
425
+ Attributes:
426
+ status: Status of the operation (e.g., 'success')
427
+ message: Descriptive message about the operation result
428
+ id: Question identifier (added for convenience)
279
429
  """
280
430
 
281
431
  # Fields directly from the API response
@@ -289,7 +439,29 @@ class AnswerQuestionOutput(BaseModel):
289
439
 
290
440
 
291
441
  class Finding(BaseModel):
292
- """Details of a finding."""
442
+ """Details of a finding.
443
+
444
+ Attributes:
445
+ finding_id: Unique identifier for the finding
446
+ resource_id: ID of the affected resource
447
+ resource_type: Type of the affected resource (e.g., AWS::EC2::Instance)
448
+ account_number: AWS account number where the resource exists
449
+ region_code: AWS region where the resource exists
450
+ check_id: ID of the check that generated this finding
451
+ recommendation: Recommended action to address the finding
452
+ remediation: Whether remediation is available
453
+ created_at: Timestamp when the finding was created
454
+ question_id: Related assessment question ID
455
+ question: Related assessment question text
456
+ pillar_id: ID of the related pillar
457
+ severity: Severity level (HIGH, MEDIUM, LOW)
458
+ status: Current status of the finding (OPEN, RESOLVED, SUPPRESSED)
459
+ title: Title of the finding
460
+ description: Detailed description of the finding
461
+ best_practice_id: ID of the related best practice
462
+ best_practice: Description of the best practice
463
+ best_practice_risk: Risk level associated with not following the best practice
464
+ """
293
465
 
294
466
  finding_id: str = Field(alias="FindingId")
295
467
  resource_id: str = Field(alias="ResourceId")
@@ -315,7 +487,12 @@ class Finding(BaseModel):
315
487
 
316
488
 
317
489
  class ListFindingsOutput(BaseModel):
318
- """Output of list_findings operation."""
490
+ """Output of list_findings operation.
491
+
492
+ Attributes:
493
+ records: List of finding details
494
+ next_page_token: Token for fetching the next page of results
495
+ """
319
496
 
320
497
  records: List[Finding] = Field(alias="Records", default=[])
321
498
  next_page_token: Optional[str] = Field(None, alias="NextPageToken")
@@ -1,7 +1,7 @@
1
1
  import re
2
2
  from typing import Any, Dict, Optional
3
3
 
4
- from pydantic import BaseModel, ConfigDict, Field, root_validator
4
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
5
5
 
6
6
 
7
7
  # Utility function to convert camelCase or PascalCase to snake_case
@@ -10,7 +10,34 @@ def to_snake_case(name: str) -> str:
10
10
 
11
11
 
12
12
  class GetCostByChargeTypeOutput(BaseModel):
13
- """Details of cost breakdown by charge type."""
13
+ """Details of cost breakdown by charge type.
14
+
15
+ This model provides a comprehensive breakdown of costs by various charge types.
16
+ It supports dynamic fields, automatically converting any additional charge types
17
+ from the API response to snake_case attributes.
18
+
19
+ Attributes:
20
+ total_cost: Total cost for the period
21
+ usage: Standard usage charges
22
+ bundled_discount: Bundled discount amount
23
+ credit: Credits applied
24
+ discount: General discounts applied
25
+ discounted_usage: Usage charges after discounts
26
+ fee: General fees
27
+ refund: Refund amounts
28
+ ri_fee: Reserved Instance fees
29
+ tax: Tax charges
30
+ savings_plan_upfront_fee: Upfront fee for Savings Plans
31
+ savings_plan_recurring_fee: Recurring fee for Savings Plans
32
+ savings_plan_covered_usage: Usage covered by Savings Plans
33
+ savings_plan_negation: Savings Plan negation amounts
34
+ spp_discount: SPP (Service Provider Program) discounts
35
+ distributor_discount: Distributor discounts
36
+
37
+ Note:
38
+ Additional charge types returned by the API are automatically added as
39
+ attributes with their names converted to snake_case format.
40
+ """
14
41
 
15
42
  total_cost: Optional[float] = Field(None, alias="TotalCost")
16
43
  usage: Optional[float] = Field(None, alias="Usage")
@@ -38,7 +65,7 @@ class GetCostByChargeTypeOutput(BaseModel):
38
65
  # Allow extra fields
39
66
  model_config = ConfigDict(extra="allow")
40
67
 
41
- @root_validator(pre=True)
68
+ @model_validator(mode="before")
42
69
  @classmethod
43
70
  def handle_dynamic_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
44
71
  """Handle dynamic extra fields by converting them to snake_case."""
@@ -7,7 +7,22 @@ from pydantic import BaseModel, ConfigDict, Field
7
7
 
8
8
 
9
9
  class TenantDetails(BaseModel):
10
- """Details of a tenant."""
10
+ """Details of a tenant.
11
+
12
+ Attributes:
13
+ id: Unique identifier of the tenant
14
+ name: Name of the tenant
15
+ description: Description of the tenant
16
+ parent_tenant_id: ID of the parent tenant if this is a sub-tenant
17
+ created_at: Timestamp when the tenant was created
18
+ created_by: User who created the tenant
19
+ modified_at: Timestamp of last modification
20
+ modified_by: User who last modified the tenant
21
+ owner: Owner of the tenant
22
+ document_url: URL to the tenant's documentation
23
+ feature: Feature set associated with the tenant
24
+ category_id: Category identifier for the tenant
25
+ """
11
26
 
12
27
  id: Optional[str] = Field(None, alias="ID")
13
28
  name: str = Field(alias="Name")
@@ -27,7 +42,12 @@ class TenantDetails(BaseModel):
27
42
 
28
43
 
29
44
  class ListTenantsOutput(BaseModel):
30
- """Output of list_tenants operation."""
45
+ """Output of list_tenants operation.
46
+
47
+ Attributes:
48
+ tenants: List of tenant details
49
+ next_page_token: Token for fetching the next page of results
50
+ """
31
51
 
32
52
  tenants: List[TenantDetails] = Field(alias="Tenants", default=[])
33
53
  next_page_token: Optional[str] = Field(None, alias="NextPageToken")
@@ -37,11 +57,24 @@ class ListTenantsOutput(BaseModel):
37
57
 
38
58
 
39
59
  class GetTenantOutput(TenantDetails):
40
- """Output of get_tenant operation."""
60
+ """Output of get_tenant operation.
61
+
62
+ This class inherits all attributes from TenantDetails without modification.
63
+ See TenantDetails for the complete list of attributes.
64
+ """
41
65
 
42
66
 
43
67
  class Account(BaseModel):
44
- """Details of an account."""
68
+ """Details of an account.
69
+
70
+ Attributes:
71
+ number: AWS account number
72
+ name: Account name
73
+ status: Current status of the account
74
+ type: Type of the account
75
+ permission_model: Permission model used for the account
76
+ onboarded_date: Date when the account was onboarded
77
+ """
45
78
 
46
79
  number: str = Field(alias="Number")
47
80
  name: str = Field(alias="Name")
@@ -55,7 +88,13 @@ class Account(BaseModel):
55
88
 
56
89
 
57
90
  class ListAccountsOutput(BaseModel):
58
- """Output of list_accounts operation."""
91
+ """Output of list_accounts operation.
92
+
93
+ Attributes:
94
+ accounts: List of account details
95
+ has_more: Whether there are more pages of results
96
+ page_number: Current page number
97
+ """
59
98
 
60
99
  accounts: List[Account] = Field(alias="Accounts", default=[])
61
100
  has_more: bool = Field(alias="HasMore", default=False)
@@ -1,6 +1,7 @@
1
1
  """Resource implementations for the MontyCloud DAY2 SDK."""
2
2
 
3
3
  from day2.resources.assessment import AssessmentClient
4
+ from day2.resources.cost import CostClient
4
5
  from day2.resources.tenant import TenantClient
5
6
 
6
- __all__ = ["TenantClient", "AssessmentClient"]
7
+ __all__ = ["TenantClient", "AssessmentClient", "CostClient"]
@@ -43,8 +43,7 @@ class AssessmentClient(BaseClient):
43
43
  status: Status to filter assessments by. Must be one of ["PENDING", "COMPLETED"].
44
44
  keyword: Optional keyword to filter assessments by name or description.
45
45
  page_size: The number of assessments per page (default: 10).
46
- page_token: Token for pagination. If provided, page_number is ignored.
47
- page_number: The page number to retrieve (deprecated, use page_token instead).
46
+ page_token: Token for pagination.
48
47
 
49
48
  Returns:
50
49
  ListAssessmentsOutput: Object containing list of assessments and pagination info.
@@ -55,7 +54,7 @@ class AssessmentClient(BaseClient):
55
54
  AuthenticationError: If authentication fails.
56
55
 
57
56
  Examples:
58
- >>> session = Session(api_key="your-api-key")
57
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
59
58
  >>> client = session.assessment
60
59
  >>> response = client.list_assessments(tenant_id="tenant-123", status="PENDING", page_size=10)
61
60
  >>> for assessment in response.assessments:
@@ -98,7 +97,7 @@ class AssessmentClient(BaseClient):
98
97
  AuthenticationError: If authentication fails.
99
98
 
100
99
  Examples:
101
- >>> session = Session(api_key="your-api-key")
100
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
102
101
  >>> client = session.assessment
103
102
  >>> response = client.get_assessment("tenant-123", "assessment-123")
104
103
  >>> print(f"Assessment name: {response.name}")
@@ -126,7 +125,7 @@ class AssessmentClient(BaseClient):
126
125
  AuthenticationError: If authentication fails.
127
126
 
128
127
  Examples:
129
- >>> session = Session(api_key="your-api-key")
128
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
130
129
  >>> client = session.assessment
131
130
  >>> data = CreateAssessmentInput(
132
131
  ... name="My Assessment",
@@ -195,7 +194,7 @@ class AssessmentClient(BaseClient):
195
194
  AuthenticationError: If authentication fails.
196
195
 
197
196
  Examples:
198
- >>> session = Session(api_key="your-api-key")
197
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
199
198
  >>> client = session.assessment
200
199
  >>> response = client.list_questions("tenant-123", "assessment-123", "pillar-123")
201
200
  >>> for question in response.questions:
@@ -230,7 +229,7 @@ class AssessmentClient(BaseClient):
230
229
  AuthenticationError: If authentication fails.
231
230
 
232
231
  Examples:
233
- >>> session = Session(api_key="your-api-key")
232
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
234
233
  >>> client = session.assessment
235
234
  >>> response = client.get_question("tenant-123", "assessment-123", "question-123")
236
235
  >>> print(f"Question: {response.title}")
@@ -284,7 +283,7 @@ class AssessmentClient(BaseClient):
284
283
  AuthenticationError: If authentication fails.
285
284
 
286
285
  Examples:
287
- >>> session = Session(api_key="your-api-key")
286
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
288
287
  >>> client = session.assessment
289
288
  >>> data = AnswerQuestionInput(
290
289
  ... Reason="ARCHITECTURE_CONSTRAINTS",
@@ -357,7 +356,7 @@ class AssessmentClient(BaseClient):
357
356
  AuthenticationError: If authentication fails.
358
357
 
359
358
  Examples:
360
- >>> session = Session(api_key="your-api-key")
359
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
361
360
  >>> client = session.assessment
362
361
  >>> response = client.list_findings(
363
362
  ... tenant_id="tenant-123",
@@ -40,7 +40,7 @@ class CostClient(BaseClient):
40
40
  AuthenticationError: If authentication fails.
41
41
 
42
42
  Examples:
43
- >>> session = Session(api_key="your-api-key")
43
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
44
44
  >>> client = session.cost
45
45
  >>> response = client.get_cost_by_charge_type(
46
46
  ... tenant_id="tenant-123",
@@ -36,7 +36,7 @@ class TenantClient(BaseClient):
36
36
  AuthenticationError: If authentication fails.
37
37
 
38
38
  Examples:
39
- >>> session = Session(api_key="your-api-key")
39
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
40
40
  >>> client = session.tenant
41
41
  >>> response = client.list_tenants(page_size=10)
42
42
  >>> for tenant in response.tenants:
@@ -71,7 +71,7 @@ class TenantClient(BaseClient):
71
71
  AuthenticationError: If authentication fails.
72
72
 
73
73
  Examples:
74
- >>> session = Session(api_key="your-api-key")
74
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
75
75
  >>> client = session.tenant
76
76
  >>> response = client.get_tenant("tenant-123")
77
77
  >>> print(f"Tenant name: {response.name}")
@@ -100,7 +100,7 @@ class TenantClient(BaseClient):
100
100
  AuthenticationError: If authentication fails.
101
101
 
102
102
  Examples:
103
- >>> session = Session(api_key="your-api-key")
103
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
104
104
  >>> client = session.tenant
105
105
  >>> response = client.list_accounts("tenant-123", page_size=10)
106
106
  >>> for account in response.accounts:
@@ -20,6 +20,22 @@ class Session:
20
20
  """Session for interacting with the MontyCloud API.
21
21
 
22
22
  The Session manages authentication credentials, tenant context, and client creation.
23
+ It provides access to all DAY2 API resources through dedicated client properties.
24
+
25
+ Attributes:
26
+ credentials: Authentication credentials for API access.
27
+ tenant_id: Current tenant ID context for API operations.
28
+
29
+ Examples:
30
+ Create a session with API credentials:
31
+
32
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
33
+ >>> tenants = session.tenant.list_tenants()
34
+
35
+ Create a session with custom configuration:
36
+
37
+ >>> config = Config(timeout=60, max_retries=5)
38
+ >>> session = Session(api_key="key", api_secret_key="secret", config=config)
23
39
  """
24
40
 
25
41
  def __init__(
@@ -33,11 +49,16 @@ class Session:
33
49
  """Initialize a new session.
34
50
 
35
51
  Args:
36
- api_key: API key for authentication.
37
- api_secret_key: API secret key for authentication.
38
- credentials: Credentials object for authentication.
39
- tenant_id: Tenant ID to use for API calls.
40
- config: Configuration object.
52
+ api_key: API key for authentication. If not provided, will attempt to load
53
+ from environment variables or configuration file.
54
+ api_secret_key: API secret key for authentication. Required if api_key is provided.
55
+ credentials: Pre-configured Credentials object. Takes precedence over api_key/api_secret_key.
56
+ tenant_id: Default tenant ID for API operations. Can be changed later with set_tenant().
57
+ config: Configuration object with API settings. If not provided, loads from file or uses defaults.
58
+
59
+ Raises:
60
+ ValueError: If api_key is provided without api_secret_key.
61
+ AuthenticationError: If no valid credentials are found.
41
62
  """
42
63
  # Initialize credentials
43
64
  if credentials:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: day2
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: MontyCloud DAY2 SDK for Python
5
5
  Author-email: MontyCloud <info@montycloud.com>
6
6
  Requires-Python: >=3.11
@@ -152,7 +152,7 @@ The SDK supports authentication with both an API key and an API secret key. You
152
152
  1. Directly in code:
153
153
  ```python
154
154
  # Simplified API
155
- session = day2.Session(api_key="your-api-key")
155
+ session = day2.Session(api_key="your-api-key", api_secret_key="your-secret-key")
156
156
 
157
157
  # With API secret key
158
158
  session = day2.Session(
@@ -191,7 +191,7 @@ The SDK supports multi-tenant operations through session-based tenant context:
191
191
 
192
192
  ```python
193
193
  # Create a session
194
- session = day2.Session(api_key="your-api-key")
194
+ session = day2.Session(api_key="your-api-key", api_secret_key="your-secret-key")
195
195
 
196
196
  # Set tenant context
197
197
  session.set_tenant("org-123")
@@ -194,6 +194,11 @@ def get_assessment(
194
194
  if result.updated_at
195
195
  else "N/A"
196
196
  )
197
+ last_run_at = (
198
+ result.last_run_at.strftime("%Y-%m-%d %H:%M:%S")
199
+ if result.last_run_at
200
+ else "N/A"
201
+ )
197
202
 
198
203
  # Convert assessment object to dictionary for the formatter
199
204
  assessment_data = {
@@ -201,14 +206,30 @@ def get_assessment(
201
206
  "Name": result.name,
202
207
  "Description": result.description or "N/A",
203
208
  "Status": result.status,
204
- "Scope": result.scope or "N/A",
209
+ "Assessment ARN": result.assessment_arn,
210
+ "Owner": result.owner,
211
+ "Diagram URL": result.diagram_url or "N/A",
205
212
  "Environment": result.environment or "N/A",
213
+ "Improvement Status": result.improvement_status,
214
+ "In Sync": result.in_sync,
215
+ "Industry": result.industry or "N/A",
216
+ "Industry Type": result.industry_type or "N/A",
206
217
  "Region": result.region_code or "N/A",
218
+ "Scope": result.scope or "N/A",
219
+ "Risk Counts": result.risk_counts,
207
220
  "Total Questions": result.total_questions,
208
221
  "Answered Questions": result.answered_questions,
209
222
  "Lenses": ", ".join(result.lenses) if result.lenses else "N/A",
223
+ "Lens Alias": result.lens_alias,
224
+ "Lens ARN": result.lens_arn,
225
+ "Lens Version": result.lens_version,
226
+ "Lens Name": result.lens_name,
227
+ "Lens Status": result.lens_status,
228
+ "AWS Updated At": result.aws_updated_at,
210
229
  "Created At": created_at,
211
230
  "Updated At": updated_at,
231
+ "Last Run At": last_run_at,
232
+ "Execution ID": result.execution_id or "N/A",
212
233
  }
213
234
 
214
235
  # Format and output the results
@@ -697,7 +718,9 @@ def answer_question(
697
718
  if notes:
698
719
  console.print(f"[bold]Notes:[/bold] {notes}")
699
720
  if not applicable:
700
- console.print("[bold]Applicability:[/bold] Not applicable to this workload")
721
+ console.print(
722
+ "[bold]Applicability:[/bold] Not applicable to this assessment"
723
+ )
701
724
 
702
725
  console.print(
703
726
  "\n[dim]To view the updated question details, use the 'question' command.[/dim]"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "day2"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "MontyCloud DAY2 SDK for Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -50,6 +50,12 @@ cli = [
50
50
  requires = ["setuptools>=61.0.0", "wheel"]
51
51
  build-backend = "setuptools.build_meta"
52
52
 
53
+ [tool.uv]
54
+ # Note: This project requires uv to be installed globally
55
+ # See DEVELOPER.md for installation instructions
56
+ # Minimum version to ensure lock file compatibility
57
+ required-version = ">=0.7.0"
58
+
53
59
  [tool.mypy]
54
60
  python_version = "3.11"
55
61
  disallow_untyped_defs = true
@@ -27,7 +27,7 @@ def test_assessment_parse():
27
27
  "Description": "Test Description",
28
28
  "Status": "COMPLETED",
29
29
  "AssessmentArn": "arn:aws:wellarchitected:us-east-1:123456789012:workload/assessment-123",
30
- "Lenses": ["wellarchitected"],
30
+ "Lenses": ["AWS Well-Architected Framework"],
31
31
  "TotalQuestions": 50,
32
32
  "AnsweredQuestions": 25,
33
33
  }
@@ -42,7 +42,7 @@ def test_assessment_parse():
42
42
  assessment.assessment_arn
43
43
  == "arn:aws:wellarchitected:us-east-1:123456789012:workload/assessment-123"
44
44
  )
45
- assert assessment.lenses == ["wellarchitected"]
45
+ assert assessment.lenses == ["AWS Well-Architected Framework"]
46
46
  assert assessment.total_questions == 50
47
47
  assert assessment.answered_questions == 25
48
48
 
@@ -57,21 +57,18 @@ def test_list_assessments_output_parse():
57
57
  "Description": "Test Description",
58
58
  "Status": "COMPLETED",
59
59
  "AssessmentArn": "arn:aws:wellarchitected:us-east-1:123456789012:workload/assessment-123",
60
- "Lenses": ["wellarchitected"],
60
+ "Lenses": ["AWS Well-Architected Framework"],
61
61
  "TotalQuestions": 50,
62
62
  "AnsweredQuestions": 25,
63
63
  }
64
64
  ],
65
- "PageNumber": 1,
66
- "HasMore": False,
65
+ "NextPageToken": "next-page-token",
67
66
  }
68
67
 
69
68
  list_output = ListAssessmentsOutput.model_validate(data)
70
69
 
71
70
  assert len(list_output.assessments) == 1
72
- assert list_output.page_number == 1
73
- assert list_output.has_more is False
74
-
71
+ assert list_output.next_page_token == "next-page-token"
75
72
  assessment = list_output.assessments[0]
76
73
  assert assessment.id == "assessment-123"
77
74
  assert assessment.name == "Test Assessment"
@@ -91,7 +88,7 @@ def test_create_assessment_input_serialization():
91
88
  {"AccountNumber": "111122223333", "Regions": ["us-east-1", "us-east-2"]}
92
89
  ],
93
90
  },
94
- Lenses=["wellarchitected"],
91
+ Lenses=["AWS Well-Architected Framework"],
95
92
  RegionCode="us-east-1",
96
93
  Environment="PRODUCTION",
97
94
  )
@@ -105,7 +102,7 @@ def test_create_assessment_input_serialization():
105
102
  assert len(data["Scope"]["Accounts"]) == 1
106
103
  assert data["Scope"]["Accounts"][0]["AccountNumber"] == "111122223333"
107
104
  assert "us-east-1" in data["Scope"]["Accounts"][0]["Regions"]
108
- assert data["Lenses"] == ["wellarchitected"]
105
+ assert data["Lenses"] == ["AWS Well-Architected Framework"]
109
106
  assert data["RegionCode"] == "us-east-1"
110
107
  assert data["Environment"] == "PRODUCTION"
111
108
 
@@ -118,7 +115,7 @@ def test_create_assessment_output_parse():
118
115
  "Description": "Test Description",
119
116
  "Status": "PENDING",
120
117
  "AssessmentArn": "arn:aws:wellarchitected:us-east-1:123456789012:workload/assessment-123",
121
- "Lenses": ["wellarchitected"],
118
+ "Lenses": ["AWS Well-Architected Framework"],
122
119
  "TotalQuestions": 50,
123
120
  "AnsweredQuestions": 0,
124
121
  }
@@ -167,6 +164,8 @@ def test_get_assessment_output_parse():
167
164
  "LensName": "AWS Well-Architected Framework",
168
165
  "LensStatus": "CURRENT",
169
166
  "AWSUpdatedAt": "2025-04-09T11:54:33+05:30",
167
+ "LastRunAt": "2025-04-09T06:24:34",
168
+ "ExecutionId": "9575dde75568c808c46e525ed88d9978",
170
169
  }
171
170
 
172
171
  output = GetAssessmentOutput.model_validate(data)
@@ -202,6 +201,8 @@ def test_get_assessment_output_parse():
202
201
  assert output.lens_name == "AWS Well-Architected Framework"
203
202
  assert output.lens_status == "CURRENT"
204
203
  assert output.aws_updated_at == "2025-04-09T11:54:33+05:30"
204
+ assert output.last_run_at == datetime.fromisoformat("2025-04-09T06:24:34")
205
+ assert output.execution_id == "9575dde75568c808c46e525ed88d9978"
205
206
 
206
207
 
207
208
  def test_get_question_output_parse():
@@ -154,6 +154,8 @@ def test_get_assessment(mock_session):
154
154
  "LensName": "AWS Well-Architected Framework",
155
155
  "LensStatus": "CURRENT",
156
156
  "AWSUpdatedAt": "2025-04-09T11:54:33+05:30",
157
+ "LastRunAt": "2025-04-09T11:54:33+05:30",
158
+ "ExecutionId": "9575dde75568c808c46e525ed88d9978",
157
159
  }
158
160
 
159
161
  # Set up the mock
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