"""Клиент S3 (MinIO) сервиса: чтение исходника и запись файла результата (design D-4). `Storage` — протокол, от которого зависит воркер (design D-7): в unit-тестах его подменяет фейк, в приложении — `S3Storage` поверх `miniopy_async.Minio`. Бакет передаётся в каждый вызов: исходник читается из `request.s3.bucket`, результат пишется в тот же бакет. """ import json from io import BytesIO from typing import Any, Protocol, runtime_checkable from miniopy_async import Minio JSON_CONTENT_TYPE = "application/json" @runtime_checkable class Storage(Protocol): """Хранилище объектов, нужное воркеру: байты исходника и JSON результата.""" async def get_bytes(self, bucket: str, key: str) -> bytes: ... async def put_json(self, bucket: str, key: str, obj: Any) -> None: ... class S3Storage: """`Storage` поверх MinIO. `bucket` — бакет из настроек (проверяется при старте), методы принимают бакет явно.""" def __init__(self, client: Minio, bucket_name: str) -> None: self.client = client self.bucket = bucket_name async def get_bytes(self, bucket: str, key: str) -> bytes: """Содержимое объекта целиком; ошибки MinIO/сети пробрасываются (воркер превращает их в `FILE_READ_ERROR`).""" response = await self.client.get_object(bucket, key) try: return await response.read() finally: response.release() async def put_json(self, bucket: str, key: str, obj: Any) -> None: """Записывает JSON-сериализуемый `obj` как UTF-8 (`application/json`, кириллица без экранирования).""" body = json.dumps(obj, ensure_ascii=False).encode("utf-8") await self.client.put_object(bucket, key, BytesIO(body), length=len(body), content_type=JSON_CONTENT_TYPE)