amsdal 0.3.3__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.
Files changed (244) hide show
  1. amsdal/Third-Party Materials - AMSDAL Dependencies - License Notices.md +56 -2
  2. amsdal/__about__.py +1 -1
  3. amsdal/__init__.py +20 -0
  4. amsdal/__init__.pyi +9 -0
  5. amsdal/__migrations__/0000_initial.py +23 -190
  6. amsdal/__migrations__/0001_create_class_file.py +61 -0
  7. amsdal/__migrations__/0002_create_class_file.py +109 -0
  8. amsdal/__migrations__/0003_update_class_file.py +91 -0
  9. amsdal/__migrations__/0004_update_class_file.py +45 -0
  10. amsdal/cloud/__init__.cpython-311-darwin.so +0 -0
  11. amsdal/cloud/client.cpython-311-darwin.so +0 -0
  12. amsdal/cloud/constants.cpython-311-darwin.so +0 -0
  13. amsdal/cloud/enums.cpython-311-darwin.so +0 -0
  14. amsdal/cloud/models/__init__.cpython-311-darwin.so +0 -0
  15. amsdal/cloud/models/base.cpython-311-darwin.so +0 -0
  16. amsdal/cloud/services/__init__.cpython-311-darwin.so +0 -0
  17. amsdal/cloud/services/actions/__init__.cpython-311-darwin.so +0 -0
  18. amsdal/cloud/services/actions/add_allowlist_ip.cpython-311-darwin.so +0 -0
  19. amsdal/cloud/services/actions/add_basic_auth.cpython-311-darwin.so +0 -0
  20. amsdal/cloud/services/actions/add_dependency.cpython-311-darwin.so +0 -0
  21. amsdal/cloud/services/actions/add_secret.cpython-311-darwin.so +0 -0
  22. amsdal/cloud/services/actions/base.cpython-311-darwin.so +0 -0
  23. amsdal/cloud/services/actions/create_deploy.cpython-311-darwin.so +0 -0
  24. amsdal/cloud/services/actions/create_env.cpython-311-darwin.so +0 -0
  25. amsdal/cloud/services/actions/create_session.cpython-311-darwin.so +0 -0
  26. amsdal/cloud/services/actions/delete_allowlist_ip.cpython-311-darwin.so +0 -0
  27. amsdal/cloud/services/actions/delete_basic_auth.cpython-311-darwin.so +0 -0
  28. amsdal/cloud/services/actions/delete_dependency.cpython-311-darwin.so +0 -0
  29. amsdal/cloud/services/actions/delete_env.cpython-311-darwin.so +0 -0
  30. amsdal/cloud/services/actions/delete_secret.cpython-311-darwin.so +0 -0
  31. amsdal/cloud/services/actions/destroy_deploy.cpython-311-darwin.so +0 -0
  32. amsdal/cloud/services/actions/expose_db.cpython-311-darwin.so +0 -0
  33. amsdal/cloud/services/actions/get_basic_auth_credentials.cpython-311-darwin.so +0 -0
  34. amsdal/cloud/services/actions/get_monitoring_info.cpython-311-darwin.so +0 -0
  35. amsdal/cloud/services/actions/list_dependencies.cpython-311-darwin.so +0 -0
  36. amsdal/cloud/services/actions/list_deploys.cpython-311-darwin.so +0 -0
  37. amsdal/cloud/services/actions/list_envs.cpython-311-darwin.so +0 -0
  38. amsdal/cloud/services/actions/list_secrets.cpython-311-darwin.so +0 -0
  39. amsdal/cloud/services/actions/manager.cpython-311-darwin.so +0 -0
  40. amsdal/cloud/services/actions/signup_action.cpython-311-darwin.so +0 -0
  41. amsdal/cloud/services/actions/update_deploy.cpython-311-darwin.so +0 -0
  42. amsdal/cloud/services/auth/__init__.cpython-311-darwin.so +0 -0
  43. amsdal/cloud/services/auth/base.cpython-311-darwin.so +0 -0
  44. amsdal/cloud/services/auth/credentials.cpython-311-darwin.so +0 -0
  45. amsdal/cloud/services/auth/credentials.pyi +0 -1
  46. amsdal/cloud/services/auth/manager.cpython-311-darwin.so +0 -0
  47. amsdal/cloud/services/auth/signup_service.cpython-311-darwin.so +0 -0
  48. amsdal/cloud/services/auth/token.cpython-311-darwin.so +0 -0
  49. amsdal/cloud/services/auth/token.pyi +0 -1
  50. amsdal/configs/main.py +40 -20
  51. amsdal/configs/main.pyi +19 -18
  52. amsdal/contrib/__init__.cpython-311-darwin.so +0 -0
  53. amsdal/contrib/auth/errors.py +36 -0
  54. amsdal/contrib/auth/errors.pyi +12 -0
  55. amsdal/contrib/auth/fixtures/basic_permissions.json +64 -0
  56. amsdal/contrib/auth/lifecycle/consumer.py +13 -13
  57. amsdal/contrib/auth/lifecycle/consumer.pyi +3 -0
  58. amsdal/contrib/auth/migrations/0000_initial.py +69 -31
  59. amsdal/contrib/auth/migrations/0001_add_mfa_support.py +188 -0
  60. amsdal/contrib/auth/models/__init__.py +1 -0
  61. amsdal/contrib/auth/models/backup_code.py +85 -0
  62. amsdal/contrib/auth/models/email_mfa_device.py +108 -0
  63. amsdal/contrib/auth/models/login_session.py +235 -0
  64. amsdal/contrib/auth/models/mfa_device.py +86 -0
  65. amsdal/contrib/auth/models/permission.py +23 -0
  66. amsdal/contrib/auth/models/sms_device.py +113 -0
  67. amsdal/contrib/auth/models/totp_device.py +58 -0
  68. amsdal/contrib/auth/models/user.py +156 -0
  69. amsdal/contrib/auth/services/__init__.py +1 -0
  70. amsdal/contrib/auth/services/mfa_device_service.py +544 -0
  71. amsdal/contrib/auth/services/mfa_device_service.pyi +216 -0
  72. amsdal/contrib/auth/services/totp_service.py +358 -0
  73. amsdal/contrib/auth/services/totp_service.pyi +158 -0
  74. amsdal/contrib/auth/settings.py +8 -0
  75. amsdal/contrib/auth/settings.pyi +8 -0
  76. amsdal/contrib/auth/transactions/__init__.py +1 -0
  77. amsdal/contrib/auth/transactions/mfa_device_transactions.py +463 -0
  78. amsdal/contrib/auth/transactions/mfa_device_transactions.pyi +226 -0
  79. amsdal/contrib/auth/transactions/totp_transactions.py +206 -0
  80. amsdal/contrib/auth/transactions/totp_transactions.pyi +113 -0
  81. amsdal/contrib/auth/utils/__init__.py +0 -0
  82. amsdal/contrib/auth/utils/__init__.pyi +0 -0
  83. amsdal/contrib/auth/utils/mfa.py +257 -0
  84. amsdal/contrib/auth/utils/mfa.pyi +119 -0
  85. amsdal/contrib/frontend_configs/conversion/convert.py +85 -25
  86. amsdal/contrib/frontend_configs/conversion/convert.pyi +0 -1
  87. amsdal/contrib/frontend_configs/lifecycle/consumer.py +32 -13
  88. amsdal/contrib/frontend_configs/lifecycle/consumer.pyi +1 -1
  89. amsdal/contrib/frontend_configs/migrations/0000_initial.py +167 -195
  90. amsdal/contrib/frontend_configs/migrations/0001_update_frontend_control_config.py +245 -0
  91. amsdal/contrib/frontend_configs/migrations/0002_add_button_and_invoke_actions.py +352 -0
  92. amsdal/contrib/frontend_configs/migrations/0003_create_class_frontendconfigdashboardelement.py +145 -0
  93. amsdal/contrib/frontend_configs/models/__init__.py +0 -0
  94. amsdal/contrib/frontend_configs/models/frontend_activator_config.py +22 -0
  95. amsdal/contrib/frontend_configs/models/frontend_config_async_validator.py +11 -0
  96. amsdal/contrib/frontend_configs/models/frontend_config_control_action.py +110 -0
  97. amsdal/contrib/frontend_configs/models/frontend_config_dashboard.py +51 -0
  98. amsdal/contrib/frontend_configs/models/frontend_config_group_validator.py +21 -0
  99. amsdal/contrib/frontend_configs/models/frontend_config_option.py +12 -0
  100. amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base.py +17 -0
  101. amsdal/contrib/frontend_configs/models/frontend_config_slider_option.py +13 -0
  102. amsdal/contrib/frontend_configs/models/frontend_config_text_mask.py +14 -0
  103. amsdal/contrib/frontend_configs/models/frontend_config_validator.py +28 -0
  104. amsdal/contrib/frontend_configs/models/frontend_control_config.py +110 -0
  105. amsdal/contrib/frontend_configs/models/frontend_model_config.py +14 -0
  106. amsdal/errors.py +0 -3
  107. amsdal/errors.pyi +0 -1
  108. amsdal/fixtures/__init__.cpython-311-darwin.so +0 -0
  109. amsdal/fixtures/manager.cpython-311-darwin.so +0 -0
  110. amsdal/fixtures/manager.pyi +73 -123
  111. amsdal/fixtures/utils.cpython-311-darwin.so +0 -0
  112. amsdal/fixtures/utils.pyi +9 -0
  113. amsdal/manager.cpython-311-darwin.so +0 -0
  114. amsdal/manager.pyi +9 -96
  115. amsdal/mixins/__init__.cpython-311-darwin.so +0 -0
  116. amsdal/mixins/class_versions_mixin.cpython-311-darwin.so +0 -0
  117. amsdal/models/__init__.py +19 -0
  118. amsdal/models/core/__init__.py +0 -0
  119. amsdal/models/core/class_object.py +38 -0
  120. amsdal/models/core/class_property.py +26 -0
  121. amsdal/models/core/file.py +243 -0
  122. amsdal/models/core/fixture.py +25 -0
  123. amsdal/models/core/option.py +11 -0
  124. amsdal/models/core/storage_metadata.py +15 -0
  125. amsdal/models/core/validator.py +12 -0
  126. amsdal/models/mixins.py +31 -0
  127. amsdal/models/types/__init__.py +0 -0
  128. amsdal/models/types/object.py +26 -0
  129. amsdal/queryset/__init__.py +21 -0
  130. amsdal/queryset/__init__.pyi +6 -0
  131. amsdal/schemas/core/class_object/model.json +20 -0
  132. amsdal/schemas/core/class_property/model.json +19 -0
  133. amsdal/schemas/core/file/properties/from_file.py +1 -1
  134. amsdal/schemas/core/file/properties/validate_data.py +3 -4
  135. amsdal/schemas/core/storage_metadata/model.json +52 -0
  136. amsdal/schemas/interfaces.py +25 -0
  137. amsdal/schemas/interfaces.pyi +20 -0
  138. amsdal/schemas/manager.cpython-311-darwin.so +0 -0
  139. amsdal/schemas/manager.py +0 -116
  140. amsdal/schemas/manager.pyi +0 -65
  141. amsdal/schemas/mixins/__init__.py +0 -0
  142. amsdal/schemas/mixins/__init__.pyi +0 -0
  143. amsdal/schemas/mixins/check_dependencies_mixin.py +130 -0
  144. amsdal/schemas/mixins/check_dependencies_mixin.pyi +45 -0
  145. amsdal/schemas/mixins/verify_schemas_mixin.py +96 -0
  146. amsdal/schemas/mixins/verify_schemas_mixin.pyi +33 -0
  147. amsdal/schemas/repository.py +84 -0
  148. amsdal/schemas/repository.pyi +22 -0
  149. amsdal/schemas/utils.py +16 -0
  150. amsdal/schemas/utils.pyi +10 -0
  151. amsdal/services/__init__.py +11 -0
  152. amsdal/services/__init__.pyi +4 -0
  153. amsdal/services/external_connections.py +262 -0
  154. amsdal/services/external_connections.pyi +190 -0
  155. amsdal/services/external_model_generator.py +350 -0
  156. amsdal/services/external_model_generator.pyi +134 -0
  157. amsdal/services/transaction_execution.cpython-311-darwin.so +0 -0
  158. amsdal/services/transaction_execution.pyi +2 -1
  159. amsdal/storages/__init__.py +20 -0
  160. amsdal/storages/__init__.pyi +8 -0
  161. amsdal/storages/file_system.py +214 -0
  162. amsdal/storages/file_system.pyi +36 -0
  163. amsdal/transactions/__init__.py +13 -0
  164. amsdal/transactions/__init__.pyi +4 -0
  165. amsdal/utils/rollback/__init__.py +99 -54
  166. amsdal/utils/rollback/__init__.pyi +6 -0
  167. amsdal/utils/tests/enums.py +0 -2
  168. amsdal/utils/tests/helpers.py +253 -231
  169. amsdal/utils/tests/migrations.py +157 -0
  170. {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info}/METADATA +17 -10
  171. amsdal-0.5.29.dist-info/RECORD +276 -0
  172. {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info}/WHEEL +1 -1
  173. amsdal/__migrations__/0001_datetime_type.py +0 -18
  174. amsdal/__migrations__/0002_fixture_order.py +0 -40
  175. amsdal/__migrations__/0003_schema_type_in_class_meta.py +0 -56
  176. amsdal/contrib/auth/models/login_session/hooks/pre_init.py +0 -68
  177. amsdal/contrib/auth/models/login_session/model.json +0 -23
  178. amsdal/contrib/auth/models/login_session/modifiers/display_name.py +0 -11
  179. amsdal/contrib/auth/models/permission/fixtures/basic_permissions.json +0 -62
  180. amsdal/contrib/auth/models/permission/model.json +0 -18
  181. amsdal/contrib/auth/models/permission/modifiers/display_name.py +0 -11
  182. amsdal/contrib/auth/models/user/hooks/post_init.py +0 -76
  183. amsdal/contrib/auth/models/user/hooks/pre_create.py +0 -8
  184. amsdal/contrib/auth/models/user/model.json +0 -25
  185. amsdal/contrib/auth/models/user/modifiers/display_name.py +0 -19
  186. amsdal/contrib/frontend_configs/models/frontend_activator_config/model.json +0 -11
  187. amsdal/contrib/frontend_configs/models/frontend_config_async_validator/model.json +0 -11
  188. amsdal/contrib/frontend_configs/models/frontend_config_group_validator/model.json +0 -52
  189. amsdal/contrib/frontend_configs/models/frontend_config_option/model.json +0 -15
  190. amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base/model.json +0 -6
  191. amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base/properties/model_dump.py +0 -13
  192. amsdal/contrib/frontend_configs/models/frontend_config_slider_option/model.json +0 -19
  193. amsdal/contrib/frontend_configs/models/frontend_config_text_mask/model.json +0 -26
  194. amsdal/contrib/frontend_configs/models/frontend_config_validator/model.json +0 -41
  195. amsdal/contrib/frontend_configs/models/frontend_control_config/model.json +0 -250
  196. amsdal/contrib/frontend_configs/models/frontend_model_config/fixtures/permissions.json +0 -24
  197. amsdal/contrib/frontend_configs/models/frontend_model_config/model.json +0 -17
  198. amsdal/contrib/frontend_configs/models/frontent_config_control_action/model.json +0 -54
  199. amsdal/contrib/frontend_configs/models/frontent_config_control_action/properties/action_validate.py +0 -33
  200. amsdal/migration/__init__.cpython-311-darwin.so +0 -0
  201. amsdal/migration/base_migration_schemas.cpython-311-darwin.so +0 -0
  202. amsdal/migration/base_migration_schemas.pyi +0 -120
  203. amsdal/migration/data_classes.cpython-311-darwin.so +0 -0
  204. amsdal/migration/data_classes.pyi +0 -172
  205. amsdal/migration/executors/__init__.cpython-311-darwin.so +0 -0
  206. amsdal/migration/executors/base.cpython-311-darwin.so +0 -0
  207. amsdal/migration/executors/base.pyi +0 -118
  208. amsdal/migration/executors/default_executor.cpython-311-darwin.so +0 -0
  209. amsdal/migration/executors/default_executor.pyi +0 -184
  210. amsdal/migration/executors/state_executor.cpython-311-darwin.so +0 -0
  211. amsdal/migration/executors/state_executor.pyi +0 -78
  212. amsdal/migration/file_migration_executor.cpython-311-darwin.so +0 -0
  213. amsdal/migration/file_migration_executor.pyi +0 -68
  214. amsdal/migration/file_migration_generator.cpython-311-darwin.so +0 -0
  215. amsdal/migration/file_migration_generator.pyi +0 -139
  216. amsdal/migration/file_migration_store.cpython-311-darwin.so +0 -0
  217. amsdal/migration/file_migration_store.pyi +0 -61
  218. amsdal/migration/file_migration_writer.cpython-311-darwin.so +0 -0
  219. amsdal/migration/file_migration_writer.pyi +0 -73
  220. amsdal/migration/migrations.cpython-311-darwin.so +0 -0
  221. amsdal/migration/migrations.pyi +0 -166
  222. amsdal/migration/migrations_loader.cpython-311-darwin.so +0 -0
  223. amsdal/migration/migrations_loader.pyi +0 -32
  224. amsdal/migration/schemas_loaders.cpython-311-darwin.so +0 -0
  225. amsdal/migration/schemas_loaders.pyi +0 -37
  226. amsdal/migration/templates/data_migration.tmpl +0 -18
  227. amsdal/migration/templates/dict_validator.tmpl +0 -4
  228. amsdal/migration/templates/migration.tmpl +0 -6
  229. amsdal/migration/templates/model_class.tmpl +0 -8
  230. amsdal/migration/templates/model_class_layout.tmpl +0 -24
  231. amsdal/migration/templates/options_validator.tmpl +0 -4
  232. amsdal/migration/utils.cpython-311-darwin.so +0 -0
  233. amsdal/migration/utils.pyi +0 -58
  234. amsdal/mixins/build_mixin.cpython-311-darwin.so +0 -0
  235. amsdal/mixins/build_mixin.pyi +0 -78
  236. amsdal/schemas/core/class_object_meta/model.json +0 -59
  237. amsdal/schemas/core/class_property_meta/model.json +0 -23
  238. amsdal/services/__init__.cpython-311-darwin.so +0 -0
  239. amsdal-0.3.3.dist-info/RECORD +0 -257
  240. amsdal-0.3.3.dist-info/licenses/LICENSE.txt +0 -107
  241. /amsdal/{migration → contrib/auth/services}/__init__.pyi +0 -0
  242. /amsdal/{migration/executors → contrib/auth/transactions}/__init__.pyi +0 -0
  243. {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info/licenses}/LICENSE.txt +0 -0
  244. {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,214 @@
1
+ import asyncio
2
+ import os
3
+ from contextlib import suppress
4
+ from pathlib import Path
5
+ from typing import IO
6
+ from typing import Any
7
+ from typing import BinaryIO
8
+
9
+ from amsdal_models.storage.base import Storage
10
+ from amsdal_models.storage.errors import StorageError
11
+ from amsdal_models.storage.helpers import build_storage_address
12
+ from amsdal_models.storage.types import FileProtocol
13
+ from amsdal_utils.config.manager import AmsdalConfigManager
14
+
15
+ CHUNK_SIZE = 8 * 1024
16
+
17
+
18
+ class FileSystemStorage(Storage):
19
+ """
20
+ Simple filesystem-based storage backend.
21
+
22
+ - base_dir: root directory for stored files
23
+ - base_url: URL prefix for building public URLs (optional)
24
+ """
25
+
26
+ keeps_local_copy = False
27
+
28
+ def __init__(
29
+ self,
30
+ base_dir: str | os.PathLike[Any] | None = None,
31
+ base_url: str | None = None,
32
+ *,
33
+ serialize_base_dir: bool = True,
34
+ serialize_base_url: bool = True,
35
+ ) -> None:
36
+ from amsdal.configs.main import settings
37
+
38
+ # If AMSDAL is configured to run in async mode, ensure aiofiles is installed.
39
+ if AmsdalConfigManager().get_config().async_mode:
40
+ try:
41
+ import aiofiles # noqa: F401
42
+ except ImportError as e:
43
+ msg = (
44
+ "AMSDAL is configured to run in async mode, but the 'aiofiles' package is not installed.\n"
45
+ 'Please install it to enable async file operations.\n\n'
46
+ 'Example:\n'
47
+ ' pip install aiofiles\n\n'
48
+ f'Original error: {e}'
49
+ )
50
+ raise ImportError(msg) from e
51
+
52
+ _base_dir = base_dir or settings.MEDIA_ROOT
53
+ _base_url = base_url or settings.MEDIA_URL
54
+
55
+ self.base_dir = Path(_base_dir).resolve()
56
+ self.base_url = _base_url
57
+ self.base_dir.mkdir(parents=True, exist_ok=True)
58
+ self.serialize_base_dir = serialize_base_dir
59
+ self.serialize_base_url = serialize_base_url
60
+
61
+ def save(self, file: FileProtocol, content: BinaryIO) -> str:
62
+ suggested = file.filename
63
+ final_name = self._get_available_name(suggested)
64
+ full_path = self._full_path(final_name)
65
+ full_path.parent.mkdir(parents=True, exist_ok=True)
66
+
67
+ with full_path.open('wb') as f:
68
+ # Ensure reading from start
69
+ with suppress(Exception):
70
+ if hasattr(content, 'seek'):
71
+ content.seek(0)
72
+
73
+ chunk = content.read(CHUNK_SIZE)
74
+
75
+ while chunk:
76
+ f.write(chunk)
77
+ chunk = content.read(CHUNK_SIZE)
78
+
79
+ file.storage_address = build_storage_address(self, final_name)
80
+ return final_name
81
+
82
+ def open(self, file: FileProtocol, mode: str = 'rb') -> IO[Any]:
83
+ name = self._name_for(file)
84
+ full_path = self._full_path(name)
85
+
86
+ if not full_path.exists():
87
+ msg = f'File not found: {name}'
88
+ raise StorageError(msg)
89
+ return full_path.open(mode)
90
+
91
+ def delete(self, file: FileProtocol) -> None:
92
+ name = self._name_for(file)
93
+ try:
94
+ self._full_path(name).unlink(missing_ok=True)
95
+ except Exception as e:
96
+ msg = f"Failed to delete '{name}': {e}"
97
+ raise StorageError(msg) from e
98
+
99
+ def exists(self, file: FileProtocol) -> bool:
100
+ name = self._name_for(file)
101
+ return self._full_path(name).exists()
102
+
103
+ def url(self, file: FileProtocol) -> str:
104
+ name = self._name_for(file)
105
+ if not self.base_url:
106
+ # Return file path as fallback
107
+ return self._full_path(name).as_uri()
108
+
109
+ prefix = self.base_url.rstrip('/')
110
+ name_posix = Path(name).as_posix().lstrip('/')
111
+
112
+ return f'{prefix}/{name_posix}'
113
+
114
+ # ---- async counterparts ----
115
+ async def asave(self, file: FileProtocol, content: BinaryIO) -> str:
116
+ import aiofiles.os
117
+
118
+ suggested = file.filename
119
+ final_name = self._get_available_name(suggested)
120
+ full_path = self._full_path(final_name)
121
+ # Ensure directory exists
122
+ await aiofiles.os.makedirs(str(full_path.parent), exist_ok=True)
123
+
124
+ # Ensure reading from start
125
+ with suppress(Exception):
126
+ if hasattr(content, 'seek'):
127
+ with suppress(Exception):
128
+ await asyncio.to_thread(content.seek, 0)
129
+
130
+ async with aiofiles.open(str(full_path), 'wb') as f: # type: ignore[call-arg]
131
+ while True:
132
+ chunk = await asyncio.to_thread(content.read, CHUNK_SIZE)
133
+ if not chunk:
134
+ break
135
+ await f.write(chunk)
136
+
137
+ file.storage_address = build_storage_address(self, final_name)
138
+
139
+ return final_name
140
+
141
+ async def aopen(self, file: FileProtocol, mode: str = 'rb') -> Any:
142
+ import aiofiles.ospath
143
+
144
+ name = self._name_for(file)
145
+ full_path = self._full_path(name)
146
+
147
+ if not await aiofiles.ospath.exists(str(full_path)):
148
+ msg = f'File not found: {name}'
149
+ raise StorageError(msg)
150
+
151
+ return aiofiles.open(str(full_path), mode) # type: ignore[call-overload]
152
+
153
+ async def adelete(self, file: FileProtocol) -> None:
154
+ import aiofiles.os
155
+ import aiofiles.ospath
156
+
157
+ name = self._name_for(file)
158
+ full_path = self._full_path(name)
159
+
160
+ if await aiofiles.ospath.exists(str(full_path)):
161
+ try:
162
+ await aiofiles.os.remove(str(full_path))
163
+ except Exception as e: # pragma: no cover - error path
164
+ msg = f"Failed to delete '{name}': {e}"
165
+ raise StorageError(msg) from e
166
+
167
+ async def aexists(self, file: FileProtocol) -> bool:
168
+ import aiofiles.ospath
169
+
170
+ name = self._name_for(file)
171
+ return await aiofiles.ospath.exists(str(self._full_path(name)))
172
+
173
+ async def aurl(self, file: FileProtocol) -> str:
174
+ # Pure computation; no disk I/O.
175
+ return self.url(file)
176
+
177
+ def _full_path(self, name: str) -> Path:
178
+ # Sanitize name to avoid path traversal
179
+ norm = Path(name).as_posix().lstrip('/')
180
+
181
+ return (self.base_dir / norm).resolve()
182
+
183
+ def _get_available_name(self, name: str) -> str:
184
+ # If file exists, add suffixes to avoid collision
185
+ candidate = Path(name)
186
+ base = candidate.stem
187
+ suffix = candidate.suffix
188
+ parent = candidate.parent.as_posix()
189
+ i = 0
190
+ final = candidate.as_posix()
191
+
192
+ # Check filesystem directly to avoid relying on public exists signature
193
+ while self._full_path(final).exists():
194
+ i += 1
195
+ new_name = f'{base}_{i}{suffix}'
196
+ final = str(Path(parent) / new_name) if parent and parent != '.' else new_name
197
+ return final
198
+
199
+ def _name_for(self, file: FileProtocol) -> str:
200
+ if getattr(file, 'storage_address', None) and getattr(file.storage_address, 'ref', None) is not None:
201
+ if getattr(file.storage_address.ref, 'object_id', None) is not None: # type: ignore[union-attr]
202
+ return str(file.storage_address.ref.object_id) # type: ignore[union-attr]
203
+ return file.filename
204
+
205
+ def _export_kwargs(self) -> dict[str, Any]:
206
+ kwargs = {}
207
+
208
+ if self.serialize_base_dir:
209
+ kwargs['base_dir'] = str(self.base_dir)
210
+
211
+ if self.serialize_base_url:
212
+ kwargs['base_url'] = self.base_url
213
+
214
+ return kwargs
@@ -0,0 +1,36 @@
1
+ import os
2
+ from _typeshed import Incomplete
3
+ from amsdal_models.storage.base import Storage
4
+ from amsdal_models.storage.types import FileProtocol
5
+ from pathlib import Path
6
+ from typing import Any, BinaryIO, IO
7
+
8
+ CHUNK_SIZE: Incomplete
9
+
10
+ class FileSystemStorage(Storage):
11
+ """
12
+ Simple filesystem-based storage backend.
13
+
14
+ - base_dir: root directory for stored files
15
+ - base_url: URL prefix for building public URLs (optional)
16
+ """
17
+ keeps_local_copy: bool
18
+ base_dir: Incomplete
19
+ base_url: Incomplete
20
+ serialize_base_dir: Incomplete
21
+ serialize_base_url: Incomplete
22
+ def __init__(self, base_dir: str | os.PathLike[Any] | None = None, base_url: str | None = None, *, serialize_base_dir: bool = True, serialize_base_url: bool = True) -> None: ...
23
+ def save(self, file: FileProtocol, content: BinaryIO) -> str: ...
24
+ def open(self, file: FileProtocol, mode: str = 'rb') -> IO[Any]: ...
25
+ def delete(self, file: FileProtocol) -> None: ...
26
+ def exists(self, file: FileProtocol) -> bool: ...
27
+ def url(self, file: FileProtocol) -> str: ...
28
+ async def asave(self, file: FileProtocol, content: BinaryIO) -> str: ...
29
+ async def aopen(self, file: FileProtocol, mode: str = 'rb') -> Any: ...
30
+ async def adelete(self, file: FileProtocol) -> None: ...
31
+ async def aexists(self, file: FileProtocol) -> bool: ...
32
+ async def aurl(self, file: FileProtocol) -> str: ...
33
+ def _full_path(self, name: str) -> Path: ...
34
+ def _get_available_name(self, name: str) -> str: ...
35
+ def _name_for(self, file: FileProtocol) -> str: ...
36
+ def _export_kwargs(self) -> dict[str, Any]: ...
@@ -0,0 +1,13 @@
1
+ from amsdal_data.transactions import async_transaction
2
+ from amsdal_data.transactions import transaction
3
+ from amsdal_data.transactions.background.schedule import SCHEDULE_TYPE
4
+ from amsdal_data.transactions.background.schedule import Crontab
5
+ from amsdal_data.transactions.background.schedule import ScheduleConfig
6
+
7
+ __all__ = [
8
+ 'SCHEDULE_TYPE',
9
+ 'Crontab',
10
+ 'ScheduleConfig',
11
+ 'async_transaction',
12
+ 'transaction',
13
+ ]
@@ -0,0 +1,4 @@
1
+ from amsdal_data.transactions import async_transaction as async_transaction, transaction as transaction
2
+ from amsdal_data.transactions.background.schedule import Crontab as Crontab, SCHEDULE_TYPE as SCHEDULE_TYPE, ScheduleConfig as ScheduleConfig
3
+
4
+ __all__ = ['SCHEDULE_TYPE', 'Crontab', 'ScheduleConfig', 'async_transaction', 'transaction']
@@ -4,7 +4,7 @@ from amsdal_data.application import DataApplication
4
4
  from amsdal_data.transactions.decorators import async_transaction
5
5
  from amsdal_data.transactions.decorators import transaction
6
6
  from amsdal_data.transactions.errors import AmsdalTransactionError
7
- from amsdal_models.classes.manager import ClassManager
7
+ from amsdal_models.classes.class_manager import ClassManager
8
8
  from amsdal_models.querysets.executor import LAKEHOUSE_DB_ALIAS
9
9
 
10
10
 
@@ -28,14 +28,21 @@ def rollback_to_timestamp(timestamp: float) -> None:
28
28
  table=glue.SchemaReference(name='Metadata', version=glue.Version.LATEST),
29
29
  where=glue.Conditions(
30
30
  glue.Condition(
31
- field=glue.FieldReference(field=glue.Field(name='updated_at'), table_name='Metadata'),
31
+ left=glue.FieldReferenceExpression(
32
+ field_reference=glue.FieldReference(field=glue.Field(name='updated_at'), table_name='Metadata'),
33
+ ),
32
34
  lookup=glue.FieldLookup.GT,
33
- value=glue.Value(timestamp),
35
+ right=glue.Value(timestamp),
34
36
  ),
35
37
  glue.Condition(
36
- field=glue.FieldReference(field=glue.Field(name='prior_version'), table_name='Metadata'),
38
+ left=glue.FieldReferenceExpression(
39
+ field_reference=glue.FieldReference(
40
+ field=glue.Field(name='prior_version'),
41
+ table_name='Metadata',
42
+ ),
43
+ ),
37
44
  lookup=glue.FieldLookup.ISNULL,
38
- value=glue.Value(True),
45
+ right=glue.Value(True),
39
46
  ),
40
47
  ),
41
48
  )
