fast-clean 1.2.3__tar.gz → 1.4.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.
Files changed (83) hide show
  1. {fast_clean-1.2.3 → fast_clean-1.4.0}/PKG-INFO +2 -3
  2. fast_clean-1.4.0/fast_clean/cli/__init__.py +6 -0
  3. fast_clean-1.4.0/fast_clean/cli/cryptography.py +53 -0
  4. fast_clean-1.4.0/fast_clean/cli/load_seed.py +31 -0
  5. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/healthcheck/schemas.py +0 -1
  6. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/monitoring/middleware.py +1 -1
  7. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/db.py +37 -22
  8. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/depends.py +20 -19
  9. fast_clean-1.4.0/fast_clean/middleware.py +40 -0
  10. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/models.py +11 -4
  11. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/crud/__init__.py +1 -3
  12. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/crud/db.py +2 -10
  13. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/crud/in_memory.py +1 -9
  14. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/storage/__init__.py +20 -2
  15. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/storage/local.py +16 -6
  16. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/storage/reader.py +8 -0
  17. fast_clean-1.4.0/fast_clean/repositories/storage/s3.py +169 -0
  18. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/storage/schemas.py +8 -3
  19. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/services/cryptography/__init__.py +1 -1
  20. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/settings.py +4 -3
  21. fast_clean-1.4.0/fast_clean/utils/toml.py +34 -0
  22. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean.egg-info/PKG-INFO +2 -3
  23. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean.egg-info/SOURCES.txt +4 -0
  24. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean.egg-info/requires.txt +1 -2
  25. {fast_clean-1.2.3 → fast_clean-1.4.0}/pyproject.toml +5 -5
  26. fast_clean-1.2.3/fast_clean/middleware.py +0 -23
  27. fast_clean-1.2.3/fast_clean/repositories/storage/s3.py +0 -118
  28. {fast_clean-1.2.3 → fast_clean-1.4.0}/README.md +0 -0
  29. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/__init__.py +0 -0
  30. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/broker.py +0 -0
  31. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/container.py +0 -0
  32. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/__init__.py +0 -0
  33. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/healthcheck/__init__.py +0 -0
  34. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/healthcheck/router.py +0 -0
  35. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/logging/__init__.py +0 -0
  36. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/logging/enums.py +0 -0
  37. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/logging/sentry.py +0 -0
  38. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/monitoring/__init__.py +0 -0
  39. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/monitoring/router.py +0 -0
  40. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/sqlalchemy_utils/__init__.py +0 -0
  41. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/contrib/sqlalchemy_utils/utils.py +0 -0
  42. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/enums.py +0 -0
  43. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/exceptions.py +0 -0
  44. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/loggers.py +0 -0
  45. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/py.typed +0 -0
  46. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/redis.py +0 -0
  47. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/__init__.py +0 -0
  48. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/cache/__init__.py +0 -0
  49. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/cache/in_memory.py +0 -0
  50. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/cache/redis.py +0 -0
  51. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/crud/type_vars.py +0 -0
  52. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/settings/__init__.py +0 -0
  53. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/settings/enums.py +0 -0
  54. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/settings/env.py +0 -0
  55. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/settings/exceptions.py +0 -0
  56. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/settings/type_vars.py +0 -0
  57. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/repositories/storage/enums.py +0 -0
  58. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/schemas/__init__.py +0 -0
  59. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/schemas/exceptions.py +0 -0
  60. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/schemas/pagination.py +0 -0
  61. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/schemas/repository.py +0 -0
  62. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/schemas/request_response.py +0 -0
  63. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/services/__init__.py +0 -0
  64. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/services/cryptography/aes.py +0 -0
  65. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/services/cryptography/enums.py +0 -0
  66. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/services/lock.py +0 -0
  67. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/services/seed.py +0 -0
  68. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/services/transaction.py +0 -0
  69. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/utils/__init__.py +0 -0
  70. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/utils/process.py +0 -0
  71. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/utils/pydantic.py +0 -0
  72. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/utils/ssl_context.py +0 -0
  73. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/utils/string.py +0 -0
  74. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/utils/thread.py +0 -0
  75. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/utils/time.py +0 -0
  76. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/utils/type_converters.py +0 -0
  77. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean/utils/typer.py +0 -0
  78. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean.egg-info/dependency_links.txt +0 -0
  79. {fast_clean-1.2.3 → fast_clean-1.4.0}/fast_clean.egg-info/top_level.txt +0 -0
  80. {fast_clean-1.2.3 → fast_clean-1.4.0}/setup.cfg +0 -0
  81. {fast_clean-1.2.3 → fast_clean-1.4.0}/tests/test_broker.py +0 -0
  82. {fast_clean-1.2.3 → fast_clean-1.4.0}/tests/test_db.py +0 -0
  83. {fast_clean-1.2.3 → fast_clean-1.4.0}/tests/test_exceptions.py +0 -0
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fast-clean
3
- Version: 1.2.3
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
@@ -0,0 +1,6 @@
1
+ """
2
+ Пакет, содержащий команды Typer.
3
+ """
4
+
5
+ from .cryptography import use_cryptography as use_cryptography
6
+ from .load_seed import use_load_seed as use_load_seed
@@ -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)
@@ -1,4 +1,3 @@
1
-
2
1
  from fast_clean.schemas.request_response import ResponseSchema
