fast-clean 1.2.3__py3-none-any.whl → 1.4.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.
- fast_clean/cli/__init__.py +6 -0
- fast_clean/cli/cryptography.py +53 -0
- fast_clean/cli/load_seed.py +31 -0
- fast_clean/contrib/healthcheck/schemas.py +0 -1
- fast_clean/contrib/monitoring/middleware.py +1 -1
- fast_clean/db.py +37 -22
- fast_clean/depends.py +20 -19
- fast_clean/middleware.py +23 -6
- fast_clean/models.py +11 -4
- fast_clean/repositories/crud/__init__.py +1 -3
- fast_clean/repositories/crud/db.py +2 -10
- fast_clean/repositories/crud/in_memory.py +1 -9
- fast_clean/repositories/storage/__init__.py +20 -2
- fast_clean/repositories/storage/local.py +16 -6
- fast_clean/repositories/storage/reader.py +8 -0
- fast_clean/repositories/storage/s3.py +112 -61
- fast_clean/repositories/storage/schemas.py +8 -3
- fast_clean/services/cryptography/__init__.py +1 -1
- fast_clean/settings.py +4 -3
- fast_clean/utils/toml.py +34 -0
- {fast_clean-1.2.3.dist-info → fast_clean-1.4.0.dist-info}/METADATA +2 -3
- {fast_clean-1.2.3.dist-info → fast_clean-1.4.0.dist-info}/RECORD +24 -20
- {fast_clean-1.2.3.dist-info → fast_clean-1.4.0.dist-info}/WHEEL +0 -0
- {fast_clean-1.2.3.dist-info → fast_clean-1.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
"""
|
2
|
+
Модуль, содержащий команды криптографии для шифрования секретных параметров.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Annotated
|
6
|
+
|
7
|
+
import typer
|
8
|
+
from rich import print
|
9
|
+
|
10
|
+
from fast_clean.container import get_container
|
11
|
+
from fast_clean.services import CryptographicAlgorithmEnum, CryptographyServiceFactory
|
12
|
+
from fast_clean.utils import typer_async
|
13
|
+
|
14
|
+
|
15
|
+
@typer_async
|
16
|
+
async def encrypt(
|
17
|
+
data: Annotated[str, typer.Argument(help='Данные для шифровки.')],
|
18
|
+
algorithm: Annotated[
|
19
|
+
CryptographicAlgorithmEnum, typer.Option(help='Криптографический алгоритм')
|
20
|
+
] = CryptographicAlgorithmEnum.AES_GCM,
|
21
|
+
) -> None:
|
22
|
+
"""
|
23
|
+
Зашифровываем данные.
|
24
|
+
"""
|
25
|
+
async with get_container() as container:
|
26
|
+
cryptography_service_factory = await container.get(CryptographyServiceFactory)
|
27
|
+
cryptography_service = await cryptography_service_factory.make(algorithm)
|
28
|
+
print(cryptography_service.encrypt(data))
|
29
|
+
|
30
|
+
|
31
|
+
@typer_async
|
32
|
+
async def decrypt(
|
33
|
+
data: Annotated[str, typer.Argument(help='Данные для расшифровки.')],
|
34
|
+
algorithm: Annotated[
|
35
|
+
CryptographicAlgorithmEnum, typer.Option(help='Криптографический алгоритм')
|
36
|
+
] = CryptographicAlgorithmEnum.AES_GCM,
|
37
|
+
) -> None:
|
38
|
+
"""
|
39
|
+
Расшифровываем данные.
|
40
|
+
"""
|
41
|
+
async with get_container() as container:
|
42
|
+
cryptography_service_factory = await container.get(CryptographyServiceFactory)
|
43
|
+
cryptography_service = await cryptography_service_factory.make(algorithm)
|
44
|
+
print(cryptography_service.decrypt(data))
|
45
|
+
|
46
|
+
|
47
|
+
def use_cryptography(app: typer.Typer) -> None:
|
48
|
+
"""
|
49
|
+
Регистрируем команды криптографии для шифрования секретных параметров.
|
50
|
+
"""
|
51
|
+
|
52
|
+
app.command()(encrypt)
|
53
|
+
app.command()(decrypt)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
"""
|
2
|
+
Модуль, содержащий команды загрузки данных из файлов.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Annotated
|
6
|
+
|
7
|
+
import typer
|
8
|
+
|
9
|
+
from fast_clean.container import get_container
|
10
|
+
from fast_clean.services import SeedService
|
11
|
+
from fast_clean.utils import typer_async
|
12
|
+
|
13
|
+
|
14
|
+
@typer_async
|
15
|
+
async def load_seed(
|
16
|
+
path: Annotated[str | None, typer.Argument(help='Путь к директории для загрузки данных.')] = None,
|
17
|
+
) -> None:
|
18
|
+
"""
|
19
|
+
Загружаем данные из файлов.
|
20
|
+
"""
|
21
|
+
async with get_container() as container:
|
22
|
+
seed_service = await container.get(SeedService)
|
23
|
+
await seed_service.load_data(path)
|
24
|
+
|
25
|
+
|
26
|
+
def use_load_seed(app: typer.Typer) -> None:
|
27
|
+
"""
|
28
|
+
Регистрируем команды загрузки данных из файлов.
|
29
|
+
"""
|
30
|
+
|
31
|
+
app.command()(load_seed)
|
fast_clean/db.py
CHANGED
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|
7
7
|
import uuid
|
8
8
|
from collections.abc import AsyncIterator
|
9
9
|
from contextlib import asynccontextmanager
|
10
|
-
from typing import TYPE_CHECKING, AsyncContextManager, Protocol, Self
|
10
|
+
from typing import TYPE_CHECKING, Any, AsyncContextManager, Protocol, Self
|
11
11
|
|
12
12
|
import sqlalchemy as sa
|
13
13
|
from sqlalchemy import MetaData
|
@@ -18,11 +18,9 @@ from sqlalchemy.ext.asyncio import (
|
|
18
18
|
async_sessionmaker,
|
19
19
|
create_async_engine,
|
20
20
|
)
|
21
|
-
from sqlalchemy.ext.declarative import declared_attr
|
22
21
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
23
22
|
from sqlalchemy.sql import func
|
24
|
-
from sqlalchemy_utils.types
|
25
|
-
from stringcase import snakecase
|
23
|
+
from sqlalchemy_utils.types import UUIDType
|
26
24
|
|
27
25
|
from .settings import CoreDbSettingsSchema, CoreSettingsSchema
|
28
26
|
|
@@ -40,24 +38,46 @@ POSTGRES_INDEXES_NAMING_CONVENTION = {
|
|
40
38
|
metadata = MetaData(naming_convention=POSTGRES_INDEXES_NAMING_CONVENTION)
|
41
39
|
|
42
40
|
|
43
|
-
def make_async_engine(
|
41
|
+
def make_async_engine(
|
42
|
+
db_dsn: str,
|
43
|
+
*,
|
44
|
+
scheme: str = 'public',
|
45
|
+
echo: bool = False,
|
46
|
+
pool_pre_ping: bool = True,
|
47
|
+
disable_prepared_statements: bool = True,
|
48
|
+
) -> AsyncEngine:
|
44
49
|
"""
|
45
50
|
Создаем асинхронный движок.
|
46
51
|
"""
|
52
|
+
connect_args: dict[str, Any] = {}
|
53
|
+
if disable_prepared_statements:
|
54
|
+
connect_args['prepare_threshold'] = None
|
47
55
|
return create_async_engine(
|
48
56
|
db_dsn,
|
49
|
-
connect_args={'options': f'-csearch_path={scheme}'},
|
50
57
|
echo=echo,
|
58
|
+
pool_pre_ping=pool_pre_ping,
|
59
|
+
connect_args=connect_args,
|
51
60
|
)
|
52
61
|
|
53
62
|
|
54
63
|
def make_async_session_factory(
|
55
|
-
db_dsn: str,
|
64
|
+
db_dsn: str,
|
65
|
+
*,
|
66
|
+
scheme: str = 'public',
|
67
|
+
echo: bool = False,
|
68
|
+
pool_pre_ping: bool = True,
|
69
|
+
disable_prepared_statements: bool = True,
|
56
70
|
) -> async_sessionmaker[AsyncSession]:
|
57
71
|
"""
|
58
72
|
Создаем фабрику асинхронных сессий.
|
59
73
|
"""
|
60
|
-
asyncio_engine = make_async_engine(
|
74
|
+
asyncio_engine = make_async_engine(
|
75
|
+
db_dsn,
|
76
|
+
scheme=scheme,
|
77
|
+
echo=echo,
|
78
|
+
pool_pre_ping=pool_pre_ping,
|
79
|
+
disable_prepared_statements=disable_prepared_statements,
|
80
|
+
)
|
61
81
|
return async_sessionmaker(asyncio_engine, expire_on_commit=False, autoflush=False)
|
62
82
|
|
63
83
|
|
@@ -85,10 +105,6 @@ class BaseUUID(Base):
|
|
85
105
|
server_default=func.gen_random_uuid(),
|
86
106
|
)
|
87
107
|
|
88
|
-
@declared_attr.directive
|
89
|
-
def __tablename__(cls) -> str:
|
90
|
-
return snakecase(cls.__name__)
|
91
|
-
|
92
108
|
|
93
109
|
class BaseInt(Base):
|
94
110
|
"""
|
@@ -100,10 +116,6 @@ class BaseInt(Base):
|
|
100
116
|
id: Mapped[int] = mapped_column(primary_key=True)
|
101
117
|
|
102
118
|
|
103
|
-
@declared_attr.directive
|
104
|
-
def __tablename__(cls) -> str:
|
105
|
-
return snakecase(cls.__name__)
|
106
|
-
|
107
119
|
class SessionFactory:
|
108
120
|
"""
|
109
121
|
Фабрика сессий.
|
@@ -125,16 +137,13 @@ class SessionFactory:
|
|
125
137
|
yield session
|
126
138
|
|
127
139
|
@classmethod
|
128
|
-
@asynccontextmanager
|
129
140
|
async def make_async_session_dynamic(
|
130
141
|
cls, settings_repository: SettingsRepositoryProtocol
|
131
|
-
) ->
|
142
|
+
) -> async_sessionmaker[AsyncSession]:
|
132
143
|
"""
|
133
144
|
Создаем асинхронную сессию с помощью динамической фабрики.
|
134
145
|
"""
|
135
|
-
|
136
|
-
async with async_session_factory() as session:
|
137
|
-
yield session
|
146
|
+
return await cls.make_async_session_factory(settings_repository)
|
138
147
|
|
139
148
|
@staticmethod
|
140
149
|
async def make_async_session_factory(
|
@@ -145,7 +154,13 @@ class SessionFactory:
|
|
145
154
|
"""
|
146
155
|
settings = await settings_repository.get(CoreSettingsSchema)
|
147
156
|
db_settings = await settings_repository.get(CoreDbSettingsSchema)
|
148
|
-
return make_async_session_factory(
|
157
|
+
return make_async_session_factory(
|
158
|
+
db_settings.dsn,
|
159
|
+
scheme=db_settings.scheme,
|
160
|
+
echo=settings.debug,
|
161
|
+
pool_pre_ping=db_settings.pool_pre_ping,
|
162
|
+
disable_prepared_statements=db_settings.disable_prepared_statements,
|
163
|
+
)
|
149
164
|
|
150
165
|
|
151
166
|
class SessionManagerProtocol(Protocol):
|
fast_clean/depends.py
CHANGED
@@ -82,18 +82,14 @@ class CoreProvider(Provider):
|
|
82
82
|
Провайдер зависимостей.
|
83
83
|
"""
|
84
84
|
|
85
|
-
scope = Scope.
|
85
|
+
scope = Scope.APP
|
86
86
|
|
87
87
|
# --- repositories ---
|
88
88
|
|
89
|
-
settings_repository_factory = provide(
|
90
|
-
|
91
|
-
)
|
92
|
-
storage_repository_factory = provide(
|
93
|
-
StorageRepositoryFactoryImpl, provides=StorageRepositoryFactoryProtocol, scope=Scope.APP
|
94
|
-
)
|
89
|
+
settings_repository_factory = provide(SettingsRepositoryFactoryImpl, provides=SettingsRepositoryFactoryProtocol)
|
90
|
+
storage_repository_factory = provide(StorageRepositoryFactoryImpl, provides=StorageRepositoryFactoryProtocol)
|
95
91
|
|
96
|
-
@provide
|
92
|
+
@provide
|
97
93
|
@staticmethod
|
98
94
|
async def get_settings_repository(
|
99
95
|
settings_repository_factory: SettingsRepositoryFactoryProtocol,
|
@@ -103,7 +99,7 @@ class CoreProvider(Provider):
|
|
103
99
|
"""
|
104
100
|
return await settings_repository_factory.make(SettingsSourceEnum.ENV)
|
105
101
|
|
106
|
-
@provide
|
102
|
+
@provide
|
107
103
|
@staticmethod
|
108
104
|
async def get_settings(settings_repository: SettingsRepositoryProtocol) -> CoreSettingsSchema:
|
109
105
|
"""
|
@@ -119,7 +115,7 @@ class CoreProvider(Provider):
|
|
119
115
|
kafka_settings = await settings_repository.get(CoreKafkaSettingsSchema)
|
120
116
|
yield BrokerFactory.make_static(kafka_settings)
|
121
117
|
|
122
|
-
@provide
|
118
|
+
@provide
|
123
119
|
@staticmethod
|
124
120
|
async def get_cache_repository(settings_repository: SettingsRepositoryProtocol) -> CacheRepositoryProtocol:
|
125
121
|
"""
|
@@ -135,25 +131,29 @@ class CoreProvider(Provider):
|
|
135
131
|
return CacheManager.cache_repository
|
136
132
|
raise ValueError('Cache is not initialized')
|
137
133
|
|
138
|
-
@provide(scope=Scope.
|
134
|
+
@provide(scope=Scope.REQUEST)
|
139
135
|
@staticmethod
|
140
136
|
async def get_storage_repository(
|
141
137
|
settings_repository: SettingsRepositoryProtocol,
|
142
138
|
storage_repository_factory: StorageRepositoryFactoryProtocol,
|
143
|
-
) -> StorageRepositoryProtocol:
|
139
|
+
) -> AsyncIterator[StorageRepositoryProtocol]:
|
144
140
|
"""
|
145
141
|
Получаем репозиторий файлового хранилища.
|
146
142
|
"""
|
147
143
|
storage_settings = await settings_repository.get(CoreStorageSettingsSchema)
|
148
144
|
if storage_settings.provider == 's3' and storage_settings.s3 is not None:
|
149
|
-
|
145
|
+
storage_repository = await storage_repository_factory.make(
|
150
146
|
StorageTypeEnum.S3,
|
151
147
|
S3StorageParamsSchema.model_validate(storage_settings.s3.model_dump()),
|
152
148
|
)
|
153
|
-
|
154
|
-
|
149
|
+
async with storage_repository:
|
150
|
+
yield storage_repository
|
151
|
+
elif storage_settings.provider == 'local':
|
152
|
+
storage_repository = await storage_repository_factory.make(
|
155
153
|
StorageTypeEnum.LOCAL, LocalStorageParamsSchema(path=storage_settings.dir)
|
156
154
|
)
|
155
|
+
async with storage_repository:
|
156
|
+
yield storage_repository
|
157
157
|
raise NotImplementedError(f'Storage {storage_settings.provider} not allowed')
|
158
158
|
|
159
159
|
# --- db ---
|
@@ -164,7 +164,8 @@ class CoreProvider(Provider):
|
|
164
164
|
"""
|
165
165
|
Получаем асинхронную сессию.
|
166
166
|
"""
|
167
|
-
|
167
|
+
session_maker = await SessionFactory.make_async_session_dynamic(settings_repository)
|
168
|
+
async with session_maker() as session:
|
168
169
|
yield session
|
169
170
|
|
170
171
|
@provide
|
@@ -180,7 +181,7 @@ class CoreProvider(Provider):
|
|
180
181
|
seed_service = provide(SeedService)
|
181
182
|
transaction_service = provide(TransactionService)
|
182
183
|
|
183
|
-
@provide
|
184
|
+
@provide
|
184
185
|
@staticmethod
|
185
186
|
def get_cryptography_service_factory(settings: CoreSettingsSchema) -> CryptographyServiceFactory:
|
186
187
|
"""
|
@@ -188,7 +189,7 @@ class CoreProvider(Provider):
|
|
188
189
|
"""
|
189
190
|
return CryptographyServiceFactory(settings.secret_key)
|
190
191
|
|
191
|
-
@provide
|
192
|
+
@provide
|
192
193
|
@staticmethod
|
193
194
|
async def get_cryptography_service(
|
194
195
|
cryptography_service_factory: CryptographyServiceFactory,
|
@@ -198,7 +199,7 @@ class CoreProvider(Provider):
|
|
198
199
|
"""
|
199
200
|
return await cryptography_service_factory.make(CryptographicAlgorithmEnum.AES_GCM)
|
200
201
|
|
201
|
-
@provide
|
202
|
+
@provide
|
202
203
|
@staticmethod
|
203
204
|
def get_lock_service(settings: CoreSettingsSchema) -> LockServiceProtocol:
|
204
205
|
"""
|
fast_clean/middleware.py
CHANGED
@@ -2,22 +2,39 @@
|
|
2
2
|
Модуль, содержащий middleware.
|
3
3
|
"""
|
4
4
|
|
5
|
-
|
5
|
+
import time
|
6
|
+
from typing import Awaitable, Callable
|
7
|
+
|
8
|
+
from fastapi import FastAPI, Request, Response
|
6
9
|
from starlette.middleware.cors import CORSMiddleware
|
7
10
|
|
8
|
-
|
11
|
+
|
12
|
+
async def add_process_time_header(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
13
|
+
start_time = time.perf_counter()
|
14
|
+
response = await call_next(request)
|
15
|
+
response.headers['x-process-time'] = f'{time.perf_counter() - start_time}'
|
16
|
+
return response
|
9
17
|
|
10
18
|
|
11
|
-
def use_middleware(
|
19
|
+
def use_middleware(
|
20
|
+
app: FastAPI,
|
21
|
+
name: str,
|
22
|
+
cors_origins: list[str],
|
23
|
+
*,
|
24
|
+
allow_methods: list[str] | None = None,
|
25
|
+
allow_headers: list[str] | None = None,
|
26
|
+
) -> FastAPI:
|
12
27
|
"""
|
13
28
|
Регистрируем middleware.
|
14
29
|
"""
|
30
|
+
|
15
31
|
app.add_middleware(
|
16
32
|
CORSMiddleware,
|
17
33
|
allow_origins=cors_origins,
|
18
34
|
allow_credentials=True,
|
19
|
-
allow_methods=['*'],
|
20
|
-
allow_headers=['*'],
|
35
|
+
allow_methods=allow_methods or ['*'],
|
36
|
+
allow_headers=allow_headers or ['*'],
|
21
37
|
)
|
22
|
-
|
38
|
+
|
39
|
+
app.middleware('http')(add_process_time_header)
|
23
40
|
return app
|
fast_clean/models.py
CHANGED
@@ -9,9 +9,9 @@ from sqlalchemy.orm import Mapped, mapped_column
|
|
9
9
|
from sqlalchemy.sql import func
|
10
10
|
|
11
11
|
|
12
|
-
class
|
12
|
+
class CreatedAtMixin:
|
13
13
|
"""
|
14
|
-
Миксин, содержащий дату и время создания
|
14
|
+
Миксин, содержащий дату и время создания записи.
|
15
15
|
"""
|
16
16
|
|
17
17
|
created_at: Mapped[dt.datetime] = mapped_column(
|
@@ -19,15 +19,22 @@ class TimestampMixin:
|
|
19
19
|
default=lambda: dt.datetime.now(dt.UTC),
|
20
20
|
server_default=func.now(),
|
21
21
|
)
|
22
|
+
|
23
|
+
|
24
|
+
class UpdatedAtMixin:
|
22
25
|
"""
|
23
|
-
|
26
|
+
Миксин, содержащий дату и время обновления записи.
|
24
27
|
"""
|
28
|
+
|
25
29
|
updated_at: Mapped[dt.datetime] = mapped_column(
|
26
30
|
DateTime(timezone=True),
|
27
31
|
default=lambda: dt.datetime.now(dt.UTC),
|
28
32
|
server_default=func.now(),
|
29
33
|
onupdate=lambda: dt.datetime.now(dt.UTC),
|
30
34
|
)
|
35
|
+
|
36
|
+
|
37
|
+
class TimestampMixin(CreatedAtMixin, UpdatedAtMixin):
|
31
38
|
"""
|
32
|
-
|
39
|
+
Миксин, содержащий дату и время создания и обновления записи.
|
33
40
|
"""
|
@@ -8,7 +8,7 @@
|
|
8
8
|
|
9
9
|
import uuid
|
10
10
|
from collections.abc import Iterable, Sequence
|
11
|
-
from typing import
|
11
|
+
from typing import Protocol, Self
|
12
12
|
|
13
13
|
from fast_clean.schemas import PaginationResultSchema, PaginationSchema
|
14
14
|
|
@@ -70,8 +70,6 @@ class CrudRepositoryBaseProtocol(
|
|
70
70
|
async def paginate(
|
71
71
|
self: Self,
|
72
72
|
pagination: PaginationSchema,
|
73
|
-
user: Any,
|
74
|
-
policies: list[str],
|
75
73
|
*,
|
76
74
|
search: str | None = None,
|
77
75
|
search_by: Iterable[str] | None = None,
|
@@ -173,8 +173,6 @@ class DbCrudRepositoryBase(
|
|
173
173
|
async def paginate(
|
174
174
|
self: Self,
|
175
175
|
pagination: PaginationSchema,
|
176
|
-
user: Any,
|
177
|
-
policies: list[str],
|
178
176
|
*,
|
179
177
|
search: str | None = None,
|
180
178
|
search_by: Iterable[str] | None = None,
|
@@ -185,8 +183,6 @@ class DbCrudRepositoryBase(
|
|
185
183
|
"""
|
186
184
|
return await self.paginate_with_filter(
|
187
185
|
pagination,
|
188
|
-
user,
|
189
|
-
policies,
|
190
186
|
search=search,
|
191
187
|
search_by=search_by,
|
192
188
|
sorting=sorting,
|
@@ -475,8 +471,6 @@ class DbCrudRepositoryBase(
|
|
475
471
|
async def paginate_with_filter(
|
476
472
|
self: Self,
|
477
473
|
pagination: PaginationSchema,
|
478
|
-
user: Any,
|
479
|
-
policies: list[str],
|
480
474
|
*,
|
481
475
|
search: str | None = None,
|
482
476
|
search_by: Iterable[str] | None = None,
|
@@ -486,8 +480,6 @@ class DbCrudRepositoryBase(
|
|
486
480
|
"""
|
487
481
|
Получаем список моделей с пагинацией, поиском, сортировкой и фильтрами.
|
488
482
|
"""
|
489
|
-
if len(policies) == 0:
|
490
|
-
return PaginationResultSchema(objects=[], count=0)
|
491
483
|
search_by = search_by or []
|
492
484
|
sorting = sorting or []
|
493
485
|
async with self.session_manager.get_session() as s:
|
@@ -510,11 +502,11 @@ class DbCrudRepositoryBase(
|
|
510
502
|
count = (await s.execute(count_statement)).scalar_one()
|
511
503
|
return PaginationResultSchema(count=count, objects=objects)
|
512
504
|
|
513
|
-
def get_order_by_expr(self: Self, sorting: Iterable[str]) -> list[sa.UnaryExpression]:
|
505
|
+
def get_order_by_expr(self: Self, sorting: Iterable[str]) -> list[sa.UnaryExpression[Any]]:
|
514
506
|
"""
|
515
507
|
Получаем выражение сортировки.
|
516
508
|
"""
|
517
|
-
order_by_expr: list[sa.UnaryExpression] = []
|
509
|
+
order_by_expr: list[sa.UnaryExpression[Any]] = []
|
518
510
|
for st in sorting:
|
519
511
|
try:
|
520
512
|
if st[0] == '-':
|
@@ -7,7 +7,7 @@ import uuid
|
|
7
7
|
from abc import ABC, abstractmethod
|
8
8
|
from collections.abc import Iterable, Sequence
|
9
9
|
from itertools import groupby
|
10
|
-
from typing import
|
10
|
+
from typing import Callable, Generic, Self, cast, get_args
|
11
11
|
|
12
12
|
from fast_clean.enums import ModelActionEnum
|
13
13
|
from fast_clean.exceptions import ModelIntegrityError, ModelNotFoundError
|
@@ -145,8 +145,6 @@ class InMemoryCrudRepositoryBase(ABC, Generic[ReadSchemaBaseType, CreateSchemaBa
|
|
145
145
|
async def paginate(
|
146
146
|
self: Self,
|
147
147
|
pagination: PaginationSchema,
|
148
|
-
user: Any,
|
149
|
-
policies: list[str],
|
150
148
|
*,
|
151
149
|
search: str | None = None,
|
152
150
|
search_by: Iterable[str] | None = None,
|
@@ -157,8 +155,6 @@ class InMemoryCrudRepositoryBase(ABC, Generic[ReadSchemaBaseType, CreateSchemaBa
|
|
157
155
|
"""
|
158
156
|
return self.paginate_with_filter(
|
159
157
|
pagination,
|
160
|
-
user,
|
161
|
-
policies,
|
162
158
|
search=search,
|
163
159
|
search_by=search_by,
|
164
160
|
sorting=sorting,
|
@@ -251,8 +247,6 @@ class InMemoryCrudRepositoryBase(ABC, Generic[ReadSchemaBaseType, CreateSchemaBa
|
|
251
247
|
def paginate_with_filter(
|
252
248
|
self: Self,
|
253
249
|
pagination: PaginationSchema,
|
254
|
-
user: Any,
|
255
|
-
policies: list[str],
|
256
250
|
*,
|
257
251
|
search: str | None = None,
|
258
252
|
search_by: Iterable[str] | None = None,
|
@@ -262,8 +256,6 @@ class InMemoryCrudRepositoryBase(ABC, Generic[ReadSchemaBaseType, CreateSchemaBa
|
|
262
256
|
"""
|
263
257
|
Получаем список моделей с пагинацией, поиском, сортировкой и фильтрами.
|
264
258
|
"""
|
265
|
-
if len(policies) == 0:
|
266
|
-
return PaginationResultSchema(objects=[], count=0)
|
267
259
|
search_by = search_by or []
|
268
260
|
sorting = sorting or []
|
269
261
|
models = list(filter(select_filter, self.models.values()) if select_filter else self.models.values())
|
@@ -6,11 +6,13 @@
|
|
6
6
|
- S3
|
7
7
|
"""
|
8
8
|
|
9
|
+
from collections.abc import AsyncIterator
|
9
10
|
from pathlib import Path
|
10
11
|
from typing import AsyncContextManager, Protocol, Self
|
11
12
|
|
12
13
|
from .enums import StorageTypeEnum
|
13
14
|
from .local import LocalStorageRepository
|
15
|
+
from .reader import AsyncStreamReaderProtocol as AsyncStreamReaderProtocol
|
14
16
|
from .reader import StreamReaderProtocol, StreamReadProtocol
|
15
17
|
from .s3 import S3StorageRepository
|
16
18
|
from .schemas import (
|
@@ -25,6 +27,18 @@ class StorageRepositoryProtocol(Protocol):
|
|
25
27
|
Протокол репозитория файлового хранилища.
|
26
28
|
"""
|
27
29
|
|
30
|
+
async def __aenter__(self: Self) -> Self:
|
31
|
+
"""
|
32
|
+
Вход в контекст менеджера.
|
33
|
+
"""
|
34
|
+
...
|
35
|
+
|
36
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
37
|
+
"""
|
38
|
+
Выход из контекст менеджера.
|
39
|
+
"""
|
40
|
+
...
|
41
|
+
|
28
42
|
async def exists(self: Self, path: str | Path) -> bool:
|
29
43
|
"""
|
30
44
|
Проверяем существует ли файл.
|
@@ -71,14 +85,18 @@ class StorageRepositoryProtocol(Protocol):
|
|
71
85
|
self: Self,
|
72
86
|
path: str | Path,
|
73
87
|
stream: StreamReadProtocol,
|
74
|
-
length: int = -1,
|
75
|
-
part_size: int = 0,
|
76
88
|
) -> None:
|
77
89
|
"""
|
78
90
|
Создаем файл или переписываем существующий в потоковом режиме.
|
79
91
|
"""
|
80
92
|
...
|
81
93
|
|
94
|
+
def straming_read(self: Self, path: str | Path) -> AsyncIterator[bytes]:
|
95
|
+
"""
|
96
|
+
Возвращаем асинхронные итератор потока байт.
|
97
|
+
"""
|
98
|
+
...
|
99
|
+
|
82
100
|
async def delete(self: Self, path: str | Path) -> None:
|
83
101
|
"""
|
84
102
|
Удаляем файл.
|
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
import asyncio
|
6
6
|
import os
|
7
|
-
from collections.abc import AsyncGenerator, Awaitable, Callable
|
7
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable
|
8
8
|
from contextlib import asynccontextmanager
|
9
9
|
from logging import getLogger
|
10
10
|
from pathlib import Path
|
@@ -28,6 +28,14 @@ class LocalStorageRepository:
|
|
28
28
|
os.makedirs(self.work_dir)
|
29
29
|
self.logger = getLogger(__name__)
|
30
30
|
|
31
|
+
async def __aenter__(self: Self) -> Self:
|
32
|
+
"""
|
33
|
+
Вход в контекст менеджера.
|
34
|
+
"""
|
35
|
+
return self
|
36
|
+
|
37
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb): ...
|
38
|
+
|
31
39
|
async def exists(self: Self, path: str | Path) -> bool:
|
32
40
|
"""
|
33
41
|
Проверяем существует ли файл.
|
@@ -78,6 +86,12 @@ class LocalStorageRepository:
|
|
78
86
|
async with open(path, 'rb') as f:
|
79
87
|
yield AiofilesStreamReader(f)
|
80
88
|
|
89
|
+
async def straming_read(self: Self, path: str | Path) -> AsyncIterator[bytes]:
|
90
|
+
path = self.work_dir / path
|
91
|
+
async with open(path, 'rb') as f:
|
92
|
+
async for chunk in f:
|
93
|
+
yield chunk
|
94
|
+
|
81
95
|
async def write(self: Self, path: str | Path, content: str | bytes) -> None:
|
82
96
|
"""
|
83
97
|
Создаем файл или переписываем существующий.
|
@@ -91,15 +105,11 @@ class LocalStorageRepository:
|
|
91
105
|
self: Self,
|
92
106
|
path: str | Path,
|
93
107
|
stream: StreamReadProtocol,
|
94
|
-
length: int = -1,
|
95
|
-
part_size: int = 0,
|
96
108
|
) -> None:
|
97
109
|
"""
|
98
110
|
Создаем файл или переписываем существующий в потоковом режиме.
|
99
111
|
"""
|
100
|
-
|
101
|
-
self.logger.warning('Параметр length не используется для LocalStorage.')
|
102
|
-
part_size = part_size or 1024
|
112
|
+
part_size = 1024 * 1024
|
103
113
|
path = self.work_dir / path
|
104
114
|
is_co_function = asyncio.iscoroutinefunction(stream.read)
|
105
115
|
async with open(path, 'wb') as f:
|
@@ -35,6 +35,14 @@ class StreamReadAsyncProtocol(Protocol):
|
|
35
35
|
...
|
36
36
|
|
37
37
|
|
38
|
+
class AsyncStreamReaderProtocol(Protocol):
|
39
|
+
async def read(self: Self, size: int = -1) -> bytes:
|
40
|
+
"""
|
41
|
+
Потоковое чтение файлов.
|
42
|
+
"""
|
43
|
+
...
|
44
|
+
|
45
|
+
|
38
46
|
StreamReadProtocol = StreamReadAsyncProtocol | StreamReadSyncProtocol
|
39
47
|
|
40
48
|
|
@@ -1,112 +1,163 @@
|
|
1
1
|
"""
|
2
|
-
|
2
|
+
Модуль содержит имплементация работы с репозиторием по средствам протокола S3.
|
3
3
|
"""
|
4
4
|
|
5
|
-
import
|
6
|
-
from collections.abc import AsyncGenerator
|
5
|
+
from collections.abc import AsyncIterator
|
7
6
|
from contextlib import asynccontextmanager
|
8
7
|
from pathlib import Path
|
9
|
-
from typing import Self
|
8
|
+
from typing import TYPE_CHECKING, Self, cast
|
10
9
|
|
11
|
-
import
|
12
|
-
import
|
13
|
-
from
|
10
|
+
import aiobotocore
|
11
|
+
import aiobotocore.session
|
12
|
+
from aiobotocore.response import StreamingBody
|
13
|
+
from aiobotocore.session import AioSession
|
14
|
+
from botocore.exceptions import ClientError
|
14
15
|
|
15
|
-
from .
|
16
|
-
|
16
|
+
from fast_clean.repositories.storage.schemas import S3StorageParamsSchema
|
17
|
+
|
18
|
+
from .reader import AiofilesStreamReader, StreamReaderProtocol, StreamReadProtocol
|
19
|
+
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from types_aiobotocore_s3.client import S3Client as AioBaseClient
|
22
|
+
else:
|
23
|
+
from aiobotocore.client import AioBaseClient
|
17
24
|
|
18
25
|
|
19
26
|
class S3StorageRepository:
|
20
27
|
"""
|
21
|
-
Репозиторий хранилища S3.
|
28
|
+
Репозиторий хранилища S3 с использованием aiobotocore.
|
22
29
|
"""
|
23
30
|
|
24
|
-
def __init__(self
|
31
|
+
def __init__(self, params: S3StorageParamsSchema) -> None:
|
25
32
|
self.params = params
|
26
33
|
self.bucket = self.params.bucket
|
27
|
-
self.client = miniopy_async.Minio( # type: ignore
|
28
|
-
f'{self.params.endpoint}:{self.params.port}',
|
29
|
-
access_key=self.params.access_key,
|
30
|
-
secret_key=self.params.secret_key,
|
31
|
-
secure=self.params.secure,
|
32
|
-
)
|
33
34
|
|
34
|
-
|
35
|
+
self.session: AioSession | None = None
|
36
|
+
self.client: AioBaseClient | None = None
|
37
|
+
|
38
|
+
protocol = 'https' if self.params.secure else 'http'
|
39
|
+
self.endpoint_url = f'{protocol}://{self.params.endpoint}:{self.params.port}'
|
40
|
+
|
41
|
+
async def __aenter__(self: Self) -> Self:
|
42
|
+
self.session = aiobotocore.session.get_session()
|
43
|
+
self.client = await self.session.create_client(
|
44
|
+
's3',
|
45
|
+
endpoint_url=self.endpoint_url,
|
46
|
+
aws_access_key_id=self.params.aws_access_key_id,
|
47
|
+
aws_secret_access_key=self.params.aws_secret_access_key,
|
48
|
+
region_name=self.params.region_name,
|
49
|
+
).__aenter__()
|
50
|
+
return self
|
51
|
+
|
52
|
+
async def __aexit__(self: Self, exc_type, exc_val, exc_tb) -> None:
|
35
53
|
"""
|
36
|
-
|
54
|
+
Выход из контектсного менеджера сессии.
|
37
55
|
"""
|
56
|
+
if self.client:
|
57
|
+
await self.client.__aexit__(exc_type, exc_val, exc_tb)
|
58
|
+
self.client = None
|
59
|
+
self.session = None
|
60
|
+
|
61
|
+
async def exists(self: Self, path: str | Path) -> bool:
|
62
|
+
assert self.client
|
63
|
+
key = self.get_str_path(path)
|
64
|
+
if key == '':
|
65
|
+
key = '/'
|
38
66
|
try:
|
39
|
-
await self.client.
|
67
|
+
await self.client.head_object(Bucket=self.bucket, Key=key)
|
40
68
|
return True
|
41
|
-
except
|
69
|
+
except ClientError:
|
42
70
|
return False
|
43
71
|
|
44
72
|
async def listdir(self: Self, path: str | Path) -> list[str]:
|
45
73
|
"""
|
46
|
-
Получаем список файлов и директорий в
|
47
|
-
"""
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
74
|
+
Получаем список файлов и директорий в указанной лиректории.
|
75
|
+
"""
|
76
|
+
assert self.client
|
77
|
+
prefix = self.get_str_path(path)
|
78
|
+
if prefix and not prefix.endswith('/'):
|
79
|
+
prefix += '/'
|
80
|
+
objects = []
|
81
|
+
paginator = self.client.get_paginator('list_objects_v2')
|
82
|
+
async for page in paginator.paginate(Bucket=self.bucket, Prefix=prefix, Delimiter='/'):
|
83
|
+
if 'Contents' in page:
|
84
|
+
objects.extend([obj.get('Key') for obj in page['Contents'] if obj.get('Key') != prefix])
|
85
|
+
if 'CommonPrefixes' in page:
|
86
|
+
objects.extend([folder.get('Prefix') for folder in page['CommonPrefixes']])
|
87
|
+
return objects
|
53
88
|
|
54
89
|
async def is_file(self: Self, path: str | Path) -> bool:
|
55
90
|
"""
|
56
|
-
|
91
|
+
Проверяем, является ли путь файлом.
|
57
92
|
"""
|
58
|
-
return
|
93
|
+
return await self.exists(path)
|
59
94
|
|
60
95
|
async def is_dir(self: Self, path: str | Path) -> bool:
|
61
96
|
"""
|
62
|
-
|
63
|
-
"""
|
64
|
-
|
97
|
+
Проверяем, является ли путь дирректорией.
|
98
|
+
"""
|
99
|
+
assert self.client
|
100
|
+
prefix = self.get_str_path(path)
|
101
|
+
if prefix != '' and not prefix.endswith('/'):
|
102
|
+
prefix += '/'
|
103
|
+
response = await self.client.list_objects_v2(
|
104
|
+
Bucket=self.bucket,
|
105
|
+
Prefix=prefix,
|
106
|
+
MaxKeys=1,
|
107
|
+
Delimiter='/',
|
108
|
+
)
|
109
|
+
return 'Contents' in response or 'CommonPrefixes' in response
|
65
110
|
|
66
111
|
async def read(self: Self, path: str | Path) -> bytes:
|
67
112
|
"""
|
68
113
|
Читаем содержимое файла.
|
69
114
|
"""
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
return await
|
115
|
+
assert self.client
|
116
|
+
key = self.get_str_path(path)
|
117
|
+
response = await self.client.get_object(Bucket=self.bucket, Key=key)
|
118
|
+
async with response['Body'] as stream:
|
119
|
+
return await stream.read()
|
75
120
|
|
76
|
-
|
77
|
-
async def stream_read(self: Self, path: str | Path) -> AsyncGenerator[StreamReaderProtocol, None]:
|
121
|
+
async def write(self: Self, path: str | Path, content: str | bytes) -> None:
|
78
122
|
"""
|
79
|
-
|
123
|
+
Создаем файл или перезаписываем существующий.
|
80
124
|
"""
|
81
|
-
|
82
|
-
|
83
|
-
|
125
|
+
assert self.client
|
126
|
+
key = self.get_str_path(path)
|
127
|
+
content = content.encode('utf-8') if isinstance(content, str) else content
|
128
|
+
await self.client.put_object(Bucket=self.bucket, Key=key, Body=content)
|
84
129
|
|
85
|
-
|
130
|
+
@asynccontextmanager
|
131
|
+
async def stream_read(self: Self, path: str | Path) -> AsyncIterator[StreamReaderProtocol]:
|
86
132
|
"""
|
87
|
-
|
133
|
+
Читаем содержимое в потоковом режиме.
|
88
134
|
"""
|
89
|
-
|
90
|
-
|
91
|
-
await self.client.
|
135
|
+
assert self.client
|
136
|
+
key = self.get_str_path(path)
|
137
|
+
response = await self.client.get_object(Bucket=self.bucket, Key=key)
|
138
|
+
yield AiofilesStreamReader(response['Body'])
|
139
|
+
|
140
|
+
async def straming_read(self: Self, path: str | Path) -> AsyncIterator[bytes]:
|
141
|
+
assert self.client
|
142
|
+
key = self.get_str_path(path)
|
143
|
+
response = await self.client.get_object(Bucket=self.bucket, Key=key)
|
144
|
+
async for chunk in response['Body']:
|
145
|
+
yield chunk
|
92
146
|
|
93
|
-
async def stream_write(
|
94
|
-
self: Self,
|
95
|
-
path: str | Path,
|
96
|
-
stream: StreamReadProtocol,
|
97
|
-
length: int = -1,
|
98
|
-
part_size: int = 0,
|
99
|
-
) -> None:
|
147
|
+
async def stream_write(self: Self, path: str | Path, stream: StreamReadProtocol) -> None:
|
100
148
|
"""
|
101
|
-
Создаем
|
149
|
+
Создаем поток на запись файла и перезаписываем существующий, если он есть.
|
102
150
|
"""
|
103
|
-
|
151
|
+
assert self.client
|
152
|
+
await self.client.put_object(Bucket=self.bucket, Key=self.get_str_path(path), Body=cast(StreamingBody, stream))
|
104
153
|
|
105
154
|
async def delete(self: Self, path: str | Path) -> None:
|
106
155
|
"""
|
107
|
-
|
156
|
+
Удаление файла.
|
108
157
|
"""
|
109
|
-
|
158
|
+
assert self.client
|
159
|
+
key = self.get_str_path(path)
|
160
|
+
await self.client.delete_object(Bucket=self.bucket, Key=key)
|
110
161
|
|
111
162
|
@staticmethod
|
112
163
|
def get_str_path(path: str | Path) -> str:
|
@@ -114,5 +165,5 @@ class S3StorageRepository:
|
|
114
165
|
Получаем путь в виде строки.
|
115
166
|
"""
|
116
167
|
if isinstance(path, Path):
|
117
|
-
return '
|
168
|
+
return '' if path == Path('') else str(path)
|
118
169
|
return path
|
@@ -12,12 +12,17 @@ class S3StorageParamsSchema(BaseModel):
|
|
12
12
|
Параметры настроек для S3Storage.
|
13
13
|
"""
|
14
14
|
|
15
|
+
"""
|
16
|
+
Параметры настроек для S3Storage.
|
17
|
+
"""
|
18
|
+
|
15
19
|
endpoint: str
|
16
|
-
|
17
|
-
|
20
|
+
aws_secret_access_key: str
|
21
|
+
aws_access_key_id: str
|
18
22
|
port: int
|
19
23
|
bucket: str
|
20
|
-
secure: bool =
|
24
|
+
secure: bool = True
|
25
|
+
region_name: str = 'us-east-1'
|
21
26
|
|
22
27
|
|
23
28
|
class LocalStorageParamsSchema(BaseModel):
|
fast_clean/settings.py
CHANGED
@@ -27,6 +27,8 @@ class CoreDbSettingsSchema(BaseModel):
|
|
27
27
|
password: str
|
28
28
|
name: str
|
29
29
|
|
30
|
+
pool_pre_ping: bool = True
|
31
|
+
disable_prepared_statements: bool = True
|
30
32
|
scheme: str = 'public'
|
31
33
|
|
32
34
|
@property
|
@@ -53,9 +55,8 @@ class CoreS3SettingsSchema(BaseModel):
|
|
53
55
|
"""
|
54
56
|
|
55
57
|
endpoint: str
|
56
|
-
|
57
|
-
|
58
|
-
secret_key: str
|
58
|
+
aws_access_key_id: str
|
59
|
+
aws_secret_access_key: str
|
59
60
|
port: int
|
60
61
|
bucket: str
|
61
62
|
secure: bool = False
|
fast_clean/utils/toml.py
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
import tomllib
|
2
|
+
from functools import lru_cache
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
__all__ = (
|
9
|
+
'use_toml_info',
|
10
|
+
'ProjectInfo',
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
class ProjectInfo(BaseModel):
|
15
|
+
"""
|
16
|
+
Схема для получения информации о проекте.
|
17
|
+
"""
|
18
|
+
|
19
|
+
name: str
|
20
|
+
version: str
|
21
|
+
description: str | None = None
|
22
|
+
|
23
|
+
|
24
|
+
def use_toml(dir: Path) -> dict[str, Any]:
|
25
|
+
with open(Path(dir) / 'pyproject.toml', 'rb') as f:
|
26
|
+
return tomllib.load(f)
|
27
|
+
|
28
|
+
|
29
|
+
@lru_cache(maxsize=1)
|
30
|
+
def use_toml_info(dir: Path) -> ProjectInfo:
|
31
|
+
"""
|
32
|
+
Получение версии приложения из pyproject.toml.
|
33
|
+
"""
|
34
|
+
return ProjectInfo.model_validate(use_toml(dir)['project'])
|
@@ -1,10 +1,11 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: fast-clean
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.4.0
|
4
4
|
Summary: FastAPI Clean Architecture implementation
|
5
5
|
Author-email: Luferov Victor <luferovvs@yandex.ru>, Orlov Artem <squakrazv@yandex.ru>, Kashapov Rustam <hardtechnik91@gmail.com>
|
6
6
|
Requires-Python: >=3.13
|
7
7
|
Description-Content-Type: text/markdown
|
8
|
+
Requires-Dist: aiobotocore>=2.23.2
|
8
9
|
Requires-Dist: aiofiles>=24.1.0
|
9
10
|
Requires-Dist: aiokafka>=0.12.0
|
10
11
|
Requires-Dist: aioprometheus>=23.12.0
|
@@ -15,14 +16,12 @@ Requires-Dist: fastapi>=0.115.8
|
|
15
16
|
Requires-Dist: fastapi-cache2[redis]>=0.2.2
|
16
17
|
Requires-Dist: faststream>=0.5.34
|
17
18
|
Requires-Dist: flatten-dict>=0.4.2
|
18
|
-
Requires-Dist: miniopy-async>=1.21.1
|
19
19
|
Requires-Dist: overrides>=7.7.0
|
20
20
|
Requires-Dist: psycopg[binary]>=3.2.4
|
21
21
|
Requires-Dist: pydantic>=2.10.6
|
22
22
|
Requires-Dist: pydantic-settings>=2.8.0
|
23
23
|
Requires-Dist: pyyaml>=6.0.2
|
24
24
|
Requires-Dist: sentry-sdk[fastapi]>=2.32.0
|
25
|
-
Requires-Dist: snakecase>=1.0.1
|
26
25
|
Requires-Dist: sqlalchemy-utils>=0.41.2
|
27
26
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.38
|
28
27
|
Requires-Dist: stringcase>=1.2.0
|
@@ -1,25 +1,28 @@
|
|
1
1
|
fast_clean/__init__.py,sha256=sT4tb75t5PXws8W_7wpA0jNtNxkWPFLAMrPlDGS7RHw,51
|
2
2
|
fast_clean/broker.py,sha256=CHnL4Jd6jF5gKgtUXi33j9QFG2EUM4uqhVqdLuxIrZs,4474
|
3
3
|
fast_clean/container.py,sha256=E1e0H1JqGOacH4uBNwkjTDXYhzN56yZi0AmWXQ3DkEQ,3535
|
4
|
-
fast_clean/db.py,sha256=
|
5
|
-
fast_clean/depends.py,sha256=
|
4
|
+
fast_clean/db.py,sha256=uZHXXHdLstqhyzGtBL5Z7VvXwIe6mxuPOUheJEzSMyM,5776
|
5
|
+
fast_clean/depends.py,sha256=e5KQlvVPDPZrMaZBWAPOuLCt-HO7b9NA4UTMaTsB1Y8,7807
|
6
6
|
fast_clean/enums.py,sha256=lPhC_2_r6YFby7Mq-9u_JSiuyZ0e57F2VxBfUwnBZ18,826
|
7
7
|
fast_clean/exceptions.py,sha256=Sp-k-a5z1Gedu0slzj1-rORnr4GP1FXDHKCKRaJq-7o,9485
|
8
8
|
fast_clean/loggers.py,sha256=hVvZSDMMxYnK-p_yyjd4R7SyHpmxQF3eKQEeMu9Q-jo,705
|
9
|
-
fast_clean/middleware.py,sha256=
|
10
|
-
fast_clean/models.py,sha256=
|
9
|
+
fast_clean/middleware.py,sha256=p0Tv_qu89ZQtzKZ10tNepArJyHlFX2II9UmivGWnycw,1037
|
10
|
+
fast_clean/models.py,sha256=H7-Hk3gZP9i2TiJHu0u2Nex76c8ZbDhMR4lF41_PMyI,1057
|
11
11
|
fast_clean/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
12
|
fast_clean/redis.py,sha256=H_SLnDhY_ute8pYHdhIypUGlCwMcVfFA4S2j8vLUph0,578
|
13
|
-
fast_clean/settings.py,sha256=
|
13
|
+
fast_clean/settings.py,sha256=YgmdhW38quBHQty254Pv-vFZ5eGbl__VQ1lIgoVg8kI,4864
|
14
|
+
fast_clean/cli/__init__.py,sha256=m8n09uN47JGtAfgWVbXCJOxpzlrUazogqtLo6xPWe3s,181
|
15
|
+
fast_clean/cli/cryptography.py,sha256=ACaYAOn4KEKIdsTuCYkX1m6g2YpMczNCjJcVfLE2Rzo,1936
|
16
|
+
fast_clean/cli/load_seed.py,sha256=Tm5_r_myrC5dl_WyC6Bx2WKFAkfLf-Pch4ZK6zWN2Qg,867
|
14
17
|
fast_clean/contrib/__init__.py,sha256=AcFNyhc0QGsOnYvzQGanDN3QIAsKpn4d8RIj73F-sGc,63
|
15
18
|
fast_clean/contrib/healthcheck/__init__.py,sha256=p8hUCLdv2qGngTwAeTGIV4h_ZGDm9ZNWMrA5_k3Yi0E,106
|
16
19
|
fast_clean/contrib/healthcheck/router.py,sha256=7uq0D6ldhxB3Jsa9Ia1zpiRAyC3hQgUv8jF4W8SPi88,398
|
17
|
-
fast_clean/contrib/healthcheck/schemas.py,sha256=
|
20
|
+
fast_clean/contrib/healthcheck/schemas.py,sha256=s9HcXDWYUsFXHownoem5qOEL741IZWp5yOLu2LKtkkU,199
|
18
21
|
fast_clean/contrib/logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
19
22
|
fast_clean/contrib/logging/enums.py,sha256=a-Tz3k4j0aAbUXDvTV6sl2pKKEGuKG94qc3plXixezU,154
|
20
23
|
fast_clean/contrib/logging/sentry.py,sha256=gey6ynlkZtrU2qzwdKvpkYy0JO0AEyHDpiiRcIzfiDg,593
|
21
24
|
fast_clean/contrib/monitoring/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
|
-
fast_clean/contrib/monitoring/middleware.py,sha256=
|
25
|
+
fast_clean/contrib/monitoring/middleware.py,sha256=nRhiARjpHm21qwBgkdh2Sdkuc8maDQcb6ofCRthv_O0,190
|
23
26
|
fast_clean/contrib/monitoring/router.py,sha256=94gffX34VE_Yb6TLaQOP4YyXDQsClzOn4APb45V_HyA,153
|
24
27
|
fast_clean/contrib/sqlalchemy_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
28
|
fast_clean/contrib/sqlalchemy_utils/utils.py,sha256=5xktmJlQifGpIXMruhe7qofEEu_ExncBTUmlrtFK1YQ,1061
|
@@ -27,21 +30,21 @@ fast_clean/repositories/__init__.py,sha256=mHJ6CW1fYkkiSnnYiO3GRAa5YVCPN1otOOKkj
|
|
27
30
|
fast_clean/repositories/cache/__init__.py,sha256=pD7qIS6H8DrnhOptJiXrlGcWYUCIU3VmVQCLccyxx4Q,2511
|
28
31
|
fast_clean/repositories/cache/in_memory.py,sha256=Hb68UrTmQozALcyLrmYPBIfJfi67NvsCTDe1RfqwBHQ,2259
|
29
32
|
fast_clean/repositories/cache/redis.py,sha256=UjrA2CXQtMfHTpowz6Ot952y73YjTEr6zJlBbWblaws,1908
|
30
|
-
fast_clean/repositories/crud/__init__.py,sha256=
|
31
|
-
fast_clean/repositories/crud/db.py,sha256=
|
32
|
-
fast_clean/repositories/crud/in_memory.py,sha256
|
33
|
+
fast_clean/repositories/crud/__init__.py,sha256=z_-zY3esEbUEHSGb9WInU-vvRuTpTu4M-Qe5UhCN0Pc,4359
|
34
|
+
fast_clean/repositories/crud/db.py,sha256=wyvDvEjvncfSVHlaguhrgCP7wIsiKRoGZEesxHzDVHI,23212
|
35
|
+
fast_clean/repositories/crud/in_memory.py,sha256=37VBQJTIV4z1_Om9DhYqpa1t98hGGhY8gumoyV-fhDg,13172
|
33
36
|
fast_clean/repositories/crud/type_vars.py,sha256=Gb4ew1T1NkitL87hJ75KtpTjOi6PuML5fU_zFAsVUqA,1318
|
34
37
|
fast_clean/repositories/settings/__init__.py,sha256=ZxrncvTDs8pNkhWSy2cxV3a8uElTnrM-b1-vq4ouJok,1485
|
35
38
|
fast_clean/repositories/settings/enums.py,sha256=coqZg6xe_mRFWeihBfnSkCByLuD0pT8Vv4g02tpBk-w,358
|
36
39
|
fast_clean/repositories/settings/env.py,sha256=maQttYENMJyTf4vnSXa4L3R6tKiLmb-d0Q5VS-r9ZuE,2153
|
37
40
|
fast_clean/repositories/settings/exceptions.py,sha256=SKU45z-ahPzI_G6k4A9twupx1v3GaXDj2pbFkg3YgFE,348
|
38
41
|
fast_clean/repositories/settings/type_vars.py,sha256=_Oe8x4JwwrN9WOVjLA05BN6gv7cBcBmq2YR2ZI4Hz5w,197
|
39
|
-
fast_clean/repositories/storage/__init__.py,sha256=
|
42
|
+
fast_clean/repositories/storage/__init__.py,sha256=HoSOCWbntw74W0OlXM0Nn4QN29AXftKbo5ZtVcfycQU,4155
|
40
43
|
fast_clean/repositories/storage/enums.py,sha256=bS4L63aEXNaGnJql8A1jmsK4KY916cWnzTW5p_PyLmg,375
|
41
|
-
fast_clean/repositories/storage/local.py,sha256=
|
42
|
-
fast_clean/repositories/storage/reader.py,sha256=
|
43
|
-
fast_clean/repositories/storage/s3.py,sha256=
|
44
|
-
fast_clean/repositories/storage/schemas.py,sha256=
|
44
|
+
fast_clean/repositories/storage/local.py,sha256=W7aV-vRbN1E2Sn-V-n7ztDZiZe1xFmFa9AJucDM1XJc,4525
|
45
|
+
fast_clean/repositories/storage/reader.py,sha256=T-5BLkiUSg-3fo3ACrHO0qqE-OKi40L6Wu2fXJJO9L4,3491
|
46
|
+
fast_clean/repositories/storage/s3.py,sha256=70bkMd48YNYyZxLKIOD8vwoaePfD9mUNkHJtMo93JwU,6445
|
47
|
+
fast_clean/repositories/storage/schemas.py,sha256=zJcjl3jrxcO0A6d7ohD5GRLsgRL5f2mcGZQ95pJimc8,755
|
45
48
|
fast_clean/schemas/__init__.py,sha256=VgJKIY20qoZZOV55zLGnH2FYWoHpPfJS31HJAj_nGIo,1283
|
46
49
|
fast_clean/schemas/exceptions.py,sha256=E7G9jv4G82Ede7OQ3619vPGwEywc7tKmXW6EolOGRFQ,723
|
47
50
|
fast_clean/schemas/pagination.py,sha256=GEQ-Tbhx6xkMMXhDNWrTEhPv8IdnAOJxH2P1tscmn60,1384
|
@@ -51,7 +54,7 @@ fast_clean/services/__init__.py,sha256=Lvdb5ZibRGwoMn_WENrk9wERUViTsPrU8E_71XtPF
|
|
51
54
|
fast_clean/services/lock.py,sha256=SLF9_wRx3rgHMw829XwflJgAlGJyXj57o4iVPvGwe78,1653
|
52
55
|
fast_clean/services/seed.py,sha256=M0yA2I5z-jLM2UcW_x7287mwIFW5Vt0fPFaplakGFc0,2836
|
53
56
|
fast_clean/services/transaction.py,sha256=djXR6e6ukgpBXDbVmU095MvRJAIqdOPMgAcege52Qxg,762
|
54
|
-
fast_clean/services/cryptography/__init__.py,sha256=
|
57
|
+
fast_clean/services/cryptography/__init__.py,sha256=XK9-z6HT1Jgfc4-IpNY6fZihjW2dqeO83cz5ZvjJIbo,1559
|
55
58
|
fast_clean/services/cryptography/aes.py,sha256=_k0WtnKDaEKdUBegfwmqerE75ER44307CEQ-I2W0abo,4616
|
56
59
|
fast_clean/services/cryptography/enums.py,sha256=cLibSGv6LNVTUI2rm3_DtDwU68GYIAf4kY3GGbtnw1A,494
|
57
60
|
fast_clean/utils/__init__.py,sha256=Q3OiJNdWl51Vd_wSP7iuZQIq4_SjM1mYkqIWPaw94WU,709
|
@@ -61,9 +64,10 @@ fast_clean/utils/ssl_context.py,sha256=I3tM9bDB6LVMaKCDcrpREzBE4AoTWr3NQDU3_A0Kt
|
|
61
64
|
fast_clean/utils/string.py,sha256=8Dy3MeDHn-V9SUknuYZp8M6iakuU_UAmkMC9UreoN8k,630
|
62
65
|
fast_clean/utils/thread.py,sha256=ChEWBLupnSEMq4Wro_aiW0QvCLUKedKc0TQFMu7Zg4g,565
|
63
66
|
fast_clean/utils/time.py,sha256=nvavbtG4zR_gkrGSbsqKAsBdePxO3LuTeoISbFZIgn0,307
|
67
|
+
fast_clean/utils/toml.py,sha256=NbP7EfgKNYQ18LH8Hc-DmY1gks92bUSBW3D3-tMrY4E,737
|
64
68
|
fast_clean/utils/type_converters.py,sha256=bMEJeoQB9Q6Qok1-ppn4Ii8ZpIkZwJbD2IzCydSStHw,523
|
65
69
|
fast_clean/utils/typer.py,sha256=1O7BsNGn68bBzNbj0-Ycfhv35WpLzwvYTKn510YNXQQ,663
|
66
|
-
fast_clean-1.
|
67
|
-
fast_clean-1.
|
68
|
-
fast_clean-1.
|
69
|
-
fast_clean-1.
|
70
|
+
fast_clean-1.4.0.dist-info/METADATA,sha256=ltPh_moagnnuYT0IIVzx_hxD4I3PddBCpKp27xiy45s,1200
|
71
|
+
fast_clean-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
72
|
+
fast_clean-1.4.0.dist-info/top_level.txt,sha256=QfsGs-QLmPCZWWPFOukD0zhMnokH68FoO2KeObl6ZIA,11
|
73
|
+
fast_clean-1.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|