fast-clean 1.3.0__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.3.0 → 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.3.0 → fast_clean-1.4.0}/fast_clean/contrib/healthcheck/schemas.py +0 -1
  6. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/monitoring/middleware.py +1 -1
  7. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/db.py +37 -22
  8. {fast_clean-1.3.0 → 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.3.0 → fast_clean-1.4.0}/fast_clean/models.py +11 -4
  11. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/crud/db.py +2 -2
  12. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/storage/__init__.py +20 -2
  13. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/storage/local.py +16 -6
  14. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/storage/reader.py +8 -0
  15. fast_clean-1.4.0/fast_clean/repositories/storage/s3.py +169 -0
  16. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/storage/schemas.py +8 -3
  17. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/cryptography/__init__.py +1 -1
  18. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/settings.py +4 -3
  19. fast_clean-1.4.0/fast_clean/utils/toml.py +34 -0
  20. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean.egg-info/PKG-INFO +2 -3
  21. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean.egg-info/SOURCES.txt +4 -0
  22. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean.egg-info/requires.txt +1 -2
  23. {fast_clean-1.3.0 → fast_clean-1.4.0}/pyproject.toml +5 -5
  24. fast_clean-1.3.0/fast_clean/middleware.py +0 -23
  25. fast_clean-1.3.0/fast_clean/repositories/storage/s3.py +0 -118
  26. {fast_clean-1.3.0 → fast_clean-1.4.0}/README.md +0 -0
  27. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/__init__.py +0 -0
  28. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/broker.py +0 -0
  29. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/container.py +0 -0
  30. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/__init__.py +0 -0
  31. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/healthcheck/__init__.py +0 -0
  32. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/healthcheck/router.py +0 -0
  33. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/logging/__init__.py +0 -0
  34. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/logging/enums.py +0 -0
  35. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/logging/sentry.py +0 -0
  36. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/monitoring/__init__.py +0 -0
  37. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/monitoring/router.py +0 -0
  38. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/sqlalchemy_utils/__init__.py +0 -0
  39. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/contrib/sqlalchemy_utils/utils.py +0 -0
  40. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/enums.py +0 -0
  41. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/exceptions.py +0 -0
  42. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/loggers.py +0 -0
  43. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/py.typed +0 -0
  44. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/redis.py +0 -0
  45. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/__init__.py +0 -0
  46. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/cache/__init__.py +0 -0
  47. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/cache/in_memory.py +0 -0
  48. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/cache/redis.py +0 -0
  49. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/crud/__init__.py +0 -0
  50. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/crud/in_memory.py +0 -0
  51. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/crud/type_vars.py +0 -0
  52. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/settings/__init__.py +0 -0
  53. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/settings/enums.py +0 -0
  54. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/settings/env.py +0 -0
  55. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/settings/exceptions.py +0 -0
  56. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/settings/type_vars.py +0 -0
  57. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/repositories/storage/enums.py +0 -0
  58. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/schemas/__init__.py +0 -0
  59. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/schemas/exceptions.py +0 -0
  60. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/schemas/pagination.py +0 -0
  61. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/schemas/repository.py +0 -0
  62. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/schemas/request_response.py +0 -0
  63. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/__init__.py +0 -0
  64. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/cryptography/aes.py +0 -0
  65. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/cryptography/enums.py +0 -0
  66. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/lock.py +0 -0
  67. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/seed.py +0 -0
  68. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/services/transaction.py +0 -0
  69. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/__init__.py +0 -0
  70. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/process.py +0 -0
  71. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/pydantic.py +0 -0
  72. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/ssl_context.py +0 -0
  73. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/string.py +0 -0
  74. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/thread.py +0 -0
  75. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/time.py +0 -0
  76. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/type_converters.py +0 -0
  77. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean/utils/typer.py +0 -0
  78. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean.egg-info/dependency_links.txt +0 -0
  79. {fast_clean-1.3.0 → fast_clean-1.4.0}/fast_clean.egg-info/top_level.txt +0 -0
  80. {fast_clean-1.3.0 → fast_clean-1.4.0}/setup.cfg +0 -0
  81. {fast_clean-1.3.0 → fast_clean-1.4.0}/tests/test_broker.py +0 -0
  82. {fast_clean-1.3.0 → fast_clean-1.4.0}/tests/test_db.py +0 -0
  83. {fast_clean-1.3.0 → 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.3.0
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
  """
@@ -502,11 +502,11 @@ class DbCrudRepositoryBase(
502
502
  count = (await s.execute(count_statement)).scalar_one()
503
503
  return PaginationResultSchema(count=count, objects=objects)
504
504
 
505
- 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]]:
506
506
  """
507
507
  Получаем выражение сортировки.
508
508
  """
509
- order_by_expr: list[sa.UnaryExpression] = []
509
+ order_by_expr: list[sa.UnaryExpression[Any]] = []
510
510
  for st in sorting:
511
511
  try:
512
512
  if st[0] == '-':
@@ -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:
@@ -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