@@ -48,14 +55,21 @@ def rollback_to_timestamp(timestamp: float) -> None:
48
55
  table=glue.SchemaReference(name='Metadata', version=glue.Version.LATEST),
49
56
  where=glue.Conditions(
50
57
  glue.Condition(
51
- field=glue.FieldReference(field=glue.Field(name='updated_at'), table_name='Metadata'),
58
+ left=glue.FieldReferenceExpression(
59
+ field_reference=glue.FieldReference(field=glue.Field(name='updated_at'), table_name='Metadata'),
60
+ ),
52
61
  lookup=glue.FieldLookup.GT,
53
- value=glue.Value(timestamp),
62
+ right=glue.Value(timestamp),
54
63
  ),
55
64
  glue.Condition(
56
- field=glue.FieldReference(field=glue.Field(name='prior_version'), table_name='Metadata'),
65
+ left=glue.FieldReferenceExpression(
66
+ field_reference=glue.FieldReference(
67
+ field=glue.Field(name='prior_version'),
68
+ table_name='Metadata',
69
+ ),
70
+ ),
57
71
  lookup=glue.FieldLookup.ISNULL,
58
- value=glue.Value(False),
72
+ right=glue.Value(False),
59
73
  ),
60
74
  ),
61
75
  )
@@ -83,9 +97,12 @@ def rollback_to_timestamp(timestamp: float) -> None:
83
97
  _parent_field.child.child.parent = _parent_field.child # type: ignore[union-attr]
