day2 0.3.2__tar.gz → 0.3.3__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 (81) hide show
  1. {day2-0.3.2 → day2-0.3.3}/PKG-INFO +1 -1
  2. {day2-0.3.2 → day2-0.3.3}/day2/client/base.py +16 -4
  3. {day2-0.3.2 → day2-0.3.3}/day2/client/config.py +23 -0
  4. {day2-0.3.2 → day2-0.3.3}/day2/models/account.py +20 -0
  5. {day2-0.3.2 → day2-0.3.3}/day2/models/tenant.py +2 -0
  6. day2-0.3.3/day2/models/user.py +74 -0
  7. {day2-0.3.2 → day2-0.3.3}/day2/resources/account.py +20 -5
  8. day2-0.3.3/day2/resources/user.py +128 -0
  9. {day2-0.3.2 → day2-0.3.3}/day2.egg-info/PKG-INFO +1 -1
  10. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/account.py +19 -0
  11. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/tenant.py +2 -0
  12. day2-0.3.3/day2_cli/commands/user.py +162 -0
  13. {day2-0.3.2 → day2-0.3.3}/pyproject.toml +1 -1
  14. day2-0.3.2/day2/models/user.py +0 -35
  15. day2-0.3.2/day2/resources/user.py +0 -70
  16. day2-0.3.2/day2_cli/commands/user.py +0 -73
  17. {day2-0.3.2 → day2-0.3.3}/LICENSE +0 -0
  18. {day2-0.3.2 → day2-0.3.3}/README.md +0 -0
  19. {day2-0.3.2 → day2-0.3.3}/day2/__init__.py +0 -0
  20. {day2-0.3.2 → day2-0.3.3}/day2/_version.py +0 -0
  21. {day2-0.3.2 → day2-0.3.3}/day2/auth/__init__.py +0 -0
  22. {day2-0.3.2 → day2-0.3.3}/day2/auth/credentials.py +0 -0
  23. {day2-0.3.2 → day2-0.3.3}/day2/client/__init__.py +0 -0
  24. {day2-0.3.2 → day2-0.3.3}/day2/client/constants.py +0 -0
  25. {day2-0.3.2 → day2-0.3.3}/day2/client/profile.py +0 -0
  26. {day2-0.3.2 → day2-0.3.3}/day2/client/user_context.py +0 -0
  27. {day2-0.3.2 → day2-0.3.3}/day2/exceptions.py +0 -0
  28. {day2-0.3.2 → day2-0.3.3}/day2/models/__init__.py +0 -0
  29. {day2-0.3.2 → day2-0.3.3}/day2/models/assessment.py +0 -0
  30. {day2-0.3.2 → day2-0.3.3}/day2/models/azure_account.py +0 -0
  31. {day2-0.3.2 → day2-0.3.3}/day2/models/azure_assessment.py +0 -0
  32. {day2-0.3.2 → day2-0.3.3}/day2/models/bot.py +0 -0
  33. {day2-0.3.2 → day2-0.3.3}/day2/models/cost.py +0 -0
  34. {day2-0.3.2 → day2-0.3.3}/day2/models/project.py +0 -0
  35. {day2-0.3.2 → day2-0.3.3}/day2/models/report.py +0 -0
  36. {day2-0.3.2 → day2-0.3.3}/day2/models/resource.py +0 -0
  37. {day2-0.3.2 → day2-0.3.3}/day2/models/role.py +0 -0
  38. {day2-0.3.2 → day2-0.3.3}/day2/resources/__init__.py +0 -0
  39. {day2-0.3.2 → day2-0.3.3}/day2/resources/assessment.py +0 -0
  40. {day2-0.3.2 → day2-0.3.3}/day2/resources/azure_account.py +0 -0
  41. {day2-0.3.2 → day2-0.3.3}/day2/resources/azure_assessment.py +0 -0
  42. {day2-0.3.2 → day2-0.3.3}/day2/resources/bot.py +0 -0
  43. {day2-0.3.2 → day2-0.3.3}/day2/resources/cost.py +0 -0
  44. {day2-0.3.2 → day2-0.3.3}/day2/resources/project.py +0 -0
  45. {day2-0.3.2 → day2-0.3.3}/day2/resources/report.py +0 -0
  46. {day2-0.3.2 → day2-0.3.3}/day2/resources/resource.py +0 -0
  47. {day2-0.3.2 → day2-0.3.3}/day2/resources/role.py +0 -0
  48. {day2-0.3.2 → day2-0.3.3}/day2/resources/tenant.py +0 -0
  49. {day2-0.3.2 → day2-0.3.3}/day2/session.py +0 -0
  50. {day2-0.3.2 → day2-0.3.3}/day2/utils/__init__.py +0 -0
  51. {day2-0.3.2 → day2-0.3.3}/day2/utils/helpers.py +0 -0
  52. {day2-0.3.2 → day2-0.3.3}/day2.egg-info/SOURCES.txt +0 -0
  53. {day2-0.3.2 → day2-0.3.3}/day2.egg-info/dependency_links.txt +0 -0
  54. {day2-0.3.2 → day2-0.3.3}/day2.egg-info/entry_points.txt +0 -0
  55. {day2-0.3.2 → day2-0.3.3}/day2.egg-info/requires.txt +0 -0
  56. {day2-0.3.2 → day2-0.3.3}/day2.egg-info/top_level.txt +0 -0
  57. {day2-0.3.2 → day2-0.3.3}/day2_cli/__init__.py +0 -0
  58. {day2-0.3.2 → day2-0.3.3}/day2_cli/cli.py +0 -0
  59. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/__init__.py +0 -0
  60. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/assessment.py +0 -0
  61. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/auth.py +0 -0
  62. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/azure_account.py +0 -0
  63. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/azure_assessment.py +0 -0
  64. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/bot.py +0 -0
  65. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/cost.py +0 -0
  66. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/profile.py +0 -0
  67. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/project.py +0 -0
  68. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/report.py +0 -0
  69. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/resource.py +0 -0
  70. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/role.py +0 -0
  71. {day2-0.3.2 → day2-0.3.3}/day2_cli/commands/tenant_docs_cli.py +0 -0
  72. {day2-0.3.2 → day2-0.3.3}/day2_cli/utils/__init__.py +0 -0
  73. {day2-0.3.2 → day2-0.3.3}/day2_cli/utils/context.py +0 -0
  74. {day2-0.3.2 → day2-0.3.3}/day2_cli/utils/exit_codes.py +0 -0
  75. {day2-0.3.2 → day2-0.3.3}/day2_cli/utils/formatters.py +0 -0
  76. {day2-0.3.2 → day2-0.3.3}/day2_cli/utils/helpers.py +0 -0
  77. {day2-0.3.2 → day2-0.3.3}/day2_cli/utils/output_formatter.py +0 -0
  78. {day2-0.3.2 → day2-0.3.3}/setup.cfg +0 -0
  79. {day2-0.3.2 → day2-0.3.3}/tests/test_exceptions.py +0 -0
  80. {day2-0.3.2 → day2-0.3.3}/tests/test_rate_limiting.py +0 -0
  81. {day2-0.3.2 → day2-0.3.3}/tests/test_rate_limiting_retry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: day2
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: MontyCloud DAY2 SDK for Python
5
5
  Author-email: MontyCloud <info@montycloud.com>
6
6
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ import json
4
4
  import logging
5
5
 
6
6
  # Use TYPE_CHECKING to avoid circular imports
7
- from typing import TYPE_CHECKING, Any, Dict
7
+ from typing import TYPE_CHECKING, Any, Dict, Optional
8
8
 
9
9
  import requests
10
10
  from tenacity import (
@@ -43,11 +43,14 @@ class BaseClient:
43
43
  self.service_name = service_name
44
44
  self._config = session._config
45
45
 
46
- def _get_endpoint_url(self, endpoint: str) -> str:
46
+ def _get_endpoint_url(
47
+ self, endpoint: str, api_version: Optional[str] = None
48
+ ) -> str:
47
49
  """Get the full URL for an endpoint.
48
50
 
49
51
  Args:
50
52
  endpoint: API endpoint path.
53
+ api_version: API version to use instead of the default.(optional)
51
54
 
52
55
  Returns:
53
56
  Full URL for the endpoint.
@@ -55,6 +58,10 @@ class BaseClient:
55
58
  if endpoint.startswith("/"):
56
59
  endpoint = endpoint[1:]
57
60
 
61
+ # Handle custom version if provided
62
+ if api_version:
63
+ return f"{self._config.get_api_url_with_version(api_version)}/{endpoint}"
64
+
58
65
  # For MontyCloud API, we don't include the service name in the URL
59
66
  return f"{self._config.api_url}/{endpoint}"
60
67
 
@@ -213,13 +220,18 @@ class BaseClient:
213
220
  return r(_do_request)()
214
221
 
215
222
  def _make_request(
216
- self, method: str, endpoint: str, **kwargs: Any
223
+ self,
224
+ method: str,
225
+ endpoint: str,
226
+ api_version: Optional[str] = None,
227
+ **kwargs: Any,
217
228
  ) -> Dict[str, Any]:
218
229
  """Make an API request.
219
230
 
220
231
  Args:
221
232
  method: HTTP method (GET, POST, PUT, DELETE, etc.).
222
233
  endpoint: API endpoint path.
234
+ api_version: API version to use instead of the default (optional).
223
235
  **kwargs: Additional arguments to pass to requests.
224
236
 
225
237
  Returns:
@@ -229,7 +241,7 @@ class BaseClient:
229
241
  if "json_data" in kwargs:
230
242
  kwargs["json"] = kwargs.pop("json_data")
231
243
 
232
- url = self._get_endpoint_url(endpoint)
244
+ url = self._get_endpoint_url(endpoint, api_version)
233
245
  logger.debug("Making request to %s %s", method, url)
234
246
  # Pass the full URL to the request method
235
247
  return self._request_with_retry(method, url, **kwargs)
@@ -5,6 +5,9 @@ from dataclasses import dataclass
5
5
  from pathlib import Path
6
6
  from typing import Any, Dict, Optional
7
7
 
8
+ API_VERSION_V1 = "v1"
9
+ API_VERSION_V2 = "v2"
10
+
8
11
 
9
12
  @dataclass
10
13
  class Config:
@@ -61,6 +64,26 @@ class Config:
61
64
  """
62
65
  return f"{self.base_url}/{self.api_version}"
63
66
 
67
+ def get_api_url_with_version(self, api_version: str) -> str:
68
+ """Get the full API URL with a custom version.
69
+
70
+ Args:
71
+ api_version: API version to use instead of the default.
72
+
73
+ Returns:
74
+ Full API URL with the specified version.
75
+
76
+ Raises:
77
+ ValueError: If the API version is not valid.
78
+ """
79
+ # Validate API version is in the list of supported versions
80
+ if api_version not in [API_VERSION_V1, API_VERSION_V2]:
81
+ raise ValueError(
82
+ f"Invalid API version '{api_version}'. Valid versions are: v1, v2"
83
+ )
84
+
85
+ return f"{self.base_url}/{api_version}"
86
+
64
87
  def to_dict(self) -> Dict[str, Any]:
65
88
  """Convert config to dictionary.
66
89
 
@@ -10,6 +10,8 @@ class GenerateOnboardingTemplateInput(BaseModel):
10
10
 
11
11
  Attributes:
12
12
  account_type: Type of AWS account to onboard: STANDALONE or MANAGEMENT
13
+ account_name: Name of the AWS account to onboard
14
+ account_number: AWS account number to onboard
13
15
  regions: List of AWS regions to onboard
14
16
  """
15
17
 
@@ -18,6 +20,16 @@ class GenerateOnboardingTemplateInput(BaseModel):
18
20
  alias="AccountType",
19
21
  description="Type of AWS account to onboard: STANDALONE or MANAGEMENT",
20
22
  )
23
+ account_name: str = Field(
24
+ ...,
25
+ alias="AccountName",
26
+ description="Name of the AWS account to onboard",
27
+ )
28
+ account_number: str = Field(
29
+ ...,
30
+ alias="AccountNumber",
31
+ description="AWS account number to onboard",
32
+ )
21
33
  regions: List[str] = Field(
22
34
  default_factory=list,
23
35
  alias="Regions",
@@ -29,6 +41,7 @@ class AccountItem(BaseModel):
29
41
  """Details of an account.
30
42
 
31
43
  Attributes:
44
+ account_id: Unique account identifier
32
45
  number: AWS account number
33
46
  name: Account name
34
47
  status: Current status of the account
@@ -37,6 +50,7 @@ class AccountItem(BaseModel):
37
50
  onboarded_date: Date when the account was onboarded
38
51
  """
39
52
 
53
+ account_id: str = Field(alias="AccountId")
40
54
  number: str = Field(alias="Number")
41
55
  name: str = Field(alias="Name")
42
56
  status: str = Field(alias="Status")
@@ -53,6 +67,7 @@ class GenerateOnboardingTemplateOutput(BaseModel):
53
67
 
54
68
  Attributes:
55
69
  account_id: The account ID for the onboarding template
70
+ expires_on: The expiration date and time of the onboarding template
56
71
  onboarding_template_url: URL to the generated onboarding template
57
72
  """
58
73
 
@@ -61,6 +76,11 @@ class GenerateOnboardingTemplateOutput(BaseModel):
61
76
  alias="AccountId",
62
77
  description="The account ID for the onboarding template",
63
78
  )
