diff --git a/.github/workflows/check-solutions.yaml b/.github/workflows/check-solutions.yaml index 271e3c2f..373681f6 100644 --- a/.github/workflows/check-solutions.yaml +++ b/.github/workflows/check-solutions.yaml @@ -27,12 +27,16 @@ jobs: - name: Lint code run: | - ruff check ./solutions + ruff check ./solutions ./homeworks - name: Check code format run: | - ruff format --check ./solutions + ruff format --check ./solutions ./homeworks - - name: Test code + - name: Test code run: | pytest -v --cov=./solutions ./tests/ + + - name: Test code hw + run: | + pytest -v --cov=./homeworks ./tests_hw/ diff --git a/homeworks/__init__.py b/homeworks/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/homeworks/hw1/__init__.py b/homeworks/hw1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/homeworks/hw1/aggregate_segmentation.py b/homeworks/hw1/aggregate_segmentation.py new file mode 100644 index 00000000..1cdc176a --- /dev/null +++ b/homeworks/hw1/aggregate_segmentation.py @@ -0,0 +1,28 @@ +ALLOWED_TYPES = { + "spotter_word", + "voice_human", + "voice_bot", +} + + +def aggregate_segmentation( + segmentation_data: list[dict[str, str | float | None]], +) -> tuple[dict[str, dict[str, dict[str, str | float]]], list[str]]: + """ + Функция для валидации и агрегации данных разметки аудио сегментов. + + Args: + segmentation_data: словарь, данные разметки аудиосегментов с полями: + "audio_id" - уникальный идентификатор аудио. + "segment_id" - уникальный идентификатор сегмента. + "segment_start" - время начала сегмента. + "segment_end" - время окончания сегмента. + "type" - тип голоса в сегменте. + + Returns: + Словарь с валидными сегментами, объединёнными по `audio_id`; + Список `audio_id` (str), которые требуют переразметки. + """ + + # ваш код + return {}, [] diff --git a/homeworks/hw1/backoff.py b/homeworks/hw1/backoff.py new file mode 100644 index 00000000..696ffa73 --- /dev/null +++ b/homeworks/hw1/backoff.py @@ -0,0 +1,38 @@ +from random import uniform +from time import sleep +from typing import ( + Callable, + ParamSpec, + TypeVar, +) + +P = ParamSpec("P") +R = TypeVar("R") + + +def backoff( + retry_amount: int = 3, + timeout_start: float = 0.5, + timeout_max: float = 10.0, + backoff_scale: float = 2.0, + backoff_triggers: tuple[type[Exception]] = (Exception,), +) -> Callable[[Callable[P, R]], Callable[P, R]]: + """ + Параметризованный декоратор для повторных запусков функций. + + Args: + retry_amount: максимальное количество попыток выполнения функции; + timeout_start: начальное время ожидания перед первой повторной попыткой (в секундах); + timeout_max: максимальное время ожидания между попытками (в секундах); + backoff_scale: множитель, на который увеличивается задержка после каждой неудачной попытки; + backoff_triggers: кортеж типов исключений, при которых нужно выполнить повторный вызов. + + Returns: + Декоратор для непосредственного использования. + + Raises: + ValueError, если были переданы невозможные аргументы. + """ + + # ваш код + pass diff --git a/homeworks/hw1/cache.py b/homeworks/hw1/cache.py new file mode 100644 index 00000000..9eb1d5d2 --- /dev/null +++ b/homeworks/hw1/cache.py @@ -0,0 +1,27 @@ +from typing import ( + Callable, + ParamSpec, + TypeVar, +) + +P = ParamSpec("P") +R = TypeVar("R") + + +def lru_cache(capacity: int) -> Callable[[Callable[P, R]], Callable[P, R]]: + """ + Параметризованный декоратор для реализации LRU-кеширования. + + Args: + capacity: целое число, максимальный возможный размер кеша. + + Returns: + Декоратор для непосредственного использования. + + Raises: + TypeError, если capacity не может быть округлено и использовано + для получения целого числа. + ValueError, если после округления capacity - число, меньшее 1. + """ + # ваш код + pass diff --git a/homeworks/hw1/convert_exception.py b/homeworks/hw1/convert_exception.py new file mode 100644 index 00000000..fe5c770f --- /dev/null +++ b/homeworks/hw1/convert_exception.py @@ -0,0 +1,28 @@ +from typing import ( + Callable, + ParamSpec, + TypeVar, +) + +P = ParamSpec("P") +R = TypeVar("R") + + +def convert_exceptions_to_api_compitable_ones( + exception_to_api_exception: dict[type[Exception], type[Exception] | Exception], +) -> Callable[[Callable[P, R]], Callable[P, R]]: + """ + Параметризованный декоратор для замены внутренних исключений на API-исключении. + + Args: + exception_to_api_exception: словарь: + ключи - внутренние исключения, которые надо заменить, + значения - API-исключения, которые надо возбудить + вместо внутренних исключений + + Returns: + Декоратор для непосредственного использования. + """ + + # ваш код + pass diff --git a/homeworks/hw1/description.md b/homeworks/hw1/description.md new file mode 100644 index 00000000..1c95544d --- /dev/null +++ b/homeworks/hw1/description.md @@ -0,0 +1,284 @@ +## Задача 1: Агрегация и фильтрация + +**Условие:** + +Вы занимаетесь организацией процесса работы с разметкой аудиоданных. Ваша задача - реализовать функционал для обработки разметки аудиоданных, которые будут в дальнейшем использоваться для обучения голосового ассистента. Аудиоданные представлены аудиодорожками. Каждая аудиодорожка имеет уникальный идентификатор - `audio_id`. Т.к. аудиодорожки могут быть очень длинными (до нескольких десятков минут), для удобства разметки их разбили на сегменты - короткие аудиодорожки, длиной не более десятка секунд. Каждый сегмент также обладает уникальный идентификатором `segment_id`. Эти сегменты были отданы разметчикам, которые, находя голос, отмечали, что сегмент имеет определённый тип речи. + +На вход вы получаете данные разметки аудио сегментов, которые необходимо обработать. Разметка аудиосегментов заключается в выделении фрагментов с речью и указанием типа речи. Данные разметки аудиосегмента описываются следующими характеристиками: +- `audio_id` - ID оригинальной аудиодорожки, к которой относится сегмент. Описывается строкой в формате UUID. +- `segment_id` - ID размеченного аудиосегмента. Описывается строкой в формате `UUID`. +- `segment_start` - время начала фрагмента с речью. Описывается числом с плавающей точкой или `None`. +- `segment_end` - время конца фрагмента с речью. Описывается числом с плавающей точкой или `None`. +- `type` - тип речи. Описывается строкой или `None`. + +Если все три поля (`type`, `segment_start`, `segment_end`) равны `None`, то это означает, что в сегменте не найден голос. Такие данные мы также хотим использовать для обучения, чтобы голосовой ассистент понимал, когда речи нет. + +К сожалению, у некоторых разметчиков немного кривые руки, поэтому надо проверить, что все данные валидные, а если нет, то отправить их на переразметку. + +Таким образом, вам необходимо реализовать функцию, которая должна: +- Проверять валидность каждого сегмента; +- Объединять валидные сегменты в словарь по `audio_id`; +- Возвращать словарь с валидными сегментами; +- Возвращать список `audio_id`, которые требуют переразметки, так как содержат **хотя бы один** не валидный сегмент. + +Сегмент считается **не валидным**, если: +- Отсутствует `segment_id` (если отсутствует `audio_id`, то сегмент просто игнорируется); +- Тип `type` не `str`, или тип `segment_start` / `segment_end` не `float`; +- Хотя бы одно из полей `type`, `segment_start`, `segment_end` равно `None`, **но не все сразу**; +- Поле `type` не входит в список допустимых типов, которые находятся в глобальной константе `ALLOWED_TYPES`; +- Для одного и того же `audio_id` и `segment_id` встречаются **разные значения** `start`, `end` или `type`. + +В результирующем словаре сегменты объединяются по ключу `audio_id`. Если для какого-либо `audio_id` были переданы только сегменты без речи (все поля `type`, `segment_start`, `segment_end` равны `None`), то в результирующем словаре для этого `audio_id` должен быть создан **пустой словарь** (`{}`). Это означает, что аудио существует и было размечено как не содержащее голоса, и оно всё равно должно быть учтено при обучении нейросети. Такой подход позволяет сохранить информацию о том, что аудио было обработано, даже если в нём не было обнаружено речи. + +Структура результирующего словаря: +- Ключ - `audio_id` (str) - id аудиодорожки, для которого были переданы сегменты (все они были валидными). +- Значение - вложенный словарь, где: + - Ключ - `segment_id` (str), id сегментов, которые соответствуют id аудиодорожки из ключа. + - Значение - данные сегмента - словарь с полями: + - `type` (str) - тип речи. + - `start` (float) - время начала фрагмента с речью. + - `end` (float) - время окончания фрагмента с речью. + +Заготовка функции находится в файле [`aggregate_segmentation.py`](./aggregate_segmentation.py). + +--- + +**Входные данные:** + +- `segmentation_data`: список словарей, каждый из которых содержит: + - `audio_id` (str) - строка в формате `UUID` - уникальный идентификатор аудио. + - `segment_id` (str) - строка в формате `UUID` - уникальный идентификатор сегмента. + - `segment_start` (float или None) - время начала сегмента, от 0 до 10 сек. + - `segment_end` (float или None) - время окончания сегмента, от 0 до 10 сек. + - `type` (str или None) - тип голоса в сегменте. + +--- + +**Выходные данные:** + +- Словарь с валидными данными, вида: + ```json + { + "audio_id1": { + "segment_id1": { + "start": 1.0, + "end": 2.0, + "type": "voice_bot", + }, + "segment_id2": { + "start": 3.0, + "end": 5.0, + "type": "voice_human", + }, + "segment_id3": + { + ... + }, + ... + }, + "audio_id2": { + "segment_id4": { + "start": 1.5, + "end": 2.0, + "type": "spotter_word", + }, + "segment_id5": { + ... + }, + ... + }, + ... + } + ``` +- Список `audio_id` (str), которые содержат хотя бы один не валидный сегмент и требуют переразметки. + +--- + +**Пример использования:** +```python +audio_segments = [ + { + "audio_id": str(uuid.uuid4()), + "segment_id": str(uuid.uuid4()), + "segment_start": 2.0, + "segment_end": 5.5, + "type": "voice_bot", + }, + { + "audio_id": str(uuid.uuid4()), + "segment_id": str(uuid.uuid4()), + "segment_start": 5.5, + "segment_end": 9.5, + "type": "voice_human", + }, + { + "audio_id": str(uuid.uuid4()), + "segment_id": str(uuid.uuid4()), + "segment_start": None, + "segment_end": 9.5, + "type": "voice_human", + }, +] + +valid_data, audio_ids_re_marking = aggregate_segmentation(audio_segments) + +print(valid_data) +# { +# '664c077e-18fd-4e21-8fef-f905a5360786': +# { +# '787e02f5-221d-4f8c-8119-097d22028854': +# {'start': 2.0, 'end': 5.5, 'type': 'voice_bot'} +# }, +# 'c55c3cd4-8bae-4e5b-ad3d-138a5718332b': +# { +# '18c54b69-f7ce-4c54-9719-a204c1c73ce5': +# {'start': 5.5, 'end': 9.5, 'type': 'voice_human'} +# } +# } + +print(audio_ids_re_marking) +# ['9ef608ed-b709-4927-bb62-d872db83c6b3'] +``` + +## Задача 2: Обработка и замена исключений + +**Условие:** + +В процессе разработки системы для обучения голосового ассистента возникает необходимость стандартизировать формат ошибок, возвращаемых клиентам API. Внутренняя логика приложения может использовать широкий спектр встроенных или кастомных исключений (например, `ValueError`, `KeyError`, `ConnectionError`), однако для конечного пользователя или фронтенда важно предоставлять понятные, структурированные и предсказуемые ошибки, соответствующие контрактам API. Например, внутренняя ошибка валидации данных аудио должна транслироваться в `ApiValidationError`, а ошибка доступа к базе данных - в `ApiDatabaseError`. Это позволяет отделить внутреннюю реализацию от публичного интерфейса и обеспечивает согласованность ответов. + +В рамках системы мы работаем с базой данных, из которой извлекаем аудиофайлы, разметку, сегменты и другую информацию, необходимую для обучения. Ошибки при обращении к базе данных, обработке аудио или валидации запросов могут происходить на разных этапах, и важно, чтобы они были корректно обработаны и приведены к единому формату, понятному внешним системам. Для решения этой задачи требуется реализовать **декоратор**, который автоматически преобразует внутренние исключения в заранее определённые API-совместимые ошибки. Декоратор должен принимать на вход словарь сопоставления: ключи - типы внутренних исключений, значения - соответствующие типы или экземпляры API-исключений. При вызове декорируемой функции, если было выброшено исключение, входящее в этот словарь, оно заменяется на указанное API-исключение. При этом детали внутреннего исключения не передаются дальше. Если же исключение не входит в словарь, оно пробрасывается без изменений. Такой подход позволяет централизованно управлять преобразованием ошибок и легко расширять систему при появлении новых типов ошибок. + +Заготовка декоратора находится в файле [`convert_exception.py`](./convert_exception.py). + +--- + +**Входные данные:** + +- `exception_to_api_exception`: словарь, где: + - Ключ - тип внутреннего исключения (например, `ValueError`, `KeyError`); + - Значение - тип или экземпляр API-совместимого исключения (например, `ApiValueError`, `ApiKeyError`). + +--- + +**Выходные данные:** + +- Декоратор, который: + - Оборачивает функцию и перехватывает исключения; + - Если тип исключения присутствует в сопоставлении, заменяет его на соответствующее API-совместимое исключение, а детали изначального исключения дальше не передаются; + - Если исключение не найдено в сопоставлении, оно пробрасывается без изменений. + +**Пример использования:** +```python +@convert_exceptions_to_api_compitable_ones( + exception_to_api_exception={ValueError: ValueError("it is worked")} +) +def raise_key_error() -> None: + raise KeyError("missed") + +raise_key_error() +``` + +## Задача 3: Повторение мать учения + +**Условие:** + +В системе для обучения голосового ассистента происходит интенсивное взаимодействие с базой данных, из которой извлекаются аудиофайлы, разметка, сегменты и другие данные. Однако при работе с базой данных могут возникать временные сбои, такие как таймауты, блокировки или временная недоступность соединения. Чтобы повысить устойчивость системы и избежать преждевременных падений, необходимо реализовать механизм повторных попыток (retries) при возникновении таких ошибок. Это особенно важно при обработке больших объёмов аудио, где кратковременные сбои не должны прерываться весь процесс обучения. + +Для решения этой задачи требуется создать декоратор, который будет оборачивать функции, обращающиеся к базе данных, и повторно вызывать их в случае возникновения указанных исключений. Декоратор должен поддерживать экспоненциальное увеличение задержки между попытками (`backoff`): каждая новая задержка вычисляется как предыдущая, умноженная на коэффициент роста, но не превышающая максимальное значение. Это позволяет постепенно снижать нагрузку на базу данных и дать ей время восстановиться. Также необходимо добавить случайное смещение (`дрожь` или `jitter`) к каждой задержке, равномерно распределённое в диапазоне от 0 до 0.5 секунд. Это предотвращает синхронизацию повторных вызовов от множества процессов или потоков, что может привести к множеству повторных запросов и дальнейшей перегрузке базы данных. + +Декоратор должен принимать параметры: максимальное количество попыток, начальное время ожидания, максимальное время ожидания и множитель роста задержки. При этом детали внутреннего исключения не передаются дальше. Такой подход обеспечит стабильную работу системы при кратковременных сбоях в работе базы данных. + +Все числовые параметры должны быть валидированы: они должны быть положительными. Если были переданы неверные значения, то необходимо возбудить исключение `ValueError`. + +Заготовка декоратора находится в файле [`backoff.py`](./backoff.py). + +*Примечание:* Для добавления "дрожи" можно использовать функцию `random.uniform`: +```python +from random import uniform +jitter_time = uniform(0, 0.5) +``` + +--- + +**Входные данные:** + +- `retry_amount` (int, по умолчанию 3) - максимальное количество попыток выполнения функции от 1 до 100 (может быть не положительным в случае не валидных аргументов); +- `timeout_start` (float, по умолчанию 0.5) - начальное время ожидания перед первой повторной попыткой (в секундах), принадлежит полуинтервалу `(0, 10)` (может быть не положительным в случае не валидных аргументов); +- `timeout_max` (float, по умолчанию 10.0) - максимальное время ожидания между попытками (в секундах), принадлежит полуинтервалу `(0, 10)` (может быть не положительным в случае не валидных аргументов); +- `backoff_scale` (float, по умолчанию 2.0) - множитель, на который увеличивается задержка после каждой неудачной попытки, принадлежит полуинтервалу `(0, 10)` (может быть не положительным в случае не валидных аргументов); +- `backoff_triggers` (tuple[type[Exception]], по умолчанию (Exception,)) - кортеж типов исключений, при которых нужно выполнить повторный вызов (например, `ConnectionError`, `TimeoutError`, `OperationalError` и т.д.). + +--- + +**Выходные данные:** + +- Декоратор, который: + - Валидирует аргументы; + - Оборачивает функцию и повторяет её вызов при возникновении указанных исключений; + - Увеличивает задержку между попытками по экспоненциальному закону (с ограничением `timeout_max`); + - Добавляет случайную "дрожь" к задержке (от 0 до 0.5 секунд); + - Ограничивает количество попыток значением `retry_amount`; + - Если все попытки исчерпаны, пробрасывает последнее исключение. + +## Задача 4. LRU + +**Задача** + +Выполнение некоторых алгоритмов, даже при учете оптимальной реализации, может потребовать большого количества времени, поэтому вычисления, реализуемые с помощью данного алгоритма, будут стоить очень дорого (и в смысле времени, и в смысле денежных средств). Можно попытаться решить эту проблему несколькими способами. Например, можно попытаться оптимизировать реализацию алгоритма, в надеже на то, что это сильно увеличит быстродействие. Однако, далеко не всегда это возможно. Поэтому инженерное сообщество стало думать о том, как уменьшить расходы на вычисления путем сокращения вызовов функций. + +Самый простой способ реализации такого подхода - словарь, который бы хранил пары вида `{func_agrs: func_result}`. Тогда при вызове вычислительно дорогой функции, мы сначала проверим, есть ли результат вычисления данной функции с переданными параметрами в словаре, и если он есть, то просто вернем это значение. Если результата нет, то нам придется вычислить его, а затем поместить в словарь, чтобы при последующих вызовах функции с данными параметрами, нам бы не пришлось выполнять уже проделанные вычисления. + +Однако, данный подход обладает существенным минусом - бесконечный рост словаря. Именно поэтому инженерное сообщество пыталось придумать эффективный способ ограничения числа запоминаемых значений. Один из таких способов - это алгоритм `LRU cache`. **LRU** - сокращение от английского *Least Recent Used*. Идейно алгоритм работает следующим образом: +- у нас имеется некоторый словарь ограниченного размера; +- у каждой пары в словаре есть свой приоритет на удаление: чем дольше не использовалась какая-либо пара, тем больше ее приоритет на удаление; +- если для данного набора параметров, переданных во время вызова функции, в словаре имеются данные, то мы используем данные из словаря и понижаем приоритет на удаление использованной пары до минимального; +- если для данного набора параметров, переданных во время вызова функции, в словаре нет данных, то мы вызываем саму функцию, а результат сохраняем в словарь с наименьшим приоритетом на удаление; +- если размер словаря превышает установленные ограничения, мы удаляем из него пару с наибольшим приоритетом на удаление; + +Логика в том, что мы пытаемся сохранить в словаре часто используемые значения, и избавиться от значений, которые используются редко. Отсюда и название алгоритма. + +Итак, ваша задача - реализовать алгоритм `LRU cache` в виде параметризованного декоратора `lru_cache` для реализации возможности добавления кеша к различным функциям. Заготовка декоратора находится в файле [`cache.py`](./cache.py). + +Параметром декоратора является целое число `capacity` - максимально возможный размер кеша. Ожидается, что `capacity` будет целым числом, большим единицы, однако допустимо передавать любой объект, который может быть округлен и преобразован в целое число с помощью вызова встроенной функции `round()`. Если переданный объект несовместим с вызовом функции `round()`, необходимо возбудить исключение `TypeError`. Если после использования функции `round()` с переданным объектом было получено число, меньшее 1, необходимо возбудить исключение `ValueError`. + +Все операции с кешем должны работать в среднем за $O(1)$ (за константное время, т.е. время выполнения операции с кешем не должно зависеть от размера кеша). + +**Допущение**: В рамках данной задачи считаем, что все аргументы функции - хешируемые объекты. + +**ВАЖНО**: в Python существует готовый декоратор `lru_cache`, который находится в модуле `functools`. Использование этого декоратора, как и любых других готовых решений - **ЗАПРЕЩЕНО**. Такие решения будут оценены в 0 баллов. Вам необходимо реализовать алгоритм **САМОСТОЯТЕЛЬНО**. + +**Пример использования** + +```python +from cache import lru_cache + + +@lru_cache(capacity=2) +def get_greeting(name: str) -> str: + greeting = f"Hello, {name}!" + print(f"call func for name: {name}") + + return greeting + + +print(get_greeting("Mr.White")) +print(get_greeting("Mike")) +print(get_greeting("Mr.White")) +print(get_greeting("Saul Goodman")) +print(get_greeting("Mr.White")) +print(get_greeting("Mike")) +``` + +**Вывод** +```console +call func for name: Mr.White +Hello, Mr.White! +call func for name: Mike +Hello, Mike! +Hello, Mr.White! +call func for name: Saul Goodman +Hello, Saul Goodman! +Hello, Mr.White! +call func for name: Mike +Hello, Mike! +``` \ No newline at end of file diff --git a/tests_hw/__init__.py b/tests_hw/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests_hw/hw1_test_data/__init__.py b/tests_hw/hw1_test_data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests_hw/hw1_test_data/cache_test_data.py b/tests_hw/hw1_test_data/cache_test_data.py new file mode 100644 index 00000000..0d515e83 --- /dev/null +++ b/tests_hw/hw1_test_data/cache_test_data.py @@ -0,0 +1,166 @@ +TESTCASE_DATA = ( + ( + 1, + [ + ((), {}), + ((), {}), + ((), {}), + ], + 1, + ), + ( + 1, + [ + ((1, 2), {"a": 3}), + ((1, 2), {"a": 3}), + ((1, 2), {"a": 3}), + ], + 1, + ), + ( + 1, + [ + ((1, 2), {}), + ((1, 2, 3), {}), + ((1, 2), {}), + ((1, 2, 3), {}), + ], + 4, + ), + ( + 3, + [ + ((1, 2), {"a": 3}), + ((1, 2), {"a": 3}), + ((1, 2), {"a": 3}), + ], + 1, + ), + ( + 3, + [ + ((1, 2), {}), + ((1, 2, 3), {}), + ((1, 2), {"a": 3}), + ((1, 2), {}), + ((1, 2, 3), {}), + ((1, 2), {"a": 3}), + ((1, 2), {}), + ((1, 2, 3), {}), + ((1, 2), {"a": 3}), + ], + 3, + ), + ( + 3, + [ + ((1, 2), {}), + ((1, 2, 3), {}), + ((1, 2), {"a": 3}), + ((1, 2), {}), + ((1, 2, 3), {}), + ((1, 2), {"a": 3}), + ((1,), {"c": 4}), + ((1, 2), {}), + ((1, 2, 3), {}), + ((1, 2), {"a": 3}), + ], + 7, + ), + ( + 3, + [ + ((1, 2), {}), + ((1, 2, 3), {}), + ((1, 2), {"a": 3}), + ((1,), {"a": 1, "b": 2}), + ((1,), {"a": 1, "c": 3}), + ((1, 2), {}), + ], + 6, + ), + ( + 3, + [ + ((1, 1), {}), + ((1, 1), {}), + ((1, 1), {}), + ((2, 2), {}), + ((2, 2), {}), + ((2, 2), {}), + ((3, 3), {}), + ((3, 3), {}), + ((3, 3), {}), + ], + 3, + ), + ( + 3, + [ + (("a", "a"), {}), + (("b", "b"), {}), + (("c", "c"), {}), + (("a", "a"), {}), + (("d", "d"), {}), + (("e", "e"), {}), + (("a", "a"), {}), + (("b", "b"), {}), + (("c", "c"), {}), + (("a", "a"), {}), + ], + 7, + ), + ( + 5, + [ + ((), {"a": "a"}), + ((), {"b": "b"}), + ((), {"c": "c"}), + ((), {"d": "d"}), + ((), {"e": "e"}), + ((), {"c": "c"}), + ((), {"f": "f"}), + ((), {"g": "g"}), + ((), {"c": "c"}), + ((), {"a": "a"}), + ((), {"b": "b"}), + ((), {"d": "d"}), + ((), {"e": "e"}), + ((), {"f": "f"}), + ((), {"c": "c"}), + ], + 13, + ), + ( + 3, + [ + ((1, 2), {"a": "a", "b": "b", "c": "c"}), + ((1, 2), {"b": "b", "c": "c", "a": "a"}), + ((1, 2), {"c": "c", "a": "a", "b": "b"}), + ], + 1, + ), + ( + 3, + [ + ((1, 2, 3), {"a": "a", "b": "b", "c": "c"}), + ((3, 1, 2), {"b": "b", "c": "c", "a": "a"}), + ((2, 3, 1), {"c": "c", "a": "a", "b": "b"}), + ], + 3, + ), +) +TESTCASE_IDS = ( + "cap-1-empty-input-several-calls", + "cap-1-same-calls", + "cap-1-pair-of-different-calls", + "cap-3-all-same-calls", + "cap-3-same-triplet-calls", + "cap-3-same-triplet-then-pop", + "cap-3-different-calls", + "cap-3-most-top-touch", + "cap-3-most-bottom-touch", + "cap-5-middle-touch", + "cap-3-kwards-sort", + "cap-3-positional-ordering", +) diff --git a/tests_hw/test_hw1_tasks.py b/tests_hw/test_hw1_tasks.py new file mode 100644 index 00000000..ebc3771c --- /dev/null +++ b/tests_hw/test_hw1_tasks.py @@ -0,0 +1,159 @@ +import pytest +import uuid +from unittest.mock import MagicMock, patch, Mock + +from homeworks.hw1.aggregate_segmentation import aggregate_segmentation, ALLOWED_TYPES +from homeworks.hw1.backoff import backoff +from homeworks.hw1.cache import lru_cache +from homeworks.hw1.convert_exception import convert_exceptions_to_api_compitable_ones +from tests_hw.hw1_test_data.cache_test_data import ( + TESTCASE_DATA, + TESTCASE_IDS, +) + +NAME_BACKOFF_MODULE = "homeworks.hw1.backoff" # название модуля с backoff + +def test_valid_segments() -> None: + """Тест: валидные сегменты.""" + list_allow_types = list(ALLOWED_TYPES) + audio_id_1 = str(uuid.uuid4()) + audio_id_2 = str(uuid.uuid4()) + audio_id_3 = str(uuid.uuid4()) + + segment_id_1 = str(uuid.uuid4()) + segment_id_2 = str(uuid.uuid4()) + segment_id_3 = str(uuid.uuid4()) + segment_id_4 = str(uuid.uuid4()) + segment_id_5 = str(uuid.uuid4()) + + input_data = [ + { + "audio_id": audio_id_1, + "segment_id": segment_id_1, + "segment_start": 0.0, + "segment_end": 1.0, + "type": list_allow_types[0] + }, + { + "audio_id": audio_id_1, + "segment_id": segment_id_2, + "segment_start": 2.5, + "segment_end": 3.5, + "type": list_allow_types[1] + }, + { + "audio_id": audio_id_2, + "segment_id": segment_id_3, + "segment_start": 4.5, + "segment_end": 4.6, + "type": list_allow_types[0] + }, + { + "audio_id": audio_id_2, + "segment_id": segment_id_4, + "segment_start": 5.5, + "segment_end": 6.5, + "type": list_allow_types[1] + }, + { + "audio_id": audio_id_3, + "segment_id": segment_id_5, + "segment_start": None, + "segment_end": None, + "type": None + }, + { + "audio_id": "audio3", + "segment_id": "seg5", + "segment_start": 0.0, + "segment_end": 1.0, + "type": "invalid_type" + }, + ] + + expected_valid = { + audio_id_1: { + segment_id_1: {"start": 0.0, "end": 1.0, "type": list_allow_types[0]}, + segment_id_2: {"start": 2.5, "end": 3.5, "type": list_allow_types[1]} + }, + audio_id_2: { + segment_id_3: {"start": 4.5, "end": 4.6, "type": list_allow_types[0]}, + segment_id_4: {"start": 5.5, "end": 6.5, "type": list_allow_types[1]} + }, + audio_id_3: {}, + } + expected_forbidden = ["audio3"] + + result_valid, result_forbidden = aggregate_segmentation(input_data) + assert result_valid == expected_valid + assert result_forbidden == expected_forbidden + +def test_convert_matching_exception() -> None: + """Тест: исключение заменяется на API-совместимое.""" + + class ApiValueError(Exception): + pass + + @convert_exceptions_to_api_compitable_ones({ValueError: ApiValueError}) + def func(): + raise ValueError("Внутренняя ошибка") + + @convert_exceptions_to_api_compitable_ones({ValueError: ApiValueError}) + def func2(): + raise KeyError("Внутренняя ошибка") + + with pytest.raises(ApiValueError): + func() + + with pytest.raises(KeyError): + func2() + +@patch(NAME_BACKOFF_MODULE + '.sleep') +def test_exponential_backoff_and_jitter(mock_sleep: MagicMock) -> None: + """Тест: задержки увеличиваются, но не выше timeout_max и к ним добавляется дрожь.""" + attempts = 0 + timeout_max = 4 + retry_amount = 4 + timeouts = [1, 2, 4, 4] + + @backoff( + retry_amount=retry_amount, + timeout_start=1, + timeout_max=timeout_max, + backoff_scale=2.0 + ) + def func(): + nonlocal attempts + attempts += 1 + if attempts < retry_amount: + raise ConnectionError("Ошибка подключения") + return "успех" + + result = func() + assert result == "успех" + assert mock_sleep.call_count == retry_amount - 1 + + count_more_av_time = 0 + args_list = map(lambda call_val: call_val.args[0], mock_sleep.call_args_list) + for av_time, args in zip(timeouts, args_list): + count_more_av_time += args > av_time + assert av_time <= args <= av_time + 0.5 + + assert count_more_av_time # есть добавление "дрожи" + +def test_success() -> None: + capacity = 2 + call_args = [ + (1, 2), + (1, 2), + (2, 2), + ] + call_count_expected = 2 + + mock_func = Mock() + func_cached = lru_cache(capacity=capacity)(mock_func) + + for args in call_args: + func_cached(args) + + assert mock_func.call_count == call_count_expected \ No newline at end of file