amsdal 0.5.7__cp312-cp312-macosx_10_13_universal2.whl → 0.5.8__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.
- amsdal/__about__.py +1 -1
- amsdal/__migrations__/0003_update_class_file.py +91 -0
- amsdal/cloud/__init__.cpython-312-darwin.so +0 -0
- amsdal/cloud/client.cpython-312-darwin.so +0 -0
- amsdal/cloud/constants.cpython-312-darwin.so +0 -0
- amsdal/cloud/enums.cpython-312-darwin.so +0 -0
- amsdal/cloud/models/__init__.cpython-312-darwin.so +0 -0
- amsdal/cloud/models/base.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/__init__.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/__init__.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/add_allowlist_ip.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/add_basic_auth.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/add_dependency.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/add_secret.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/base.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/create_deploy.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/create_env.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/create_session.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_allowlist_ip.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_basic_auth.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_dependency.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_env.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_secret.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/destroy_deploy.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/expose_db.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/get_basic_auth_credentials.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/get_monitoring_info.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/list_dependencies.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/list_deploys.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/list_envs.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/list_secrets.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/manager.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/signup_action.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/update_deploy.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/__init__.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/base.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/credentials.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/manager.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/signup_service.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/token.cpython-312-darwin.so +0 -0
- amsdal/configs/main.py +5 -0
- amsdal/configs/main.pyi +3 -0
- amsdal/contrib/__init__.cpython-312-darwin.so +0 -0
- amsdal/contrib/auth/lifecycle/consumer.pyi +1 -1
- amsdal/fixtures/__init__.cpython-312-darwin.so +0 -0
- amsdal/fixtures/manager.cpython-312-darwin.so +0 -0
- amsdal/fixtures/manager.pyi +1 -1
- amsdal/fixtures/utils.cpython-312-darwin.so +0 -0
- amsdal/manager.cpython-312-darwin.so +0 -0
- amsdal/mixins/__init__.cpython-312-darwin.so +0 -0
- amsdal/mixins/class_versions_mixin.cpython-312-darwin.so +0 -0
- amsdal/models/core/class_property.py +1 -0
- amsdal/models/core/file.py +180 -81
- amsdal/schemas/core/file/properties/validate_data.py +2 -3
- amsdal/schemas/manager.cpython-312-darwin.so +0 -0
- amsdal/services/__init__.cpython-312-darwin.so +0 -0
- amsdal/services/transaction_execution.cpython-312-darwin.so +0 -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/tests/migrations.py +45 -66
- {amsdal-0.5.7.dist-info → amsdal-0.5.8.dist-info}/METADATA +1 -1
- {amsdal-0.5.7.dist-info → amsdal-0.5.8.dist-info}/RECORD +67 -62
- {amsdal-0.5.7.dist-info → amsdal-0.5.8.dist-info}/WHEEL +0 -0
- {amsdal-0.5.7.dist-info → amsdal-0.5.8.dist-info}/licenses/LICENSE.txt +0 -0
- {amsdal-0.5.7.dist-info → amsdal-0.5.8.dist-info}/top_level.txt +0 -0
amsdal/__about__.py
CHANGED
@@ -0,0 +1,91 @@
|
|
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", "data"],
|
13
|
+
"properties": {
|
14
|
+
"filename": {"type": "string", "title": "Filename"},
|
15
|
+
"data": {"type": "binary", "title": "Data"},
|
16
|
+
"size": {"type": "number", "title": "Size"},
|
17
|
+
},
|
18
|
+
"custom_code": 'import base64\nfrom pathlib import Path\nfrom typing import BinaryIO\n\nfrom pydantic import field_validator\n\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 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 data = file_or_path.read_bytes()\n filename = file_or_path.name\n else:\n file_or_path.seek(0)\n data = file_or_path.read()\n filename = Path(file_or_path.name).name\n return cls(data=data, filename=filename)\n\n@field_validator(\'data\')\n@classmethod\ndef data_base64_decode(cls, v: bytes) -> bytes:\n """\n Decodes a base64-encoded byte string if it is base64-encoded.\n\n This method checks if the provided byte string is base64-encoded and decodes it if true.\n If the byte string is not base64-encoded, it returns the original byte string.\n\n Args:\n cls: The class this method belongs to.\n v (bytes): The byte string to be checked and potentially decoded.\n\n Returns:\n bytes: The decoded byte string if it was base64-encoded, otherwise the original byte string.\n """\n is_base64: bool = False\n try:\n is_base64 = base64.b64encode(base64.b64decode(v)) == v\n except Exception:\n ...\n if is_base64:\n return base64.b64decode(v)\n return v\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\nasync def apre_create(self) -> None:\n """\n Prepares the object for creation by setting its size attribute.\n\n This method calculates the size of the object\'s data and assigns it to the size attribute.\n If the data is None, it defaults to an empty byte string.\n\n Args:\n None\n """\n self.size = len(self.data or b\'\')\n\nasync def apre_update(self) -> None:\n """\n Prepares the object for update by setting its size attribute.\n\n This method calculates the size of the object\'s data and assigns it to the size attribute.\n If the data is None, it defaults to an empty byte string.\n\n Args:\n None\n """\n self.size = len(self.data or b\'\')\n\ndef __repr__(self) -> str:\n return f\'File<{self.filename}>({self.size or len(self.data) or 0} bytes)\'\n\ndef __str__(self) -> str:\n return repr(self)\n\ndef pre_create(self) -> None:\n """\n Prepares the object for creation by setting its size attribute.\n\n This method calculates the size of the object\'s data and assigns it to the size attribute.\n If the data is None, it defaults to an empty byte string.\n\n Args:\n None\n """\n self.size = len(self.data or b\'\')\n\ndef pre_update(self) -> None:\n """\n Prepares the object for update by setting its size attribute.\n\n This method calculates the size of the object\'s data and assigns it to the size attribute.\n If the data is None, it defaults to an empty byte string.\n\n Args:\n None\n """\n self.size = len(self.data or b\'\')\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 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(self.data)\n else:\n file_or_path.write(self.data)\n file_or_path.seek(0)',
|
19
|
+
"storage_metadata": {
|
20
|
+
"table_name": "File",
|
21
|
+
"db_fields": {},
|
22
|
+
"primary_key": ["partition_key"],
|
23
|
+
"foreign_keys": {},
|
24
|
+
},
|
25
|
+
},
|
26
|
+
new_schema={
|
27
|
+
"title": "File",
|
28
|
+
"required": ["filename"],
|
29
|
+
"properties": {
|
30
|
+
"filename": {"type": "string", "title": "Filename"},
|
31
|
+
"data": {"type": "binary", "title": "Data"},
|
32
|
+
"size": {"type": "number", "title": "Size"},
|
33
|
+
"storage_address": {"type": "anything", "title": "Storage Reference"},
|
34
|
+
},
|
35
|
+
"custom_code": 'import base64\nimport io\nfrom contextlib import suppress\nfrom inspect import isawaitable\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 data = f.read()\n if isawaitable(data):\n return await data\n return data\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)',
|
36
|
+
"storage_metadata": {
|
37
|
+
"table_name": "File",
|
38
|
+
"db_fields": {},
|
39
|
+
"primary_key": ["partition_key"],
|
40
|
+
"foreign_keys": {},
|
41
|
+
},
|
42
|
+
},
|
43
|
+
),
|
44
|
+
migrations.UpdateClass(
|
45
|
+
module_type=ModuleType.CORE,
|
46
|
+
class_name="ClassProperty",
|
47
|
+
old_schema={
|
48
|
+
"title": "ClassProperty",
|
49
|
+
"required": ["type"],
|
50
|
+
"properties": {
|
51
|
+
"title": {"type": "string", "title": "Title"},
|
52
|
+
"type": {"type": "string", "title": "Type"},
|
53
|
+
"default": {"type": "anything", "title": "Default"},
|
54
|
+
"options": {"type": "array", "items": {"type": "Option", "title": "Option"}, "title": "Options"},
|
55
|
+
"items": {
|
56
|
+
"type": "dictionary",
|
57
|
+
"items": {"key": {"type": "string"}, "value": {"type": "anything"}},
|
58
|
+
"title": "Items",
|
59
|
+
},
|
60
|
+
"discriminator": {"type": "string", "title": "Discriminator"},
|
61
|
+
},
|
62
|
+
"meta_class": "TypeMeta",
|
63
|
+
"custom_code": "from typing import Any\n\nfrom amsdal_models.builder.validators.dict_validators import validate_non_empty_keys\nfrom pydantic.functional_validators import field_validator\n\nfrom amsdal.models.core.option import *\n\n\n@field_validator('items')\n@classmethod\ndef _non_empty_keys_items(cls: type, value: Any) -> Any:\n return validate_non_empty_keys(value)",
|
64
|
+
"storage_metadata": {"table_name": "ClassProperty", "db_fields": {}, "foreign_keys": {}},
|
65
|
+
},
|
66
|
+
new_schema={
|
67
|
+
"title": "ClassProperty",
|
68
|
+
"required": ["type"],
|
69
|
+
"properties": {
|
70
|
+
"title": {"type": "string", "title": "Title"},
|
71
|
+
"type": {"type": "string", "title": "Type"},
|
72
|
+
"default": {"type": "anything", "title": "Default"},
|
73
|
+
"options": {"type": "array", "items": {"type": "Option", "title": "Option"}, "title": "Options"},
|
74
|
+
"items": {
|
75
|
+
"type": "dictionary",
|
76
|
+
"items": {"key": {"type": "string"}, "value": {"type": "anything"}},
|
77
|
+
"title": "Items",
|
78
|
+
},
|
79
|
+
"discriminator": {"type": "string", "title": "Discriminator"},
|
80
|
+
"extra": {
|
81
|
+
"type": "dictionary",
|
82
|
+
"items": {"key": {"type": "string"}, "value": {"type": "anything"}},
|
83
|
+
"title": "Extra",
|
84
|
+
},
|
85
|
+
},
|
86
|
+
"meta_class": "TypeMeta",
|
87
|
+
"custom_code": "from typing import Any\n\nfrom amsdal_models.builder.validators.dict_validators import validate_non_empty_keys\nfrom pydantic.functional_validators import field_validator\n\nfrom amsdal.models.core.option import *\n\n\n@field_validator('items')\n@classmethod\ndef _non_empty_keys_items(cls: type, value: Any) -> Any:\n return validate_non_empty_keys(value)",
|
88
|
+
"storage_metadata": {"table_name": "ClassProperty", "db_fields": {}, "foreign_keys": {}},
|
89
|
+
},
|
90
|
+
),
|
91
|
+
]
|
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
@@ -71,6 +71,11 @@ class Settings(BaseSettings):
|
|
71
71
|
CONTRIB_MODELS_PACKAGE_NAME: str = 'models'
|
72
72
|
CONTRIB_MIGRATIONS_DIRECTORY_NAME: str = 'migrations'
|
73
73
|
|
74
|
+
# File Storage
|
75
|
+
AMSDAL_MEDIA_ROOT: Path = Path('./media')
|
76
|
+
AMSDAL_MEDIA_URL: str = '/media/'
|
77
|
+
AMSDAL_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
|
"""
|
amsdal/configs/main.pyi
CHANGED
@@ -52,6 +52,9 @@ class Settings(BaseSettings):
|
|
52
52
|
CONTRIBS: list[str]
|
53
53
|
CONTRIB_MODELS_PACKAGE_NAME: str
|
54
54
|
CONTRIB_MIGRATIONS_DIRECTORY_NAME: str
|
55
|
+
AMSDAL_MEDIA_ROOT: Path
|
56
|
+
AMSDAL_MEDIA_URL: str
|
57
|
+
AMSDAL_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.
|
Binary file
|
@@ -1,7 +1,7 @@
|
|
1
1
|
from _typeshed import Incomplete
|
2
2
|
from amsdal.contrib.auth.errors import AuthenticationError as AuthenticationError
|
3
3
|
from amsdal_data.transactions.decorators import async_transaction, transaction
|
4
|
-
from amsdal_models.classes.model import Model
|
4
|
+
from amsdal_models.classes.model import Model
|
5
5
|
from amsdal_utils.lifecycle.consumer import LifecycleConsumer
|
6
6
|
from typing import Any
|
7
7
|
|
Binary file
|
Binary file
|
amsdal/fixtures/manager.pyi
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from _typeshed import Incomplete
|
2
2
|
from amsdal.fixtures.utils import process_fixture_value as process_fixture_value
|
3
|
-
from amsdal_models.classes.model import Model
|
3
|
+
from amsdal_models.classes.model import Model
|
4
4
|
from collections.abc import Generator
|
5
5
|
from pathlib import Path
|
6
6
|
from pydantic import BaseModel
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -18,6 +18,7 @@ class ClassProperty(TypeModel):
|
|
18
18
|
options: list['Option'] | None = Field(None, title='Options') # noqa: F405
|
19
19
|
items: dict[str, Any | None] | None = Field(None, title='Items')
|
20
20
|
discriminator: str | None = Field(None, title='Discriminator')
|
21
|
+
extra: dict[str, Any | None] = Field(default_factory=dict, title='Extra')
|
21
22
|
|
22
23
|
@field_validator('items')
|
23
24
|
@classmethod
|
amsdal/models/core/file.py
CHANGED
@@ -1,74 +1,102 @@
|
|
1
1
|
import base64
|
2
|
+
import io
|
3
|
+
from contextlib import suppress
|
4
|
+
from inspect import isawaitable
|
2
5
|
from pathlib import Path
|
6
|
+
from typing import IO
|
7
|
+
from typing import Any
|
3
8
|
from typing import BinaryIO
|
4
9
|
from typing import ClassVar
|
5
10
|
|
6
11
|
from amsdal_models.classes.model import Model
|
12
|
+
from amsdal_models.storage.backends.db import DBStorage
|
13
|
+
from amsdal_models.storage.base import Storage
|
14
|
+
from amsdal_utils.models.data_models.reference import Reference
|
7
15
|
from amsdal_utils.models.enums import ModuleType
|
8
|
-
from pydantic import
|
16
|
+
from pydantic import PrivateAttr
|
17
|
+
from pydantic import model_validator
|
9
18
|
from pydantic.fields import Field
|
10
19
|
|
11
20
|
|
12
21
|
class File(Model):
|
13
22
|
__module_type__: ClassVar[ModuleType] = ModuleType.CORE
|
14
23
|
filename: str = Field(title='Filename')
|
15
|
-
data: bytes = Field(title='Data')
|
16
|
-
size: float | None = Field(None, title='Size')
|
24
|
+
data: bytes | None = Field(default=None, title='Data')
|
25
|
+
size: float | None = Field(default=None, title='Size')
|
26
|
+
storage_address: Reference | None = Field(default=None, title='Storage Reference')
|
27
|
+
|
28
|
+
_source: BinaryIO | None = PrivateAttr(default=None)
|
29
|
+
_needs_persist: bool = PrivateAttr(default=False)
|
30
|
+
_storage: Storage | None = PrivateAttr(default=None)
|
31
|
+
|
32
|
+
@property
|
33
|
+
def storage(self) -> Storage:
|
34
|
+
from amsdal.storages import default_storage
|
35
|
+
|
36
|
+
if self._storage:
|
37
|
+
return self._storage
|
38
|
+
|
39
|
+
if self.storage_address:
|
40
|
+
return Storage.from_storage_spec({'storage_class': self.storage_address.ref.resource})
|
41
|
+
|
42
|
+
return default_storage()
|
17
43
|
|
18
44
|
def __repr__(self) -> str:
|
19
|
-
return f'File<{self.filename}>({self.size or len(self.data) or 0} bytes)'
|
45
|
+
return f'File<{self.filename}>({self.size or len(self.data or "") or 0} bytes)'
|
20
46
|
|
21
47
|
def __str__(self) -> str:
|
22
48
|
return repr(self)
|
23
49
|
|
24
|
-
|
25
|
-
|
26
|
-
|
50
|
+
def pre_create(self) -> None:
|
51
|
+
if self._needs_persist:
|
52
|
+
from amsdal_models.storage.persistence import persist_file
|
27
53
|
|
28
|
-
|
29
|
-
If the data is None, it defaults to an empty byte string.
|
54
|
+
persist_file(self, storage=self.storage)
|
30
55
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
56
|
+
def pre_update(self) -> None:
|
57
|
+
if self._needs_persist:
|
58
|
+
from amsdal_models.storage.persistence import persist_file
|
59
|
+
|
60
|
+
persist_file(self, storage=self.storage)
|
61
|
+
|
62
|
+
async def apre_create(self) -> None:
|
63
|
+
if self._needs_persist:
|
64
|
+
from amsdal_models.storage.persistence import apersist_file
|
65
|
+
|
66
|
+
await apersist_file(self, storage=self.storage)
|
35
67
|
|
36
68
|
async def apre_update(self) -> None:
|
37
|
-
|
38
|
-
|
69
|
+
if self._needs_persist:
|
70
|
+
from amsdal_models.storage.persistence import apersist_file
|
39
71
|
|
40
|
-
|
41
|
-
If the data is None, it defaults to an empty byte string.
|
72
|
+
await apersist_file(self, storage=self.storage)
|
42
73
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
74
|
+
@model_validator(mode='before')
|
75
|
+
@classmethod
|
76
|
+
def validate_model_data(cls, data: Any) -> Any:
|
77
|
+
if isinstance(data, dict):
|
78
|
+
if 'data' in data:
|
79
|
+
if data['data']:
|
80
|
+
data['data'] = cls.data_base64_decode(data['data'])
|
81
|
+
data['size'] = len(data['data'])
|
82
|
+
else:
|
83
|
+
data['size'] = 0
|
84
|
+
return data
|
47
85
|
|
48
|
-
@field_validator('data')
|
49
86
|
@classmethod
|
50
|
-
def data_base64_decode(cls,
|
51
|
-
|
52
|
-
|
87
|
+
def data_base64_decode(cls, data: Any) -> bytes:
|
88
|
+
if isinstance(data, str):
|
89
|
+
data = data.encode('utf-8')
|
53
90
|
|
54
|
-
|
55
|
-
If the byte string is not base64-encoded, it returns the original byte string.
|
91
|
+
is_base64: bool = False
|
56
92
|
|
57
|
-
|
58
|
-
|
59
|
-
v (bytes): The byte string to be checked and potentially decoded.
|
93
|
+
with suppress(Exception):
|
94
|
+
is_base64 = base64.b64encode(base64.b64decode(data)) == data
|
60
95
|
|
61
|
-
Returns:
|
62
|
-
bytes: The decoded byte string if it was base64-encoded, otherwise the original byte string.
|
63
|
-
"""
|
64
|
-
is_base64: bool = False
|
65
|
-
try:
|
66
|
-
is_base64 = base64.b64encode(base64.b64decode(v)) == v
|
67
|
-
except Exception:
|
68
|
-
...
|
69
96
|
if is_base64:
|
70
|
-
return base64.b64decode(
|
71
|
-
|
97
|
+
return base64.b64decode(data)
|
98
|
+
|
99
|
+
return data
|
72
100
|
|
73
101
|
@classmethod
|
74
102
|
def from_file(cls, file_or_path: Path | BinaryIO) -> 'File':
|
@@ -84,73 +112,144 @@ class File(Model):
|
|
84
112
|
Raises:
|
85
113
|
ValueError: If the provided path is a directory.
|
86
114
|
"""
|
115
|
+
f: BinaryIO | io.BufferedReader
|
116
|
+
|
87
117
|
if isinstance(file_or_path, Path):
|
88
118
|
if file_or_path.is_dir():
|
89
119
|
msg = f'{file_or_path} is a directory'
|
90
120
|
raise ValueError(msg)
|
91
|
-
|
121
|
+
f = file_or_path.open('rb')
|
92
122
|
filename = file_or_path.name
|
123
|
+
size = file_or_path.stat().st_size
|
124
|
+
|
93
125
|
else:
|
94
|
-
file_or_path
|
95
|
-
|
96
|
-
|
97
|
-
|
126
|
+
f = file_or_path
|
127
|
+
filename = Path(getattr(f, 'name', 'unnamed')).name
|
128
|
+
|
129
|
+
try:
|
130
|
+
if f.seekable():
|
131
|
+
f.seek(0, io.SEEK_END)
|
132
|
+
size = f.tell()
|
133
|
+
f.seek(0)
|
134
|
+
else:
|
135
|
+
size = None
|
136
|
+
except (OSError, AttributeError):
|
137
|
+
size = None
|
138
|
+
|
139
|
+
obj = cls(filename=filename, size=size)
|
140
|
+
obj._source = f
|
141
|
+
obj._needs_persist = True
|
142
|
+
return obj
|
98
143
|
|
99
|
-
@
|
100
|
-
def
|
144
|
+
@classmethod
|
145
|
+
def from_bytes(cls, filename: str, data: bytes) -> 'File':
|
101
146
|
"""
|
102
|
-
|
147
|
+
Creates a `File` object from a byte string.
|
103
148
|
|
104
|
-
|
149
|
+
Args:
|
150
|
+
filename (str): The filename of the file.
|
151
|
+
data (bytes): The byte string containing the file data.:
|
105
152
|
|
106
153
|
Returns:
|
107
|
-
|
154
|
+
File: The created `File` object.
|
108
155
|
"""
|
109
|
-
|
156
|
+
obj = cls(filename=filename, data=data, size=len(data))
|
157
|
+
obj._needs_persist = True
|
158
|
+
return obj
|
110
159
|
|
111
|
-
|
112
|
-
|
113
|
-
def pre_create(self) -> None:
|
160
|
+
def to_file(self, file_or_path: Path | BinaryIO) -> None:
|
114
161
|
"""
|
115
|
-
|
116
|
-
|
117
|
-
This method calculates the size of the object's data and assigns it to the size attribute.
|
118
|
-
If the data is None, it defaults to an empty byte string.
|
162
|
+
Writes the object's data to a file path or a binary file object.
|
119
163
|
|
120
164
|
Args:
|
165
|
+
file_or_path (Path | BinaryIO): The file path or binary file object where the data will be written.
|
166
|
+
|
167
|
+
Returns:
|
121
168
|
None
|
169
|
+
|
170
|
+
Raises:
|
171
|
+
ValueError: If the provided path is a directory.
|
122
172
|
"""
|
123
|
-
self.
|
173
|
+
with self.open() as f:
|
174
|
+
if isinstance(file_or_path, Path):
|
175
|
+
if file_or_path.is_dir():
|
176
|
+
file_or_path = file_or_path / self.name
|
177
|
+
file_or_path.write_bytes(f.read()) # type: ignore[union-attr]
|
178
|
+
else:
|
179
|
+
file_or_path.write(f.read())
|
180
|
+
file_or_path.seek(0)
|
124
181
|
|
125
|
-
def
|
182
|
+
def url(self) -> str:
|
126
183
|
"""
|
127
|
-
|
184
|
+
Return a URL for this file using its storage_address.
|
128
185
|
|
129
|
-
|
130
|
-
|
186
|
+
Raises StateError if storage_address is missing.
|
187
|
+
"""
|
188
|
+
return self.storage.url(self)
|
131
189
|
|
132
|
-
|
133
|
-
None
|
190
|
+
def open(self, mode: str = 'rb') -> IO[Any]:
|
134
191
|
"""
|
135
|
-
|
192
|
+
Open a binary stream for reading (or other modes if supported) using storage_address.
|
136
193
|
|
137
|
-
|
194
|
+
Raises StateError if storage_address is missing.
|
138
195
|
"""
|
139
|
-
|
196
|
+
return self.storage.open(self, mode)
|
140
197
|
|
141
|
-
|
142
|
-
|
198
|
+
async def aurl(self) -> str:
|
199
|
+
"""
|
200
|
+
Async variant of url().
|
143
201
|
|
144
|
-
|
145
|
-
|
202
|
+
Uses the resolved storage to call aurl(); if the backend does not implement
|
203
|
+
async, falls back to the sync url().
|
204
|
+
"""
|
205
|
+
try:
|
206
|
+
return await self.storage.aurl(self) # type: ignore[attr-defined]
|
207
|
+
except NotImplementedError:
|
208
|
+
return self.storage.url(self)
|
146
209
|
|
147
|
-
|
148
|
-
ValueError: If the provided path is a directory.
|
210
|
+
async def aopen(self, mode: str = 'rb') -> Any:
|
149
211
|
"""
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
212
|
+
Async variant of open().
|
213
|
+
|
214
|
+
Uses the resolved storage to call aopen(); if the backend does not implement
|
215
|
+
async, falls back to the sync open().
|
216
|
+
"""
|
217
|
+
try:
|
218
|
+
return await self.storage.aopen(self, mode)
|
219
|
+
except NotImplementedError:
|
220
|
+
return self.storage.open(self, mode)
|
221
|
+
|
222
|
+
@property
|
223
|
+
def mimetype(self) -> str | None:
|
224
|
+
"""
|
225
|
+
Returns the MIME type of the file based on its filename.
|
226
|
+
|
227
|
+
This method uses the `mimetypes` module to guess the MIME type of the file.
|
228
|
+
|
229
|
+
Returns:
|
230
|
+
str | None: The guessed MIME type of the file, or None if it cannot be determined.
|
231
|
+
"""
|
232
|
+
import mimetypes
|
233
|
+
|
234
|
+
return mimetypes.guess_type(self.filename)[0]
|
235
|
+
|
236
|
+
def read_bytes(self) -> bytes:
|
237
|
+
with self.open() as f:
|
238
|
+
return f.read()
|
239
|
+
|
240
|
+
async def aread_bytes(self) -> bytes:
|
241
|
+
async with await self.aopen() as f:
|
242
|
+
data = f.read()
|
243
|
+
|
244
|
+
if isawaitable(data):
|
245
|
+
return await data
|
246
|
+
return data
|
247
|
+
|
248
|
+
def set_data(self, data: bytes | str) -> None:
|
249
|
+
if not isinstance(self.storage, DBStorage):
|
250
|
+
msg = 'Cannot set data on a file that is not stored in a database. Use `File.from_bytes` instead.'
|
251
|
+
raise ValueError(msg)
|
252
|
+
|
253
|
+
self.data = self.data_base64_decode(data)
|
254
|
+
self.size = len(self.data)
|
255
|
+
self._needs_persist = True
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import base64
|
2
|
+
from contextlib import suppress
|
2
3
|
|
3
4
|
from pydantic import field_validator
|
4
5
|
|
@@ -21,10 +22,8 @@ def data_base64_decode(cls, v: bytes) -> bytes: # type: ignore[no-untyped-def]
|
|
21
22
|
"""
|
22
23
|
is_base64: bool = False
|
23
24
|
|
24
|
-
|
25
|
+
with suppress(Exception):
|
25
26
|
is_base64 = base64.b64encode(base64.b64decode(v)) == v
|
26
|
-
except Exception:
|
27
|
-
...
|
28
27
|
|
29
28
|
if is_base64:
|
30
29
|
return base64.b64decode(v)
|