amsdal 0.4.10__cp311-cp311-macosx_10_9_universal2.whl → 0.5.29__cp311-cp311-macosx_10_9_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.
- amsdal/Third-Party Materials - AMSDAL Dependencies - License Notices.md +28 -0
- amsdal/__about__.py +1 -1
- amsdal/__migrations__/0000_initial.py +22 -203
- amsdal/__migrations__/0001_create_class_file.py +61 -0
- amsdal/__migrations__/0002_create_class_file.py +109 -0
- amsdal/__migrations__/0003_update_class_file.py +91 -0
- amsdal/__migrations__/0004_update_class_file.py +45 -0
- amsdal/cloud/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/client.cpython-311-darwin.so +0 -0
- amsdal/cloud/constants.cpython-311-darwin.so +0 -0
- amsdal/cloud/enums.cpython-311-darwin.so +0 -0
- amsdal/cloud/models/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/models/base.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_allowlist_ip.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_basic_auth.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_dependency.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_secret.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/base.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/create_deploy.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/create_env.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/create_session.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_allowlist_ip.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_basic_auth.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_dependency.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_env.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_secret.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/destroy_deploy.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/expose_db.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/get_basic_auth_credentials.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/get_monitoring_info.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_dependencies.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_deploys.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_envs.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_secrets.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/manager.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/signup_action.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/update_deploy.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/base.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/credentials.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/manager.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/signup_service.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/token.cpython-311-darwin.so +0 -0
- amsdal/configs/main.py +17 -1
- amsdal/configs/main.pyi +7 -3
- amsdal/contrib/__init__.cpython-311-darwin.so +0 -0
- amsdal/contrib/auth/errors.py +36 -0
- amsdal/contrib/auth/errors.pyi +12 -0
- amsdal/contrib/auth/lifecycle/consumer.py +3 -3
- amsdal/contrib/auth/lifecycle/consumer.pyi +3 -0
- amsdal/contrib/auth/migrations/0000_initial.py +55 -52
- amsdal/contrib/auth/migrations/0001_add_mfa_support.py +188 -0
- amsdal/contrib/auth/models/__init__.py +1 -0
- amsdal/contrib/auth/models/backup_code.py +85 -0
- amsdal/contrib/auth/models/email_mfa_device.py +108 -0
- amsdal/contrib/auth/models/login_session.py +117 -0
- amsdal/contrib/auth/models/mfa_device.py +86 -0
- amsdal/contrib/auth/models/sms_device.py +113 -0
- amsdal/contrib/auth/models/totp_device.py +58 -0
- amsdal/contrib/auth/models/user.py +50 -0
- amsdal/contrib/auth/services/__init__.py +1 -0
- amsdal/contrib/auth/services/mfa_device_service.py +544 -0
- amsdal/contrib/auth/services/mfa_device_service.pyi +216 -0
- amsdal/contrib/auth/services/totp_service.py +358 -0
- amsdal/contrib/auth/services/totp_service.pyi +158 -0
- amsdal/contrib/auth/settings.py +8 -0
- amsdal/contrib/auth/settings.pyi +8 -0
- amsdal/contrib/auth/transactions/__init__.py +1 -0
- amsdal/contrib/auth/transactions/mfa_device_transactions.py +463 -0
- amsdal/contrib/auth/transactions/mfa_device_transactions.pyi +226 -0
- amsdal/contrib/auth/transactions/totp_transactions.py +206 -0
- amsdal/contrib/auth/transactions/totp_transactions.pyi +113 -0
- amsdal/contrib/auth/utils/mfa.py +257 -0
- amsdal/contrib/auth/utils/mfa.pyi +119 -0
- amsdal/contrib/frontend_configs/conversion/convert.py +32 -5
- amsdal/contrib/frontend_configs/migrations/0000_initial.py +154 -183
- amsdal/contrib/frontend_configs/migrations/0001_update_frontend_control_config.py +245 -0
- amsdal/contrib/frontend_configs/migrations/0002_add_button_and_invoke_actions.py +352 -0
- amsdal/contrib/frontend_configs/migrations/0003_create_class_frontendconfigdashboardelement.py +145 -0
- amsdal/contrib/frontend_configs/models/frontend_config_control_action.py +57 -1
- amsdal/contrib/frontend_configs/models/frontend_config_dashboard.py +51 -0
- amsdal/contrib/frontend_configs/models/frontend_control_config.py +69 -46
- amsdal/fixtures/__init__.cpython-311-darwin.so +0 -0
- amsdal/fixtures/manager.cpython-311-darwin.so +0 -0
- amsdal/fixtures/utils.cpython-311-darwin.so +0 -0
- amsdal/manager.cpython-311-darwin.so +0 -0
- amsdal/manager.pyi +5 -0
- amsdal/mixins/__init__.cpython-311-darwin.so +0 -0
- amsdal/mixins/class_versions_mixin.cpython-311-darwin.so +0 -0
- amsdal/models/core/class_object.py +7 -6
- amsdal/models/core/class_property.py +7 -1
- amsdal/models/core/file.py +168 -81
- amsdal/models/core/storage_metadata.py +15 -0
- amsdal/models/mixins.py +31 -0
- amsdal/models/types/object.py +3 -3
- amsdal/schemas/core/class_object/model.json +20 -0
- amsdal/schemas/core/class_property/model.json +19 -0
- amsdal/schemas/core/file/properties/validate_data.py +2 -3
- amsdal/schemas/core/storage_metadata/model.json +52 -0
- amsdal/schemas/interfaces.pyi +1 -1
- amsdal/schemas/manager.cpython-311-darwin.so +0 -0
- amsdal/schemas/mixins/check_dependencies_mixin.py +23 -8
- amsdal/schemas/mixins/check_dependencies_mixin.pyi +5 -2
- amsdal/schemas/utils.pyi +2 -2
- amsdal/services/__init__.py +11 -0
- amsdal/services/__init__.pyi +4 -0
- amsdal/services/external_connections.py +262 -0
- amsdal/services/external_connections.pyi +190 -0
- amsdal/services/external_model_generator.py +350 -0
- amsdal/services/external_model_generator.pyi +134 -0
- amsdal/services/transaction_execution.cpython-311-darwin.so +0 -0
- amsdal/services/transaction_execution.pyi +1 -0
- amsdal/storages/__init__.py +20 -0
- amsdal/storages/__init__.pyi +8 -0
- amsdal/storages/file_system.py +214 -0
- amsdal/storages/file_system.pyi +36 -0
- amsdal/utils/rollback/__init__.pyi +6 -0
- amsdal/utils/tests/enums.py +0 -2
- amsdal/utils/tests/helpers.py +213 -381
- amsdal/utils/tests/migrations.py +157 -0
- {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/METADATA +13 -8
- {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/RECORD +131 -124
- {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/WHEEL +1 -1
- amsdal/__migrations__/0001_datetime_type.py +0 -18
- amsdal/__migrations__/0002_fixture_order.py +0 -44
- amsdal/__migrations__/0003_schema_type_in_class_meta.py +0 -44
- amsdal/contrib/auth/models/login_session.pyi +0 -37
- amsdal/contrib/auth/models/permission.pyi +0 -18
- amsdal/contrib/auth/models/user.pyi +0 -46
- amsdal/contrib/frontend_configs/models/frontend_activator_config.pyi +0 -12
- amsdal/contrib/frontend_configs/models/frontend_config_async_validator.pyi +0 -7
- amsdal/contrib/frontend_configs/models/frontend_config_control_action.pyi +0 -32
- amsdal/contrib/frontend_configs/models/frontend_config_group_validator.pyi +0 -11
- amsdal/contrib/frontend_configs/models/frontend_config_option.pyi +0 -8
- amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base.pyi +0 -8
- amsdal/contrib/frontend_configs/models/frontend_config_slider_option.pyi +0 -9
- amsdal/contrib/frontend_configs/models/frontend_config_text_mask.pyi +0 -10
- amsdal/contrib/frontend_configs/models/frontend_config_validator.pyi +0 -15
- amsdal/contrib/frontend_configs/models/frontend_control_config.pyi +0 -35
- amsdal/contrib/frontend_configs/models/frontend_model_config.pyi +0 -9
- amsdal/models/__init__.pyi +0 -9
- amsdal/models/core/class_object.pyi +0 -24
- amsdal/models/core/class_object_meta.py +0 -26
- amsdal/models/core/class_object_meta.pyi +0 -15
- amsdal/models/core/class_property.pyi +0 -11
- amsdal/models/core/class_property_meta.py +0 -15
- amsdal/models/core/class_property_meta.pyi +0 -10
- amsdal/models/core/file.pyi +0 -104
- amsdal/models/core/fixture.pyi +0 -14
- amsdal/models/core/option.pyi +0 -8
- amsdal/models/core/validator.pyi +0 -8
- amsdal/models/types/object.pyi +0 -16
- amsdal/schemas/core/class_object_meta/model.json +0 -59
- amsdal/schemas/core/class_property_meta/model.json +0 -23
- amsdal/services/__init__.cpython-311-darwin.so +0 -0
- /amsdal/contrib/auth/{models → services}/__init__.pyi +0 -0
- /amsdal/contrib/{frontend_configs/models → auth/transactions}/__init__.pyi +0 -0
- /amsdal/{models/core/__init__.pyi → contrib/auth/utils/__init__.py} +0 -0
- /amsdal/{models/types → contrib/auth/utils}/__init__.pyi +0 -0
- {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/licenses/LICENSE.txt +0 -0
- {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from amsdal_models.migration import migrations
|
|
2
|
+
from amsdal_utils.models.enums import ModuleType
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Migration(migrations.Migration):
|
|
6
|
+
operations: list[migrations.Operation] = [
|
|
7
|
+
migrations.UpdateClass(
|
|
8
|
+
module_type=ModuleType.CORE,
|
|
9
|
+
class_name="File",
|
|
10
|
+
old_schema={
|
|
11
|
+
"title": "File",
|
|
12
|
+
"required": ["filename"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"filename": {"type": "string", "title": "Filename"},
|
|
15
|
+
"data": {"type": "binary", "title": "Data"},
|
|
16
|
+
"size": {"type": "number", "title": "Size"},
|
|
17
|
+
"storage_address": {"type": "anything", "title": "Storage Reference"},
|
|
18
|
+
},
|
|
19
|
+
"custom_code": 'import base64\nimport io\nfrom contextlib import suppress\nfrom pathlib import Path\nfrom typing import IO\nfrom typing import Any\nfrom typing import BinaryIO\n\nfrom amsdal_models.storage.backends.db import DBStorage\nfrom amsdal_models.storage.base import Storage\nfrom pydantic import model_validator\n\n\n@classmethod\ndef data_base64_decode(cls, data: Any) -> bytes:\n if isinstance(data, str):\n data = data.encode(\'utf-8\')\n is_base64: bool = False\n with suppress(Exception):\n is_base64 = base64.b64encode(base64.b64decode(data)) == data\n if is_base64:\n return base64.b64decode(data)\n return data\n\n@classmethod\ndef from_bytes(cls, filename: str, data: bytes) -> \'File\':\n """\n Creates a `File` object from a byte string.\n\n Args:\n filename (str): The filename of the file.\n data (bytes): The byte string containing the file data.:\n\n Returns:\n File: The created `File` object.\n """\n obj = cls(filename=filename, data=data, size=len(data))\n obj._needs_persist = True\n return obj\n\n@classmethod\ndef from_file(cls, file_or_path: Path | BinaryIO) -> \'File\':\n """\n Creates a `File` object from a file path or a binary file object.\n\n Args:\n file_or_path (Path | BinaryIO): The file path or binary file object.\n\n Returns:\n File: The created `File` object.\n\n Raises:\n ValueError: If the provided path is a directory.\n """\n f: BinaryIO | io.BufferedReader\n if isinstance(file_or_path, Path):\n if file_or_path.is_dir():\n msg = f\'{file_or_path} is a directory\'\n raise ValueError(msg)\n f = file_or_path.open(\'rb\')\n filename = file_or_path.name\n size = file_or_path.stat().st_size\n else:\n f = file_or_path\n filename = Path(getattr(f, \'name\', \'unnamed\')).name\n try:\n if f.seekable():\n f.seek(0, io.SEEK_END)\n size = f.tell()\n f.seek(0)\n else:\n size = None\n except (OSError, AttributeError):\n size = None\n obj = cls(filename=filename, size=size)\n obj._source = f\n obj._needs_persist = True\n return obj\n\n@model_validator(mode=\'before\')\n@classmethod\ndef validate_model_data(cls, data: Any) -> Any:\n if isinstance(data, dict):\n if \'data\' in data:\n if data[\'data\']:\n data[\'data\'] = cls.data_base64_decode(data[\'data\'])\n data[\'size\'] = len(data[\'data\'])\n else:\n data[\'size\'] = 0\n return data\n\n@property\ndef mimetype(self) -> str | None:\n """\n Returns the MIME type of the file based on its filename.\n\n This method uses the `mimetypes` module to guess the MIME type of the file.\n\n Returns:\n str | None: The guessed MIME type of the file, or None if it cannot be determined.\n """\n import mimetypes\n return mimetypes.guess_type(self.filename)[0]\n\n@property\ndef storage(self) -> Storage:\n from amsdal.storages import default_storage\n if self._storage:\n return self._storage\n if self.storage_address:\n return Storage.from_storage_spec({\'storage_class\': self.storage_address.ref.resource})\n return default_storage()\n\nasync def aopen(self, mode: str=\'rb\') -> Any:\n """\n Async variant of open().\n\n Uses the resolved storage to call aopen(); if the backend does not implement\n async, falls back to the sync open().\n """\n try:\n return await self.storage.aopen(self, mode)\n except NotImplementedError:\n return self.storage.open(self, mode)\n\nasync def apre_create(self) -> None:\n if self._needs_persist:\n from amsdal_models.storage.persistence import apersist_file\n await apersist_file(self, storage=self.storage)\n\nasync def apre_update(self) -> None:\n if self._needs_persist:\n from amsdal_models.storage.persistence import apersist_file\n await apersist_file(self, storage=self.storage)\n\nasync def aread_bytes(self) -> bytes:\n async with await self.aopen() as f:\n return await f.read()\n\nasync def aurl(self) -> str:\n """\n Async variant of url().\n\n Uses the resolved storage to call aurl(); if the backend does not implement\n async, falls back to the sync url().\n """\n try:\n return await self.storage.aurl(self)\n except NotImplementedError:\n return self.storage.url(self)\n\ndef __repr__(self) -> str:\n return f"File<{self.filename}>({self.size or len(self.data or \'\') or 0} bytes)"\n\ndef __str__(self) -> str:\n return repr(self)\n\ndef open(self, mode: str=\'rb\') -> IO[Any]:\n """\n Open a binary stream for reading (or other modes if supported) using storage_address.\n\n Raises StateError if storage_address is missing.\n """\n return self.storage.open(self, mode)\n\ndef pre_create(self) -> None:\n if self._needs_persist:\n from amsdal_models.storage.persistence import persist_file\n persist_file(self, storage=self.storage)\n\ndef pre_update(self) -> None:\n if self._needs_persist:\n from amsdal_models.storage.persistence import persist_file\n persist_file(self, storage=self.storage)\n\ndef read_bytes(self) -> bytes:\n with self.open() as f:\n return f.read()\n\ndef set_data(self, data: bytes | str) -> None:\n if not isinstance(self.storage, DBStorage):\n msg = \'Cannot set data on a file that is not stored in a database. Use `File.from_bytes` instead.\'\n raise ValueError(msg)\n self.data = self.data_base64_decode(data)\n self.size = len(self.data)\n self._needs_persist = True\n\ndef to_file(self, file_or_path: Path | BinaryIO) -> None:\n """\n Writes the object\'s data to a file path or a binary file object.\n\n Args:\n file_or_path (Path | BinaryIO): The file path or binary file object where the data will be written.\n\n Returns:\n None\n\n Raises:\n ValueError: If the provided path is a directory.\n """\n with self.open() as f:\n if isinstance(file_or_path, Path):\n if file_or_path.is_dir():\n file_or_path = file_or_path / self.name\n file_or_path.write_bytes(f.read())\n else:\n file_or_path.write(f.read())\n file_or_path.seek(0)\n\ndef url(self) -> str:\n """\n Return a URL for this file using its storage_address.\n\n Raises StateError if storage_address is missing.\n """\n return self.storage.url(self)',
|
|
20
|
+
"storage_metadata": {
|
|
21
|
+
"table_name": "File",
|
|
22
|
+
"db_fields": {},
|
|
23
|
+
"primary_key": ["partition_key"],
|
|
24
|
+
"foreign_keys": {},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
new_schema={
|
|
28
|
+
"title": "File",
|
|
29
|
+
"required": ["filename"],
|
|
30
|
+
"properties": {
|
|
31
|
+
"filename": {"type": "string", "title": "Filename"},
|
|
32
|
+
"data": {"type": "binary", "title": "Data"},
|
|
33
|
+
"size": {"type": "number", "title": "Size"},
|
|
34
|
+
"storage_address": {"type": "anything", "title": "Storage Reference"},
|
|
35
|
+
},
|
|
36
|
+
"custom_code": 'import base64\nimport io\nfrom contextlib import suppress\nfrom pathlib import Path\nfrom typing import IO\nfrom typing import Any\nfrom typing import BinaryIO\n\nfrom amsdal_models.storage.backends.db import AsyncFileWrapper\nfrom amsdal_models.storage.backends.db import DBStorage\nfrom amsdal_models.storage.base import Storage\nfrom pydantic import model_validator\n\n\n@classmethod\ndef data_base64_decode(cls, data: Any) -> bytes:\n if isinstance(data, str):\n data = data.encode(\'utf-8\')\n is_base64: bool = False\n with suppress(Exception):\n is_base64 = base64.b64encode(base64.b64decode(data)) == data\n if is_base64:\n return base64.b64decode(data)\n return data\n\n@classmethod\ndef from_bytes(cls, filename: str, data: bytes) -> \'File\':\n """\n Creates a `File` object from a byte string.\n\n Args:\n filename (str): The filename of the file.\n data (bytes): The byte string containing the file data.:\n\n Returns:\n File: The created `File` object.\n """\n obj = cls(filename=filename, data=data, size=len(data))\n return obj\n\n@classmethod\ndef from_file(cls, file_or_path: Path | BinaryIO) -> \'File\':\n """\n Creates a `File` object from a file path or a binary file object.\n\n Args:\n file_or_path (Path | BinaryIO): The file path or binary file object.\n\n Returns:\n File: The created `File` object.\n\n Raises:\n ValueError: If the provided path is a directory.\n """\n f: BinaryIO | io.BufferedReader\n if isinstance(file_or_path, Path):\n if file_or_path.is_dir():\n msg = f\'{file_or_path} is a directory\'\n raise ValueError(msg)\n f = file_or_path.open(\'rb\')\n filename = file_or_path.name\n size = file_or_path.stat().st_size\n else:\n f = file_or_path\n filename = Path(getattr(f, \'name\', \'unnamed\')).name\n try:\n if f.seekable():\n f.seek(0, io.SEEK_END)\n size = f.tell()\n f.seek(0)\n else:\n size = None\n except (OSError, AttributeError):\n size = None\n obj = cls(filename=filename, size=size)\n obj._source = f\n return obj\n\n@model_validator(mode=\'before\')\n@classmethod\ndef validate_model_data(cls, data: Any) -> Any:\n if isinstance(data, dict):\n if \'data\' in data:\n if data[\'data\']:\n data[\'data\'] = cls.data_base64_decode(data[\'data\'])\n data[\'size\'] = len(data[\'data\'])\n else:\n data[\'size\'] = 0\n return data\n\n@property\ndef mimetype(self) -> str | None:\n """\n Returns the MIME type of the file based on its filename.\n\n This method uses the `mimetypes` module to guess the MIME type of the file.\n\n Returns:\n str | None: The guessed MIME type of the file, or None if it cannot be determined.\n """\n import mimetypes\n return mimetypes.guess_type(self.filename)[0]\n\n@property\ndef storage(self) -> Storage:\n from amsdal.storages import default_storage\n if self._storage:\n return self._storage\n if self.storage_address:\n return Storage.from_storage_spec({\'storage_class\': self.storage_address.ref.resource})\n return default_storage()\n\nasync def aopen(self, mode: str=\'rb\') -> Any:\n """\n Async variant of open().\n\n Uses the resolved storage to call aopen(); if the backend does not implement\n async, falls back to the sync open().\n """\n try:\n return await self.storage.aopen(self, mode)\n except NotImplementedError:\n return AsyncFileWrapper(self.storage.open(self, mode))\n\nasync def apre_create(self) -> None:\n from amsdal_models.storage.persistence import apersist_file\n await apersist_file(self, storage=self.storage)\n\nasync def apre_update(self) -> None:\n from amsdal_models.storage.persistence import apersist_file\n await apersist_file(self, storage=self.storage)\n\nasync def aread_bytes(self) -> bytes:\n async with await self.aopen() as f:\n return await f.read()\n\nasync def aurl(self) -> str:\n """\n Async variant of url().\n\n Uses the resolved storage to call aurl(); if the backend does not implement\n async, falls back to the sync url().\n """\n try:\n return await self.storage.aurl(self)\n except NotImplementedError:\n return self.storage.url(self)\n\ndef __repr__(self) -> str:\n return f"File<{self.filename}>({self.size or len(self.data or \'\') or 0} bytes)"\n\ndef __str__(self) -> str:\n return repr(self)\n\ndef open(self, mode: str=\'rb\') -> IO[Any]:\n """\n Open a binary stream for reading (or other modes if supported) using storage_address.\n\n Raises StateError if storage_address is missing.\n """\n return self.storage.open(self, mode)\n\ndef pre_create(self) -> None:\n from amsdal_models.storage.persistence import persist_file\n persist_file(self, storage=self.storage)\n\ndef pre_update(self) -> None:\n from amsdal_models.storage.persistence import persist_file\n persist_file(self, storage=self.storage)\n\ndef read_bytes(self) -> bytes:\n with self.open() as f:\n return f.read()\n\ndef set_data(self, data: bytes | str) -> None:\n if not isinstance(self.storage, DBStorage):\n msg = \'Cannot set data on a file that is not stored in a database. Use `File.from_bytes` instead.\'\n raise ValueError(msg)\n self.data = self.data_base64_decode(data)\n self.size = len(self.data)\n\ndef to_file(self, file_or_path: Path | BinaryIO) -> None:\n """\n Writes the object\'s data to a file path or a binary file object.\n\n Args:\n file_or_path (Path | BinaryIO): The file path or binary file object where the data will be written.\n\n Returns:\n None\n\n Raises:\n ValueError: If the provided path is a directory.\n """\n with self.open() as f:\n if isinstance(file_or_path, Path):\n if file_or_path.is_dir():\n file_or_path = file_or_path / self.name\n file_or_path.write_bytes(f.read())\n else:\n file_or_path.write(f.read())\n file_or_path.seek(0)\n\ndef url(self) -> str:\n """\n Return a URL for this file using its storage_address.\n\n Raises StateError if storage_address is missing.\n """\n return self.storage.url(self)',
|
|
37
|
+
"storage_metadata": {
|
|
38
|
+
"table_name": "File",
|
|
39
|
+
"db_fields": {},
|
|
40
|
+
"primary_key": ["partition_key"],
|
|
41
|
+
"foreign_keys": {},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
]
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
amsdal/configs/main.py
CHANGED
|
@@ -64,13 +64,18 @@ class Settings(BaseSettings):
|
|
|
64
64
|
SECRET_ACCESS_KEY: str | None = None
|
|
65
65
|
ACCESS_TOKEN: str | None = None
|
|
66
66
|
SANDBOX_ENVIRONMENT: bool | None = None
|
|
67
|
-
CONTRIBS: list[str] = [
|
|
67
|
+
CONTRIBS: list[str] | str = [
|
|
68
68
|
'amsdal.contrib.auth.app.AuthAppConfig',
|
|
69
69
|
'amsdal.contrib.frontend_configs.app.FrontendConfigAppConfig',
|
|
70
70
|
]
|
|
71
71
|
CONTRIB_MODELS_PACKAGE_NAME: str = 'models'
|
|
72
72
|
CONTRIB_MIGRATIONS_DIRECTORY_NAME: str = 'migrations'
|
|
73
73
|
|
|
74
|
+
# File Storage
|
|
75
|
+
MEDIA_ROOT: Path = Path('./media')
|
|
76
|
+
MEDIA_URL: str = '/media/'
|
|
77
|
+
DEFAULT_FILE_STORAGE: str = 'amsdal_models.storage.backends.db.DBStorage'
|
|
78
|
+
|
|
74
79
|
@field_validator('CONTRIBS', mode='after')
|
|
75
80
|
def load_contrib_modules(cls, value: list[str]) -> list[str]: # noqa: N805
|
|
76
81
|
"""
|
|
@@ -187,6 +192,17 @@ class Settings(BaseSettings):
|
|
|
187
192
|
|
|
188
193
|
return self
|
|
189
194
|
|
|
195
|
+
@model_validator(mode='before')
|
|
196
|
+
@classmethod
|
|
197
|
+
def normalize_complex_values(cls, data: Any) -> Any:
|
|
198
|
+
if isinstance(data, dict):
|
|
199
|
+
if 'CONTRIBS' in data:
|
|
200
|
+
contribs = data['CONTRIBS']
|
|
201
|
+
|
|
202
|
+
if isinstance(contribs, str) and '[' not in contribs:
|
|
203
|
+
data['CONTRIBS'] = [item.strip() for item in contribs.split(',')]
|
|
204
|
+
return data
|
|
205
|
+
|
|
190
206
|
|
|
191
207
|
if TYPE_CHECKING:
|
|
192
208
|
base: TypeAlias = Settings
|
amsdal/configs/main.pyi
CHANGED
|
@@ -49,9 +49,12 @@ class Settings(BaseSettings):
|
|
|
49
49
|
SECRET_ACCESS_KEY: str | None
|
|
50
50
|
ACCESS_TOKEN: str | None
|
|
51
51
|
SANDBOX_ENVIRONMENT: bool | None
|
|
52
|
-
CONTRIBS: list[str]
|
|
52
|
+
CONTRIBS: list[str] | str
|
|
53
53
|
CONTRIB_MODELS_PACKAGE_NAME: str
|
|
54
54
|
CONTRIB_MIGRATIONS_DIRECTORY_NAME: str
|
|
55
|
+
MEDIA_ROOT: Path
|
|
56
|
+
MEDIA_URL: str
|
|
57
|
+
DEFAULT_FILE_STORAGE: str
|
|
55
58
|
def load_contrib_modules(cls, value: list[str]) -> list[str]:
|
|
56
59
|
"""
|
|
57
60
|
Loads and initializes contrib modules.
|
|
@@ -130,8 +133,9 @@ class Settings(BaseSettings):
|
|
|
130
133
|
Returns:
|
|
131
134
|
Settings: The updated settings instance with the `CONFIG_PATH` attribute set.
|
|
132
135
|
"""
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
@classmethod
|
|
137
|
+
def normalize_complex_values(cls, data: Any) -> Any: ...
|
|
138
|
+
base: TypeAlias = Settings
|
|
135
139
|
|
|
136
140
|
class SettingsProxy(base):
|
|
137
141
|
"""
|
|
Binary file
|
amsdal/contrib/auth/errors.py
CHANGED
|
@@ -5,3 +5,39 @@ class UserCreationError(AmsdalError): ...
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class AuthenticationError(AmsdalError): ...
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MFARequiredError(AuthenticationError):
|
|
11
|
+
"""Raised when MFA verification is required but not provided."""
|
|
12
|
+
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InvalidMFACodeError(AuthenticationError):
|
|
17
|
+
"""Raised when the provided MFA code is invalid."""
|
|
18
|
+
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MFADeviceNotFoundError(AmsdalError):
|
|
23
|
+
"""Raised when no valid MFA device is found for the user."""
|
|
24
|
+
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MFASetupError(AmsdalError):
|
|
29
|
+
"""Raised when there's an error during MFA device setup."""
|
|
30
|
+
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PermissionDeniedError(AmsdalError):
|
|
35
|
+
"""Raised when a user lacks permission for an operation."""
|
|
36
|
+
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class UserNotFoundError(AmsdalError):
|
|
41
|
+
"""Raised when a target user cannot be found."""
|
|
42
|
+
|
|
43
|
+
...
|
amsdal/contrib/auth/errors.pyi
CHANGED
|
@@ -2,3 +2,15 @@ from amsdal_utils.errors import AmsdalError
|
|
|
2
2
|
|
|
3
3
|
class UserCreationError(AmsdalError): ...
|
|
4
4
|
class AuthenticationError(AmsdalError): ...
|
|
5
|
+
class MFARequiredError(AuthenticationError):
|
|
6
|
+
"""Raised when MFA verification is required but not provided."""
|
|
7
|
+
class InvalidMFACodeError(AuthenticationError):
|
|
8
|
+
"""Raised when the provided MFA code is invalid."""
|
|
9
|
+
class MFADeviceNotFoundError(AmsdalError):
|
|
10
|
+
"""Raised when no valid MFA device is found for the user."""
|
|
11
|
+
class MFASetupError(AmsdalError):
|
|
12
|
+
"""Raised when there's an error during MFA device setup."""
|
|
13
|
+
class PermissionDeniedError(AmsdalError):
|
|
14
|
+
"""Raised when a user lacks permission for an operation."""
|
|
15
|
+
class UserNotFoundError(AmsdalError):
|
|
16
|
+
"""Raised when a target user cannot be found."""
|
|
@@ -63,7 +63,7 @@ class CheckAndCreateSuperUserConsumer(LifecycleConsumer):
|
|
|
63
63
|
.execute()
|
|
64
64
|
)
|
|
65
65
|
|
|
66
|
-
instance = User(
|
|
66
|
+
instance = User( # type: ignore[call-arg]
|
|
67
67
|
email=auth_settings.ADMIN_USER_EMAIL,
|
|
68
68
|
password=auth_settings.ADMIN_USER_PASSWORD.encode(),
|
|
69
69
|
permissions=[access_all_permission],
|
|
@@ -111,12 +111,12 @@ class CheckAndCreateSuperUserConsumer(LifecycleConsumer):
|
|
|
111
111
|
.aexecute()
|
|
112
112
|
)
|
|
113
113
|
|
|
114
|
-
instance = User(
|
|
114
|
+
instance = User( # type: ignore[call-arg]
|
|
115
115
|
email=auth_settings.ADMIN_USER_EMAIL,
|
|
116
116
|
password=auth_settings.ADMIN_USER_PASSWORD.encode(),
|
|
117
117
|
permissions=[access_all_permission],
|
|
118
118
|
)
|
|
119
|
-
await instance.asave(force_insert=True)
|
|
119
|
+
await instance.asave(force_insert=True) # type: ignore[misc]
|
|
120
120
|
logger.info('Super user created successfully')
|
|
121
121
|
|
|
122
122
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from _typeshed import Incomplete
|
|
2
2
|
from amsdal.contrib.auth.errors import AuthenticationError as AuthenticationError
|
|
3
|
+
from amsdal_data.transactions.decorators import async_transaction, transaction
|
|
3
4
|
from amsdal_models.classes.model import Model
|
|
4
5
|
from amsdal_utils.lifecycle.consumer import LifecycleConsumer
|
|
5
6
|
from typing import Any
|
|
@@ -13,6 +14,7 @@ class CheckAndCreateSuperUserConsumer(LifecycleConsumer):
|
|
|
13
14
|
This consumer checks if a super user exists based on the provided email and password
|
|
14
15
|
in the authentication settings. If the super user does not exist, it creates one.
|
|
15
16
|
"""
|
|
17
|
+
@transaction
|
|
16
18
|
def on_event(self) -> None:
|
|
17
19
|
"""
|
|
18
20
|
Checks for the existence of a super user and creates one if necessary.
|
|
@@ -21,6 +23,7 @@ class CheckAndCreateSuperUserConsumer(LifecycleConsumer):
|
|
|
21
23
|
in the authentication settings. If the super user does not exist, it creates one
|
|
22
24
|
with the necessary permissions.
|
|
23
25
|
"""
|
|
26
|
+
@async_transaction
|
|
24
27
|
async def on_event_async(self) -> None:
|
|
25
28
|
"""
|
|
26
29
|
Checks for the existence of a super user and creates one if necessary.
|
|
@@ -6,79 +6,82 @@ class Migration(migrations.Migration):
|
|
|
6
6
|
operations: list[migrations.Operation] = [
|
|
7
7
|
migrations.CreateClass(
|
|
8
8
|
module_type=ModuleType.CONTRIB,
|
|
9
|
-
class_name=
|
|
9
|
+
class_name="LoginSession",
|
|
10
10
|
new_schema={
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
"title": "LoginSession",
|
|
12
|
+
"required": ["email", "password"],
|
|
13
|
+
"properties": {
|
|
14
|
+
"email": {"type": "string", "title": "Email"},
|
|
15
|
+
"password": {"type": "string", "title": "Password (hash)"},
|
|
16
|
+
"token": {"type": "string", "title": "Token"},
|
|
17
|
+
},
|
|
18
|
+
"custom_code": "from datetime import UTC\nfrom datetime import datetime\nfrom datetime import timedelta\nfrom typing import Any\n\nimport jwt\n\n\n@property\ndef display_name(self) -> str:\n \"\"\"\n Returns the display name of the user.\n\n This method returns the email of the user as their display name.\n\n Returns:\n str: The email of the user.\n \"\"\"\n return self.email\n\nasync def apre_create(self) -> None:\n import bcrypt\n\n from amsdal.contrib.auth.errors import AuthenticationError\n from amsdal.contrib.auth.models.user import User\n user = await User.objects.filter(email=self.email).latest().first().aexecute()\n if not user:\n msg = 'User not found'\n raise AuthenticationError(msg)\n if not bcrypt.checkpw(self.password.encode(), user.password):\n msg = 'Invalid password'\n raise AuthenticationError(msg)\n self.password = 'validated'\n\nasync def apre_update(self) -> None:\n from amsdal.contrib.auth.errors import AuthenticationError\n msg = 'Update not allowed'\n raise AuthenticationError(msg)\n\ndef pre_create(self) -> None:\n import bcrypt\n\n from amsdal.contrib.auth.errors import AuthenticationError\n from amsdal.contrib.auth.models.user import User\n user = User.objects.filter(email=self.email).latest().first().execute()\n if not user:\n msg = 'User not found'\n raise AuthenticationError(msg)\n if not bcrypt.checkpw(self.password.encode(), user.password):\n msg = 'Invalid password'\n raise AuthenticationError(msg)\n self.password = 'validated'\n\ndef pre_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:\n \"\"\"\n Pre-initializes a user object by validating email and password, and generating a JWT token.\n\n This method checks if the object is new and validates the provided email and password.\n If the email and password are valid, it generates a JWT token and adds it to the kwargs.\n\n Args:\n is_new_object (bool): Indicates if the object is new.\n kwargs (dict[str, Any]): The keyword arguments containing user details.\n\n Raises:\n AuthenticationError: If the email or password is invalid.\n \"\"\"\n if not is_new_object or '_metadata' in kwargs:\n return\n from amsdal.contrib.auth.errors import AuthenticationError\n from amsdal.contrib.auth.settings import auth_settings\n email = kwargs.get('email', None)\n password = kwargs.get('password', None)\n if not email:\n msg = \"Email can't be empty\"\n raise AuthenticationError(msg)\n if not password:\n msg = \"Password can't be empty\"\n raise AuthenticationError(msg)\n lowercased_email = email.lower()\n if not auth_settings.AUTH_JWT_KEY:\n msg = 'JWT key is not set'\n raise AuthenticationError(msg)\n expiration_time = datetime.now(tz=UTC) + timedelta(seconds=auth_settings.AUTH_TOKEN_EXPIRATION)\n token = jwt.encode({'email': lowercased_email, 'exp': expiration_time}, key=auth_settings.AUTH_JWT_KEY, algorithm='HS256')\n kwargs['token'] = token\n\ndef pre_update(self) -> None:\n from amsdal.contrib.auth.errors import AuthenticationError\n msg = 'Update not allowed'\n raise AuthenticationError(msg)",
|
|
19
|
+
"storage_metadata": {
|
|
20
|
+
"table_name": "LoginSession",
|
|
21
|
+
"db_fields": {},
|
|
22
|
+
"primary_key": ["partition_key"],
|
|
23
|
+
"foreign_keys": {},
|
|
16
24
|
},
|
|
17
|
-
"primary_key": ["partition_key"],
|
|
18
|
-
"table_name": "Permission",
|
|
19
|
-
'custom_code': "@property # type: ignore[misc]\ndef display_name(self) -> str: # type: ignore[no-untyped-def]\n return f'{self.model}:{self.action}'",
|
|
20
25
|
},
|
|
21
26
|
),
|
|
22
27
|
migrations.CreateClass(
|
|
23
28
|
module_type=ModuleType.CONTRIB,
|
|
24
|
-
class_name=
|
|
29
|
+
class_name="Permission",
|
|
25
30
|
new_schema={
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
"title": "Permission",
|
|
32
|
+
"required": ["model", "action"],
|
|
33
|
+
"properties": {
|
|
34
|
+
"model": {"type": "string", "title": "Model"},
|
|
35
|
+
"action": {"type": "string", "title": "Action"},
|
|
36
|
+
},
|
|
37
|
+
"custom_code": '@property\ndef display_name(self) -> str:\n """\n Returns the display name of the user.\n\n This method returns a formatted string combining the model and action of the user.\n\n Returns:\n str: The formatted display name in the format \'model:action\'.\n """\n return f\'{self.model}:{self.action}\'',
|
|
38
|
+
"storage_metadata": {
|
|
39
|
+
"table_name": "Permission",
|
|
40
|
+
"db_fields": {},
|
|
41
|
+
"primary_key": ["partition_key"],
|
|
42
|
+
"foreign_keys": {},
|
|
32
43
|
},
|
|
33
|
-
"primary_key": ["partition_key"],
|
|
34
|
-
"table_name": "User",
|
|
35
|
-
'custom_code': "from typing import Any\n\n\ndef pre_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None: # type: ignore[no-untyped-def] # noqa: ARG001\n import bcrypt\n\n from amsdal.contrib.auth.errors import UserCreationError\n\n email = kwargs.get('email', None)\n password = kwargs.get('password', None)\n\n if email is None or email == '':\n msg = \"Email can't be empty\"\n raise UserCreationError(msg)\n\n if password is None or password == '':\n msg = \"Password can't be empty\"\n raise UserCreationError(msg)\n\n kwargs['email'] = email.lower()\n\n if is_new_object and '_metadata' not in kwargs:\n hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())\n kwargs['password'] = hashed_password\n kwargs['_object_id'] = email.lower()\n\ndef pre_create(self) -> None: # type: ignore[no-untyped-def] # noqa: ARG001\n pass\n\n@property # type: ignore[misc]\ndef display_name(self) -> str: # type: ignore[no-untyped-def]\n return self.email",
|
|
36
44
|
},
|
|
37
45
|
),
|
|
38
46
|
migrations.CreateClass(
|
|
39
47
|
module_type=ModuleType.CONTRIB,
|
|
40
|
-
class_name=
|
|
48
|
+
class_name="User",
|
|
41
49
|
new_schema={
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'title': 'Permission',
|
|
49
|
-
'db_field': ['permission_partition_key'],
|
|
50
|
-
},
|
|
50
|
+
"title": "User",
|
|
51
|
+
"required": ["email", "password"],
|
|
52
|
+
"properties": {
|
|
53
|
+
"email": {"type": "string", "title": "Email"},
|
|
54
|
+
"password": {"type": "binary", "title": "Password (hash)"},
|
|
55
|
+
"permissions": {"type": "array", "items": {"type": "Permission"}, "title": "Permissions"},
|
|
51
56
|
},
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
],
|
|
59
|
-
"permission": [
|
|
60
|
-
{"permission_partition_key": "string"},
|
|
61
|
-
"Permission",
|
|
62
|
-
["partition_key"],
|
|
63
|
-
],
|
|
57
|
+
"custom_code": "from typing import Any\n\nfrom amsdal.contrib.auth.models.permission import *\n\n\n@property\ndef display_name(self) -> str:\n \"\"\"\n Returns the display name of the user.\n\n This method returns the email of the user as their display name.\n\n Returns:\n str: The email of the user.\n \"\"\"\n return self.email\n\nasync def apre_update(self) -> None:\n import bcrypt\n original_object = await self.arefetch_from_db()\n password = self.password\n if original_object.password and password is not None:\n if isinstance(password, str):\n password = password.encode('utf-8')\n try:\n if not bcrypt.checkpw(password, original_object.password):\n self.password = password\n except ValueError:\n hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())\n self.password = hashed_password\n\ndef __repr__(self) -> str:\n return str(self)\n\ndef __str__(self) -> str:\n return f'User(email={self.email})'\n\ndef post_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:\n \"\"\"\n Post-initializes a user object by validating email and password, and hashing the password.\n\n This method checks if the email and password are provided and valid. If the object is new,\n it hashes the password and sets the object ID to the lowercased email.\n\n Args:\n is_new_object (bool): Indicates if the object is new.\n kwargs (dict[str, Any]): The keyword arguments containing user details.\n\n Raises:\n UserCreationError: If the email or password is invalid.\n \"\"\"\n import bcrypt\n\n from amsdal.contrib.auth.errors import UserCreationError\n email = kwargs.get('email', None)\n password = kwargs.get('password', None)\n if email is None or email == '':\n msg = \"Email can't be empty\"\n raise UserCreationError(msg)\n if password is None or password == '':\n msg = \"Password can't be empty\"\n raise UserCreationError(msg)\n kwargs['email'] = email.lower()\n if is_new_object and '_metadata' not in kwargs:\n if isinstance(password, str):\n password = password.encode('utf-8')\n hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())\n self.password = hashed_password\n self._object_id = email.lower()\n\ndef pre_create(self) -> None:\n \"\"\"\n Pre-creates a user object.\n\n This method is a placeholder for any pre-creation logic that needs to be executed\n before a user object is created.\n \"\"\"\n pass\n\ndef pre_update(self) -> None:\n import bcrypt\n original_object = self.refetch_from_db()\n password = self.password\n if original_object.password and password is not None:\n if isinstance(password, str):\n password = password.encode('utf-8')\n try:\n if not bcrypt.checkpw(password, original_object.password):\n self.password = password\n except ValueError:\n hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())\n self.password = hashed_password",
|
|
58
|
+
"storage_metadata": {
|
|
59
|
+
"table_name": "User",
|
|
60
|
+
"db_fields": {},
|
|
61
|
+
"primary_key": ["partition_key"],
|
|
62
|
+
"foreign_keys": {},
|
|
64
63
|
},
|
|
65
|
-
"table_name": "UserPermission",
|
|
66
64
|
},
|
|
67
65
|
),
|
|
68
66
|
migrations.CreateClass(
|
|
69
67
|
module_type=ModuleType.CONTRIB,
|
|
70
|
-
class_name=
|
|
68
|
+
class_name="UserPermission",
|
|
71
69
|
new_schema={
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
"title": "UserPermission",
|
|
71
|
+
"required": ["user", "permission"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"user": {"type": "User", "title": "User"},
|
|
74
|
+
"permission": {"type": "Permission", "title": "Permission"},
|
|
75
|
+
},
|
|
76
|
+
"storage_metadata": {
|
|
77
|
+
"table_name": "UserPermission",
|
|
78
|
+
"db_fields": {"user": ["user_partition_key"], "permission": ["permission_partition_key"]},
|
|
79
|
+
"primary_key": ["user", "permission"],
|
|
80
|
+
"foreign_keys": {
|
|
81
|
+
"user": [{"user_partition_key": "string"}, "User", ["partition_key"]],
|
|
82
|
+
"permission": [{"permission_partition_key": "string"}, "Permission", ["partition_key"]],
|
|
83
|
+
},
|
|
78
84
|
},
|
|
79
|
-
"primary_key": ["partition_key"],
|
|
80
|
-
"table_name": "LoginSession",
|
|
81
|
-
'custom_code': "from datetime import datetime\nfrom datetime import timedelta\nfrom datetime import timezone\nfrom typing import Any\n\nimport bcrypt\nimport jwt\nfrom amsdal_utils.models.enums import Versions\n\n\ndef pre_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None: # type: ignore[no-untyped-def] # noqa: ARG001\n if not is_new_object or '_metadata' in kwargs:\n return\n\n from amsdal.contrib.auth.errors import AuthenticationError\n from amsdal.contrib.auth.settings import auth_settings\n\n email = kwargs.get('email', None)\n password = kwargs.get('password', None)\n\n if not email:\n msg = \"Email can't be empty\"\n raise AuthenticationError(msg)\n\n if not password:\n msg = \"Password can't be empty\"\n raise AuthenticationError(msg)\n\n lowercased_email = email.lower()\n\n from amsdal.contrib.auth.models.user import User # type: ignore[import-not-found]\n\n user = User.objects.filter(email=lowercased_email, _address__object_version=Versions.LATEST).get_or_none().execute()\n\n if not user:\n msg = 'Invalid email / password'\n raise AuthenticationError(msg)\n\n if not bcrypt.checkpw(password.encode('utf-8') if isinstance(password, str) else password, user.password):\n msg = 'Invalid email / password'\n raise AuthenticationError(msg)\n\n kwargs['password'] = 'validated'\n expiration_time = datetime.now(tz=timezone.utc) + timedelta(seconds=1200)\n token = jwt.encode(\n {'email': lowercased_email, 'exp': expiration_time},\n key=auth_settings.AUTH_JWT_KEY, # type: ignore[arg-type]\n algorithm='HS256',\n )\n\n kwargs['token'] = token\n\n@property # type: ignore[misc]\ndef display_name(self) -> str: # type: ignore[no-untyped-def]\n return self.email",
|
|
82
85
|
},
|
|
83
86
|
),
|
|
84
87
|
]
|