fast-clean 0.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.
Files changed (65) hide show
  1. fast_clean/__init__.py +3 -0
  2. fast_clean/broker.py +123 -0
  3. fast_clean/container.py +235 -0
  4. fast_clean/contrib/__init__.py +3 -0
  5. fast_clean/contrib/healthcheck/__init__.py +3 -0
  6. fast_clean/contrib/healthcheck/router.py +17 -0
  7. fast_clean/db.py +179 -0
  8. fast_clean/depends.py +255 -0
  9. fast_clean/enums.py +39 -0
  10. fast_clean/exceptions.py +281 -0
  11. fast_clean/loggers.py +26 -0
  12. fast_clean/middleware.py +20 -0
  13. fast_clean/models.py +33 -0
  14. fast_clean/py.typed +0 -0
  15. fast_clean/redis.py +23 -0
  16. fast_clean/repositories/__init__.py +30 -0
  17. fast_clean/repositories/cache/__init__.py +83 -0
  18. fast_clean/repositories/cache/in_memory.py +62 -0
  19. fast_clean/repositories/cache/redis.py +58 -0
  20. fast_clean/repositories/crud/__init__.py +149 -0
  21. fast_clean/repositories/crud/db.py +559 -0
  22. fast_clean/repositories/crud/in_memory.py +369 -0
  23. fast_clean/repositories/crud/type_vars.py +35 -0
  24. fast_clean/repositories/settings/__init__.py +52 -0
  25. fast_clean/repositories/settings/enums.py +16 -0
  26. fast_clean/repositories/settings/env.py +55 -0
  27. fast_clean/repositories/settings/exceptions.py +13 -0
  28. fast_clean/repositories/settings/type_vars.py +9 -0
  29. fast_clean/repositories/storage/__init__.py +114 -0
  30. fast_clean/repositories/storage/enums.py +20 -0
  31. fast_clean/repositories/storage/local.py +118 -0
  32. fast_clean/repositories/storage/reader.py +122 -0
  33. fast_clean/repositories/storage/s3.py +118 -0
  34. fast_clean/repositories/storage/schemas.py +31 -0
  35. fast_clean/schemas/__init__.py +25 -0
  36. fast_clean/schemas/exceptions.py +32 -0
  37. fast_clean/schemas/pagination.py +65 -0
  38. fast_clean/schemas/repository.py +43 -0
  39. fast_clean/schemas/request_response.py +36 -0
  40. fast_clean/schemas/status_response.py +13 -0
  41. fast_clean/services/__init__.py +16 -0
  42. fast_clean/services/cryptography/__init__.py +57 -0
  43. fast_clean/services/cryptography/aes.py +120 -0
  44. fast_clean/services/cryptography/enums.py +20 -0
  45. fast_clean/services/lock.py +57 -0
  46. fast_clean/services/seed.py +91 -0
  47. fast_clean/services/transaction.py +40 -0
  48. fast_clean/settings.py +189 -0
  49. fast_clean/tools/__init__.py +6 -0
  50. fast_clean/tools/cryptography.py +56 -0
  51. fast_clean/tools/load_seed.py +31 -0
  52. fast_clean/use_cases.py +38 -0
  53. fast_clean/utils/__init__.py +15 -0
  54. fast_clean/utils/process.py +31 -0
  55. fast_clean/utils/pydantic.py +23 -0
  56. fast_clean/utils/ssl_context.py +31 -0
  57. fast_clean/utils/string.py +28 -0
  58. fast_clean/utils/thread.py +21 -0
  59. fast_clean/utils/time.py +14 -0
  60. fast_clean/utils/type_converters.py +18 -0
  61. fast_clean/utils/typer.py +23 -0
  62. fast_clean-0.4.0.dist-info/METADATA +38 -0
  63. fast_clean-0.4.0.dist-info/RECORD +65 -0
  64. fast_clean-0.4.0.dist-info/WHEEL +5 -0
  65. fast_clean-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,120 @@
1
+ """
2
+ Модуль, содержащий реализацию сервиса криптографии с использованием алгоритма AES.
3
+ """
4
+
5
+ import base64
6
+ import os
7
+ import warnings
8
+ from typing import Self
9
+
10
+ from cryptography.hazmat.backends import default_backend
11
+ from cryptography.hazmat.primitives import padding
12
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
13
+
14
+
15
+ class AesGcmCryptographyService:
16
+ """
17
+ Сервис криптографии с использованием алгоритма AES в режиме GCM.
18
+ """
19
+
20
+ def __init__(self, secret_key: str) -> None:
21
+ self.secret_key = secret_key
22
+ self.backend = default_backend()
23
+
24
+ def encrypt(self: Self, data: str) -> str:
25
+ """
26
+ Зашифровываем данные.
27
+ """
28
+ bytes_data = data.encode('utf-8')
29
+ iv = os.urandom(12)
30
+ cipher = Cipher(algorithms.AES(self.key), modes.GCM(iv), backend=self.backend)
31
+ encryptor = cipher.encryptor()
32
+ cipher_text = encryptor.update(bytes_data) + encryptor.finalize()
33
+ assert hasattr(encryptor, 'tag')
34
+ encrypted_data = iv + cipher_text + encryptor.tag
35
+ return base64.b64encode(encrypted_data).decode('ascii')
36
+
37
+ def decrypt(self: Self, encrypted_data: str) -> str:
38
+ """
39
+ Расшифровываем данные.
40
+ """
41
+ bytes_encrypted_data = base64.b64decode(encrypted_data)
42
+ iv = bytes_encrypted_data[:12]
43
+ tag = bytes_encrypted_data[-16:]
44
+ cipher_text = bytes_encrypted_data[12:-16]
45
+ cipher = Cipher(algorithms.AES(self.key), modes.GCM(iv, tag), backend=self.backend)
46
+ decryptor = cipher.decryptor()
47
+ decrypted_data = decryptor.update(cipher_text) + decryptor.finalize()
48
+ return decrypted_data.decode('utf-8')
49
+
50
+ @property
51
+ def key(self: Self) -> bytes:
52
+ """
53
+ Ключ длиной в 32 байта.
54
+ """
55
+ key_bytes = self.secret_key.encode()
56
+ if len(key_bytes) > 32:
57
+ return key_bytes[:32]
58
+ if len(key_bytes) < 32:
59
+ diff = 32 - len(key_bytes)
60
+ return key_bytes + b'0' * diff
61
+ return key_bytes
62
+
63
+
64
+ class AesCbcCryptographyService:
65
+ """
66
+ Сервис криптографии с использованием алгоритма AES в режиме CBC.
67
+
68
+ Данный класс использует режим CBC без аутентификации и поэтому может быть небезопасным.
69
+ В будущем данный класс будет удален.
70
+ https://sonarsource.github.io/rspec/#/rspec/S5542/python
71
+ """
72
+
73
+ def __init__(self, secret_key: str) -> None:
74
+ warnings.warn(
75
+ f'{self.__class__.__name__} is deprecated and will be removed in future versions',
76
+ DeprecationWarning,
77
+ stacklevel=2,
78
+ )
79
+ self.secret_key = secret_key
80
+ self.backend = default_backend()
81
+
82
+ def encrypt(self: Self, data: str) -> str:
83
+ """
84
+ Зашифровываем данные.
85
+ """
86
+ bytes_data = data.encode(encoding='utf-8')
87
+ iv = os.urandom(16)
88
+ cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=self.backend)
89
+ encryptor = cipher.encryptor()
90
+ padder = padding.PKCS7(128).padder()
91
+ padded_data = padder.update(bytes_data) + padder.finalize()
92
+ cipher_text = encryptor.update(padded_data) + encryptor.finalize()
93
+ return base64.b64encode(iv + cipher_text).decode('ascii')
94
+
95
+ def decrypt(self: Self, encrypted_data: str) -> str:
96
+ """
97
+ Расшифровываем данные.
98
+ """
99
+ bytes_encrypted_data = base64.b64decode(encrypted_data)
100
+ iv = bytes_encrypted_data[:16]
101
+ cipher_text = bytes_encrypted_data[16:]
102
+ cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=self.backend)
103
+ decryptor = cipher.decryptor()
104
+ decrypted_data = decryptor.update(cipher_text) + decryptor.finalize()
105
+ unpadder = padding.PKCS7(128).unpadder()
106
+ unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
107
+ return unpadded_data.decode(encoding='utf-8')
108
+
109
+ @property
110
+ def key(self: Self) -> bytes:
111
+ """
112
+ Ключ длиной в 32 байта.
113
+ """
114
+ key_bytes = self.secret_key.encode()
115
+ if len(key_bytes) > 32:
116
+ return key_bytes[:32]
117
+ if len(key_bytes) < 32:
118
+ diff = 32 - len(key_bytes)
119
+ return key_bytes + b'0' * diff
120
+ return key_bytes
@@ -0,0 +1,20 @@
1
+ """
2
+ Модуль, содержащий перечисления сервиса криптографии для шифрования секретных параметров.
3
+ """
4
+
5
+ from enum import StrEnum, auto
6
+
7
+
8
+ class CryptographicAlgorithmEnum(StrEnum):
9
+ """
10
+ Криптографический алгоритм.
11
+ """
12
+
13
+ AES_GCM = auto()
14
+ """
15
+ Алгоритм AES в режиме GCM.
16
+ """
17
+ AES_CBC = auto()
18
+ """
19
+ Алгоритм AES в режиме CBC.
20
+ """
@@ -0,0 +1,57 @@
1
+ """
2
+ Модуль, содержащий сервис распределенной блокировки.
3
+ """
4
+
5
+ from collections.abc import AsyncIterator
6
+ from contextlib import asynccontextmanager
7
+ from typing import AsyncContextManager, Protocol, Self
8
+
9
+ from fast_clean.exceptions import LockError
10
+ from redis import asyncio as aioredis
11
+ from redis.exceptions import LockError as RedisLockError
12
+
13
+
14
+ class LockServiceProtocol(Protocol):
15
+ """
16
+ Протокол сервиса распределенной блокировки.
17
+ """
18
+
19
+ def lock(
20
+ self: Self,
21
+ name: str,
22
+ *,
23
+ timeout: float | None = None,
24
+ sleep: float = 0.1,
25
+ blocking_timeout: float | None = None,
26
+ ) -> AsyncContextManager[None]:
27
+ """
28
+ Осуществляем распределенную блокировку.
29
+ """
30
+ ...
31
+
32
+
33
+ class RedisLockService:
34
+ """
35
+ Сервис распределенной блокировки с помощью Redis.
36
+ """
37
+
38
+ def __init__(self, redis: aioredis.Redis) -> None:
39
+ self.redis = redis
40
+
41
+ @asynccontextmanager
42
+ async def lock(
43
+ self: Self,
44
+ name: str,
45
+ *,
46
+ timeout: float | None = None,
47
+ sleep: float = 0.1,
48
+ blocking_timeout: float | None = None,
49
+ ) -> AsyncIterator[None]:
50
+ """
51
+ Осуществляем распределенную блокировку.
52
+ """
53
+ try:
54
+ async with self.redis.lock(name, timeout=timeout, sleep=sleep, blocking_timeout=blocking_timeout):
55
+ yield
56
+ except RedisLockError as lock_error:
57
+ raise LockError() from lock_error
@@ -0,0 +1,91 @@
1
+ """
2
+ Модуль, содержащий сервис для загрузки данных из файлов.
3
+ """
4
+
5
+ import importlib
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Any, Protocol, Self, cast
10
+
11
+ import sqlalchemy as sa
12
+ from sqlalchemy.dialects.postgresql import insert
13
+ from sqlalchemy.ext.asyncio import AsyncSession
14
+
15
+ from ..db import SessionManagerProtocol
16
+
17
+
18
+ class SeedServiceProtocol(Protocol):
19
+ """
20
+ Протокол сервиса для загрузки данных из файлов.
21
+ """
22
+
23
+ async def load_data(self: Self, directory: str | Path | None = None) -> None:
24
+ """
25
+ Загружаем данные из файлов по пути.
26
+ """
27
+ ...
28
+
29
+
30
+ class SeedServiceImpl:
31
+ """
32
+ Реализация сервиса для загрузки данных из файлов.
33
+ """
34
+
35
+ def __init__(self, session_manager: SessionManagerProtocol) -> None:
36
+ self.session_manager = session_manager
37
+
38
+ async def load_data(self: Self, directory: str | Path | None = None) -> None:
39
+ """
40
+ Загружаем данные из файлов по пути.
41
+ """
42
+ directory = directory if directory is not None else self.find_directory()
43
+ directory = Path(directory) if isinstance(directory, str) else directory
44
+ async with self.session_manager.get_session() as session:
45
+ for file in sorted(os.listdir(directory)):
46
+ file_path = directory / file
47
+ with open(file_path) as f:
48
+ items = json.load(f)
49
+ for item in items:
50
+ await self.upsert_item(item, session)
51
+
52
+ @staticmethod
53
+ def find_directory() -> Path:
54
+ """
55
+ Ищем директорию с файлами для загрузки.
56
+ """
57
+ cwd = Path(os.getcwd())
58
+ virtual_env_paths = {path.parent for path in cwd.rglob('pyvenv.cfg')}
59
+ for path in cwd.rglob('seed'):
60
+ if not any(path.is_relative_to(venv) for venv in virtual_env_paths):
61
+ return path
62
+ raise ValueError('Seed directory not found')
63
+
64
+ @classmethod
65
+ async def upsert_item(cls, item: Any, session: AsyncSession) -> None:
66
+ """
67
+ Сохраняем запись в базу данных.
68
+ """
69
+ model_type = cls.import_from_string(item['model'])
70
+ primary_keys = {key.name for key in cast(Any, sa.inspect(model_type)).primary_key}
71
+ values = {**item['fields']}
72
+ item_id = item.get('id')
73
+ if item_id:
74
+ values['id'] = item_id
75
+ await session.execute(
76
+ insert(model_type)
77
+ .values(values)
78
+ .on_conflict_do_update(
79
+ index_elements=primary_keys,
80
+ set_={k: v for k, v in values.items() if k not in primary_keys},
81
+ )
82
+ )
83
+
84
+ @staticmethod
85
+ def import_from_string(import_str: str) -> sa.TableClause:
86
+ """
87
+ Импортируем таблицу.
88
+ """
89
+ package_name, model_name = import_str.rsplit('.', maxsplit=1)
90
+ package = importlib.import_module(package_name)
91
+ return getattr(package, model_name)
@@ -0,0 +1,40 @@
1
+ """
2
+ Модуль, содержащий сервис транзакций.
3
+ """
4
+
5
+ from contextlib import asynccontextmanager
6
+ from typing import AsyncContextManager, AsyncIterator, Protocol, Self
7
+
8
+ import sqlalchemy as sa
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+
11
+
12
+ class TransactionServiceProtocol(Protocol):
13
+ """
14
+ Протокол сервиса транзакций.
15
+ """
16
+
17
+ def begin(self: Self, immediate: bool = True) -> AsyncContextManager[None]:
18
+ """
19
+ Начинаем транзакцию.
20
+ """
21
+ ...
22
+
23
+
24
+ class TransactionServiceImpl:
25
+ """
26
+ Реализация сервиса транзакций.
27
+ """
28
+
29
+ def __init__(self, session: AsyncSession) -> None:
30
+ self.session = session
31
+
32
+ @asynccontextmanager
33
+ async def begin(self: Self, immediate: bool = True) -> AsyncIterator[None]:
34
+ """
35
+ Начинаем транзакцию.
36
+ """
37
+ async with self.session.begin():
38
+ if immediate:
39
+ await self.session.execute(sa.text('SET CONSTRAINTS ALL IMMEDIATE'))
40
+ yield
fast_clean/settings.py ADDED
@@ -0,0 +1,189 @@
1
+ """
2
+ Модуль, содержащий настройки.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ from pathlib import Path
9
+ from typing import ClassVar, Literal, Self
10
+
11
+ from pydantic import BaseModel, ConfigDict, RedisDsn, model_validator
12
+ from pydantic_settings import BaseSettings as PydanticBaseSettings
13
+ from pydantic_settings import SettingsConfigDict
14
+ from typing_extensions import Unpack
15
+
16
+
17
+ class CoreDbSettingsSchema(BaseModel):
18
+ """
19
+ Схема настроек базы данных.
20
+ """
21
+
22
+ provider: str = 'postgresql+psycopg_async'
23
+
24
+ host: str
25
+ port: int
26
+ user: str
27
+ password: str
28
+ name: str
29
+
30
+ scheme: str = 'public'
31
+
32
+ @property
33
+ def dsn(self: Self) -> str:
34
+ """
35
+ DSN подключения к базе данных.
36
+ """
37
+ return f'{self.provider}://{self.user}:{self.password}@{self.host}:{self.port}/{self.name}'
38
+
39
+
40
+ class CoreCacheSettingsSchema(BaseModel):
41
+ """
42
+ Схема настроек кеша.
43
+ """
44
+
45
+ provider: Literal['in_memory', 'redis'] = 'in_memory'
46
+
47
+ prefix: str
48
+
49
+
50
+ class CoreS3SettingsSchema(BaseModel):
51
+ """
52
+ Схема настроек S3.
53
+ """
54
+
55
+ endpoint: str
56
+ endpoint_url: str
57
+ access_key: str
58
+ secret_key: str
59
+ port: int
60
+ bucket: str
61
+ secure: bool = False
62
+
63
+
64
+ class CoreStorageSettingsSchema(BaseModel):
65
+ """
66
+ Схема настроек хранилища.
67
+ """
68
+
69
+ provider: Literal['local', 's3'] = 'local'
70
+
71
+ dir: Path = Path(__file__).resolve().parent.parent / 'storage'
72
+ s3: CoreS3SettingsSchema | None = None
73
+
74
+
75
+ class CoreElasticsearchSettingsSchema(BaseModel):
76
+ """
77
+ Схема настроек Elasticsearch.
78
+ """
79
+
80
+ host: str
81
+ port: int
82
+ scheme: str
83
+ username: str
84
+ password: str
85
+ cluster_name: str
86
+
87
+ cacert: str | None = None
88
+ security: bool = False
89
+ ssl: bool = False
90
+
91
+
92
+ class CoreSearchSettingsSchema(BaseModel):
93
+ """
94
+ Схема настроек движка поиска.
95
+ """
96
+
97
+ provider: Literal['elasticsearch', 'open_search'] = 'elasticsearch'
98
+ elasticsearch: CoreElasticsearchSettingsSchema | None = None
99
+
100
+
101
+ class CoreKafkaSettingsSchema(BaseModel):
102
+ """
103
+ Схема настроек Kafka.
104
+ """
105
+
106
+ bootstrap_servers: str
107
+ group_id: str
108
+ credentials: Literal['SSL', 'SASL'] | None = None
109
+ # For SSL
110
+ cert_file: str | None = None
111
+ ca_file: str | None = None
112
+ key_file: str | None = None
113
+ password: str | None = None
114
+ # For SASL
115
+ broker_username: str | None = None
116
+ broker_password: str | None = None
117
+
118
+ @model_validator(mode='after')
119
+ def validate_credentials(self: Self) -> Self:
120
+ """
121
+ Проверяем на правильность заполнение параметров для авторизации.
122
+ """
123
+ match self.credentials:
124
+ case 'SSL':
125
+ for field in ('ca_file', 'cert_file', 'key_file'):
126
+ assert bool(getattr(self, field)), f'{field} must be set when credentials={self.credentials}'
127
+ case 'SASL':
128
+ for field in ('broker_username', 'broker_password'):
129
+ assert bool(getattr(self, field)), f'{field} must be set when credentials={self.credentials}'
130
+ case _:
131
+ ...
132
+ return self
133
+
134
+
135
+ class CoreServiceSettingsSchema(BaseModel):
136
+ """
137
+ Схема настроек доступа к сервису.
138
+ """
139
+
140
+ host: str
141
+ user: str | None = None
142
+ password: str | None = None
143
+
144
+
145
+ class CoreTopicSettingsSchema(BaseModel):
146
+ """
147
+ Схема настроек топика.
148
+ """
149
+
150
+ name: str
151
+ auto_offset_reset: Literal['latest', 'earliest', 'none'] = 'latest'
152
+
153
+
154
+ class BaseSettingsSchema(PydanticBaseSettings):
155
+ """
156
+ Схема настроек с возможностью поиска через репозиторий.
157
+ """
158
+
159
+ descendant_types: ClassVar[list[type[BaseSettingsSchema]]] = []
160
+
161
+ def __init_subclass__(cls, **kwargs: Unpack[ConfigDict]) -> None:
162
+ """
163
+ Добавляем настройки в репозиторий.
164
+ """
165
+ cls.descendant_types.append(cls)
166
+
167
+ return super().__init_subclass__(**kwargs)
168
+
169
+
170
+ class CoreSettingsSchema(BaseSettingsSchema):
171
+ """
172
+ Схема базовых настроек приложения.
173
+ """
174
+
175
+ debug: bool
176
+ title: str
177
+ base_url: str
178
+ base_dir: Path = Path(os.getcwd())
179
+ secret_key: str
180
+ cors_origins: list[str]
181
+ redis_dsn: RedisDsn | None = None
182
+
183
+ model_config = SettingsConfigDict(
184
+ env_file='.env',
185
+ env_file_encoding='utf-8',
186
+ env_nested_delimiter='__',
187
+ case_sensitive=False,
188
+ extra='ignore',
189
+ )
@@ -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,56 @@
1
+ """
2
+ Модуль, содержащий команды криптографии для шифрования секретных параметров.
3
+ """
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from fast_clean.depends import get_container
10
+ from fast_clean.services import CryptographicAlgorithmEnum, CryptographyServiceFactoryProtocol
11
+ from fast_clean.utils import typer_async
12
+
13
+
14
+ @typer_async
15
+ async def encrypt(
16
+ data: Annotated[str, typer.Argument(help='Данные для шифровки.')],
17
+ algorithm: Annotated[
18
+ CryptographicAlgorithmEnum, typer.Option(help='Криптографический алгоритм')
19
+ ] = CryptographicAlgorithmEnum.AES_GCM,
20
+ ) -> None:
21
+ """
22
+ Зашифровываем данные.
23
+ """
24
+ container = get_container()
25
+ cryptography_service_factory: CryptographyServiceFactoryProtocol = await container.get_by_type(
26
+ CryptographyServiceFactoryProtocol
27
+ )
28
+ cryptography_service = await cryptography_service_factory.make(algorithm)
29
+ print(cryptography_service.encrypt(data))
30
+
31
+
32
+ @typer_async
33
+ async def decrypt(
34
+ data: Annotated[str, typer.Argument(help='Данные для расшифровки.')],
35
+ algorithm: Annotated[
36
+ CryptographicAlgorithmEnum, typer.Option(help='Криптографический алгоритм')
37
+ ] = CryptographicAlgorithmEnum.AES_GCM,
38
+ ) -> None:
39
+ """
40
+ Расшифровываем данные.
41
+ """
42
+ container = get_container()
43
+ cryptography_service_factory: CryptographyServiceFactoryProtocol = await container.get_by_type(
44
+ CryptographyServiceFactoryProtocol
45
+ )
46
+ cryptography_service = await cryptography_service_factory.make(algorithm)
47
+ print(cryptography_service.decrypt(data))
48
+
49
+
50
+ def use_cryptography(app: typer.Typer) -> None:
51
+ """
52
+ Регистрируем команды криптографии для шифрования секретных параметров.
53
+ """
54
+
55
+ app.command()(encrypt)
56
+ 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.depends import get_container
10
+ from fast_clean.services import SeedServiceProtocol
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: SeedServiceProtocol = await container.get_by_type(SeedServiceProtocol)
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)
@@ -0,0 +1,38 @@
1
+ """
2
+ Модуль, содержащий варианты использования.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from types import new_class
8
+ from typing import Any, Protocol, TypeVar, cast
9
+
10
+ UseCaseResultSchemaType = TypeVar('UseCaseResultSchemaType', covariant=True)
11
+
12
+
13
+ class UseCaseProtocol(Protocol[UseCaseResultSchemaType]):
14
+ """
15
+ Протокол варианта использования.
16
+ """
17
+
18
+ async def __call__(self) -> UseCaseResultSchemaType:
19
+ """
20
+ Вызываем вариант использования.
21
+ """
22
+ ...
23
+
24
+ def __class_getitem__(cls, params: type | tuple[type, ...]) -> type:
25
+ """
26
+ Создаем уникальный класс.
27
+
28
+ По умолчанию одинаковые обобщенные классы указывают на один и тот же _GenericAlias.
29
+ Из-за данного поведения поиск по типу становится невозможным.
30
+
31
+ UseCaseA = UseCaseProtocol[None]
32
+ UseCaseB = UseCaseProtocol[None]
33
+ assert UseCaseA is UseCaseB
34
+
35
+ Поэтому вместо _GenericAlias возвращаем уникального наследника.
36
+ """
37
+ generic_alias = cast(Any, super()).__class_getitem__(params)
38
+ return new_class(cls.__name__, (generic_alias,))
@@ -0,0 +1,15 @@
1
+ """
2
+ Пакет, содержащий вспомогательные функции и классы.
3
+ """
4
+
5
+ from .process import run_in_processpool as run_in_processpool
6
+ from .pydantic import rebuild_schemas as rebuild_schemas
7
+ from .ssl_context import CertificateSchema as CertificateSchema
8
+ from .ssl_context import make_ssl_context as make_ssl_context
9
+ from .string import decode_base64 as decode_base64
10
+ from .string import encode_base64 as encode_base64
11
+ from .string import make_random_string as make_random_string
12
+ from .thread import run_in_threadpool as run_in_threadpool
13
+ from .time import ts_now as ts_now
14
+ from .type_converters import str_to_bool as str_to_bool
15
+ from .typer import typer_async as typer_async