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.
- fast_clean/__init__.py +3 -0
- fast_clean/broker.py +123 -0
- fast_clean/container.py +235 -0
- fast_clean/contrib/__init__.py +3 -0
- fast_clean/contrib/healthcheck/__init__.py +3 -0
- fast_clean/contrib/healthcheck/router.py +17 -0
- fast_clean/db.py +179 -0
- fast_clean/depends.py +255 -0
- fast_clean/enums.py +39 -0
- fast_clean/exceptions.py +281 -0
- fast_clean/loggers.py +26 -0
- fast_clean/middleware.py +20 -0
- fast_clean/models.py +33 -0
- fast_clean/py.typed +0 -0
- fast_clean/redis.py +23 -0
- fast_clean/repositories/__init__.py +30 -0
- fast_clean/repositories/cache/__init__.py +83 -0
- fast_clean/repositories/cache/in_memory.py +62 -0
- fast_clean/repositories/cache/redis.py +58 -0
- fast_clean/repositories/crud/__init__.py +149 -0
- fast_clean/repositories/crud/db.py +559 -0
- fast_clean/repositories/crud/in_memory.py +369 -0
- fast_clean/repositories/crud/type_vars.py +35 -0
- fast_clean/repositories/settings/__init__.py +52 -0
- fast_clean/repositories/settings/enums.py +16 -0
- fast_clean/repositories/settings/env.py +55 -0
- fast_clean/repositories/settings/exceptions.py +13 -0
- fast_clean/repositories/settings/type_vars.py +9 -0
- fast_clean/repositories/storage/__init__.py +114 -0
- fast_clean/repositories/storage/enums.py +20 -0
- fast_clean/repositories/storage/local.py +118 -0
- fast_clean/repositories/storage/reader.py +122 -0
- fast_clean/repositories/storage/s3.py +118 -0
- fast_clean/repositories/storage/schemas.py +31 -0
- fast_clean/schemas/__init__.py +25 -0
- fast_clean/schemas/exceptions.py +32 -0
- fast_clean/schemas/pagination.py +65 -0
- fast_clean/schemas/repository.py +43 -0
- fast_clean/schemas/request_response.py +36 -0
- fast_clean/schemas/status_response.py +13 -0
- fast_clean/services/__init__.py +16 -0
- fast_clean/services/cryptography/__init__.py +57 -0
- fast_clean/services/cryptography/aes.py +120 -0
- fast_clean/services/cryptography/enums.py +20 -0
- fast_clean/services/lock.py +57 -0
- fast_clean/services/seed.py +91 -0
- fast_clean/services/transaction.py +40 -0
- fast_clean/settings.py +189 -0
- fast_clean/tools/__init__.py +6 -0
- fast_clean/tools/cryptography.py +56 -0
- fast_clean/tools/load_seed.py +31 -0
- fast_clean/use_cases.py +38 -0
- fast_clean/utils/__init__.py +15 -0
- fast_clean/utils/process.py +31 -0
- fast_clean/utils/pydantic.py +23 -0
- fast_clean/utils/ssl_context.py +31 -0
- fast_clean/utils/string.py +28 -0
- fast_clean/utils/thread.py +21 -0
- fast_clean/utils/time.py +14 -0
- fast_clean/utils/type_converters.py +18 -0
- fast_clean/utils/typer.py +23 -0
- fast_clean-0.4.0.dist-info/METADATA +38 -0
- fast_clean-0.4.0.dist-info/RECORD +65 -0
- fast_clean-0.4.0.dist-info/WHEEL +5 -0
- 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,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)
|
fast_clean/use_cases.py
ADDED
@@ -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
|