persistence-kit 3.2.0__tar.gz → 3.3.0__tar.gz
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.
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/PKG-INFO +2 -1
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/__init__.py +2 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/settings/__init__.py +2 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/settings/app_settings.py +10 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/storage/__init__.py +10 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/storage/contracts.py +4 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/storage/factory.py +70 -1
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/storage/local.py +34 -0
- persistence_kit-3.3.0/persistence_kit/storage/media.py +107 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/storage/s3.py +30 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/pyproject.toml +8 -7
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/LICENSE +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/README.md +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/api/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/api/common.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/api/error_handlers.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/api/exceptions.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/api/rate_limit.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/api/route_loader.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/authenticated_user.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/bootstrap/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/bootstrap/configuration.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/bootstrap/seeders.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/bootstrap/startup.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/contracts/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/contracts/repository.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/contracts/view_repository.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/py.typed +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/dynamodb_repo/dynamodb_mapper.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/dynamodb_repo/dynamodb_repo.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/filter_ops.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/memory_repo/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/memory_repo/memory_repo.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/mongo_repo/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/mongo_repo/mongo_mapper.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/mongo_repo/mongo_repo.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/sqlalchemy_repo/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/sqlalchemy_repo/schema_evolve.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_dataclass_mapper.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_engine.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/sqlalchemy_repo/sqlalchemy_repo.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/sqlalchemy_repo/table_factory.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository_factory/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository_factory/factory/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository_factory/factory/repository_factory.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository_factory/registry/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository_factory/registry/entity_registry.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository_factory/view/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository_factory/view/populating_repository.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/factory.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/ports.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/providers/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/providers/cognito_identity_provider.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/providers/memory_security_provider.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/registration.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/token_verifiers/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/token_verifiers/cognito_jwt_verifier.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/token_verifiers/memory_jwt_verifier.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/settings/constants.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/settings/parsers.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/settings/repo_settings.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/storage/errors.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/storage/routes.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/utils/__init__.py +0 -0
- {persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/utils/upsert.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: persistence-kit
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Reusable persistence and repository toolkit
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -32,6 +32,7 @@ Requires-Dist: fastapi (>=0.115.0,<0.116.0) ; extra == "api" or extra == "storag
|
|
|
32
32
|
Requires-Dist: motor (>=3.7.1,<4.0.0)
|
|
33
33
|
Requires-Dist: pydantic-settings (>=2.3.0,<3.0.0)
|
|
34
34
|
Requires-Dist: pyjwt[crypto] (>=2.10.1,<3.0.0) ; extra == "security" or extra == "security-cognito" or extra == "testing" or extra == "all"
|
|
35
|
+
Requires-Dist: python-multipart (>=0.0.20,<0.0.21) ; extra == "api" or extra == "storage-routes" or extra == "security" or extra == "security-cognito" or extra == "testing" or extra == "all"
|
|
35
36
|
Requires-Dist: sqlalchemy[asyncio] (>=2.0.43,<3.0.0)
|
|
36
37
|
Requires-Dist: typing-extensions (>=4.12.0,<5.0.0)
|
|
37
38
|
Project-URL: Documentation, https://github.com/andresfserrano/persistence-kit#readme
|
|
@@ -13,6 +13,7 @@ from .settings import (
|
|
|
13
13
|
AuthProvider,
|
|
14
14
|
DeploymentStage,
|
|
15
15
|
ExportStorageProvider,
|
|
16
|
+
MediaStorageProvider,
|
|
16
17
|
PersistenceKitSettings,
|
|
17
18
|
)
|
|
18
19
|
from .settings.constants import Database
|
|
@@ -186,6 +187,7 @@ __all__ = [
|
|
|
186
187
|
"RepoSettings",
|
|
187
188
|
"AuthProvider",
|
|
188
189
|
"ExportStorageProvider",
|
|
190
|
+
"MediaStorageProvider",
|
|
189
191
|
"DeploymentStage",
|
|
190
192
|
"LOCAL_DEFAULT_JOB_SERVICE_API_KEY",
|
|
191
193
|
"DEFAULT_MEMORY_JWT_SECRET",
|
|
@@ -6,6 +6,7 @@ from .app_settings import (
|
|
|
6
6
|
AuthProvider,
|
|
7
7
|
DeploymentStage,
|
|
8
8
|
ExportStorageProvider,
|
|
9
|
+
MediaStorageProvider,
|
|
9
10
|
PersistenceKitSettings,
|
|
10
11
|
)
|
|
11
12
|
from .constants import Database
|
|
@@ -18,6 +19,7 @@ __all__ = [
|
|
|
18
19
|
"RepoSettings",
|
|
19
20
|
"AuthProvider",
|
|
20
21
|
"ExportStorageProvider",
|
|
22
|
+
"MediaStorageProvider",
|
|
21
23
|
"DeploymentStage",
|
|
22
24
|
"LOCAL_DEFAULT_JOB_SERVICE_API_KEY",
|
|
23
25
|
"DEFAULT_MEMORY_JWT_SECRET",
|
|
@@ -17,6 +17,11 @@ class ExportStorageProvider(str, Enum):
|
|
|
17
17
|
S3 = "s3"
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class MediaStorageProvider(str, Enum):
|
|
21
|
+
LOCAL = "local"
|
|
22
|
+
S3 = "s3"
|
|
23
|
+
|
|
24
|
+
|
|
20
25
|
class DeploymentStage(str, Enum):
|
|
21
26
|
LOCAL = "local"
|
|
22
27
|
DEV = "dev"
|
|
@@ -53,6 +58,11 @@ class PersistenceKitSettings(RepoSettings):
|
|
|
53
58
|
local_export_storage_dir: str = ".local"
|
|
54
59
|
local_export_url_secret: str = "local-export-dev-secret-key-32-bytes"
|
|
55
60
|
|
|
61
|
+
# Media Storage (user-uploaded assets like product images, avatars, etc.)
|
|
62
|
+
media_storage_provider: MediaStorageProvider = MediaStorageProvider.LOCAL
|
|
63
|
+
local_media_storage_dir: str = ".local/media"
|
|
64
|
+
aws_s3_media_bucket: str | None = None
|
|
65
|
+
|
|
56
66
|
# Cognito
|
|
57
67
|
cognito_user_pool_id: str | None = None
|
|
58
68
|
cognito_app_client_id: str | None = None
|
|
@@ -10,6 +10,7 @@ from persistence_kit.storage.factory import (
|
|
|
10
10
|
EXPORT_STORAGE_BUILDERS,
|
|
11
11
|
ExportStorage,
|
|
12
12
|
ExportStorageBuilder,
|
|
13
|
+
build_media_storage,
|
|
13
14
|
get_export_storage,
|
|
14
15
|
)
|
|
15
16
|
|
|
@@ -23,6 +24,7 @@ _ROUTE_EXPORTS = {
|
|
|
23
24
|
"guess_export_media_type",
|
|
24
25
|
}
|
|
25
26
|
_S3_EXPORTS = {"S3ObjectStorage", "S3ExportStorageProvider"}
|
|
27
|
+
_MEDIA_EXPORTS = {"MediaStorage"}
|
|
26
28
|
|
|
27
29
|
__all__ = [
|
|
28
30
|
"ObjectStorage",
|
|
@@ -39,6 +41,8 @@ __all__ = [
|
|
|
39
41
|
"ExportStorageBuilder",
|
|
40
42
|
"EXPORT_STORAGE_BUILDERS",
|
|
41
43
|
"get_export_storage",
|
|
44
|
+
"MediaStorage",
|
|
45
|
+
"build_media_storage",
|
|
42
46
|
"SettingsProvider",
|
|
43
47
|
"CurrentUserDependency",
|
|
44
48
|
"ExportDownloadAuthorizer",
|
|
@@ -67,4 +71,10 @@ def __getattr__(name: str):
|
|
|
67
71
|
value = getattr(s3, name)
|
|
68
72
|
globals()[name] = value
|
|
69
73
|
return value
|
|
74
|
+
if name in _MEDIA_EXPORTS:
|
|
75
|
+
from persistence_kit.storage import media
|
|
76
|
+
|
|
77
|
+
value = getattr(media, name)
|
|
78
|
+
globals()[name] = value
|
|
79
|
+
return value
|
|
70
80
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
from functools import lru_cache
|
|
2
2
|
from typing import Callable
|
|
3
3
|
|
|
4
|
-
from persistence_kit.settings import
|
|
4
|
+
from persistence_kit.settings import (
|
|
5
|
+
ExportStorageProvider,
|
|
6
|
+
MediaStorageProvider,
|
|
7
|
+
PersistenceKitSettings,
|
|
8
|
+
)
|
|
5
9
|
from persistence_kit.storage.contracts import ObjectStorage
|
|
6
10
|
from persistence_kit.storage.errors import StorageConfigError
|
|
7
11
|
|
|
@@ -69,3 +73,68 @@ def get_export_storage(settings: PersistenceKitSettings) -> ExportStorage:
|
|
|
69
73
|
f"Proveedor de storage de exportaciones no soportado: '{provider}'."
|
|
70
74
|
)
|
|
71
75
|
return builder(settings)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@lru_cache
|
|
79
|
+
def _local_media_object_storage_cached(base_dir: str) -> ObjectStorage:
|
|
80
|
+
from persistence_kit.storage.local import LocalObjectStorage
|
|
81
|
+
|
|
82
|
+
return LocalObjectStorage(
|
|
83
|
+
base_dir=base_dir,
|
|
84
|
+
public_base_url="",
|
|
85
|
+
signing_secret="not-used-for-public-media",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@lru_cache
|
|
90
|
+
def _s3_media_object_storage_cached(bucket: str, region: str) -> ObjectStorage:
|
|
91
|
+
from persistence_kit.storage.s3 import S3ObjectStorage
|
|
92
|
+
|
|
93
|
+
return S3ObjectStorage(bucket=bucket, region=region)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _build_media_object_storage(
|
|
97
|
+
settings: PersistenceKitSettings,
|
|
98
|
+
namespace: str,
|
|
99
|
+
) -> ObjectStorage:
|
|
100
|
+
namespace = namespace.strip().strip("/").strip()
|
|
101
|
+
if not namespace:
|
|
102
|
+
raise StorageConfigError("El namespace de MediaStorage no puede estar vacío.")
|
|
103
|
+
if settings.media_storage_provider == MediaStorageProvider.LOCAL:
|
|
104
|
+
return _local_media_object_storage_cached(
|
|
105
|
+
f"{settings.local_media_storage_dir.rstrip('/')}/{namespace}"
|
|
106
|
+
)
|
|
107
|
+
if settings.media_storage_provider == MediaStorageProvider.S3:
|
|
108
|
+
if not settings.aws_s3_media_bucket:
|
|
109
|
+
raise StorageConfigError(
|
|
110
|
+
"Falta configuracion AWS_S3_MEDIA_BUCKET para el storage de media."
|
|
111
|
+
)
|
|
112
|
+
return _s3_media_object_storage_cached(
|
|
113
|
+
settings.aws_s3_media_bucket, settings.aws_region
|
|
114
|
+
)
|
|
115
|
+
provider = getattr(
|
|
116
|
+
settings.media_storage_provider, "value", settings.media_storage_provider
|
|
117
|
+
)
|
|
118
|
+
raise StorageConfigError(
|
|
119
|
+
f"Proveedor de storage de media no soportado: '{provider}'."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def build_media_storage(
|
|
124
|
+
settings: PersistenceKitSettings,
|
|
125
|
+
*,
|
|
126
|
+
namespace: str,
|
|
127
|
+
url_prefix: str,
|
|
128
|
+
allowed_content_types=None,
|
|
129
|
+
max_bytes: int | None = None,
|
|
130
|
+
):
|
|
131
|
+
from persistence_kit.storage.media import MediaStorage
|
|
132
|
+
|
|
133
|
+
storage = _build_media_object_storage(settings, namespace)
|
|
134
|
+
return MediaStorage(
|
|
135
|
+
storage=storage,
|
|
136
|
+
public_base_url=settings.public_base_url,
|
|
137
|
+
url_prefix=url_prefix,
|
|
138
|
+
allowed_content_types=allowed_content_types,
|
|
139
|
+
max_bytes=max_bytes,
|
|
140
|
+
)
|
|
@@ -91,6 +91,40 @@ class LocalObjectStorage:
|
|
|
91
91
|
).digest()
|
|
92
92
|
return f"{payload_token}.{_b64encode(signature)}"
|
|
93
93
|
|
|
94
|
+
async def delete(self, key: str) -> None:
|
|
95
|
+
destination = self.get_file_path(key)
|
|
96
|
+
|
|
97
|
+
def _delete() -> None:
|
|
98
|
+
if destination.is_file():
|
|
99
|
+
destination.unlink()
|
|
100
|
+
current = destination.parent
|
|
101
|
+
while current != self._base_dir and current.is_relative_to(self._base_dir):
|
|
102
|
+
try:
|
|
103
|
+
if not current.is_dir() or any(current.iterdir()):
|
|
104
|
+
break
|
|
105
|
+
current.rmdir()
|
|
106
|
+
except OSError:
|
|
107
|
+
break
|
|
108
|
+
current = current.parent
|
|
109
|
+
|
|
110
|
+
await asyncio.to_thread(_delete)
|
|
111
|
+
|
|
112
|
+
async def list_keys(self, prefix: str = "") -> list[str]:
|
|
113
|
+
normalized_prefix = prefix.strip("/").rstrip("/")
|
|
114
|
+
root = self._base_dir / normalized_prefix if normalized_prefix else self._base_dir
|
|
115
|
+
|
|
116
|
+
def _list() -> list[str]:
|
|
117
|
+
if not root.is_dir():
|
|
118
|
+
return []
|
|
119
|
+
collected: list[str] = []
|
|
120
|
+
for path in root.rglob("*"):
|
|
121
|
+
if path.is_file():
|
|
122
|
+
rel = path.relative_to(self._base_dir).as_posix()
|
|
123
|
+
collected.append(rel)
|
|
124
|
+
return collected
|
|
125
|
+
|
|
126
|
+
return await asyncio.to_thread(_list)
|
|
127
|
+
|
|
94
128
|
def validate_download_token(self, key: str, token: str) -> bool:
|
|
95
129
|
try:
|
|
96
130
|
payload_token, signature_token = token.split(".", maxsplit=1)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
from persistence_kit.storage.contracts import ObjectStorage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MediaStorage:
|
|
10
|
+
CONTENT_TYPE_TO_EXTENSION: dict[str, str] = {
|
|
11
|
+
"image/jpeg": "jpg",
|
|
12
|
+
"image/png": "png",
|
|
13
|
+
"image/webp": "webp",
|
|
14
|
+
"image/gif": "gif",
|
|
15
|
+
"application/pdf": "pdf",
|
|
16
|
+
}
|
|
17
|
+
DEFAULT_ALLOWED_CONTENT_TYPES: tuple[str, ...] = (
|
|
18
|
+
"image/jpeg",
|
|
19
|
+
"image/png",
|
|
20
|
+
"image/webp",
|
|
21
|
+
)
|
|
22
|
+
DEFAULT_MAX_BYTES: int = 5 * 1024 * 1024
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
storage: ObjectStorage,
|
|
28
|
+
public_base_url: str | None,
|
|
29
|
+
url_prefix: str,
|
|
30
|
+
allowed_content_types: Iterable[str] | None = None,
|
|
31
|
+
max_bytes: int | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._storage = storage
|
|
34
|
+
self._public_base_url = (public_base_url or "").rstrip("/")
|
|
35
|
+
self._url_prefix = "/" + url_prefix.strip("/")
|
|
36
|
+
self._allowed_content_types = tuple(
|
|
37
|
+
allowed_content_types
|
|
38
|
+
if allowed_content_types is not None
|
|
39
|
+
else self.DEFAULT_ALLOWED_CONTENT_TYPES
|
|
40
|
+
)
|
|
41
|
+
self._max_bytes = max_bytes if max_bytes is not None else self.DEFAULT_MAX_BYTES
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def url_prefix(self) -> str:
|
|
45
|
+
return self._url_prefix
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def allowed_content_types(self) -> tuple[str, ...]:
|
|
49
|
+
return self._allowed_content_types
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def max_bytes(self) -> int:
|
|
53
|
+
return self._max_bytes
|
|
54
|
+
|
|
55
|
+
def validate_upload(self, data: bytes, content_type: str) -> None:
|
|
56
|
+
from persistence_kit.api.exceptions import ValidationException
|
|
57
|
+
|
|
58
|
+
if not data:
|
|
59
|
+
raise ValidationException("El archivo está vacío.")
|
|
60
|
+
if content_type not in self._allowed_content_types:
|
|
61
|
+
allowed = ", ".join(self._allowed_content_types)
|
|
62
|
+
raise ValidationException(
|
|
63
|
+
f"Tipo de archivo no permitido. Permitidos: {allowed}."
|
|
64
|
+
)
|
|
65
|
+
if len(data) > self._max_bytes:
|
|
66
|
+
max_mb = self._max_bytes // (1024 * 1024)
|
|
67
|
+
raise ValidationException(
|
|
68
|
+
f"El archivo excede el tamaño máximo permitido de {max_mb}MB."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def upload(
|
|
72
|
+
self,
|
|
73
|
+
key: str,
|
|
74
|
+
data: bytes,
|
|
75
|
+
content_type: str = "application/octet-stream",
|
|
76
|
+
) -> str:
|
|
77
|
+
return await self._storage.upload(key, data, content_type=content_type)
|
|
78
|
+
|
|
79
|
+
async def upload_file(
|
|
80
|
+
self,
|
|
81
|
+
key: str,
|
|
82
|
+
file_path: Path,
|
|
83
|
+
content_type: str = "application/octet-stream",
|
|
84
|
+
) -> str:
|
|
85
|
+
return await self._storage.upload_file(key, file_path, content_type=content_type)
|
|
86
|
+
|
|
87
|
+
async def delete(self, key: str) -> None:
|
|
88
|
+
await self._storage.delete(key)
|
|
89
|
+
|
|
90
|
+
async def generate_presigned_url(self, key: str, expires_in: int = 3600) -> str:
|
|
91
|
+
return await self._storage.generate_presigned_url(key, expires_in=expires_in)
|
|
92
|
+
|
|
93
|
+
def build_url(self, key: str | None) -> str | None:
|
|
94
|
+
if not key:
|
|
95
|
+
return None
|
|
96
|
+
return f"{self._public_base_url}{self._url_prefix}/{key}"
|
|
97
|
+
|
|
98
|
+
async def remove_other_files_in_scope(self, scope: str, keep_key: str) -> None:
|
|
99
|
+
existing_keys = await self._storage.list_keys(prefix=scope)
|
|
100
|
+
for existing in existing_keys:
|
|
101
|
+
if existing != keep_key:
|
|
102
|
+
await self._storage.delete(existing)
|
|
103
|
+
|
|
104
|
+
async def remove_scope_dir(self, scope: str) -> None:
|
|
105
|
+
existing_keys = await self._storage.list_keys(prefix=scope)
|
|
106
|
+
for existing in existing_keys:
|
|
107
|
+
await self._storage.delete(existing)
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
from persistence_kit.storage.errors import (
|
|
8
8
|
StorageConfigError,
|
|
9
|
+
StorageError,
|
|
9
10
|
StoragePresignError,
|
|
10
11
|
StorageUploadError,
|
|
11
12
|
)
|
|
@@ -91,5 +92,34 @@ class S3ObjectStorage:
|
|
|
91
92
|
logger.exception("S3 presign fallido bucket=%s key=%s", self._bucket, key)
|
|
92
93
|
raise StoragePresignError(f"No fue posible generar la URL prefirmada: {exc}") from exc
|
|
93
94
|
|
|
95
|
+
async def delete(self, key: str) -> None:
|
|
96
|
+
def _delete() -> None:
|
|
97
|
+
self._client.delete_object(Bucket=self._bucket, Key=key)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
await asyncio.to_thread(_delete)
|
|
101
|
+
except ClientError as exc:
|
|
102
|
+
logger.exception("S3 delete fallido bucket=%s key=%s", self._bucket, key)
|
|
103
|
+
raise StorageError(f"No fue posible borrar el objeto en S3: {exc}") from exc
|
|
104
|
+
|
|
105
|
+
async def list_keys(self, prefix: str = "") -> list[str]:
|
|
106
|
+
normalized_prefix = prefix.lstrip("/")
|
|
107
|
+
if normalized_prefix and not normalized_prefix.endswith("/"):
|
|
108
|
+
normalized_prefix = normalized_prefix + "/"
|
|
109
|
+
|
|
110
|
+
def _list() -> list[str]:
|
|
111
|
+
keys: list[str] = []
|
|
112
|
+
paginator = self._client.get_paginator("list_objects_v2")
|
|
113
|
+
for page in paginator.paginate(Bucket=self._bucket, Prefix=normalized_prefix):
|
|
114
|
+
for obj in page.get("Contents", []) or []:
|
|
115
|
+
keys.append(obj["Key"])
|
|
116
|
+
return keys
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
return await asyncio.to_thread(_list)
|
|
120
|
+
except ClientError as exc:
|
|
121
|
+
logger.exception("S3 list fallido bucket=%s prefix=%s", self._bucket, normalized_prefix)
|
|
122
|
+
raise StorageError(f"No fue posible listar objetos en S3: {exc}") from exc
|
|
123
|
+
|
|
94
124
|
|
|
95
125
|
S3ExportStorageProvider = S3ObjectStorage
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "persistence-kit"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.3.0"
|
|
4
4
|
description = "Reusable persistence and repository toolkit"
|
|
5
5
|
authors = ["Andres Felipe Serrano Barrios <andresfserrano1@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -33,16 +33,17 @@ pydantic-settings = "^2.3.0"
|
|
|
33
33
|
fastapi = {version = "^0.115.0", optional = true}
|
|
34
34
|
boto3 = {version = "^1.35.0", optional = true}
|
|
35
35
|
pyjwt = {version = "^2.10.1", extras = ["crypto"], optional = true}
|
|
36
|
+
python-multipart = {version = "^0.0.20", optional = true}
|
|
36
37
|
|
|
37
38
|
[tool.poetry.extras]
|
|
38
|
-
api = ["fastapi"]
|
|
39
|
+
api = ["fastapi", "python-multipart"]
|
|
39
40
|
storage-s3 = ["boto3"]
|
|
40
|
-
storage-routes = ["fastapi"]
|
|
41
|
-
security = ["fastapi", "pyjwt"]
|
|
42
|
-
security-cognito = ["fastapi", "pyjwt", "boto3"]
|
|
41
|
+
storage-routes = ["fastapi", "python-multipart"]
|
|
42
|
+
security = ["fastapi", "pyjwt", "python-multipart"]
|
|
43
|
+
security-cognito = ["fastapi", "pyjwt", "boto3", "python-multipart"]
|
|
43
44
|
dynamodb = ["boto3"]
|
|
44
|
-
testing = ["fastapi", "pyjwt"]
|
|
45
|
-
all = ["fastapi", "boto3", "pyjwt"]
|
|
45
|
+
testing = ["fastapi", "pyjwt", "python-multipart"]
|
|
46
|
+
all = ["fastapi", "boto3", "pyjwt", "python-multipart"]
|
|
46
47
|
|
|
47
48
|
[tool.poetry.group.dev.dependencies]
|
|
48
49
|
pytest = "^8.3.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/contracts/view_repository.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/memory_repo/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/mongo_repo/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository/mongo_repo/mongo_repo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository_factory/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/repository_factory/view/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/providers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{persistence_kit-3.2.0 → persistence_kit-3.3.0}/persistence_kit/security/token_verifiers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|