3
2
 
4
3
 
@@ -3,4 +3,4 @@ from fastapi import FastAPI
3
3
 
4
4
 
5
5
  def use_middleware(app: FastAPI) -> None:
6
- app.add_middleware(MetricsMiddleware) # type: ignore
6
+ app.add_middleware(MetricsMiddleware) # type: ignore
@@ -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.uuid import UUIDType
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(db_dsn: str, *, scheme: str = 'public', echo: bool = False) -> AsyncEngine:
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, *, scheme: str = 'public', echo: bool = False
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(db_dsn, scheme=scheme, echo=echo)
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
- ) -> AsyncIterator[AsyncSession]:
142
+ ) -> async_sessionmaker[AsyncSession]:
132
143
  """
133
144
  Создаем асинхронную сессию с помощью динамической фабрики.
134
145
  """
135
- async_session_factory = await cls.make_async_session_factory(settings_repository)
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(db_settings.dsn, scheme=db_settings.scheme, echo=settings.debug)
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):
@@ -82,18 +82,14 @@ class CoreProvider(Provider):
82
82
  Провайдер зависимостей.
83
83
  """
84
84
 
85
- scope = Scope.REQUEST
85
+ scope = Scope.APP
86
86
 
87
87
  # --- repositories ---
88
88
 
89
- settings_repository_factory = provide(
90
- SettingsRepositoryFactoryImpl, provides=SettingsRepositoryFactoryProtocol, scope=Scope.APP
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(scope=Scope.APP)
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(scope=Scope.APP)
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(scope=Scope.APP)
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.APP)
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
- return await storage_repository_factory.make(
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
- elif storage_settings.provider == 'local' and storage_settings.dir is not None:
154
- return await storage_repository_factory.make(
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
- async with SessionFactory.make_async_session_static(settings_repository) as session:
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(scope=Scope.APP)
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(scope=Scope.APP)
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(scope=Scope.APP)
202
+ @provide
202
203
  @staticmethod
203
204
  def get_lock_service(settings: CoreSettingsSchema) -> LockServiceProtocol:
204
205
  """
@@ -0,0 +1,40 @@
1
+ """
2
+ Модуль, содержащий middleware.
3
+ """
4
+
5
+ import time
6
+ from typing import Awaitable, Callable
7
+
8
+ from fastapi import FastAPI, Request, Response
9
+ from starlette.middleware.cors import CORSMiddleware
10
+
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
17
+
18
+
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:
27
+ """
28
+ Регистрируем middleware.
29
+ """
30
+
31
+ app.add_middleware(
32
+ CORSMiddleware,
33
+ allow_origins=cors_origins,
34
+ allow_credentials=True,
35
+ allow_methods=allow_methods or ['*'],
36
+ allow_headers=allow_headers or ['*'],
37
+ )
38
+
39
+ app.middleware('http')(add_process_time_header)
40
+ return app
@@ -9,9 +9,9 @@ from sqlalchemy.orm import Mapped, mapped_column
9
9
  from sqlalchemy.sql import func
10
10
 
11
11
 
12
- class TimestampMixin:
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 Any, Protocol, Self
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 Any, Callable, Generic, Self, cast, get_args
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
- if length != -1:
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: