fast-clean 1.3.0__tar.gz → 1.4.1__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.1}/PKG-INFO +2 -3
  2. fast_clean-1.4.1/fast_clean/cli/__init__.py +6 -0
  3. fast_clean-1.4.1/fast_clean/cli/cryptography.py +53 -0
  4. fast_clean-1.4.1/fast_clean/cli/load_seed.py +31 -0
  5. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/healthcheck/schemas.py +0 -1
  6. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/monitoring/middleware.py +1 -1
  7. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/db.py +37 -22
  8. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/depends.py +24 -33
  9. fast_clean-1.4.1/fast_clean/middleware.py +40 -0
  10. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/models.py +11 -4
  11. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/cache/__init__.py +10 -4
  12. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/cache/redis.py +0 -1
  13. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/crud/db.py +2 -2
  14. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/storage/__init__.py +20 -2
  15. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/storage/local.py +16 -6
  16. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/storage/reader.py +8 -0
  17. fast_clean-1.4.1/fast_clean/repositories/storage/s3.py +169 -0
  18. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/storage/schemas.py +8 -3
  19. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/services/cryptography/__init__.py +1 -1
  20. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/services/lock.py +2 -1
  21. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/settings.py +10 -4
  22. fast_clean-1.4.1/fast_clean/utils/toml.py +34 -0
  23. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean.egg-info/PKG-INFO +2 -3
  24. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean.egg-info/SOURCES.txt +4 -1
  25. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean.egg-info/requires.txt +1 -2
  26. {fast_clean-1.3.0 → fast_clean-1.4.1}/pyproject.toml +5 -5
  27. fast_clean-1.3.0/fast_clean/middleware.py +0 -23
  28. fast_clean-1.3.0/fast_clean/redis.py +0 -23
  29. fast_clean-1.3.0/fast_clean/repositories/storage/s3.py +0 -118
  30. {fast_clean-1.3.0 → fast_clean-1.4.1}/README.md +0 -0
  31. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/__init__.py +0 -0
  32. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/broker.py +0 -0
  33. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/container.py +0 -0
  34. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/__init__.py +0 -0
  35. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/healthcheck/__init__.py +0 -0
  36. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/healthcheck/router.py +0 -0
  37. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/logging/__init__.py +0 -0
  38. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/logging/enums.py +0 -0
  39. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/logging/sentry.py +0 -0
  40. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/monitoring/__init__.py +0 -0
  41. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/monitoring/router.py +0 -0
  42. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/sqlalchemy_utils/__init__.py +0 -0
  43. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/contrib/sqlalchemy_utils/utils.py +0 -0
  44. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/enums.py +0 -0
  45. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/exceptions.py +0 -0
  46. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/loggers.py +0 -0
  47. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/py.typed +0 -0
  48. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/__init__.py +0 -0
  49. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/cache/in_memory.py +0 -0
  50. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/crud/__init__.py +0 -0
  51. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/crud/in_memory.py +0 -0
  52. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/crud/type_vars.py +0 -0
  53. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/settings/__init__.py +0 -0
  54. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/settings/enums.py +0 -0
  55. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/settings/env.py +0 -0
  56. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/settings/exceptions.py +0 -0
  57. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/settings/type_vars.py +0 -0
  58. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/repositories/storage/enums.py +0 -0
  59. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/schemas/__init__.py +0 -0
  60. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/schemas/exceptions.py +0 -0
  61. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/schemas/pagination.py +0 -0
  62. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/schemas/repository.py +0 -0
  63. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/schemas/request_response.py +0 -0
  64. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/services/__init__.py +0 -0
  65. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/services/cryptography/aes.py +0 -0
  66. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/services/cryptography/enums.py +0 -0
  67. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/services/seed.py +0 -0
  68. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/services/transaction.py +0 -0
  69. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/utils/__init__.py +0 -0
  70. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/utils/process.py +0 -0
  71. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/utils/pydantic.py +0 -0
  72. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/utils/ssl_context.py +0 -0
  73. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/utils/string.py +0 -0
  74. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/utils/thread.py +0 -0
  75. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/utils/time.py +0 -0
  76. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/utils/type_converters.py +0 -0
  77. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean/utils/typer.py +0 -0
  78. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean.egg-info/dependency_links.txt +0 -0
  79. {fast_clean-1.3.0 → fast_clean-1.4.1}/fast_clean.egg-info/top_level.txt +0 -0
  80. {fast_clean-1.3.0 → fast_clean-1.4.1}/setup.cfg +0 -0
  81. {fast_clean-1.3.0 → fast_clean-1.4.1}/tests/test_broker.py +0 -0
  82. {fast_clean-1.3.0 → fast_clean-1.4.1}/tests/test_db.py +0 -0
  83. {fast_clean-1.3.0 → fast_clean-1.4.1}/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.1
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):
@@ -16,7 +16,6 @@ from stringcase import snakecase
16
16
 
