swiftpy-framework 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.
Files changed (211) hide show
  1. swiftpy/.gitignore +0 -0
  2. swiftpy/__init__.py +572 -0
  3. swiftpy/admin/__init__.py +50 -0
  4. swiftpy/admin/auth.py +522 -0
  5. swiftpy/admin/registry.py +567 -0
  6. swiftpy/admin/views.py +1583 -0
  7. swiftpy/ai/__init__.py +199 -0
  8. swiftpy/ai/_deterministic.py +40 -0
  9. swiftpy/ai/_fake.py +604 -0
  10. swiftpy/ai/agents/__init__.py +30 -0
  11. swiftpy/ai/agents/base_agent.py +506 -0
  12. swiftpy/ai/agents/memory.py +864 -0
  13. swiftpy/ai/client.py +1011 -0
  14. swiftpy/ai/conversation.py +171 -0
  15. swiftpy/ai/embeddings.py +321 -0
  16. swiftpy/ai/exceptions.py +75 -0
  17. swiftpy/ai/history_strategies.py +157 -0
  18. swiftpy/ai/middleware.py +918 -0
  19. swiftpy/ai/observability/__init__.py +28 -0
  20. swiftpy/ai/observability/drivers.py +763 -0
  21. swiftpy/ai/observability/observer.py +364 -0
  22. swiftpy/ai/pool.py +102 -0
  23. swiftpy/ai/pricing.py +101 -0
  24. swiftpy/ai/prompts/__init__.py +6 -0
  25. swiftpy/ai/prompts/loader.py +343 -0
  26. swiftpy/ai/providers/__init__.py +22 -0
  27. swiftpy/ai/providers/anthropic_provider.py +353 -0
  28. swiftpy/ai/providers/base.py +273 -0
  29. swiftpy/ai/providers/openai_provider.py +275 -0
  30. swiftpy/ai/rag/__init__.py +53 -0
  31. swiftpy/ai/rag/context_formatter.py +263 -0
  32. swiftpy/ai/rag/pipeline_builder.py +544 -0
  33. swiftpy/ai/rag/reranker.py +484 -0
  34. swiftpy/ai/rag/result.py +78 -0
  35. swiftpy/ai/rag/retriever.py +315 -0
  36. swiftpy/ai/redis_store.py +227 -0
  37. swiftpy/ai/streaming.py +115 -0
  38. swiftpy/ai/tools/__init__.py +22 -0
  39. swiftpy/ai/tools/base_tool.py +489 -0
  40. swiftpy/ai/tools/first_party.py +384 -0
  41. swiftpy/ai/tools/tool_loop.py +417 -0
  42. swiftpy/ai/vectorstore/__init__.py +46 -0
  43. swiftpy/ai/vectorstore/base.py +542 -0
  44. swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
  45. swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
  46. swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
  47. swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
  48. swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
  49. swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
  50. swiftpy/auth/__init__.py +50 -0
  51. swiftpy/auth/api_key_guard.py +544 -0
  52. swiftpy/auth/auth_manager.py +259 -0
  53. swiftpy/auth/exceptions.py +71 -0
  54. swiftpy/auth/jwt_guard.py +230 -0
  55. swiftpy/auth/jwt_issuer.py +658 -0
  56. swiftpy/auth/token_blacklist.py +300 -0
  57. swiftpy/cache/__init__.py +14 -0
  58. swiftpy/cache/cache.py +251 -0
  59. swiftpy/cache/driver.py +216 -0
  60. swiftpy/cache/drivers/memory.py +199 -0
  61. swiftpy/cache/drivers/redis.py +421 -0
  62. swiftpy/console/__init__.py +0 -0
  63. swiftpy/console/cli.py +68 -0
  64. swiftpy/console/commands/__init__.py +0 -0
  65. swiftpy/console/commands/cache.py +164 -0
  66. swiftpy/console/commands/make.py +935 -0
  67. swiftpy/console/commands/migrate.py +280 -0
  68. swiftpy/console/commands/new.py +1806 -0
  69. swiftpy/console/commands/queue.py +306 -0
  70. swiftpy/console/commands/seed.py +252 -0
  71. swiftpy/console/commands/start.py +171 -0
  72. swiftpy/console/commands/storage.py +104 -0
  73. swiftpy/console/loader.py +21 -0
  74. swiftpy/console/stubs/__init__.py +26 -0
  75. swiftpy/console/stubs/controller.stub +14 -0
  76. swiftpy/console/stubs/event.stub +14 -0
  77. swiftpy/console/stubs/job.stub +22 -0
  78. swiftpy/console/stubs/listener.stub +14 -0
  79. swiftpy/console/stubs/mail.stub +20 -0
  80. swiftpy/console/stubs/middleware.stub +16 -0
  81. swiftpy/console/stubs/migration.alter.stub +17 -0
  82. swiftpy/console/stubs/migration.api_keys.stub +22 -0
  83. swiftpy/console/stubs/migration.blank.stub +11 -0
  84. swiftpy/console/stubs/migration.create.stub +15 -0
  85. swiftpy/console/stubs/migration.stub +11 -0
  86. swiftpy/console/stubs/migration.update.stub +17 -0
  87. swiftpy/console/stubs/model.stub +17 -0
  88. swiftpy/console/stubs/provider.stub +18 -0
  89. swiftpy/console/stubs/repository.stub +10 -0
  90. swiftpy/console/stubs/request.stub +16 -0
  91. swiftpy/console/stubs/resource.stub +21 -0
  92. swiftpy/console/stubs/schema.stub +12 -0
  93. swiftpy/console/stubs/seeder.stub +14 -0
  94. swiftpy/console/stubs/service.stub +10 -0
  95. swiftpy/core/__init__.py +0 -0
  96. swiftpy/core/application.py +1306 -0
  97. swiftpy/core/bootstrap.py +30 -0
  98. swiftpy/core/config.py +432 -0
  99. swiftpy/core/container.py +596 -0
  100. swiftpy/core/container_exceptions.py +18 -0
  101. swiftpy/core/context.py +148 -0
  102. swiftpy/core/exceptions.py +352 -0
  103. swiftpy/core/providers/base.py +35 -0
  104. swiftpy/database/__init__.py +41 -0
  105. swiftpy/database/exceptions.py +42 -0
  106. swiftpy/database/identifiers.py +105 -0
  107. swiftpy/database/migrations/__init__.py +36 -0
  108. swiftpy/database/migrations/blueprint.py +431 -0
  109. swiftpy/database/migrations/lock.py +104 -0
  110. swiftpy/database/migrations/migration.py +22 -0
  111. swiftpy/database/migrations/migrator.py +507 -0
  112. swiftpy/database/migrations/schema.py +435 -0
  113. swiftpy/database/orm/__init__.py +33 -0
  114. swiftpy/database/orm/fields.py +92 -0
  115. swiftpy/database/orm/model.py +2034 -0
  116. swiftpy/database/orm/registry.py +33 -0
  117. swiftpy/database/orm/relationships.py +836 -0
  118. swiftpy/database/pagination/__init__.py +16 -0
  119. swiftpy/database/pagination/contracts.py +60 -0
  120. swiftpy/database/pagination/paginator.py +112 -0
  121. swiftpy/database/pagination/simple_paginator.py +97 -0
  122. swiftpy/database/pool.py +218 -0
  123. swiftpy/database/query_builder.py +1803 -0
  124. swiftpy/database/seeder.py +39 -0
  125. swiftpy/database/transaction.py +276 -0
  126. swiftpy/events/__init__.py +10 -0
  127. swiftpy/events/dispatcher.py +308 -0
  128. swiftpy/events/redis_bus.py +552 -0
  129. swiftpy/filesystem/__init__.py +227 -0
  130. swiftpy/filesystem/disk.py +378 -0
  131. swiftpy/filesystem/drivers/__init__.py +11 -0
  132. swiftpy/filesystem/drivers/base.py +156 -0
  133. swiftpy/filesystem/drivers/local.py +559 -0
  134. swiftpy/filesystem/drivers/s3.py +655 -0
  135. swiftpy/filesystem/events.py +37 -0
  136. swiftpy/filesystem/exceptions.py +127 -0
  137. swiftpy/filesystem/manager.py +138 -0
  138. swiftpy/filesystem/metadata.py +22 -0
  139. swiftpy/filesystem/paths.py +99 -0
  140. swiftpy/filesystem/service_provider.py +17 -0
  141. swiftpy/filesystem/uploads/__init__.py +13 -0
  142. swiftpy/filesystem/uploads/handlers.py +362 -0
  143. swiftpy/filesystem/uploads/uploaded_file.py +222 -0
  144. swiftpy/filesystem/uploads/validators.py +186 -0
  145. swiftpy/filesystem/visibility.py +22 -0
  146. swiftpy/helpers/__init__.py +42 -0
  147. swiftpy/helpers/helper.py +180 -0
  148. swiftpy/http/exceptions.py +41 -0
  149. swiftpy/http/middleware.py +307 -0
  150. swiftpy/http/request.py +375 -0
  151. swiftpy/http/response.py +442 -0
  152. swiftpy/http/router.py +1050 -0
  153. swiftpy/mail/__init__.py +58 -0
  154. swiftpy/mail/attachments.py +255 -0
  155. swiftpy/mail/config.py +71 -0
  156. swiftpy/mail/envelope.py +183 -0
  157. swiftpy/mail/exceptions.py +90 -0
  158. swiftpy/mail/facade.py +312 -0
  159. swiftpy/mail/mailable.py +53 -0
  160. swiftpy/mail/manager.py +240 -0
  161. swiftpy/mail/message.py +186 -0
  162. swiftpy/mail/models.py +19 -0
  163. swiftpy/mail/queue/__init__.py +5 -0
  164. swiftpy/mail/queue/job.py +279 -0
  165. swiftpy/mail/service_provider.py +19 -0
  166. swiftpy/mail/templates/__init__.py +5 -0
  167. swiftpy/mail/templates/renderer.py +173 -0
  168. swiftpy/mail/transports/__init__.py +21 -0
  169. swiftpy/mail/transports/api/__init__.py +13 -0
  170. swiftpy/mail/transports/api/base.py +194 -0
  171. swiftpy/mail/transports/api/mailgun.py +158 -0
  172. swiftpy/mail/transports/api/resend.py +156 -0
  173. swiftpy/mail/transports/api/sendgrid.py +172 -0
  174. swiftpy/mail/transports/base.py +24 -0
  175. swiftpy/mail/transports/console.py +91 -0
  176. swiftpy/mail/transports/sendmail.py +128 -0
  177. swiftpy/mail/transports/smtp.py +466 -0
  178. swiftpy/queue/__init__.py +13 -0
  179. swiftpy/queue/dispatcher.py +813 -0
  180. swiftpy/queue/job.py +541 -0
  181. swiftpy/queue/worker.py +556 -0
  182. swiftpy/support/__init__.py +12 -0
  183. swiftpy/support/keys.py +108 -0
  184. swiftpy/support/redis_config.py +223 -0
  185. swiftpy/support/strings.py +114 -0
  186. swiftpy/testing/__init__.py +20 -0
  187. swiftpy/testing/fakes/__init__.py +11 -0
  188. swiftpy/testing/fakes/ai_fake.py +21 -0
  189. swiftpy/testing/fakes/vectorstore_fake.py +194 -0
  190. swiftpy/testing/fixtures.py +124 -0
  191. swiftpy/testing/http_client.py +730 -0
  192. swiftpy/testing/test_case.py +126 -0
  193. swiftpy/validation/__init__.py +97 -0
  194. swiftpy/validation/exceptions.py +26 -0
  195. swiftpy/validation/form_request.py +302 -0
  196. swiftpy/validation/protocols.py +33 -0
  197. swiftpy/validation/rules/__init__.py +89 -0
  198. swiftpy/validation/rules/builtin.py +391 -0
  199. swiftpy/validation/rules/exists.py +86 -0
  200. swiftpy/validation/rules/registry.py +290 -0
  201. swiftpy/validation/rules/required.py +55 -0
  202. swiftpy/validation/rules/unique.py +138 -0
  203. swiftpy/validation/validator.py +612 -0
  204. swiftpy/websockets/__init__.py +19 -0
  205. swiftpy/websockets/connection.py +317 -0
  206. swiftpy/websockets/manager.py +271 -0
  207. swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
  208. swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
  209. swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
  210. swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
  211. swiftpy_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator, Callable
