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.
- swiftpy/.gitignore +0 -0
- swiftpy/__init__.py +572 -0
- swiftpy/admin/__init__.py +50 -0
- swiftpy/admin/auth.py +522 -0
- swiftpy/admin/registry.py +567 -0
- swiftpy/admin/views.py +1583 -0
- swiftpy/ai/__init__.py +199 -0
- swiftpy/ai/_deterministic.py +40 -0
- swiftpy/ai/_fake.py +604 -0
- swiftpy/ai/agents/__init__.py +30 -0
- swiftpy/ai/agents/base_agent.py +506 -0
- swiftpy/ai/agents/memory.py +864 -0
- swiftpy/ai/client.py +1011 -0
- swiftpy/ai/conversation.py +171 -0
- swiftpy/ai/embeddings.py +321 -0
- swiftpy/ai/exceptions.py +75 -0
- swiftpy/ai/history_strategies.py +157 -0
- swiftpy/ai/middleware.py +918 -0
- swiftpy/ai/observability/__init__.py +28 -0
- swiftpy/ai/observability/drivers.py +763 -0
- swiftpy/ai/observability/observer.py +364 -0
- swiftpy/ai/pool.py +102 -0
- swiftpy/ai/pricing.py +101 -0
- swiftpy/ai/prompts/__init__.py +6 -0
- swiftpy/ai/prompts/loader.py +343 -0
- swiftpy/ai/providers/__init__.py +22 -0
- swiftpy/ai/providers/anthropic_provider.py +353 -0
- swiftpy/ai/providers/base.py +273 -0
- swiftpy/ai/providers/openai_provider.py +275 -0
- swiftpy/ai/rag/__init__.py +53 -0
- swiftpy/ai/rag/context_formatter.py +263 -0
- swiftpy/ai/rag/pipeline_builder.py +544 -0
- swiftpy/ai/rag/reranker.py +484 -0
- swiftpy/ai/rag/result.py +78 -0
- swiftpy/ai/rag/retriever.py +315 -0
- swiftpy/ai/redis_store.py +227 -0
- swiftpy/ai/streaming.py +115 -0
- swiftpy/ai/tools/__init__.py +22 -0
- swiftpy/ai/tools/base_tool.py +489 -0
- swiftpy/ai/tools/first_party.py +384 -0
- swiftpy/ai/tools/tool_loop.py +417 -0
- swiftpy/ai/vectorstore/__init__.py +46 -0
- swiftpy/ai/vectorstore/base.py +542 -0
- swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
- swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
- swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
- swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
- swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
- swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
- swiftpy/auth/__init__.py +50 -0
- swiftpy/auth/api_key_guard.py +544 -0
- swiftpy/auth/auth_manager.py +259 -0
- swiftpy/auth/exceptions.py +71 -0
- swiftpy/auth/jwt_guard.py +230 -0
- swiftpy/auth/jwt_issuer.py +658 -0
- swiftpy/auth/token_blacklist.py +300 -0
- swiftpy/cache/__init__.py +14 -0
- swiftpy/cache/cache.py +251 -0
- swiftpy/cache/driver.py +216 -0
- swiftpy/cache/drivers/memory.py +199 -0
- swiftpy/cache/drivers/redis.py +421 -0
- swiftpy/console/__init__.py +0 -0
- swiftpy/console/cli.py +68 -0
- swiftpy/console/commands/__init__.py +0 -0
- swiftpy/console/commands/cache.py +164 -0
- swiftpy/console/commands/make.py +935 -0
- swiftpy/console/commands/migrate.py +280 -0
- swiftpy/console/commands/new.py +1806 -0
- swiftpy/console/commands/queue.py +306 -0
- swiftpy/console/commands/seed.py +252 -0
- swiftpy/console/commands/start.py +171 -0
- swiftpy/console/commands/storage.py +104 -0
- swiftpy/console/loader.py +21 -0
- swiftpy/console/stubs/__init__.py +26 -0
- swiftpy/console/stubs/controller.stub +14 -0
- swiftpy/console/stubs/event.stub +14 -0
- swiftpy/console/stubs/job.stub +22 -0
- swiftpy/console/stubs/listener.stub +14 -0
- swiftpy/console/stubs/mail.stub +20 -0
- swiftpy/console/stubs/middleware.stub +16 -0
- swiftpy/console/stubs/migration.alter.stub +17 -0
- swiftpy/console/stubs/migration.api_keys.stub +22 -0
- swiftpy/console/stubs/migration.blank.stub +11 -0
- swiftpy/console/stubs/migration.create.stub +15 -0
- swiftpy/console/stubs/migration.stub +11 -0
- swiftpy/console/stubs/migration.update.stub +17 -0
- swiftpy/console/stubs/model.stub +17 -0
- swiftpy/console/stubs/provider.stub +18 -0
- swiftpy/console/stubs/repository.stub +10 -0
- swiftpy/console/stubs/request.stub +16 -0
- swiftpy/console/stubs/resource.stub +21 -0
- swiftpy/console/stubs/schema.stub +12 -0
- swiftpy/console/stubs/seeder.stub +14 -0
- swiftpy/console/stubs/service.stub +10 -0
- swiftpy/core/__init__.py +0 -0
- swiftpy/core/application.py +1306 -0
- swiftpy/core/bootstrap.py +30 -0
- swiftpy/core/config.py +432 -0
- swiftpy/core/container.py +596 -0
- swiftpy/core/container_exceptions.py +18 -0
- swiftpy/core/context.py +148 -0
- swiftpy/core/exceptions.py +352 -0
- swiftpy/core/providers/base.py +35 -0
- swiftpy/database/__init__.py +41 -0
- swiftpy/database/exceptions.py +42 -0
- swiftpy/database/identifiers.py +105 -0
- swiftpy/database/migrations/__init__.py +36 -0
- swiftpy/database/migrations/blueprint.py +431 -0
- swiftpy/database/migrations/lock.py +104 -0
- swiftpy/database/migrations/migration.py +22 -0
- swiftpy/database/migrations/migrator.py +507 -0
- swiftpy/database/migrations/schema.py +435 -0
- swiftpy/database/orm/__init__.py +33 -0
- swiftpy/database/orm/fields.py +92 -0
- swiftpy/database/orm/model.py +2034 -0
- swiftpy/database/orm/registry.py +33 -0
- swiftpy/database/orm/relationships.py +836 -0
- swiftpy/database/pagination/__init__.py +16 -0
- swiftpy/database/pagination/contracts.py +60 -0
- swiftpy/database/pagination/paginator.py +112 -0
- swiftpy/database/pagination/simple_paginator.py +97 -0
- swiftpy/database/pool.py +218 -0
- swiftpy/database/query_builder.py +1803 -0
- swiftpy/database/seeder.py +39 -0
- swiftpy/database/transaction.py +276 -0
- swiftpy/events/__init__.py +10 -0
- swiftpy/events/dispatcher.py +308 -0
- swiftpy/events/redis_bus.py +552 -0
- swiftpy/filesystem/__init__.py +227 -0
- swiftpy/filesystem/disk.py +378 -0
- swiftpy/filesystem/drivers/__init__.py +11 -0
- swiftpy/filesystem/drivers/base.py +156 -0
- swiftpy/filesystem/drivers/local.py +559 -0
- swiftpy/filesystem/drivers/s3.py +655 -0
- swiftpy/filesystem/events.py +37 -0
- swiftpy/filesystem/exceptions.py +127 -0
- swiftpy/filesystem/manager.py +138 -0
- swiftpy/filesystem/metadata.py +22 -0
- swiftpy/filesystem/paths.py +99 -0
- swiftpy/filesystem/service_provider.py +17 -0
- swiftpy/filesystem/uploads/__init__.py +13 -0
- swiftpy/filesystem/uploads/handlers.py +362 -0
- swiftpy/filesystem/uploads/uploaded_file.py +222 -0
- swiftpy/filesystem/uploads/validators.py +186 -0
- swiftpy/filesystem/visibility.py +22 -0
- swiftpy/helpers/__init__.py +42 -0
- swiftpy/helpers/helper.py +180 -0
- swiftpy/http/exceptions.py +41 -0
- swiftpy/http/middleware.py +307 -0
- swiftpy/http/request.py +375 -0
- swiftpy/http/response.py +442 -0
- swiftpy/http/router.py +1050 -0
- swiftpy/mail/__init__.py +58 -0
- swiftpy/mail/attachments.py +255 -0
- swiftpy/mail/config.py +71 -0
- swiftpy/mail/envelope.py +183 -0
- swiftpy/mail/exceptions.py +90 -0
- swiftpy/mail/facade.py +312 -0
- swiftpy/mail/mailable.py +53 -0
- swiftpy/mail/manager.py +240 -0
- swiftpy/mail/message.py +186 -0
- swiftpy/mail/models.py +19 -0
- swiftpy/mail/queue/__init__.py +5 -0
- swiftpy/mail/queue/job.py +279 -0
- swiftpy/mail/service_provider.py +19 -0
- swiftpy/mail/templates/__init__.py +5 -0
- swiftpy/mail/templates/renderer.py +173 -0
- swiftpy/mail/transports/__init__.py +21 -0
- swiftpy/mail/transports/api/__init__.py +13 -0
- swiftpy/mail/transports/api/base.py +194 -0
- swiftpy/mail/transports/api/mailgun.py +158 -0
- swiftpy/mail/transports/api/resend.py +156 -0
- swiftpy/mail/transports/api/sendgrid.py +172 -0
- swiftpy/mail/transports/base.py +24 -0
- swiftpy/mail/transports/console.py +91 -0
- swiftpy/mail/transports/sendmail.py +128 -0
- swiftpy/mail/transports/smtp.py +466 -0
- swiftpy/queue/__init__.py +13 -0
- swiftpy/queue/dispatcher.py +813 -0
- swiftpy/queue/job.py +541 -0
- swiftpy/queue/worker.py +556 -0
- swiftpy/support/__init__.py +12 -0
- swiftpy/support/keys.py +108 -0
- swiftpy/support/redis_config.py +223 -0
- swiftpy/support/strings.py +114 -0
- swiftpy/testing/__init__.py +20 -0
- swiftpy/testing/fakes/__init__.py +11 -0
- swiftpy/testing/fakes/ai_fake.py +21 -0
- swiftpy/testing/fakes/vectorstore_fake.py +194 -0
- swiftpy/testing/fixtures.py +124 -0
- swiftpy/testing/http_client.py +730 -0
- swiftpy/testing/test_case.py +126 -0
- swiftpy/validation/__init__.py +97 -0
- swiftpy/validation/exceptions.py +26 -0
- swiftpy/validation/form_request.py +302 -0
- swiftpy/validation/protocols.py +33 -0
- swiftpy/validation/rules/__init__.py +89 -0
- swiftpy/validation/rules/builtin.py +391 -0
- swiftpy/validation/rules/exists.py +86 -0
- swiftpy/validation/rules/registry.py +290 -0
- swiftpy/validation/rules/required.py +55 -0
- swiftpy/validation/rules/unique.py +138 -0
- swiftpy/validation/validator.py +612 -0
- swiftpy/websockets/__init__.py +19 -0
- swiftpy/websockets/connection.py +317 -0
- swiftpy/websockets/manager.py +271 -0
- swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
- swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
- swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
- swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|