84
98
  _conditions.append(
85
99
  glue.Condition(
86
- field=glue.FieldReference(field=_parent_field, table_name='Metadata'),
100
+ left=glue.FieldReferenceExpression(
101
+ field_reference=glue.FieldReference(field=_parent_field, table_name='Metadata'),
102
+ output_type=str,
103
+ ),
87
104
  lookup=glue.FieldLookup.EQ,
88
- value=glue.Value(transaction_id),
105
+ right=glue.Value(transaction_id, output_type=str),
89
106
  )
90
107
  )
91
108
 
@@ -94,9 +111,14 @@ def rollback_to_timestamp(timestamp: float) -> None:
94
111
  table=glue.SchemaReference(name='Metadata', version=glue.Version.LATEST),
95
112
  where=glue.Conditions(
96
113
  glue.Condition(
97
- field=glue.FieldReference(field=glue.Field(name='updated_at'), table_name='Metadata'),
114
+ left=glue.FieldReferenceExpression(
115
+ field_reference=glue.FieldReference(
116
+ field=glue.Field(name='updated_at'),
117
+ table_name='Metadata',
118
+ ),
119
+ ),
98
120
  lookup=glue.FieldLookup.LTE,
99
- value=glue.Value(timestamp),
121
+ right=glue.Value(timestamp),
100
122
  ),
101
123
  glue.Conditions(*_conditions, connector=glue.FilterConnector.OR),
102
124
  ),
@@ -108,9 +130,7 @@ def rollback_to_timestamp(timestamp: float) -> None:
108
130
 
109
131
  for m in metadatas_to_delete:
110
132
  class_name = m.data['class_schema_reference']['ref']['object_id']
111
- schema_type = class_manager.resolve_schema_type(class_name)
112
-
113
- model_class = class_manager.import_model_class(class_name, schema_type)
133
+ model_class = class_manager.import_class(class_name)
114
134
  obj = (
115
135
  model_class.objects.filter(_address__object_id=m.data['object_id'])
116
136
  .using(LAKEHOUSE_DB_ALIAS)
@@ -123,8 +143,7 @@ def rollback_to_timestamp(timestamp: float) -> None:
123
143
  obj.delete()
124
144
 
125
145
  for object_id, class_name in ids_to_revert:
126
- schema_type = class_manager.resolve_schema_type(class_name)
127
- model_class = class_manager.import_model_class(class_name, schema_type)
146
+ model_class = class_manager.import_class(class_name)
128
147
 
129
148
  obj = (
130
149
  model_class.objects.filter(_address__object_id=object_id)
@@ -133,13 +152,14 @@ def rollback_to_timestamp(timestamp: float) -> None:
133
152
  .first()
134
153
  .execute()
135
154
  )
136
- old_obj = (
137
- model_class.objects.filter(_address__object_id=object_id, _metadata__updated_at__lte=timestamp)
138
- .using(LAKEHOUSE_DB_ALIAS)
139
- .order_by('-_metadata__updated_at')
140
- .first()
141
- .execute()
142
- )
155
+ old_obj = obj.previous_version() # type: ignore[union-attr]
156
+ # old_obj = (
157
+ # model_class.objects.filter(_address__object_id=object_id, _metadata__updated_at__lte=timestamp)
158
+ # .using(LAKEHOUSE_DB_ALIAS)
159
+ # .order_by('-_metadata__updated_at')
160
+ # .first()
161
+ # .execute()
162
+ # )
143
163
 
144
164
  if obj and old_obj:
145
165
  for field, value in old_obj.model_dump().items():
@@ -180,9 +200,12 @@ def rollback_transaction(transaction_id: str) -> None:
180
200
  table=glue.SchemaReference(name='Metadata', version=glue.Version.LATEST),
181
201
  where=glue.Conditions(
182
202
  glue.Condition(
183
- field=glue.FieldReference(field=_parent_field, table_name='Metadata'),
203
+ left=glue.FieldReferenceExpression(
204
+ field_reference=glue.FieldReference(field=_parent_field, table_name='Metadata'),
205
+ output_type=str,
206
+ ),
184
207
  lookup=glue.FieldLookup.EQ,
185
- value=glue.Value(transaction_id),
208
+ right=glue.Value(transaction_id, output_type=str),
186
209
  )
187
210
  ),
188
211
  order_by=[
@@ -222,14 +245,21 @@ async def async_rollback_to_timestamp(timestamp: float) -> None:
222
245
  table=glue.SchemaReference(name='Metadata', version=glue.Version.LATEST),
223
246
  where=glue.Conditions(
224
247
  glue.Condition(
225
- field=glue.FieldReference(field=glue.Field(name='updated_at'), table_name='Metadata'),
248
+ left=glue.FieldReferenceExpression(
249
+ field_reference=glue.FieldReference(field=glue.Field(name='updated_at'), table_name='Metadata'),
250
+ ),
226
251
  lookup=glue.FieldLookup.GT,
227
- value=glue.Value(timestamp),
252
+ right=glue.Value(timestamp),
228
253
  ),
229
254
  glue.Condition(
230
- field=glue.FieldReference(field=glue.Field(name='prior_version'), table_name='Metadata'),
255
+ left=glue.FieldReferenceExpression(
256
+ field_reference=glue.FieldReference(
257
+ field=glue.Field(name='prior_version'),
258
+ table_name='Metadata',
259
+ ),
260
+ ),
231
261
  lookup=glue.FieldLookup.ISNULL,
232
- value=glue.Value(True),
262
+ right=glue.Value(True),
233
263
  ),
234
264
  ),
235
265
  )
@@ -242,14 +272,20 @@ async def async_rollback_to_timestamp(timestamp: float) -> None:
242
272
  table=glue.SchemaReference(name='Metadata', version=glue.Version.LATEST),
243
273
  where=glue.Conditions(
244
274
  glue.Condition(
245
- field=glue.FieldReference(field=glue.Field(name='updated_at'), table_name='Metadata'),
275
+ left=glue.FieldReferenceExpression(
276
+ field_reference=glue.FieldReference(field=glue.Field(name='updated_at'), table_name='Metadata'),
277
+ ),
246
278
  lookup=glue.FieldLookup.GT,
247
- value=glue.Value(timestamp),
279
+ right=glue.Value(timestamp),
248
280
  ),
249
281
  glue.Condition(
250
- field=glue.FieldReference(field=glue.Field(name='prior_version'), table_name='Metadata'),
282
+ left=glue.FieldReferenceExpression(
283
+ field_reference=glue.FieldReference(
284
+ field=glue.Field(name='prior_version'), table_name='Metadata'
285
+ ),
286
+ ),
251
287
  lookup=glue.FieldLookup.ISNULL,
252
- value=glue.Value(False),
288
+ right=glue.Value(False),
253
289
  ),
254
290
  ),
255
291
  )