79
+ expires_on: str = Field(
80
+ ...,
81
+ alias="ExpiresOn",
82
+ description="The expiration date and time of the onboarding template",
83
+ )
64
84
  onboarding_template_url: str = Field(
65
85
  ...,
66
86
  alias="OnboardingTemplateURL",
@@ -125,6 +125,7 @@ class Account(BaseModel):
125
125
  """Details of an account.
126
126
 
127
127
  Attributes:
128
+ account_id: Unique account identifier
128
129
  number: AWS account number
129
130
  name: Account name
130
131
  status: Current status of the account
@@ -133,6 +134,7 @@ class Account(BaseModel):
133
134
  onboarded_date: Date when the account was onboarded
134
135
  """
135
136
 
137
+ account_id: str = Field(alias="AccountId")
136
138
  number: str = Field(alias="Number")
137
139
  name: str = Field(alias="Name")
138
140
  status: str = Field(alias="Status")
@@ -0,0 +1,74 @@
1
+ """User models for the MontyCloud SDK."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class User(BaseModel):
9
+ """Details of a user in a tenant.
10
+
11
+ Attributes:
12
+ name: Name of the user
13
+ email: Email address of the user
14
+ user_id: Unique identifier of the user
15
+ """
16
+
17
+ name: str = Field(alias="Name")
18
+ email: str = Field(alias="Email")
19
+ user_id: str = Field(alias="UserId")
20
+
21
+ # Allow extra fields
22
+ model_config = ConfigDict(extra="allow")
23
+
24
+
25
+ class ListUsersOutput(BaseModel):
26
+ """Output model for ListUsers API.
27
+
28
+ Attributes:
29
+ users: List of users in the tenant
30
+ """
31
+
32
+ users: List[User] = Field(alias="Users", default=[])
33
+
34
+ # Allow extra fields
35
+ model_config = ConfigDict(extra="allow")
36
+
37
+
38
+ class CreateUserInput(BaseModel):
39
+ """
40
+ Input model for CreateUser API.
41
+
42
+ Attributes:
43
+ name: Name of the user
44
+ email: Email address of the user
45
+ temporary_password: Temporary password for the user
46
+ role_id: Role ID for the user
47
+ projects: (Optional) Project(s) List of project ids for project roles. Applicable for project specific roles
48
+ federated_access_roles: (Optional) List of federated access roles. Applicable for admin roles
49
+ """
50
+
51
+ name: str = Field(alias="Name")
52
+ email: str = Field(alias="Email")
53
+ temporary_password: str = Field(alias="TemporaryPassword")
54
+ role_id: str = Field(alias="RoleId")
55
+ projects: Optional[List[str]] = Field(None, alias="Projects")
56
+ federated_access_roles: Optional[List[str]] = Field(
57
+ None, alias="FederatedAccessRoles"
58
+ )
59
+
60
+ # Allow extra fields
61
+ model_config = ConfigDict(extra="allow")
62
+
63
+
64
+ class CreateUserOutput(BaseModel):
65
+ """Output model for CreateUser API.
66
+
67
+ Attributes:
68
+ user_id: Unique identifier of the created user
69
+ """
70
+
71
+ user_id: str = Field(alias="UserId")
72
+
73
+ # Allow extra fields
74
+ model_config = ConfigDict(extra="allow")
@@ -3,6 +3,7 @@
3
3
  from typing import List, Optional
4
4
 
5
5
  from day2.client.base import BaseClient
6
+ from day2.client.config import API_VERSION_V2
6
7
  from day2.models.account import (
7
8
  GenerateOnboardingTemplateInput,
8
9
  GenerateOnboardingTemplateOutput,
@@ -24,17 +25,24 @@ class AccountClient(BaseClient):
24
25
  super().__init__(session, "account")
25
26
 
26
27
  def get_onboarding_template(
27
- self, tenant_id: str, account_type: str, regions: Optional[List[str]] = None
28
+ self,
29
+ tenant_id: str,
30
+ account_type: str,
31
+ account_name: str,
32
+ account_number: str,
33
+ regions: Optional[List[str]] = None,
28
34
  ) -> GenerateOnboardingTemplateOutput:
29
35
  """Generate an AWS onboarding template for the specified tenant.
30
36
 
31
37
  Args:
32
38
  tenant_id: The ID of the tenant to generate the onboarding template for.
33
39
  account_type: Type of AWS account to onboard: STANDALONE or MANAGEMENT.
40
+ account_name: Name of the AWS account to onboard.
41
+ account_number: AWS account number to onboard.
34
42
  regions: List of AWS regions to onboard - Required for Assessment Feature type (Audit Permissions), not supported by other feature types (AutomatedCloudOps, ContinuousVisibility).
35
43
 
36
44
  Returns:
37
- GenerateOnboardingTemplateOutput: Object containing the account ID and onboarding template URL.
45
+ GenerateOnboardingTemplateOutput: Object containing the account ID, expiration date and time of the onboarding link and onboarding template URL.
38
46
 
39
47
  Raises:
40
48
  ClientError: If the request is invalid.
@@ -47,6 +55,8 @@ class AccountClient(BaseClient):
47
55
  >>> response = client.get_onboarding_template(
48
56
  ... tenant_id="tenant-123",
49
57
  ... account_type="STANDALONE", # STANDALONE or MANAGEMENT
58
+ ... account_name="AcmeAccount",
59
+ ... account_number="123456789012",
50
60
  ... regions=[
51
61
  ... "us-east-1",
52
62
  ... "us-west-2"
@@ -56,12 +66,16 @@ class AccountClient(BaseClient):
56
66
  >>> print(f"Onboarding URL: {response.onboarding_template_url}")
57
67
  """
58
68
  data = GenerateOnboardingTemplateInput(
59
- AccountType=account_type, Regions=regions if regions is not None else []
69
+ AccountType=account_type,
70
+ AccountName=account_name,
71
+ AccountNumber=account_number,
72
+ Regions=regions if regions is not None else [],
60
73
  )
61
74
 
62
75
  response = self._make_request(
63
76
  "POST",
64
77
  f"tenants/{tenant_id}/accounts/aws/onboarding-template",
78
+ API_VERSION_V2,
65
79
  json=data.model_dump(by_alias=True),
66
80
  )
67
81
  return GenerateOnboardingTemplateOutput.model_validate(response)
@@ -89,7 +103,8 @@ class AccountClient(BaseClient):
89
103
  >>> client = session.account
90
104
  >>> response = client.list_accounts(tenant_id="your-tenant-id", page_size=10)
91
105
  >>> for account in response.accounts:
92
- ... print(f"{account.number}: {account.name}")
106
+ ... print(f"Account ID: {account.account_id}")
107
+ ... print(f"Account Name: {account.name}")
93
108
  """
94
109
  params: dict[str, object] = {
95
110
  "PageSize": page_size,
@@ -97,7 +112,7 @@ class AccountClient(BaseClient):
97
112
  }
98
113
 
99
114
  response = self._make_request(
100
- "GET", f"tenants/{tenant_id}/accounts", params=params
115
+ "GET", f"tenants/{tenant_id}/accounts/", params=params
101
116
  )
102
117
  return ListAccountsOutput.model_validate(response)
103
118
 
@@ -0,0 +1,128 @@
1
+ """User resource client for the MontyCloud DAY2 SDK."""
2
+
3
+ import logging
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ from day2.client.base import BaseClient
7
+ from day2.client.user_context import UserContext
8
+ from day2.models.user import CreateUserInput, CreateUserOutput, ListUsersOutput
9
+
10
+ if TYPE_CHECKING:
11
+ from day2.session import Session
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class UserClient(BaseClient):
17
+ """Client for interacting with Users."""
18
+
19
+ def __init__(self, session: "Session") -> None:
20
+ """Initialize a new UserClient.
21
+
22
+ Args:
23
+ session: MontyCloud session.
24
+ """
25
+ super().__init__(session, "user")
26
+
27
+ def get_user(self) -> Optional[UserContext]:
28
+ """Get information about the authenticated user.
29
+
30
+ Returns:
31
+ UserContext containing the user's information if successful,
32
+ None if the request fails.
33
+ """
34
+ try:
35
+ # Use _make_request instead of directly calling session.request
36
+ response_data = self._make_request("GET", "auth/user")
37
+ if response_data:
38
+ logger.debug("Retrieved user context: %s", response_data)
39
+ return UserContext.from_dict(response_data)
40
+ logger.warning("Failed to get user context: empty response")
41
+ return None
42
+ except Exception as e: # pylint: disable=broad-except
43
+ logger.warning("Error retrieving user context: %s", e)
44
+ return None
45
+
46
+ def list_users(self, tenant_id: str) -> ListUsersOutput:
47
+ """Retrieves list of Users for a specific tenant.
48
+
49
+ Args:
50
+ tenant_id: ID of the tenant to list users for.
51
+
52
+ Returns:
53
+ ListUsersOutput: Object containing list of users.
54
+
55
+ Raises:
56
+ ResourceNotFoundError: If the tenant does not exist.
57
+ ClientError: If the request is invalid.
58
+ ServerError: If an internal server error occurs.
59
+ AuthenticationError: If authentication fails.
60
+
61
+ Examples:
62
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
63
+ >>> client = session.user
64
+ >>> response = client.list_users(tenant_id="your-tenant-id")
65
+ >>> for user in response.users:
66
+ ... print(f"{user.user_id}: {user.name} {user.email}")
67
+ """
68
+
69
+ response = self._make_request("GET", f"tenants/{tenant_id}/users")
70
+ return ListUsersOutput.model_validate(response)
71
+
72
+ def create_user(self, tenant_id: str, data: CreateUserInput) -> CreateUserOutput:
73
+ """Create user for a tenant.
74
+
75
+ !!! info "Federated Access Roles"
76
+ This operation is available only if the tenant's **FAA** feature is enabled.
77
+
78
+ - Allowed FAA roles: `AdminUser`, `PowerUser`, `ReadOnlyUser`
79
+ - Users with the **Business Admin** or **Cloud Admin** role may be assigned **any** FAA role.
80
+ - Users with the **Read Only** role can be assigned **only** the `ReadOnlyUser` FAA role.
81
+
82
+ Args:
83
+ tenant_id: ID of the tenant to create user for.
84
+ data: The user data to create.
85
+
86
+ Returns:
87
+ CreateUserOutput: Object containing the created user id.
88
+
89
+ Raises:
90
+ ClientError: If the request is invalid.
91
+ ServerError: If an internal server error occurs.
92
+ AuthenticationError: If authentication fails.
93
+
94
+ Examples:
95
+ >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
96
+ >>> client = session.user
97
+ # The payload for creating a user varies based on the role type:
98
+ # 1. For Cloud Admin and Business Admin roles, FederatedAccessRoles can include one or more of PowerUser, ReadOnlyUser, AdminUser, or omitted.
99
+ # 2. For the Read Only role, FederatedAccessRoles must be ReadOnlyUser or omitted.
100
+ # 3. For Project Owner, Project User, and Reports Access roles, the Projects field must include at least one project ID and FederatedAccessRoles should not be provided.
101
+ >>> data = CreateUserInput(
102
+ ... Name="UserName",
103
+ ... Email="username@example.com",
104
+ ... TemporaryPassword="*********",
105
+ ... RoleId="13acd9eb-6a65-41f8-a941-d4c6edb3f1c9",
106
+ ... Projects = ["8e5a5792-89a4-45fc-9260-dd673015df03"],
107
+ ... )
108
+ >>> response = client.create_user(tenant_id="your-tenant-id", data=data)
109
+ >>> print(f"Created user with ID: {response.user_id}")
110
+ """
111
+
112
+ create_user_payload: dict[str, str | list[str]] = {
113
+ "Name": data.name,
114
+ "Email": data.email,
115
+ "TemporaryPassword": data.temporary_password,
116
+ "RoleId": data.role_id,
117
+ }
118
+
119
+ # Add optional fields if they have values
120
+ if data.projects:
121
+ create_user_payload["Projects"] = data.projects
122
+ if data.federated_access_roles:
123
+ create_user_payload["FederatedAccessRoles"] = data.federated_access_roles
124
+
125
+ response = self._make_request(
126
+ "POST", f"tenants/{tenant_id}/users", json=create_user_payload
127
+ )
128
+ return CreateUserOutput.model_validate(response)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: day2
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: MontyCloud DAY2 SDK for Python
5
5
  Author-email: MontyCloud <info@montycloud.com>
6
6
  Requires-Python: >=3.11
@@ -26,6 +26,18 @@ def account() -> None:
26
26
  required=True,
27
27
  help="Type of AWS account to onboard: STANDALONE or MANAGEMENT",
28
28
  )
29
+ @click.option(
30
+ "--account-name",
31
+ type=str,
32
+ required=True,
33
+ help="Name of the AWS account to onboard.",
34
+ )
35
+ @click.option(
36
+ "--account-number",
37
+ type=str,
38
+ required=True,
39
+ help="AWS account number to onboard.",
40
+ )
29
41
  @click.option(
30
42
  "--regions",
31
43
  type=str,
@@ -35,6 +47,8 @@ def account() -> None:
35
47
  @with_common_options(include_tenant_id=True)
36
48
  def generate_onboarding_template(
37
49
  account_type: str,
50
+ account_name: str,
51
+ account_number: str,
38
52
  regions: str,
39
53
  output: Optional[str] = None,
40
54
  profile: Optional[str] = None,
@@ -63,12 +77,15 @@ def generate_onboarding_template(
63
77
  result = session.account.get_onboarding_template(
64
78
  tenant_id=resolved_tenant_id,
65
79
  account_type=account_type.upper(),
80
+ account_name=account_name,
81
+ account_number=account_number,
66
82
  regions=regions_list,
67
83
  )
68
84
 
69
85
  # Convert template details to dictionary for output
70
86
  template_dict = {
71
87
  "account_id": result.account_id,
88
+ "expires_on": result.expires_on,
72
89
  "onboarding_template_url": result.onboarding_template_url,
73
90
  }
74
91
 
@@ -130,6 +147,7 @@ def list_accounts(
130
147
  account_list = []
131
148
  for account_item in result.accounts:
132
149
  account_dict = {
150
+ "account_id": account_item.account_id,
133
151
  "number": account_item.number,
134
152
  "name": account_item.name,
135
153
  "status": account_item.status,
@@ -141,6 +159,7 @@ def list_accounts(
141
159
 
142
160
  # Define columns for table output
143
161
  columns = {
162
+ "account_id": "Account ID",
144
163
  "number": "Number",
145
164
  "name": "Name",
146
165
  "status": "Status",
@@ -199,6 +199,7 @@ def list_accounts(
199
199
  account_list = []
200
200
  for account_item in result.accounts:
201
201
  account_dict = {
202
+ "account_id": account_item.account_id,
202
203
  "number": account_item.number,
203
204
  "name": account_item.name,
204
205
  "status": account_item.status,
@@ -210,6 +211,7 @@ def list_accounts(
210
211
 
211
212
  # Define columns for table output
212
213
  columns = {
214
+ "account_id": "Account ID",
213
215
  "number": "Number",
214
216
  "name": "Name",
215
217
  "status": "Status",
@@ -0,0 +1,162 @@
1
+ """User commands for the MontyCloud DAY2 CLI."""
2
+
3
+ from typing import Optional
4
+
5
+ import click
6
+ from rich.console import Console
7
+
8
+ from day2.exceptions import Day2Error, ProfileNotFoundError
9
+ from day2.models.user import CreateUserInput
10
+ from day2_cli.utils.context import get_enhanced_context, with_common_options
11
+ from day2_cli.utils.exit_codes import handle_error_with_exit
12
+ from day2_cli.utils.output_formatter import format_item_output, format_list_output
13
+
14
+ console = Console()
15
+
16
+
17
+ @click.group()
18
+ def user() -> None:
19
+ """User commands."""
20
+
21
+
22
+ @user.command("list")
23
+ @with_common_options(include_tenant_id=True)
24
+ def list_users(
25
+ output: Optional[str] = None,
26
+ profile: Optional[str] = None,
27
+ tenant_id: Optional[str] = None,
28
+ ) -> None:
29
+ """List users associated with a specific tenant.
30
+
31
+ This command lists all the users in the tenant.
32
+ """
33
+ try:
34
+ # Get enhanced context
35
+ context = get_enhanced_context(
36
+ output=output, profile=profile, tenant_id=tenant_id, require_tenant=True
37
+ )
38
+ session = context["session"]
39
+ resolved_tenant_id = context["tenant_id"]
40
+ output_format = context["output_format"]
41
+
42
+ result = session.user.list_users(resolved_tenant_id)
43
+
44
+ if not result.users:
45
+ console.print("[yellow]No users found.[/yellow]")
46
+ return
47
+
48
+ # Convert user objects to dictionaries for output
49
+ user_list = []
50
+ for user_item in result.users:
51
+ user_dict = {
52
+ "name": user_item.name,
53
+ "email": user_item.email,
54
+ "user_id": user_item.user_id,
55
+ }
56
+ user_list.append(user_dict)
57
+
58
+ # Define columns for table output
59
+ columns = {
60
+ "name": "Name",
61
+ "email": "Email",
62
+ "user_id": "User ID",
63
+ }
64
+
65
+ # Format and output the user list
66
+ format_list_output(
67
+ user_list,
68
+ f"Users for Tenant: {resolved_tenant_id}",
69
+ columns,
70
+ output_format,
71
+ )
72
+
73
+ except (Day2Error, ProfileNotFoundError) as e:
74
+ handle_error_with_exit(e)
75
+
76
+
77
+ @user.command("create")
78
+ @click.option("--name", required=True, help="Name of the user")
79
+ @click.option("--email", required=True, help="Email id of the user")
80
+ @click.option(
81
+ "--temporary-password", required=True, help="Temporary password of the user"
82
+ )
83
+ @click.option("--role-id", required=True, help="Role ID to be assigned to the user")
84
+ @click.option(
85
+ "--projects",
86
+ help="Comma-separated list of project ids to be assigned to the user",
87
+ )
88
+ @click.option(
89
+ "--federated-access-roles",
90
+ help="Comma-separated list of federated access roles to be assigned to the user",
91
+ )
92
+ @with_common_options(include_tenant_id=True)
93
+ def create_user(
94
+ name: str,
95
+ email: str,
96
+ temporary_password: str,
97
+ role_id: str,
98
+ projects: Optional[str],
99
+ federated_access_roles: Optional[str],
100
+ output: Optional[str] = None,
101
+ profile: Optional[str] = None,
102
+ tenant_id: Optional[str] = None,
103
+ ) -> None:
104
+ """Create user for a tenant.
105
+
106
+ This command creates a new user for a tenant.
107
+
108
+ !!! info "Federated Access Roles"
109
+ This operation is available only if the tenant's **FAA** feature is enabled.
110
+
111
+ - Allowed FAA roles: `AdminUser`, `PowerUser`, `ReadOnlyUser`
112
+ - Users with the **Business Admin** or **Cloud Admin** role may be assigned **any** FAA role.
113
+ - Users with the **Read Only** role can be assigned **only** the `ReadOnlyUser` FAA role.
114
+ """
115
+ try:
116
+ # Get enhanced context
117
+ context = get_enhanced_context(
118
+ output=output, profile=profile, tenant_id=tenant_id, require_tenant=True
119
+ )
120
+ session = context["session"]
121
+ resolved_tenant_id = context["tenant_id"]
122
+ output_format = context["output_format"]
123
+
124
+ # Parse projects if provided
125
+ project_ids = (
126
+ [project.strip() for project in projects.split(",")] if projects else None
127
+ )
128
+
129
+ federated_access_roles_list = (
130
+ [role.strip() for role in federated_access_roles.split(",")]
131
+ if federated_access_roles
132
+ else None
133
+ )
134
+
135
+ # Create a CreateUserInput object
136
+ create_user_input = CreateUserInput(
137
+ Name=name,
138
+ Email=email,
139
+ TemporaryPassword=temporary_password,
140
+ RoleId=role_id,
141
+ Projects=project_ids,
142
+ FederatedAccessRoles=federated_access_roles_list,
143
+ )
144
+
145
+ result = session.user.create_user(
146
+ tenant_id=resolved_tenant_id, data=create_user_input
147
+ )
148
+
149
+ # Create user data for output
150
+ user_output_data = {
151
+ "userId": result.user_id,
152
+ }
153
+
154
+ # Format and output the result
155
+ format_item_output(
156
+ item=user_output_data,
157
+ title="User Created Successfully",
158
+ format_override=output_format,
159
+ )
160
+
161
+ except (Day2Error, ProfileNotFoundError) as e:
162
+ handle_error_with_exit(e)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "day2"
3
- version = "0.3.2"
3
+ version = "0.3.3"
4
4
  description = "MontyCloud DAY2 SDK for Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,35 +0,0 @@
1
- """User models for the MontyCloud SDK."""
2
-
3
- from typing import List
4
-
5
- from pydantic import BaseModel, ConfigDict, Field
6
-
7
-
8
- class User(BaseModel):
9
- """Details of a user in a tenant.
10
-
11
- Attributes:
12
- name: Name of the user
13
- email: Email address of the user
14
- user_id: Unique identifier of the user
15
- """
16
-
17
- name: str = Field(alias="Name")
18
- email: str = Field(alias="Email")
19
- user_id: str = Field(alias="UserId")
20
-
21
- # Allow extra fields
22
- model_config = ConfigDict(extra="allow")
23
-
24
-
25
- class ListUsersOutput(BaseModel):
26
- """Output model for ListUsers API.
27
-
28
- Attributes:
29
- users: List of users in the tenant
30
- """
31
-
32
- users: List[User] = Field(alias="Users", default=[])
33
-
34
- # Allow extra fields
35
- model_config = ConfigDict(extra="allow")
@@ -1,70 +0,0 @@
1
- """User resource client for the MontyCloud DAY2 SDK."""
2
-
3
- import logging
4
- from typing import TYPE_CHECKING, Optional
5
-
6
- from day2.client.base import BaseClient
7
- from day2.client.user_context import UserContext
8
- from day2.models.user import ListUsersOutput
9
-
10
- if TYPE_CHECKING:
11
- from day2.session import Session
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
-
16
- class UserClient(BaseClient):
17
- """Client for interacting with Users."""
18
-
19
- def __init__(self, session: "Session") -> None:
20
- """Initialize a new UserClient.
21
-
22
- Args:
23
- session: MontyCloud session.
24
- """
25
- super().__init__(session, "user")
26
-
27
- def get_user(self) -> Optional[UserContext]:
28
- """Get information about the authenticated user.
29
-
30
- Returns:
31
- UserContext containing the user's information if successful,
32
- None if the request fails.
33
- """
34
- try:
35
- # Use _make_request instead of directly calling session.request
36
- response_data = self._make_request("GET", "auth/user")
37
- if response_data:
38
- logger.debug("Retrieved user context: %s", response_data)
39
- return UserContext.from_dict(response_data)
40
- logger.warning("Failed to get user context: empty response")
41
- return None
42
- except Exception as e: # pylint: disable=broad-except
43
- logger.warning("Error retrieving user context: %s", e)
44
- return None
45
-
46
- def list_users(self, tenant_id: str) -> ListUsersOutput:
47
- """Retrieves list of Users for a specific tenant.
48
-
49
- Args:
50
- tenant_id: ID of the tenant to list users for.
51
-
52
- Returns:
53
- ListUsersOutput: Object containing list of users.
54
-
55
- Raises:
56
- ResourceNotFoundError: If the tenant does not exist.
57
- ClientError: If the request is invalid.
58
- ServerError: If an internal server error occurs.
59
- AuthenticationError: If authentication fails.
60
-
61
- Examples:
62
- >>> session = Session(api_key="your-api-key", api_secret_key="your-secret-key")
63
- >>> client = session.user
64
- >>> response = client.list_users(tenant_id="your-tenant-id")
65
- >>> for user in response.users:
66
- ... print(f"{user.user_id}: {user.name} {user.email}")
67
- """
68
-
69
- response = self._make_request("GET", f"tenants/{tenant_id}/users")
70
- return ListUsersOutput.model_validate(response)
@@ -1,73 +0,0 @@
1
- """User commands for the MontyCloud DAY2 CLI."""
2
-
3
- from typing import Optional
4
-
5
- import click
6
- from rich.console import Console
7
-
8
- from day2.exceptions import Day2Error, ProfileNotFoundError
9
- from day2_cli.utils.context import get_enhanced_context, with_common_options
10
- from day2_cli.utils.exit_codes import handle_error_with_exit
11
- from day2_cli.utils.output_formatter import format_list_output
12
-
13
- console = Console()
14
-
15
-
16
- @click.group()
17
- def user() -> None:
18
- """User commands."""
19
-
20
-
21
- @user.command("list")
22
- @with_common_options(include_tenant_id=True)
23
- def list_users(
24
- output: Optional[str] = None,
25
- profile: Optional[str] = None,
26
- tenant_id: Optional[str] = None,
27
- ) -> None:
28
- """List users associated with a specific tenant.
29
-
30
- This command lists all the users in the tenant.
31
- """
32
- try:
33
- # Get enhanced context
34
- context = get_enhanced_context(
35
- output=output, profile=profile, tenant_id=tenant_id, require_tenant=True
36
- )
37
- session = context["session"]
38
- resolved_tenant_id = context["tenant_id"]
39
- output_format = context["output_format"]
40
-
41
- result = session.user.list_users(resolved_tenant_id)
42
-
43
- if not result.users:
44
- console.print("[yellow]No users found.[/yellow]")
45
- return
46
-
47
- # Convert user objects to dictionaries for output
48
- user_list = []
49
- for user_item in result.users:
50
- user_dict = {
51
- "name": user_item.name,
52
- "email": user_item.email,
53
- "user_id": user_item.user_id,
54
- }
55
- user_list.append(user_dict)
56
-
57
- # Define columns for table output
58
- columns = {
59
- "name": "Name",
60
- "email": "Email",
61
- "user_id": "User ID",
62
- }
63
-
64
- # Format and output the user list
65
- format_list_output(
66
- user_list,
67
- f"Users for Tenant: {resolved_tenant_id}",
68
- columns,
69
- output_format,
70
- )
71
-
72
- except (Day2Error, ProfileNotFoundError) as e:
73
- handle_error_with_exit(e)
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
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