amsdal_storages 0.1.0__py3-none-any.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.
@@ -0,0 +1 @@
1
+ __version__ = '0.1.0'
File without changes
File without changes
File without changes
@@ -0,0 +1,201 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import os
5
+ from contextlib import asynccontextmanager
6
+ from typing import IO
7
+ from typing import Any
8
+ from typing import BinaryIO
9
+
10
+ from amsdal_models.storage.backends.db import AsyncFileWrapper
11
+ from amsdal_models.storage.base import Storage
12
+ from amsdal_models.storage.errors import ConfigurationError
13
+ from amsdal_models.storage.errors import StorageError
14
+ from amsdal_models.storage.types import FileProtocol
15
+ from amsdal_utils.config.manager import AmsdalConfigManager
16
+
17
+
18
+ class S3Storage(Storage):
19
+ """S3-backed storage implementation.
20
+
21
+ Sync methods use boto3. Async methods use aioboto3.
22
+ """
23
+
24
+ keeps_local_copy = False
25
+
26
+ def __init__(
27
+ self,
28
+ object_prefix: str = '',
29
+ presign_ttl: int = 3600,
30
+ ) -> None:
31
+ self.bucket = os.environ.get('AWS_S3_BUCKET_NAME')
32
+ self.region_name = os.environ.get('AWS_S3_REGION_NAME') or os.environ.get('AWS_REGION')
33
+ self.endpoint_url = os.environ.get('AWS_S3_ENDPOINT_URL')
34
+ self.access_key_id = os.environ.get('AWS_S3_ACCESS_KEY_ID') or os.environ.get('AWS_ACCESS_KEY_ID')
35
+ self.secret_access_key = os.environ.get('AWS_S3_SECRET_ACCESS_KEY') or os.environ.get('AWS_SECRET_ACCESS_KEY')
36
+ self.security_token = os.environ.get('AWS_SESSION_TOKEN') or os.environ.get('AWS_SECURITY_TOKEN')
37
+ self.object_prefix = object_prefix.strip('/')
38
+ self.presign_ttl = presign_ttl
39
+
40
+ if not self.bucket:
41
+ msg = 'S3Storage requires a bucket name'
42
+ raise ConfigurationError(msg)
43
+
44
+ try:
45
+ if AmsdalConfigManager().get_config().async_mode:
46
+ import aioboto3 # noqa: F401
47
+ else:
48
+ import boto3 # noqa: F401
49
+ except ImportError:
50
+ msg = 'S3 dependencies are missing. Install it with `pip install amsdal_storages[s3]`.'
51
+ raise ConfigurationError(msg) from None
52
+
53
+ @property
54
+ def _s3(self) -> Any:
55
+ import boto3
56
+
57
+ return boto3.client(
58
+ 's3',
59
+ region_name=self.region_name,
60
+ endpoint_url=self.endpoint_url,
61
+ aws_access_key_id=self.access_key_id,
62
+ aws_secret_access_key=self.secret_access_key,
63
+ aws_session_token=self.security_token,
64
+ )
65
+
66
+ @asynccontextmanager
67
+ async def _async_s3(self) -> Any:
68
+ import aioboto3
69
+
70
+ session = aioboto3.Session()
71
+ async with session.client(
72
+ 's3',
73
+ region_name=self.region_name,
74
+ endpoint_url=self.endpoint_url,
75
+ aws_access_key_id=self.access_key_id,
76
+ aws_secret_access_key=self.secret_access_key,
77
+ aws_session_token=self.security_token,
78
+ ) as client:
79
+ yield client
80
+
81
+ def _key(self, file: FileProtocol) -> str:
82
+ base = file.filename.lstrip('/')
83
+ return f'{self.object_prefix}/{base}' if self.object_prefix else base
84
+
85
+ def _export_kwargs(self) -> dict[str, Any]:
86
+ return {
87
+ 'object_prefix': self.object_prefix,
88
+ }
89
+
90
+ def save(self, file: FileProtocol, content: BinaryIO) -> str:
91
+ import botocore.exceptions
92
+
93
+ key = self._key(file)
94
+
95
+ try:
96
+ self._s3.upload_fileobj(content, self.bucket, key)
97
+ except botocore.exceptions.ClientError as e:
98
+ msg = f'Failed to upload to s3://{self.bucket}/{key}: {e}'
99
+ raise StorageError(msg) from e
100
+ return key
101
+
102
+ def open(self, file: FileProtocol, mode: str = 'rb') -> IO[Any]: # noqa: ARG002
103
+ import botocore.exceptions
104
+
105
+ key = self._key(file)
106
+
107
+ try:
108
+ obj = self._s3.get_object(Bucket=self.bucket, Key=key)
109
+ except botocore.exceptions.ClientError as e:
110
+ msg = f'Failed to open s3://{self.bucket}/{key}: {e}'
111
+ raise StorageError(msg) from e
112
+ body = obj['Body']
113
+
114
+ return body # StreamingBody implements a file-like interface
115
+
116
+ def delete(self, file: FileProtocol) -> None:
117
+ import botocore.exceptions
118
+
119
+ key = self._key(file)
120
+
121
+ try:
122
+ self._s3.delete_object(Bucket=self.bucket, Key=key)
123
+ except botocore.exceptions.ClientError as e:
124
+ msg = f'Failed to delete s3://{self.bucket}/{key}: {e}'
125
+ raise StorageError(msg) from e
126
+
127
+ def exists(self, file: FileProtocol) -> bool:
128
+ import botocore.exceptions
129
+
130
+ key = self._key(file)
131
+
132
+ try:
133
+ self._s3.head_object(Bucket=self.bucket, Key=key)
134
+ return True
135
+ except botocore.exceptions.ClientError as e:
136
+ code = e.response.get('Error', {}).get('Code')
137
+ if code in {'404', 'NotFound', 'NoSuchKey'}:
138
+ return False
139
+ raise
140
+
141
+ def url(self, file: FileProtocol) -> str:
142
+ key = self._key(file)
143
+
144
+ # fall back to presigned URL
145
+ return self._s3.generate_presigned_url(
146
+ 'get_object',
147
+ Params={'Bucket': self.bucket, 'Key': key},
148
+ ExpiresIn=self.presign_ttl,
149
+ )
150
+
151
+ async def asave(self, file: FileProtocol, content: BinaryIO) -> str:
152
+ key = self._key(file)
153
+
154
+ async with self._async_s3() as client:
155
+ try:
156
+ await client.upload_fileobj(content, self.bucket, key)
157
+ except Exception as e: # pragma: no cover - network dependent
158
+ msg = f'Failed to upload to s3://{self.bucket}/{key}: {e}'
159
+ raise StorageError(msg) from e
160
+ return key
161
+
162
+ async def aopen(self, file: FileProtocol, mode: str = 'rb') -> AsyncFileWrapper: # noqa: ARG002
163
+ key = self._key(file)
164
+
165
+ async with self._async_s3() as client:
166
+ try:
167
+ obj = await client.get_object(Bucket=self.bucket, Key=key)
168
+ body = await obj['Body'].read()
169
+ except Exception as e: # pragma: no cover
170
+ await client.__aexit__(None, None, None)
171
+ msg = f'Failed to open s3://{self.bucket}/{key}: {e}'
172
+ raise StorageError(msg) from e
173
+ return AsyncFileWrapper(io.BytesIO(body))
174
+
175
+ async def adelete(self, file: FileProtocol) -> None:
176
+ key = self._key(file)
177
+
178
+ async with self._async_s3() as client:
179
+ await client.delete_object(Bucket=self.bucket, Key=key)
180
+
181
+ async def aexists(self, file: FileProtocol) -> bool:
182
+ key = self._key(file)
183
+
184
+ async with self._async_s3() as client:
185
+ try:
186
+ await client.head_object(Bucket=self.bucket, Key=key)
187
+ return True
188
+ except Exception as e: # pragma: no cover
189
+ # aiobotocore raises ClientError similarly; detect 404-ish
190
+ msg = getattr(e, 'response', {}).get('Error', {}).get('Code') if hasattr(e, 'response') else None
191
+ if msg in {'404', 'NotFound', 'NoSuchKey'}:
192
+ return False
193
+ raise
194
+
195
+ async def aurl(self, file: FileProtocol) -> str:
196
+ key = self._key(file)
197
+
198
+ async with self._async_s3() as client:
199
+ return await client.generate_presigned_url(
200
+ 'get_object', Params={'Bucket': self.bucket, 'Key': key}, ExpiresIn=self.presign_ttl
201
+ )
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: amsdal_storages
3
+ Version: 0.1.0
4
+ Summary: amsdal_storages plugin for AMSDAL Framework
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: amsdal-models>=0.5.16
7
+ Requires-Dist: amsdal[cli]==0.5.10
8
+ Provides-Extra: s3
9
+ Requires-Dist: aioboto3>=11.0.0; extra == 's3'
10
+ Requires-Dist: boto3; extra == 's3'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # amsdal_storages
14
+
15
+ This plugin provides implementations of the Storage backends for AMSDAL.
16
+
17
+ ## Plugin Structure
18
+
19
+ - `pyproject.toml` - Plugin configuration file
20
+ - `config.yml` - Configuration for connections
21
+
22
+ ## Installing this Plugin
23
+
24
+ To use this plugin in an AMSDAL application:
25
+
26
+ 1. Copy the plugin directory to your AMSDAL application
27
+ 2. Import the models and transactions as needed
28
+ 3. Register the plugin in your application configuration
29
+
30
+ ## Development
31
+
32
+ This plugin uses sync mode.
33
+
34
+ ### Adding Models
35
+
36
+ ```bash
37
+ amsdal generate model ModelName --format py
38
+ ```
39
+
40
+ ### Adding Properties
41
+
42
+ ```bash
43
+ amsdal generate property --model ModelName property_name
44
+ ```
45
+
46
+ ### Adding Transactions
47
+
48
+ ```bash
49
+ amsdal generate transaction TransactionName
50
+ ```
51
+
52
+ ### Adding Hooks
53
+
54
+ ```bash
55
+ amsdal generate hook --model ModelName on_create
56
+ ```
57
+
58
+ ## Testing
59
+
60
+ Test your plugin by integrating it with an AMSDAL application and running the application's test suite.
@@ -0,0 +1,9 @@
1
+ amsdal_storages/Third-Party Materials - AMSDAL Dependencies - License Notices.md,sha256=uHJlGG0D4tbpUi8cq-497NNO9ltQ67a5448k-T14HTw,68241
2
+ amsdal_storages/__about__.py,sha256=IMjkMO3twhQzluVTo8Z6rE7Eg-9U79_LGKMcsWLKBkY,22
3
+ amsdal_storages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ amsdal_storages/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ amsdal_storages/s3/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ amsdal_storages/s3/storage.py,sha256=8rccyJOLdl6uhVb-VkRZ4eeZpWhm6jjo8SNY9exnHx4,7196
7
+ amsdal_storages-0.1.0.dist-info/METADATA,sha256=J66_rv0_gS_GI2S4sFOSL9C1nFgNQz9ARNudPAF8xv8,1286
8
+ amsdal_storages-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ amsdal_storages-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any