- ClosingStreamingResponse закрывает body_iterator в finally (под shield), ChainedStream закрывает исходный поток и до начала чтения; заголовок X-Content-Type-Options: nosniff; - ASCII-имя в Content-Disposition строится из file_type, а не из расширения исходного имени; - ObjectStorage.stream типизирован как AsyncGenerator, убраны два type: ignore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
31 lines
1.4 KiB
Python
31 lines
1.4 KiB
Python
"""Контракт хранилища объектов (S3), которым пользуются сервисы ядра.
|
||
|
||
Реализация — `src.core.clients.S3Storage`, заглушка тестов — `tests.conftest.FakeStorage`. Протокол вынесен из
|
||
`services/directory_import.py`: тот же интерфейс нужен загрузке файлов и маппингу результатов (Срез 2c, D-12).
|
||
"""
|
||
|
||
from collections.abc import AsyncGenerator
|
||
from typing import Protocol
|
||
|
||
|
||
class ObjectNotFoundError(Exception):
|
||
"""Объекта с таким ключом нет в бакете (S3 `NoSuchKey`)."""
|
||
|
||
|
||
class ObjectStorage(Protocol):
|
||
"""Что ядру нужно от S3: бакет, запись, чтение объекта целиком и порциями."""
|
||
|
||
bucket: str
|
||
|
||
async def put_object(self, obj_name: str, file_data: bytes) -> None: ...
|
||
|
||
async def get_bytes(self, obj_name: str) -> bytes:
|
||
"""Содержимое объекта; отсутствие объекта — `ObjectNotFoundError`."""
|
||
...
|
||
|
||
def stream(self, obj_name: str, chunk_size: int = 1 << 20) -> AsyncGenerator[bytes, None]:
|
||
"""Объект порциями по `chunk_size` байт; отсутствие объекта — `ObjectNotFoundError` до первой порции.
|
||
|
||
Закрытие итератора (`aclose`) освобождает соединение с S3."""
|
||
...
|