day2 0.2.0.dev1__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 (53) hide show
  1. {day2-0.2.0.dev1 → day2-0.2.2}/PKG-INFO +3 -11
  2. {day2-0.2.0.dev1 → day2-0.2.2}/README.md +2 -10
  3. {day2-0.2.0.dev1 → day2-0.2.2}/day2/__init__.py +18 -1
  4. {day2-0.2.0.dev1 → day2-0.2.2}/day2/client/base.py +56 -50
  5. {day2-0.2.0.dev1 → day2-0.2.2}/day2/client/config.py +15 -34
  6. {day2-0.2.0.dev1 → day2-0.2.2}/day2/models/__init__.py +2 -0
  7. {day2-0.2.0.dev1 → day2-0.2.2}/day2/models/assessment.py +196 -19
  8. day2-0.2.2/day2/models/cost.py +81 -0
  9. day2-0.2.2/day2/models/tenant.py +104 -0
  10. {day2-0.2.0.dev1 → day2-0.2.2}/day2/resources/__init__.py +2 -1
  11. {day2-0.2.0.dev1 → day2-0.2.2}/day2/resources/assessment.py +8 -9
  12. day2-0.2.2/day2/resources/cost.py +62 -0
  13. {day2-0.2.0.dev1 → day2-0.2.2}/day2/resources/tenant.py +39 -3
  14. {day2-0.2.0.dev1 → day2-0.2.2}/day2/session.py +38 -5
  15. {day2-0.2.0.dev1 → day2-0.2.2}/day2.egg-info/PKG-INFO +3 -11
  16. {day2-0.2.0.dev1 → day2-0.2.2}/day2.egg-info/SOURCES.txt +6 -0
  17. {day2-0.2.0.dev1 → day2-0.2.2}/day2_cli/cli.py +2 -0
  18. {day2-0.2.0.dev1 → day2-0.2.2}/day2_cli/commands/assessment.py +32 -6
  19. day2-0.2.2/day2_cli/commands/cost.py +129 -0
  20. {day2-0.2.0.dev1 → day2-0.2.2}/day2_cli/commands/tenant.py +83 -0
  21. {day2-0.2.0.dev1 → day2-0.2.2}/pyproject.toml +7 -1
  22. {day2-0.2.0.dev1 → day2-0.2.2}/tests/test_cli_config.py +1 -1
  23. day2-0.2.2/tests/test_client_base.py +231 -0
  24. {day2-0.2.0.dev1 → day2-0.2.2}/tests/test_config.py +3 -3
  25. {day2-0.2.0.dev1 → day2-0.2.2}/tests/test_models_assessment.py +12 -11
  26. day2-0.2.2/tests/test_models_cost.py +112 -0
  27. {day2-0.2.0.dev1 → day2-0.2.2}/tests/test_models_tenant.py +79 -15
  28. {day2-0.2.0.dev1 → day2-0.2.2}/tests/test_resources_assessment.py +2 -0
  29. day2-0.2.2/tests/test_resources_cost.py +90 -0
  30. day2-0.2.2/tests/test_resources_tenant.py +230 -0
  31. day2-0.2.0.dev1/day2/models/tenant.py +0 -45
  32. day2-0.2.0.dev1/tests/test_resources_tenant.py +0 -100
  33. {day2-0.2.0.dev1 → day2-0.2.2}/LICENSE +0 -0
  34. {day2-0.2.0.dev1 → day2-0.2.2}/day2/_version.py +0 -0
  35. {day2-0.2.0.dev1 → day2-0.2.2}/day2/auth/__init__.py +0 -0
  36. {day2-0.2.0.dev1 → day2-0.2.2}/day2/auth/credentials.py +0 -0
  37. {day2-0.2.0.dev1 → day2-0.2.2}/day2/client/__init__.py +0 -0
  38. {day2-0.2.0.dev1 → day2-0.2.2}/day2/exceptions.py +0 -0
  39. {day2-0.2.0.dev1 → day2-0.2.2}/day2.egg-info/dependency_links.txt +0 -0
  40. {day2-0.2.0.dev1 → day2-0.2.2}/day2.egg-info/entry_points.txt +0 -0
  41. {day2-0.2.0.dev1 → day2-0.2.2}/day2.egg-info/requires.txt +0 -0
  42. {day2-0.2.0.dev1 → day2-0.2.2}/day2.egg-info/top_level.txt +0 -0
  43. {day2-0.2.0.dev1 → day2-0.2.2}/day2_cli/__init__.py +0 -0
  44. {day2-0.2.0.dev1 → day2-0.2.2}/day2_cli/commands/__init__.py +0 -0
  45. {day2-0.2.0.dev1 → day2-0.2.2}/day2_cli/commands/auth.py +0 -0
  46. {day2-0.2.0.dev1 → day2-0.2.2}/day2_cli/commands/config.py +0 -0
  47. {day2-0.2.0.dev1 → day2-0.2.2}/day2_cli/utils/__init__.py +0 -0
  48. {day2-0.2.0.dev1 → day2-0.2.2}/day2_cli/utils/config.py +0 -0
  49. {day2-0.2.0.dev1 → day2-0.2.2}/day2_cli/utils/formatters.py +0 -0
  50. {day2-0.2.0.dev1 → day2-0.2.2}/day2_cli/utils/output_formatter.py +0 -0
  51. {day2-0.2.0.dev1 → day2-0.2.2}/setup.cfg +0 -0
  52. {day2-0.2.0.dev1 → day2-0.2.2}/tests/test_cli_auth.py +0 -0
  53. {day2-0.2.0.dev1 → 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.0.dev1
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")
@@ -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.
@@ -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")
@@ -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.
@@ -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
+ ]
@@ -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.info("Response data: %s", json.dumps(data, indent=2)[:1000])
109
+ logger.debug("Response data: %s", json.dumps(data, indent=2)[:1000])
116
110
  except ValueError:
117
111
  data = {"Message": response.text}
118
- logger.info("Response text: %s", response.text[:1000])
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, PUT, DELETE, etc.).
150
- url: Full URL for the request.
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
- Response data as dictionary.
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
- try:
176
- response = requests.request(
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=headers,
180
- timeout=self._config.timeout,
181
- **kwargs,
163
+ headers,
164
+ request_kwargs,
182
165
  )
183
166
 
184
- # Log response details
185
- logger.info("Received response with status %s", response.status_code)
186
- logger.debug("Response headers: %s", response.headers)
187
- logger.debug("Response content: %s", response.text[:1000])
188
-
189
- return self._handle_response(response)
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
- except requests.RequestException as e:
192
- logger.error("Request failed: %s", e)
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.info("Making request to %s %s", method, url)
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,17 +1,29 @@
1
1
  """Configuration for the MontyCloud SDK."""
2
2
 
3
3
  import configparser
4
- from dataclasses import dataclass, field
4
+ from dataclasses import dataclass
5
5
  from pathlib import Path
6
6
  from typing import Any, Dict, Optional
7
7
 
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
- base_url: str = "https://stg1-api.montycloud.com/day2/api"
26
+ base_url: str = "https://api.montycloud.com/day2/api"
15
27
 
16
28
  # API version
17
29
  api_version: str = "v1"
@@ -34,12 +46,6 @@ class Config:
34
46
  # Maximum retry delay in seconds
35
47
  retry_max_delay: float = 10.0
36
48
 
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
49
  # CLI output format (table or json)
44
50
  output_format: str = "table"
45
51
 
@@ -67,10 +73,6 @@ class Config:
67
73
  "tenant_id": self.tenant_id,
68
74
  "retry_min_delay": self.retry_min_delay,
69
75
  "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
76
  "output_format": self.output_format,
75
77
  }
76
78
 
@@ -84,16 +86,6 @@ class Config:
84
86
  Returns:
85
87
  Config object initialized with values from dictionary.
86
88
  """
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
89
  return cls(**config_dict)
98
90
 
99
91
  @classmethod
@@ -150,17 +142,6 @@ class Config:
150
142
  fallback=str(cls.retry_max_delay),
151
143
  )
152
144
  ),
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
145
  # Output format
165
146
  output_format=config_parser.get(
166
147
  "DEFAULT", "output_format", fallback=cls.output_format
@@ -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")