4
+ from datetime import datetime, timedelta
5
+ from typing import Any, BinaryIO
6
+
7
+ from swiftpy.filesystem.disk import StorageDisk
8
+ from swiftpy.filesystem.drivers.base import StorageDriver
9
+ from swiftpy.filesystem.drivers.local import LocalStorageDriver
10
+ from swiftpy.filesystem.drivers.s3 import S3StorageDriver
11
+ from swiftpy.filesystem.manager import FilesystemManager
12
+ from swiftpy.filesystem.metadata import FileMetadata
13
+ from swiftpy.filesystem.paths import PathNormalizer
14
+ from swiftpy.filesystem.uploads.uploaded_file import UploadedFile
15
+ from swiftpy.filesystem.visibility import Visibility
16
+ from swiftpy.http.response import StreamingResponse
17
+
18
+
19
+ class Storage:
20
+ """
21
+ Class-level facade for SwiftPY Filesystem & Storage.
22
+ Delegates all operations to the default or explicitly named StorageDisk.
23
+ """
24
+
25
+ @classmethod
26
+ def _manager(cls) -> FilesystemManager:
27
+ return FilesystemManager.get_instance()
28
+
29
+ @classmethod
30
+ def disk(cls, name: str | None = None) -> StorageDisk:
31
+ """Resolve a configured storage disk instance."""
32
+ return cls._manager().disk(name)
33
+
34
+ @classmethod
35
+ def extend(
36
+ cls,
37
+ driver_name: str,
38
+ factory: Callable[[dict[str, Any]], StorageDriver],
39
+ ) -> None:
40
+ """Register a custom driver creator."""
41
+ cls._manager().extend(driver_name, factory)
42
+
43
+ @classmethod
44
+ def build(cls, config: dict[str, Any], name: str = "on_demand") -> StorageDisk:
45
+ """Build an on-demand, unmanaged storage disk."""
46
+ return cls._manager().build(config, name=name)
47
+
48
+ @classmethod
49
+ async def put(
50
+ cls,
51
+ path: str,
52
+ content: bytes | str,
53
+ visibility: Visibility | str | None = None,
54
+ ) -> int:
55
+ return await cls.disk().put(path, content, visibility=visibility)
56
+
57
+ @classmethod
58
+ async def put_file(
59
+ cls,
60
+ directory: str,
61
+ file: UploadedFile,
62
+ visibility: Visibility | str | None = None,
63
+ ) -> str:
64
+ return await cls.disk().put_file(directory, file, visibility=visibility)
65
+
66
+ @classmethod
67
+ async def put_file_as(
68
+ cls,
69
+ directory: str,
70
+ file: UploadedFile,
71
+ name: str,
72
+ visibility: Visibility | str | None = None,
73
+ ) -> str:
74
+ return await cls.disk().put_file_as(directory, file, name, visibility=visibility)
75
+
76
+ @classmethod
77
+ async def write_stream(
78
+ cls,
79
+ path: str,
80
+ stream: AsyncIterator[bytes] | BinaryIO,
81
+ chunk_size: int = 64 * 1024,
82
+ visibility: Visibility | str | None = None,
83
+ ) -> int:
84
+ return await cls.disk().write_stream(
85
+ path, stream, chunk_size=chunk_size, visibility=visibility
86
+ )
87
+
88
+ @classmethod
89
+ async def get(cls, path: str) -> bytes:
90
+ return await cls.disk().get(path)
91
+
92
+ @classmethod
93
+ async def get_text(cls, path: str, encoding: str = "utf-8") -> str:
94
+ return await cls.disk().get_text(path, encoding=encoding)
95
+
96
+ @classmethod
97
+ async def read(cls, path: str) -> bytes:
98
+ return await cls.disk().read(path)
99
+
100
+ @classmethod
101
+ async def read_text(cls, path: str, encoding: str = "utf-8") -> str:
102
+ return await cls.disk().read_text(path, encoding=encoding)
103
+
104
+ @classmethod
105
+ def read_stream(
106
+ cls,
107
+ path: str,
108
+ chunk_size: int = 64 * 1024,
109
+ ) -> AsyncIterator[bytes]:
110
+ return cls.disk().read_stream(path, chunk_size=chunk_size)
111
+
112
+ @classmethod
113
+ def stream(
114
+ cls,
115
+ path: str,
116
+ chunk_size: int = 64 * 1024,
117
+ ) -> AsyncIterator[bytes]:
118
+ return cls.disk().stream(path, chunk_size=chunk_size)
119
+
120
+ @classmethod
121
+ async def exists(cls, path: str) -> bool:
122
+ return await cls.disk().exists(path)
123
+
124
+ @classmethod
125
+ async def missing(cls, path: str) -> bool:
126
+ return await cls.disk().missing(path)
127
+
128
+ @classmethod
129
+ async def delete(cls, path: str) -> bool:
130
+ return await cls.disk().delete(path)
131
+
132
+ @classmethod
133
+ async def copy(cls, source: str, destination: str) -> bool:
134
+ return await cls.disk().copy(source, destination)
135
+
136
+ @classmethod
137
+ async def move(cls, source: str, destination: str) -> bool:
138
+ return await cls.disk().move(source, destination)
139
+
140
+ @classmethod
141
+ async def size(cls, path: str) -> int:
142
+ return await cls.disk().size(path)
143
+
144
+ @classmethod
145
+ async def mime_type(cls, path: str) -> str:
146
+ return await cls.disk().mime_type(path)
147
+
148
+ @classmethod
149
+ async def last_modified(cls, path: str) -> datetime:
150
+ return await cls.disk().last_modified(path)
151
+
152
+ @classmethod
153
+ async def checksum(cls, path: str, algorithm: str = "sha256") -> str:
154
+ return await cls.disk().checksum(path, algorithm=algorithm)
155
+
156
+ @classmethod
157
+ async def files(cls, directory: str = "", recursive: bool = False) -> list[str]:
158
+ return await cls.disk().files(directory, recursive=recursive)
159
+
160
+ @classmethod
161
+ async def directories(cls, directory: str = "", recursive: bool = False) -> list[str]:
162
+ return await cls.disk().directories(directory, recursive=recursive)
163
+
164
+ @classmethod
165
+ async def make_directory(cls, directory: str) -> bool:
166
+ return await cls.disk().make_directory(directory)
167
+
168
+ @classmethod
169
+ async def delete_directory(cls, directory: str) -> bool:
170
+ return await cls.disk().delete_directory(directory)
171
+
172
+ @classmethod
173
+ async def url(cls, path: str) -> str:
174
+ return await cls.disk().url(path)
175
+
176
+ @classmethod
177
+ async def temporary_url(
178
+ cls,
179
+ path: str,
180
+ expires_in: int | timedelta,
181
+ ) -> str:
182
+ return await cls.disk().temporary_url(path, expires_in=expires_in)
183
+
184
+ @classmethod
185
+ def verify_temporary_url(
186
+ cls,
187
+ signed_url: str,
188
+ *,
189
+ method: str = "GET",
190
+ now: float | None = None,
191
+ ) -> str:
192
+ return cls.disk().verify_temporary_url(signed_url, method=method, now=now)
193
+
194
+ @classmethod
195
+ async def get_visibility(cls, path: str) -> Visibility:
196
+ return await cls.disk().get_visibility(path)
197
+
198
+ @classmethod
199
+ async def set_visibility(cls, path: str, visibility: Visibility | str) -> bool:
200
+ return await cls.disk().set_visibility(path, visibility=visibility)
201
+
202
+ @classmethod
203
+ async def metadata(cls, path: str) -> FileMetadata:
204
+ return await cls.disk().metadata(path)
205
+
206
+ @classmethod
207
+ async def download(
208
+ cls,
209
+ path: str,
210
+ name: str | None = None,
211
+ headers: dict[str, str] | None = None,
212
+ ) -> StreamingResponse:
213
+ return await cls.disk().download(path, name=name, headers=headers)
214
+
215
+
216
+ __all__ = [
217
+ "FileMetadata",
218
+ "FilesystemManager",
219
+ "LocalStorageDriver",
220
+ "PathNormalizer",
221
+ "S3StorageDriver",
222
+ "Storage",
223
+ "StorageDisk",
224
+ "StorageDriver",
225
+ "UploadedFile",
226
+ "Visibility",
227
+ ]
@@ -0,0 +1,378 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from collections.abc import AsyncIterator
5
+ from datetime import datetime, timedelta
6
+ from typing import TYPE_CHECKING, Any, BinaryIO
7
+ from urllib.parse import quote
8
+
9
+ from swiftpy.core.container import get_container
10
+ from swiftpy.events.dispatcher import EventDispatcher
11
+ from swiftpy.filesystem.drivers.base import StorageDriver
12
+ from swiftpy.filesystem.events import FileDeleted, FileMoved, FileUploaded
13
+ from swiftpy.filesystem.exceptions import (
14
+ DriverNotSupportedError,
15
+ ReadOnlyFileSystemError,
16
+ )
17
+ from swiftpy.filesystem.metadata import FileMetadata
18
+ from swiftpy.filesystem.paths import PathNormalizer
19
+ from swiftpy.filesystem.visibility import Visibility
20
+ from swiftpy.http.response import StreamingResponse
21
+
22
+ if TYPE_CHECKING:
23
+ from swiftpy.filesystem.uploads.uploaded_file import UploadedFile
24
+
25
+
26
+ def _format_content_disposition(filename: str) -> str:
27
+ """Build an RFC-safe Content-Disposition attachment header escaping quotes and stripping control bytes."""
28
+ raw = os.path.basename(filename.replace("\\", "/"))
29
+ cleaned = "".join(ch for ch in raw if ord(ch) >= 32 and ord(ch) != 127)
30
+ if not cleaned:
31
+ cleaned = "download"
32
+ ascii_name = cleaned.encode("ascii", "ignore").decode("ascii").strip() or "download"
33
+ escaped_ascii = ascii_name.replace("\\", "\\\\").replace('"', '\\"')
34
+ if cleaned != ascii_name:
35
+ encoded_utf8 = quote(cleaned, safe="")
36
+ return f'attachment; filename="{escaped_ascii}"; filename*=UTF-8\'\'{encoded_utf8}'
37
+ return f'attachment; filename="{escaped_ascii}"'
38
+
39
+
40
+ class StorageDisk:
41
+ """
42
+ Public API wrapper for a configured storage disk.
43
+ Applies path normalization, read-only guards, prefix scoping,
44
+ and storage lifecycle events.
45
+ """
46
+
47
+ def __init__(
48
+ self,
49
+ name: str,
50
+ driver: StorageDriver,
51
+ *,
52
+ read_only: bool = False,
53
+ prefix: str = "",
54
+ ) -> None:
55
+ self.name = name
56
+ self.driver = driver
57
+ self.read_only = read_only
58
+ self.prefix = PathNormalizer.normalize(prefix) if prefix else ""
59
+
60
+ def _apply_prefix(self, path: str) -> str:
61
+ clean = PathNormalizer.normalize(path)
62
+ if not self.prefix:
63
+ return clean
64
+ if not clean:
65
+ return self.prefix
66
+ return f"{self.prefix}/{clean}"
67
+
68
+ def _strip_prefix(self, path: str) -> str:
69
+ clean = PathNormalizer.normalize(path)
70
+ if not self.prefix:
71
+ return clean
72
+ if clean == self.prefix:
73
+ return ""
74
+ if clean.startswith(f"{self.prefix}/"):
75
+ return clean[len(self.prefix) + 1 :]
76
+ return clean
77
+
78
+ def _check_writable(self) -> None:
79
+ if self.read_only:
80
+ raise ReadOnlyFileSystemError(
81
+ f"Storage disk '{self.name}' is configured as read-only."
82
+ )
83
+
84
+ async def _dispatch_event(self, event_instance: Any) -> None:
85
+ try:
86
+ container = get_container()
87
+ if container.has(EventDispatcher):
88
+ dispatcher = container.resolve(EventDispatcher)
89
+ event_name = getattr(
90
+ event_instance, "EVENT_NAME", event_instance.__class__.__name__
91
+ )
92
+ await dispatcher.dispatch(event_name, event_instance)
93
+ except (RuntimeError, KeyError, AttributeError):
94
+ # Events should not abort underlying storage operation if dispatcher not bound
95
+ pass
96
+
97
+ async def put(
98
+ self,
99
+ path: str,
100
+ content: bytes | str,
101
+ visibility: Visibility | str | None = None,
102
+ ) -> int:
103
+ self._check_writable()
104
+ target = self._apply_prefix(path)
105
+ bytes_written = await self.driver.write(target, content, visibility=visibility)
106
+
107
+ clean_path = PathNormalizer.normalize(path)
108
+ mime = await self.driver.mime_type(target)
109
+ await self._dispatch_event(
110
+ FileUploaded(
111
+ disk=self.name,
112
+ path=clean_path,
113
+ size=bytes_written,
114
+ mime_type=mime,
115
+ )
116
+ )
117
+ return bytes_written
118
+
119
+ async def put_file(
120
+ self,
121
+ directory: str,
122
+ file: UploadedFile,
123
+ visibility: Visibility | str | None = None,
124
+ ) -> str:
125
+ name = PathNormalizer.generate_name(file.filename)
126
+ return await self.put_file_as(directory, file, name, visibility=visibility)
127
+
128
+ async def put_file_as(
129
+ self,
130
+ directory: str,
131
+ file: UploadedFile,
132
+ name: str,
133
+ visibility: Visibility | str | None = None,
134
+ ) -> str:
135
+ self._check_writable()
136
+ clean_dir = PathNormalizer.normalize(directory) if directory else ""
137
+ clean_name = PathNormalizer.normalize(name)
138
+ relative_path = f"{clean_dir}/{clean_name}" if clean_dir else clean_name
139
+
140
+ target = self._apply_prefix(relative_path)
141
+ await file.seek(0)
142
+ bytes_written = await self.driver.write_stream(
143
+ target, file.chunks(), visibility=visibility
144
+ )
145
+
146
+ mime = file.content_type or await self.driver.mime_type(target)
147
+ await self._dispatch_event(
148
+ FileUploaded(
149
+ disk=self.name,
150
+ path=relative_path,
151
+ size=bytes_written,
152
+ mime_type=mime,
153
+ )
154
+ )
155
+ return relative_path
156
+
157
+ async def write_stream(
158
+ self,
159
+ path: str,
160
+ stream: AsyncIterator[bytes] | BinaryIO,
161
+ chunk_size: int = 64 * 1024,
162
+ visibility: Visibility | str | None = None,
163
+ ) -> int:
164
+ self._check_writable()
165
+ target = self._apply_prefix(path)
166
+ bytes_written = await self.driver.write_stream(
167
+ target, stream, chunk_size=chunk_size, visibility=visibility
168
+ )
169
+ clean_path = PathNormalizer.normalize(path)
170
+ mime = await self.driver.mime_type(target)
171
+ await self._dispatch_event(
172
+ FileUploaded(
173
+ disk=self.name,
174
+ path=clean_path,
175
+ size=bytes_written,
176
+ mime_type=mime,
177
+ )
178
+ )
179
+ return bytes_written
180
+
181
+ async def get(self, path: str) -> bytes:
182
+ target = self._apply_prefix(path)
183
+ return await self.driver.read(target)
184
+
185
+ async def get_text(self, path: str, encoding: str = "utf-8") -> str:
186
+ target = self._apply_prefix(path)
187
+ return await self.driver.read_text(target, encoding=encoding)
188
+
189
+ async def read(self, path: str) -> bytes:
190
+ """Alias for get()."""
191
+ return await self.get(path)
192
+
193
+ async def read_text(self, path: str, encoding: str = "utf-8") -> str:
194
+ """Alias for get_text()."""
195
+ return await self.get_text(path, encoding=encoding)
196
+
197
+ def read_stream(
198
+ self,
199
+ path: str,
200
+ chunk_size: int = 64 * 1024,
201
+ ) -> AsyncIterator[bytes]:
202
+ target = self._apply_prefix(path)
203
+ return self.driver.read_stream(target, chunk_size=chunk_size)
204
+
205
+ def stream(
206
+ self,
207
+ path: str,
208
+ chunk_size: int = 64 * 1024,
209
+ ) -> AsyncIterator[bytes]:
210
+ """Alias for read_stream."""
211
+ return self.read_stream(path, chunk_size=chunk_size)
212
+
213
+ async def exists(self, path: str) -> bool:
214
+ target = self._apply_prefix(path)
215
+ return await self.driver.exists(target)
216
+
217
+ async def missing(self, path: str) -> bool:
218
+ target = self._apply_prefix(path)
219
+ return await self.driver.missing(target)
220
+
221
+ async def delete(self, path: str) -> bool:
222
+ self._check_writable()
223
+ target = self._apply_prefix(path)
224
+ deleted = await self.driver.delete(target)
225
+ if deleted:
226
+ clean_path = PathNormalizer.normalize(path)
227
+ await self._dispatch_event(FileDeleted(disk=self.name, path=clean_path))
228
+ return deleted
229
+
230
+ async def copy(self, source: str, destination: str) -> bool:
231
+ self._check_writable()
232
+ src = self._apply_prefix(source)
233
+ dst = self._apply_prefix(destination)
234
+ copied = await self.driver.copy(src, dst)
235
+ if copied:
236
+ size = await self.driver.size(dst)
237
+ mime = await self.driver.mime_type(dst)
238
+ await self._dispatch_event(
239
+ FileUploaded(
240
+ disk=self.name,
241
+ path=PathNormalizer.normalize(destination),
242
+ size=size,
243
+ mime_type=mime,
244
+ )
245
+ )
246
+ return copied
247
+
248
+ async def move(self, source: str, destination: str) -> bool:
249
+ self._check_writable()
250
+ src = self._apply_prefix(source)
251
+ dst = self._apply_prefix(destination)
252
+ moved = await self.driver.move(src, dst)
253
+ if moved:
254
+ clean_src = PathNormalizer.normalize(source)
255
+ clean_dst = PathNormalizer.normalize(destination)
256
+ await self._dispatch_event(
257
+ FileMoved(disk=self.name, source=clean_src, destination=clean_dst)
258
+ )
259
+ return moved
260
+
261
+ async def size(self, path: str) -> int:
262
+ target = self._apply_prefix(path)
263
+ return await self.driver.size(target)
264
+
265
+ async def mime_type(self, path: str) -> str:
266
+ target = self._apply_prefix(path)
267
+ return await self.driver.mime_type(target)
268
+
269
+ async def last_modified(self, path: str) -> datetime:
270
+ target = self._apply_prefix(path)
271
+ return await self.driver.last_modified(target)
272
+
273
+ async def checksum(self, path: str, algorithm: str = "sha256") -> str:
274
+ target = self._apply_prefix(path)
275
+ return await self.driver.checksum(target, algorithm=algorithm)
276
+
277
+ async def files(self, directory: str = "", recursive: bool = False) -> list[str]:
278
+ target = self._apply_prefix(directory)
279
+ raw_files = await self.driver.files(target, recursive=recursive)
280
+ return [self._strip_prefix(f) for f in raw_files]
281
+
282
+ async def directories(self, directory: str = "", recursive: bool = False) -> list[str]:
283
+ target = self._apply_prefix(directory)
284
+ raw_dirs = await self.driver.directories(target, recursive=recursive)
285
+ return [self._strip_prefix(d) for d in raw_dirs]
286
+
287
+ async def make_directory(self, directory: str) -> bool:
288
+ self._check_writable()
289
+ target = self._apply_prefix(directory)
290
+ return await self.driver.make_directory(target)
291
+
292
+ async def delete_directory(self, directory: str) -> bool:
293
+ self._check_writable()
294
+ target = self._apply_prefix(directory)
295
+ return await self.driver.delete_directory(target)
296
+
297
+ async def url(self, path: str) -> str:
298
+ target = self._apply_prefix(path)
299
+ return await self.driver.url(target)
300
+
301
+ async def temporary_url(
302
+ self,
303
+ path: str,
304
+ expires_in: int | timedelta,
305
+ ) -> str:
306
+ target = self._apply_prefix(path)
307
+ return await self.driver.temporary_url(target, expires_in=expires_in)
308
+
309
+ def verify_temporary_url(
310
+ self,
311
+ signed_url: str,
312
+ *,
313
+ method: str = "GET",
314
+ now: float | None = None,
315
+ ) -> str:
316
+ verifier = getattr(self.driver, "verify_temporary_url", None)
317
+ if not callable(verifier):
318
+ raise DriverNotSupportedError(
319
+ f"Storage driver '{type(self.driver).__name__}' does not support temporary URL verification."
320
+ )
321
+ verified_path = verifier(signed_url, method=method, now=now)
322
+ if not isinstance(verified_path, str):
323
+ raise TypeError(
324
+ f"Expected verify_temporary_url to return str path, got {type(verified_path).__name__}."
325
+ )
326
+ return self._strip_prefix(verified_path)
327
+
328
+ async def get_visibility(self, path: str) -> Visibility:
329
+ target = self._apply_prefix(path)
330
+ return await self.driver.get_visibility(target)
331
+
332
+ async def set_visibility(self, path: str, visibility: Visibility | str) -> bool:
333
+ self._check_writable()
334
+ target = self._apply_prefix(path)
335
+ return await self.driver.set_visibility(target, visibility=visibility)
336
+
337
+ async def metadata(self, path: str) -> FileMetadata:
338
+ target = self._apply_prefix(path)
339
+ meta = await self.driver.metadata(target)
340
+ return FileMetadata(
341
+ path=self._strip_prefix(meta.path),
342
+ size=meta.size,
343
+ mime_type=meta.mime_type,
344
+ last_modified=meta.last_modified,
345
+ visibility=meta.visibility,
346
+ etag=meta.etag,
347
+ checksum=meta.checksum,
348
+ extra=meta.extra,
349
+ )
350
+
351
+ async def download(
352
+ self,
353
+ path: str,
354
+ name: str | None = None,
355
+ headers: dict[str, str] | None = None,
356
+ ) -> StreamingResponse:
357
+ """
358
+ Generate a StreamingResponse for downloading the file.
359
+ Sets Content-Disposition attachment and detected Content-Type.
360
+ """
361
+ clean_path = PathNormalizer.normalize(path)
362
+ filename = name or os.path.basename(clean_path)
363
+ mime = await self.mime_type(path)
364
+ file_size = await self.size(path)
365
+
366
+ resp_headers: dict[str, str] = {
367
+ "content-type": mime,
368
+ "content-disposition": _format_content_disposition(filename),
369
+ "content-length": str(file_size),
370
+ }
371
+ if headers:
372
+ resp_headers.update(headers)
373
+
374
+ return StreamingResponse(
375
+ iterator=self.read_stream(path),
376
+ status_code=200,
377
+ headers=resp_headers,
378
+ )
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from swiftpy.filesystem.drivers.base import StorageDriver
4
+ from swiftpy.filesystem.drivers.local import LocalStorageDriver
5
+ from swiftpy.filesystem.drivers.s3 import S3StorageDriver
6
+
7
+ __all__ = [
8
+ "LocalStorageDriver",
9
+ "S3StorageDriver",
10
+ "StorageDriver",
11
+ ]