@@ -277,9 +313,12 @@ async def async_rollback_to_timestamp(timestamp: float) -> None:
277
313
  _parent_field.child.child.parent = _parent_field.child # type: ignore[union-attr]
278
314
  _conditions.append(
279
315
  glue.Condition(
280
- field=glue.FieldReference(field=_parent_field, table_name='Metadata'),
316
+ left=glue.FieldReferenceExpression(
317
+ field_reference=glue.FieldReference(field=_parent_field, table_name='Metadata'),
318
+ output_type=str,
319
+ ),
281
320
  lookup=glue.FieldLookup.EQ,
282
- value=glue.Value(transaction_id),
321
+ right=glue.Value(transaction_id, output_type=str),
283
322
  )
284
323
  )
285
324
 
@@ -288,9 +327,14 @@ async def async_rollback_to_timestamp(timestamp: float) -> None:
288
327
  table=glue.SchemaReference(name='Metadata', version=glue.Version.LATEST),
289
328
  where=glue.Conditions(
290
329
  glue.Condition(
291
- field=glue.FieldReference(field=glue.Field(name='updated_at'), table_name='Metadata'),
330
+ left=glue.FieldReferenceExpression(
331
+ field_reference=glue.FieldReference(
332
+ field=glue.Field(name='updated_at'),
333
+ table_name='Metadata',
334
+ ),
335
+ ),
292
336
  lookup=glue.FieldLookup.LTE,
293
- value=glue.Value(timestamp),
337
+ right=glue.Value(timestamp),
294
338
  ),
295
339
  glue.Conditions(*_conditions, connector=glue.FilterConnector.OR),
296
340
  ),
@@ -302,9 +346,7 @@ async def async_rollback_to_timestamp(timestamp: float) -> None:
302
346
 
303
347
  for m in metadatas_to_delete:
304
348
  class_name = m.data['class_schema_reference']['ref']['object_id']
305
- schema_type = class_manager.resolve_schema_type(class_name)
306
-
307
- model_class = class_manager.import_model_class(class_name, schema_type)
349
+ model_class = class_manager.import_class(class_name)
308
350
  obj = await (
309
351
  model_class.objects.filter(_address__object_id=m.data['object_id'])
310
352
  .using(LAKEHOUSE_DB_ALIAS)
@@ -317,8 +359,7 @@ async def async_rollback_to_timestamp(timestamp: float) -> None:
317
359
  await obj.adelete()
318
360
 
319
361
  for object_id, class_name in ids_to_revert:
320
- schema_type = class_manager.resolve_schema_type(class_name)
321
- model_class = class_manager.import_model_class(class_name, schema_type)
362
+ model_class = class_manager.import_class(class_name)
322
363
 
323
364
  obj = await (
324
365
  model_class.objects.filter(_address__object_id=object_id)
@@ -327,16 +368,17 @@ async def async_rollback_to_timestamp(timestamp: float) -> None:
327
368
  .first()
328
369
  .aexecute()
329
370
  )
330
- old_obj = await (
331
- model_class.objects.filter(_address__object_id=object_id, _metadata__updated_at__lte=timestamp)
332
- .using(LAKEHOUSE_DB_ALIAS)
333
- .order_by('-_metadata__updated_at')
334
- .first()
335
- .aexecute()
336
- )
371
+ old_obj = await obj.aprevious_version() # type: ignore[union-attr]
372
+ # old_obj = await (
373
+ # model_class.objects.filter(_address__object_id=object_id, _metadata__updated_at__lte=timestamp)
374
+ # .using(LAKEHOUSE_DB_ALIAS)
375
+ # .order_by('-_metadata__updated_at')
376
+ # .first()
377
+ # .aexecute()
378
+ # )
337
379
 
338
380
  if obj and old_obj:
339
- for field, value in old_obj.model_dump().items():
381
+ for field, value in (await old_obj.amodel_dump()).items():
340
382
  setattr(obj, field, value)
341
383
 
342
384
  await obj.asave()
@@ -373,9 +415,12 @@ async def async_rollback_transaction(transaction_id: str) -> None:
373
415
  table=glue.SchemaReference(name='Metadata', version=glue.Version.LATEST),
374
416
  where=glue.Conditions(
375
417
  glue.Condition(
376
- field=glue.FieldReference(field=_parent_field, table_name='Metadata'),
418
+ left=glue.FieldReferenceExpression(
419
+ field_reference=glue.FieldReference(field=_parent_field, table_name='Metadata'),
420
+ output_type=str,
421
+ ),
377
422
  lookup=glue.FieldLookup.EQ,
378
- value=glue.Value(transaction_id),
423
+ right=glue.Value(transaction_id, output_type=str),
379
424
  )
380
425
  ),
381
426
  order_by=[
@@ -1,3 +1,6 @@
1
+ from amsdal_data.transactions.decorators import async_transaction, transaction
2
+
3
+ @transaction
1
4
  def rollback_to_timestamp(timestamp: float) -> None:
2
5
  """
3
6
  Rollback the data to the given timestamp
@@ -6,6 +9,7 @@ def rollback_to_timestamp(timestamp: float) -> None:
6
9
  Returns:
7
10
  None
8
11
  """
12
+ @transaction
9
13
  def rollback_transaction(transaction_id: str) -> None:
10
14
  """
11
15
  Rollback the data to the point in time before the given transaction
@@ -14,6 +18,7 @@ def rollback_transaction(transaction_id: str) -> None:
14
18
  Returns:
15
19
  None
16
20
  """
21
+ @async_transaction
17
22
  async def async_rollback_to_timestamp(timestamp: float) -> None:
18
23
  """
19
24
  Rollback the data to the given timestamp
@@ -22,6 +27,7 @@ async def async_rollback_to_timestamp(timestamp: float) -> None:
22
27
  Returns:
23
28
  None
24
29
  """
30
+ @async_transaction
25
31
  async def async_rollback_transaction(transaction_id: str) -> None:
26
32
  """
27
33
  Rollback the data to the point in time before the given transaction
@@ -13,6 +13,4 @@ class StateOption(str, Enum):
13
13
 
14
14
  class LakehouseOption(str, Enum):
15
15
  postgres = 'postgres'
16
- postgres_immutable = 'postgres-immutable'
17
16
  sqlite = 'sqlite'
18
- sqlite_immutable = 'sqlite-immutable'