amsdal 0.4.13__cp312-cp312-macosx_10_13_universal2.whl → 0.5.33__cp312-cp312-macosx_10_13_universal2.whl

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 (156) hide show
  1. amsdal/Third-Party Materials - AMSDAL Dependencies - License Notices.md +28 -0
  2. amsdal/__about__.py +1 -1
  3. amsdal/__migrations__/0000_initial.py +22 -203
  4. amsdal/__migrations__/0001_create_class_file.py +61 -0
  5. amsdal/__migrations__/0002_create_class_file.py +109 -0
  6. amsdal/__migrations__/0003_update_class_file.py +91 -0
  7. amsdal/__migrations__/0004_update_class_file.py +45 -0
  8. amsdal/cloud/__init__.cpython-312-darwin.so +0 -0
  9. amsdal/cloud/client.cpython-312-darwin.so +0 -0
  10. amsdal/cloud/constants.cpython-312-darwin.so +0 -0
  11. amsdal/cloud/enums.cpython-312-darwin.so +0 -0
  12. amsdal/cloud/models/__init__.cpython-312-darwin.so +0 -0
  13. amsdal/cloud/models/base.cpython-312-darwin.so +0 -0
  14. amsdal/cloud/services/__init__.cpython-312-darwin.so +0 -0
  15. amsdal/cloud/services/actions/__init__.cpython-312-darwin.so +0 -0
  16. amsdal/cloud/services/actions/add_allowlist_ip.cpython-312-darwin.so +0 -0
  17. amsdal/cloud/services/actions/add_basic_auth.cpython-312-darwin.so +0 -0
  18. amsdal/cloud/services/actions/add_dependency.cpython-312-darwin.so +0 -0
  19. amsdal/cloud/services/actions/add_secret.cpython-312-darwin.so +0 -0
  20. amsdal/cloud/services/actions/base.cpython-312-darwin.so +0 -0
  21. amsdal/cloud/services/actions/create_deploy.cpython-312-darwin.so +0 -0
  22. amsdal/cloud/services/actions/create_env.cpython-312-darwin.so +0 -0
  23. amsdal/cloud/services/actions/create_session.cpython-312-darwin.so +0 -0
  24. amsdal/cloud/services/actions/delete_allowlist_ip.cpython-312-darwin.so +0 -0
  25. amsdal/cloud/services/actions/delete_basic_auth.cpython-312-darwin.so +0 -0
  26. amsdal/cloud/services/actions/delete_dependency.cpython-312-darwin.so +0 -0
  27. amsdal/cloud/services/actions/delete_env.cpython-312-darwin.so +0 -0
  28. amsdal/cloud/services/actions/delete_secret.cpython-312-darwin.so +0 -0
  29. amsdal/cloud/services/actions/destroy_deploy.cpython-312-darwin.so +0 -0
  30. amsdal/cloud/services/actions/expose_db.cpython-312-darwin.so +0 -0
  31. amsdal/cloud/services/actions/get_basic_auth_credentials.cpython-312-darwin.so +0 -0
  32. amsdal/cloud/services/actions/get_monitoring_info.cpython-312-darwin.so +0 -0
  33. amsdal/cloud/services/actions/list_dependencies.cpython-312-darwin.so +0 -0
  34. amsdal/cloud/services/actions/list_deploys.cpython-312-darwin.so +0 -0
  35. amsdal/cloud/services/actions/list_envs.cpython-312-darwin.so +0 -0
  36. amsdal/cloud/services/actions/list_secrets.cpython-312-darwin.so +0 -0
  37. amsdal/cloud/services/actions/manager.cpython-312-darwin.so +0 -0
  38. amsdal/cloud/services/actions/signup_action.cpython-312-darwin.so +0 -0
  39. amsdal/cloud/services/actions/update_deploy.cpython-312-darwin.so +0 -0
  40. amsdal/cloud/services/auth/__init__.cpython-312-darwin.so +0 -0
  41. amsdal/cloud/services/auth/base.cpython-312-darwin.so +0 -0
  42. amsdal/cloud/services/auth/credentials.cpython-312-darwin.so +0 -0
  43. amsdal/cloud/services/auth/manager.cpython-312-darwin.so +0 -0
  44. amsdal/cloud/services/auth/signup_service.cpython-312-darwin.so +0 -0
  45. amsdal/cloud/services/auth/token.cpython-312-darwin.so +0 -0
  46. amsdal/configs/main.py +17 -1
  47. amsdal/configs/main.pyi +6 -1
  48. amsdal/contrib/__init__.cpython-312-darwin.so +0 -0
  49. amsdal/contrib/auth/errors.py +36 -0
  50. amsdal/contrib/auth/errors.pyi +12 -0
  51. amsdal/contrib/auth/lifecycle/consumer.py +2 -2
  52. amsdal/contrib/auth/migrations/0000_initial.py +55 -52
  53. amsdal/contrib/auth/migrations/0001_add_mfa_support.py +188 -0
  54. amsdal/contrib/auth/models/__init__.py +1 -0
  55. amsdal/contrib/auth/models/backup_code.py +85 -0
  56. amsdal/contrib/auth/models/email_mfa_device.py +108 -0
  57. amsdal/contrib/auth/models/login_session.py +117 -0
  58. amsdal/contrib/auth/models/mfa_device.py +86 -0
  59. amsdal/contrib/auth/models/sms_device.py +113 -0
  60. amsdal/contrib/auth/models/totp_device.py +58 -0
  61. amsdal/contrib/auth/models/user.py +50 -0
  62. amsdal/contrib/auth/services/__init__.py +1 -0
  63. amsdal/contrib/auth/services/mfa_device_service.py +544 -0
  64. amsdal/contrib/auth/services/mfa_device_service.pyi +216 -0
  65. amsdal/contrib/auth/services/totp_service.py +358 -0
  66. amsdal/contrib/auth/services/totp_service.pyi +158 -0
  67. amsdal/contrib/auth/settings.py +8 -0
  68. amsdal/contrib/auth/settings.pyi +8 -0
  69. amsdal/contrib/auth/transactions/__init__.py +1 -0
  70. amsdal/contrib/auth/transactions/mfa_device_transactions.py +458 -0
  71. amsdal/contrib/auth/transactions/mfa_device_transactions.pyi +226 -0
  72. amsdal/contrib/auth/transactions/totp_transactions.py +203 -0
  73. amsdal/contrib/auth/transactions/totp_transactions.pyi +113 -0
  74. amsdal/contrib/auth/utils/mfa.py +257 -0
  75. amsdal/contrib/auth/utils/mfa.pyi +119 -0
  76. amsdal/contrib/frontend_configs/conversion/convert.py +24 -0
  77. amsdal/contrib/frontend_configs/migrations/0000_initial.py +154 -183
  78. amsdal/contrib/frontend_configs/migrations/0001_update_frontend_control_config.py +245 -0
  79. amsdal/contrib/frontend_configs/migrations/0002_add_button_and_invoke_actions.py +352 -0
  80. amsdal/contrib/frontend_configs/migrations/0003_create_class_frontendconfigdashboardelement.py +145 -0
  81. amsdal/contrib/frontend_configs/models/frontend_config_control_action.py +57 -1
  82. amsdal/contrib/frontend_configs/models/frontend_config_dashboard.py +51 -0
  83. amsdal/contrib/frontend_configs/models/frontend_control_config.py +69 -46
  84. amsdal/fixtures/__init__.cpython-312-darwin.so +0 -0
  85. amsdal/fixtures/manager.cpython-312-darwin.so +0 -0
  86. amsdal/fixtures/utils.cpython-312-darwin.so +0 -0
  87. amsdal/manager.cpython-312-darwin.so +0 -0
  88. amsdal/mixins/__init__.cpython-312-darwin.so +0 -0
  89. amsdal/mixins/class_versions_mixin.cpython-312-darwin.so +0 -0
  90. amsdal/models/core/class_object.py +7 -6
  91. amsdal/models/core/class_property.py +7 -1
  92. amsdal/models/core/file.py +168 -81
  93. amsdal/models/core/storage_metadata.py +15 -0
  94. amsdal/models/mixins.py +31 -0
  95. amsdal/models/types/object.py +3 -3
  96. amsdal/schemas/core/class_object/model.json +20 -0
  97. amsdal/schemas/core/class_property/model.json +19 -0
  98. amsdal/schemas/core/file/properties/validate_data.py +2 -3
  99. amsdal/schemas/core/storage_metadata/model.json +52 -0
  100. amsdal/schemas/manager.cpython-312-darwin.so +0 -0
  101. amsdal/schemas/mixins/check_dependencies_mixin.py +8 -3
  102. amsdal/services/__init__.py +11 -0
  103. amsdal/services/__init__.pyi +4 -0
  104. amsdal/services/external_connections.py +262 -0
  105. amsdal/services/external_connections.pyi +190 -0
  106. amsdal/services/external_model_generator.py +350 -0
  107. amsdal/services/external_model_generator.pyi +134 -0
  108. amsdal/services/transaction_execution.cpython-312-darwin.so +0 -0
  109. amsdal/storages/__init__.py +20 -0
  110. amsdal/storages/__init__.pyi +8 -0
  111. amsdal/storages/file_system.py +214 -0
  112. amsdal/storages/file_system.pyi +36 -0
  113. amsdal/utils/tests/enums.py +0 -2
  114. amsdal/utils/tests/helpers.py +213 -381
  115. amsdal/utils/tests/migrations.py +157 -0
  116. {amsdal-0.4.13.dist-info → amsdal-0.5.33.dist-info}/METADATA +17 -11
  117. {amsdal-0.4.13.dist-info → amsdal-0.5.33.dist-info}/RECORD +124 -117
  118. amsdal/__migrations__/0001_datetime_type.py +0 -18
  119. amsdal/__migrations__/0002_fixture_order.py +0 -44
  120. amsdal/__migrations__/0003_schema_type_in_class_meta.py +0 -44
  121. amsdal/contrib/auth/models/login_session.pyi +0 -37
  122. amsdal/contrib/auth/models/permission.pyi +0 -18
  123. amsdal/contrib/auth/models/user.pyi +0 -46
  124. amsdal/contrib/frontend_configs/models/frontend_activator_config.pyi +0 -12
  125. amsdal/contrib/frontend_configs/models/frontend_config_async_validator.pyi +0 -7
  126. amsdal/contrib/frontend_configs/models/frontend_config_control_action.pyi +0 -32
  127. amsdal/contrib/frontend_configs/models/frontend_config_group_validator.pyi +0 -11
  128. amsdal/contrib/frontend_configs/models/frontend_config_option.pyi +0 -8
  129. amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base.pyi +0 -8
  130. amsdal/contrib/frontend_configs/models/frontend_config_slider_option.pyi +0 -9
  131. amsdal/contrib/frontend_configs/models/frontend_config_text_mask.pyi +0 -10
  132. amsdal/contrib/frontend_configs/models/frontend_config_validator.pyi +0 -15
  133. amsdal/contrib/frontend_configs/models/frontend_control_config.pyi +0 -35
  134. amsdal/contrib/frontend_configs/models/frontend_model_config.pyi +0 -9
  135. amsdal/models/__init__.pyi +0 -9
  136. amsdal/models/core/class_object.pyi +0 -24
  137. amsdal/models/core/class_object_meta.py +0 -26
  138. amsdal/models/core/class_object_meta.pyi +0 -15
  139. amsdal/models/core/class_property.pyi +0 -11
  140. amsdal/models/core/class_property_meta.py +0 -15
  141. amsdal/models/core/class_property_meta.pyi +0 -10
  142. amsdal/models/core/file.pyi +0 -104
  143. amsdal/models/core/fixture.pyi +0 -14
  144. amsdal/models/core/option.pyi +0 -8
  145. amsdal/models/core/validator.pyi +0 -8
  146. amsdal/models/types/object.pyi +0 -16
  147. amsdal/schemas/core/class_object_meta/model.json +0 -59
  148. amsdal/schemas/core/class_property_meta/model.json +0 -23
  149. amsdal/services/__init__.cpython-312-darwin.so +0 -0
  150. /amsdal/contrib/auth/{models → services}/__init__.pyi +0 -0
  151. /amsdal/contrib/{frontend_configs/models → auth/transactions}/__init__.pyi +0 -0
  152. /amsdal/{models/core/__init__.pyi → contrib/auth/utils/__init__.py} +0 -0
  153. /amsdal/{models/types → contrib/auth/utils}/__init__.pyi +0 -0
  154. {amsdal-0.4.13.dist-info → amsdal-0.5.33.dist-info}/WHEEL +0 -0
  155. {amsdal-0.4.13.dist-info → amsdal-0.5.33.dist-info}/licenses/LICENSE.txt +0 -0
  156. {amsdal-0.4.13.dist-info → amsdal-0.5.33.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,86 @@
1
+ from datetime import UTC
2
+ from datetime import datetime
3
+ from typing import ClassVar
4
+
5
+ from amsdal_models.classes.model import Model
6
+ from amsdal_utils.models.enums import ModuleType
7
+ from pydantic.fields import Field
8
+
9
+ from amsdal.contrib.auth.utils.mfa import DeviceType
10
+
11
+
12
+ def _now_utc() -> datetime:
13
+ """Return current UTC datetime."""
14
+ return datetime.now(tz=UTC)
15
+
16
+
17
+ class MFADevice(Model):
18
+ """
19
+ Base model for Multi-Factor Authentication devices.
20
+
21
+ This model serves as the base class for all MFA device types (TOTP, Backup Codes, Email, SMS).
22
+ Each device is associated with a user and must be confirmed before it can be used for authentication.
23
+
24
+ Attributes:
25
+ user_email (str): Email of the user who owns this device (reference to User).
26
+ device_type (str): Type of MFA device ('totp', 'backup_code', 'email', 'sms').
27
+ name (str): User-friendly name for the device.
28
+ is_active (bool): Whether the device is currently active and can be used.
29
+ confirmed (bool): Whether the device has been verified during setup.
30
+ created_at (datetime): When the device was created.
31
+ last_used_at (datetime | None): When the device was last used for authentication.
32
+ """
33
+
34
+ __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
35
+
36
+ user_email: str = Field(title='User Email')
37
+ device_type: DeviceType | None = Field(default=None, title='Device Type')
38
+ name: str = Field(title='Device Name')
39
+ is_active: bool = Field(True, title='Is Active')
40
+ confirmed: bool = Field(False, title='Confirmed')
41
+ created_at: datetime = Field(default_factory=_now_utc, title='Created At')
42
+ last_used_at: datetime | None = Field(None, title='Last Used At')
43
+
44
+ @property
45
+ def display_name(self) -> str:
46
+ """
47
+ Returns the display name of the device.
48
+
49
+ Returns:
50
+ str: The device name and type.
51
+ """
52
+ return f'{self.name} ({self.device_type})'
53
+
54
+ def __repr__(self) -> str:
55
+ return str(self)
56
+
57
+ def __str__(self) -> str:
58
+ return f'MFADevice(name={self.name}, type={self.device_type}, user={self.user_email})'
59
+
60
+ def has_object_permission(self, user: 'User', action: str) -> bool: # type: ignore # noqa: F821
61
+ """
62
+ Check if a user has permission to perform an action on this device.
63
+
64
+ Users can only manage their own devices. Admins with wildcard permissions
65
+ can manage all devices.
66
+
67
+ Args:
68
+ user: The user requesting the action.
69
+ action: The action being requested (read, update, delete, etc.).
70
+
71
+ Returns:
72
+ bool: True if the user has permission, False otherwise.
73
+ """
74
+ # Users can only manage their own devices
75
+ if self.user_email == user.email:
76
+ return True
77
+
78
+ # Check if user has admin permissions (wildcard model permissions)
79
+ if user.permissions:
80
+ for permission in user.permissions:
81
+ if permission.model == '*' and permission.action in ('*', action):
82
+ return True
83
+ if permission.model == 'MFADevice' and permission.action in ('*', action):
84
+ return True
85
+
86
+ return False
@@ -0,0 +1,113 @@
1
+ from datetime import datetime
2
+ from typing import Any
3
+ from typing import ClassVar
4
+
5
+ from amsdal_utils.models.enums import ModuleType
6
+ from pydantic.fields import Field
7
+
8
+ from amsdal.contrib.auth.models.mfa_device import MFADevice
9
+ from amsdal.contrib.auth.utils.mfa import DeviceType
10
+
11
+
12
+ class SMSDevice(MFADevice):
13
+ """
14
+ SMS-based MFA device model (future implementation).
15
+
16
+ This model represents an SMS-based MFA method where a temporary code is sent
17
+ to the user's phone number for authentication.
18
+
19
+ Note:
20
+ This is a placeholder for future SMS support. Full implementation requires
21
+ integration with an SMS service provider (e.g., Twilio, AWS SNS).
22
+
23
+ Attributes:
24
+ phone_number (str): The phone number to send codes to.
25
+ code (str | None): Temporary MFA code (stored temporarily, expires after use).
26
+ code_expires_at (datetime | None): When the current code expires.
27
+ """
28
+
29
+ __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
30
+
31
+ phone_number: str = Field(title='Phone Number')
32
+ code: str | None = Field(None, title='Current Code')
33
+ code_expires_at: datetime | None = Field(None, title='Code Expiration')
34
+
35
+ def post_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:
36
+ """
37
+ Post-initializes an SMS MFA device by setting the device type.
38
+
39
+ Args:
40
+ is_new_object (bool): Indicates if the object is new.
41
+ kwargs (dict[str, Any]): The keyword arguments containing device details.
42
+ """
43
+ super().post_init(is_new_object=is_new_object, kwargs=kwargs)
44
+ self.device_type = DeviceType.SMS
45
+
46
+ def generate_and_send_code(self) -> str:
47
+ """
48
+ Generate a new MFA code and send it via SMS.
49
+
50
+ This method generates a random numeric code, sets its expiration time,
51
+ and sends it to the configured phone number.
52
+
53
+ Returns:
54
+ str: The generated code (for testing purposes).
55
+
56
+ Raises:
57
+ NotImplementedError: This feature requires SMS service integration.
58
+
59
+ Note:
60
+ Full implementation requires integration with an SMS provider.
61
+ Example providers: Twilio, AWS SNS, MessageBird, etc.
62
+ """
63
+ from amsdal.contrib.auth.utils.mfa import generate_email_mfa_code
64
+ from amsdal.contrib.auth.utils.mfa import get_email_code_expiration
65
+
66
+ # Generate new code
67
+ self.code = generate_email_mfa_code()
68
+ self.code_expires_at = get_email_code_expiration()
69
+
70
+ # TODO: Implement SMS sending with your SMS provider
71
+ # Example with Twilio:
72
+ # from twilio.rest import Client
73
+ # client = Client(account_sid, auth_token)
74
+ # client.messages.create(
75
+ # to=self.phone_number,
76
+ # from_=your_twilio_number,
77
+ # body=f'Your MFA code is: {self.code}'
78
+ # )
79
+
80
+ msg = 'SMS MFA is not yet implemented. Please integrate with an SMS service provider (e.g., Twilio, AWS SNS).'
81
+ raise NotImplementedError(msg)
82
+
83
+ def verify_code(self, code: str) -> bool:
84
+ """
85
+ Verify an SMS MFA code.
86
+
87
+ Args:
88
+ code (str): The code to verify.
89
+
90
+ Returns:
91
+ bool: True if the code is valid and not expired, False otherwise.
92
+ """
93
+ from amsdal.contrib.auth.utils.mfa import is_email_code_valid
94
+
95
+ # Check if code matches
96
+ if not self.code or self.code != code:
97
+ return False
98
+
99
+ # Check if code is expired
100
+ if not self.code_expires_at or not is_email_code_valid(self.code_expires_at):
101
+ return False
102
+
103
+ return True
104
+
105
+ def clear_code(self) -> None:
106
+ """
107
+ Clear the current code after successful use or expiration.
108
+ """
109
+ self.code = None
110
+ self.code_expires_at = None
111
+
112
+ def __str__(self) -> str:
113
+ return f'SMSDevice(name={self.name}, phone={self.phone_number}, user={self.user_email})'
@@ -0,0 +1,58 @@
1
+ from typing import Any
2
+ from typing import ClassVar
3
+
4
+ from amsdal_utils.models.enums import ModuleType
5
+ from pydantic.fields import Field
6
+
7
+ from amsdal.contrib.auth.models.mfa_device import MFADevice
8
+ from amsdal.contrib.auth.utils.mfa import DeviceType
9
+
10
+
11
+ class TOTPDevice(MFADevice):
12
+ """
13
+ Time-based One-Time Password (TOTP) device model.
14
+
15
+ This model represents an authenticator app device (e.g., Google Authenticator, Authy)
16
+ that generates time-based one-time passwords following RFC 6238.
17
+
18
+ Attributes:
19
+ secret (str): The encrypted TOTP secret key shared with the authenticator app.
20
+ qr_code_url (str | None): URL for the QR code used during device setup.
21
+ digits (int): Number of digits in the generated code (default: 6).
22
+ step (int): Time step in seconds for code generation (default: 30).
23
+ """
24
+
25
+ __module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
26
+
27
+ secret: str = Field(title='TOTP Secret')
28
+ qr_code_url: str | None = Field(None, title='QR Code URL')
29
+ digits: int = Field(6, title='Code Digits')
30
+ step: int = Field(30, title='Time Step (seconds)')
31
+
32
+ def post_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:
33
+ """
34
+ Post-initializes a TOTP device by setting the device type.
35
+
36
+ Args:
37
+ is_new_object (bool): Indicates if the object is new.
38
+ kwargs (dict[str, Any]): The keyword arguments containing device details.
39
+ """
40
+ super().post_init(is_new_object=is_new_object, kwargs=kwargs)
41
+ self.device_type = DeviceType.TOTP
42
+
43
+ def verify_code(self, code: str) -> bool:
44
+ """
45
+ Verify a TOTP code against this device's secret.
46
+
47
+ Args:
48
+ code (str): The TOTP code to verify.
49
+
50
+ Returns:
51
+ bool: True if the code is valid, False otherwise.
52
+ """
53
+ from amsdal.contrib.auth.utils.mfa import verify_totp_code
54
+
55
+ return verify_totp_code(self.secret, code, self.digits, self.step)
56
+
57
+ def __str__(self) -> str:
58
+ return f'TOTPDevice(name={self.name}, user={self.user_email})'
@@ -47,6 +47,56 @@ class User(Model):
47
47
  """
48
48
  return self.email
49
49
 
50
+ @property
51
+ def requires_mfa(self) -> bool:
52
+ """
53
+ Determines if MFA is required for this user.
54
+
55
+ This checks both the per-user override (mfa_required) and the global
56
+ REQUIRE_MFA_BY_DEFAULT setting.
57
+
58
+ Returns:
59
+ bool: True if MFA is required, False otherwise.
60
+ """
61
+ from amsdal.contrib.auth.settings import auth_settings
62
+
63
+ # Fall back to global setting
64
+ return auth_settings.REQUIRE_MFA_BY_DEFAULT
65
+
66
+ async def ahas_valid_mfa_device(self) -> bool:
67
+ """
68
+ Check if the user has at least one confirmed and active MFA device.
69
+
70
+ Returns:
71
+ bool: True if the user has a valid MFA device, False otherwise.
72
+ """
73
+ from amsdal.contrib.auth.utils.mfa import aget_active_user_devices
74
+
75
+ devices = await aget_active_user_devices(self)
76
+ for device_list in devices.values():
77
+ if device_list:
78
+ return True
79
+ return False
80
+
81
+ def has_valid_mfa_device(self) -> bool:
82
+ """
83
+ Check if the user has at least one confirmed and active MFA device (sync version).
84
+
85
+ Returns:
86
+ bool: True if the user has a valid MFA device, False otherwise.
87
+ """
88
+ from amsdal.contrib.auth.utils.mfa import get_active_user_devices
89
+
90
+ devices = get_active_user_devices(self)
91
+ for device_list in devices.values():
92
+ if device_list:
93
+ return True
94
+ return False
95
+
96
+ def pre_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None: # noqa: ARG002
97
+ if 'email' in kwargs and isinstance(kwargs['email'], str):
98
+ kwargs['email'] = kwargs['email'].lower()
99
+
50
100
  def post_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:
51
101
  """
52
102
  Post-initializes a user object by validating email and password, and hashing the password.
@@ -0,0 +1 @@
1
+ """MFA device management services."""