17
17
  from .broker import BrokerFactory
18
18
  from .db import SessionFactory, SessionManagerImpl, SessionManagerProtocol
19
- from .redis import RedisManager
20
19
  from .repositories import (
21
20
  CacheManager,
22
21
  CacheRepositoryProtocol,
@@ -82,18 +81,14 @@ class CoreProvider(Provider):
82
81
  Провайдер зависимостей.
83
82
  """
84
83
 
85
- scope = Scope.REQUEST
84
+ scope = Scope.APP
86
85
 
87
86
  # --- repositories ---
88
87
 
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
- )
88
+ settings_repository_factory = provide(SettingsRepositoryFactoryImpl, provides=SettingsRepositoryFactoryProtocol)
89
+ storage_repository_factory = provide(StorageRepositoryFactoryImpl, provides=StorageRepositoryFactoryProtocol)
95
90
 
96
- @provide(scope=Scope.APP)
91
+ @provide
97
92
  @staticmethod
98
93
  async def get_settings_repository(
99
94
  settings_repository_factory: SettingsRepositoryFactoryProtocol,
@@ -103,7 +98,7 @@ class CoreProvider(Provider):
103
98
  """
104
99
  return await settings_repository_factory.make(SettingsSourceEnum.ENV)
105
100
 
106
- @provide(scope=Scope.APP)
101
+ @provide
107
102
  @staticmethod
108
103
  async def get_settings(settings_repository: SettingsRepositoryProtocol) -> CoreSettingsSchema:
109
104
  """
@@ -119,41 +114,38 @@ class CoreProvider(Provider):
119
114
  kafka_settings = await settings_repository.get(CoreKafkaSettingsSchema)
120
115
  yield BrokerFactory.make_static(kafka_settings)
121
116
 
122
- @provide(scope=Scope.APP)
117
+ @provide
123
118
  @staticmethod
124
119
  async def get_cache_repository(settings_repository: SettingsRepositoryProtocol) -> CacheRepositoryProtocol:
125
120
  """
126
121
  Получаем репозиторий кеша.
127
122
  """
128
- settings = await settings_repository.get(CoreSettingsSchema)
129
- if settings.redis_dsn is not None:
130
- RedisManager.init(settings.redis_dsn)
131
123
  cache_settings = await settings_repository.get(CoreCacheSettingsSchema)
132
- if CacheManager.cache_repository is None:
133
- CacheManager.init(cache_settings, RedisManager.redis)
134
- if CacheManager.cache_repository is not None:
135
- return CacheManager.cache_repository
136
- raise ValueError('Cache is not initialized')
124
+ return CacheManager.init(cache_settings)
137
125
 
138
- @provide(scope=Scope.APP)
126
+ @provide(scope=Scope.REQUEST)
139
127
  @staticmethod
140
128
  async def get_storage_repository(
141
129
  settings_repository: SettingsRepositoryProtocol,
142
130
  storage_repository_factory: StorageRepositoryFactoryProtocol,
143
- ) -> StorageRepositoryProtocol:
131
+ ) -> AsyncIterator[StorageRepositoryProtocol]:
144
132
  """
145
133
  Получаем репозиторий файлового хранилища.
146
134
  """
147
135
  storage_settings = await settings_repository.get(CoreStorageSettingsSchema)
148
136
  if storage_settings.provider == 's3' and storage_settings.s3 is not None:
149
- return await storage_repository_factory.make(
137
+ storage_repository = await storage_repository_factory.make(
150
138
  StorageTypeEnum.S3,
151
139
  S3StorageParamsSchema.model_validate(storage_settings.s3.model_dump()),
152
140
  )
153
- elif storage_settings.provider == 'local' and storage_settings.dir is not None:
154
- return await storage_repository_factory.make(
141
+ async with storage_repository:
142
+ yield storage_repository
143
+ elif storage_settings.provider == 'local':
144
+ storage_repository = await storage_repository_factory.make(
155
145
  StorageTypeEnum.LOCAL, LocalStorageParamsSchema(path=storage_settings.dir)
156
146
  )
147
+ async with storage_repository:
148
+ yield storage_repository
157
149
  raise NotImplementedError(f'Storage {storage_settings.provider} not allowed')
158
150
 
159
151
  # --- db ---
@@ -164,7 +156,8 @@ class CoreProvider(Provider):
164
156
  """
165
157
  Получаем асинхронную сессию.
166
158
  """
167
- async with SessionFactory.make_async_session_static(settings_repository) as session:
159
+ session_maker = await SessionFactory.make_async_session_dynamic(settings_repository)
160
+ async with session_maker() as session:
168
161
  yield session
169
162
 
170
163
  @provide
@@ -180,7 +173,7 @@ class CoreProvider(Provider):
180
173
  seed_service = provide(SeedService)
181
174
  transaction_service = provide(TransactionService)
182
175
 
183
- @provide(scope=Scope.APP)
176
+ @provide
184
177
  @staticmethod
185
178
  def get_cryptography_service_factory(settings: CoreSettingsSchema) -> CryptographyServiceFactory:
186
179
  """
@@ -188,7 +181,7 @@ class CoreProvider(Provider):
188
181
  """
189
182
  return CryptographyServiceFactory(settings.secret_key)
190
183
 
191
- @provide(scope=Scope.APP)
184
+ @provide
192
185
  @staticmethod
193
186
  async def get_cryptography_service(
194
187
  cryptography_service_factory: CryptographyServiceFactory,
@@ -198,16 +191,14 @@ class CoreProvider(Provider):
198
191
  """
199
192
  return await cryptography_service_factory.make(CryptographicAlgorithmEnum.AES_GCM)
200
193
 
201
- @provide(scope=Scope.APP)
194
+ @provide
202
195
  @staticmethod
203
- def get_lock_service(settings: CoreSettingsSchema) -> LockServiceProtocol:
196
+ def get_lock_service(cache_settings: CoreCacheSettingsSchema) -> LockServiceProtocol:
204
197
  """
205
198
  Получаем сервис распределенной блокировки.
206
199
  """
207
- assert settings.redis_dsn is not None
208
- RedisManager.init(settings.redis_dsn)
209
- assert RedisManager.redis is not None
210
- return RedisLockService(RedisManager.redis)
200
+ redis_client = CacheManager.init(cache_settings)
201
+ return RedisLockService(redis_client) # type: ignore
211
202
 
212
203
 
213
204
  provider = CoreProvider()
@@ -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
  """
@@ -9,9 +9,9 @@
9
9
  from typing import ClassVar, Protocol, Self, cast
10
10
 
11
11
  from fastapi_cache import FastAPICache
12
+ from redis import asyncio as aioredis
12
13
 
13
14
  from fast_clean.settings import CoreCacheSettingsSchema
14
- from redis import asyncio as aioredis
15
15
 
16
16
  from .in_memory import InMemoryCacheRepository as InMemoryCacheRepository
17
17
  from .redis import RedisCacheRepository as RedisCacheRepository
@@ -67,7 +67,7 @@ class CacheManager:
67
67
  cache_repository: ClassVar[CacheRepositoryProtocol | None] = None
68
68
 
69
69
  @classmethod
70
- def init(cls, cache_settings: CoreCacheSettingsSchema, redis: aioredis.Redis | None) -> None:
70
+ def init(cls, cache_settings: CoreCacheSettingsSchema):
71
71
  """
72
72
  Инициализируем кеш.
73
73
  """
@@ -77,7 +77,13 @@ class CacheManager:
77
77
  case 'in_memory':
78
78
  cache_backend = InMemoryCacheRepository()
79
79
  case 'redis':
80
- assert redis is not None
81
- cache_backend = RedisCacheRepository(redis)
80
+ if not cache_settings.redis:
81
+ raise ValueError('Redis not configured in settings')
82
+ cache_backend = RedisCacheRepository(
83
+ aioredis.from_url(url=str(cache_settings.redis.dsn), decode_responses=True) # type: ignore
84
+ )
85
+ case _:
86
+ raise ValueError('Cache is not initialized')
82
87
  FastAPICache.init(cache_backend, prefix=cache_settings.prefix)
83
88
  cls.cache_repository = cast(CacheRepositoryProtocol, cache_backend)
89
+ return cls.cache_repository
@@ -6,7 +6,6 @@ from typing import Self
6
6
 
7
7
  from fastapi_cache.backends.redis import RedisBackend
8
8
  from overrides import override
9
-
10
9
  from redis.asyncio.client import Redis
11
10
 
12
11
 
@@ -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