diff --git a/reports/Melnik/lab4/rep/rep.pdf b/reports/Melnik/lab4/rep/rep.pdf new file mode 100644 index 00000000..2ce3c79a Binary files /dev/null and b/reports/Melnik/lab4/rep/rep.pdf differ diff --git a/reports/Melnik/lab4/src/last_releases.json b/reports/Melnik/lab4/src/last_releases.json new file mode 100644 index 00000000..e69de29b diff --git a/reports/Melnik/lab4/src/task.py b/reports/Melnik/lab4/src/task.py new file mode 100644 index 00000000..af205c3f --- /dev/null +++ b/reports/Melnik/lab4/src/task.py @@ -0,0 +1,87 @@ +# pylint: disable=invalid-name +""" +Скрипт для автоматического отслеживания новых релизов в репозиториях GitHub. +Использует GitHub REST API и сохраняет состояние в JSON файл. +""" + +import json +import os +import requests + +STATE_FILE = "last_releases.json" + + +def get_latest_release(repo: str) -> dict: + """ + Получает информацию о последнем релизе репозитория через GitHub API. + """ + url = f"https://api.github.com/repos/{repo}/releases/latest" + headers = {"Accept": "application/vnd.github.v3+json"} + + try: + response = requests.get(url, headers=headers, timeout=10) + if response.status_code == 200: + return response.json() + return None + except requests.exceptions.RequestException as err: + print(f"Ошибка сети для {repo}: {err}") + return None + + +def load_state() -> dict: + """Загружает данные о прошлых проверках из JSON файла.""" + if os.path.exists(STATE_FILE): + with open(STATE_FILE, "r", encoding="utf-8") as f: + return json.load(f) + return {} + + +def save_state(state: dict) -> None: + """Сохраняет текущие данные о версиях в JSON файл.""" + with open(STATE_FILE, "w", encoding="utf-8") as f: + json.dump(state, f, ensure_ascii=False, indent=4) + + +def main() -> None: + """Основная логика мониторинга обновлений.""" + user_input = input("Введите репозитории для отслеживания (через запятую): ") + repo_list = [r.strip() for r in user_input.split(",") if r.strip()] + + if not repo_list: + print("Список репозиториев пуст.") + return + + last_state = load_state() + new_state = last_state.copy() + + for repo in repo_list: + print(f"\nПроверяем обновления для {repo}...") + release_data = get_latest_release(repo) + + if not release_data: + print(f"Информация о релизах в {repo} не найдена.") + continue + + version = release_data.get("tag_name") + last_seen_version = last_state.get(repo) + + if version != last_seen_version: + date = release_data.get("published_at", "не указана")[:10] + link = release_data.get("html_url") + body = release_data.get("body", "Нет описания") + changelog = "\n".join(body.splitlines()[:5]) + + print(f"✅ НАЙДЕН НОВЫЙ РЕЛИЗ: {version} ({date})") + print(f" Ссылка: {link}") + print(f" Основные изменения:\n{changelog}...") + + new_state[repo] = version + else: + print(f"😴 Новых обновлений для {repo} нет (текущая: {version}).") + + save_state(new_state) + print("\nПроверка завершена. Состояние сохранено в last_releases.json") + + +if __name__ == "__main__": + main() diff --git a/reports/Melnik/lab6/rep/rep6.pdf b/reports/Melnik/lab6/rep/rep6.pdf new file mode 100644 index 00000000..c8627b67 Binary files /dev/null and b/reports/Melnik/lab6/rep/rep6.pdf differ diff --git a/reports/Melnik/lab6/src/lab.py b/reports/Melnik/lab6/src/lab.py new file mode 100644 index 00000000..e69de29b diff --git a/reports/Melnik/lab6/src/shopping.py b/reports/Melnik/lab6/src/shopping.py new file mode 100644 index 00000000..8516c822 --- /dev/null +++ b/reports/Melnik/lab6/src/shopping.py @@ -0,0 +1,48 @@ +""" +Мини-библиотека покупок. +Обеспечивает работу с корзиной, товарами, скидками и купонами. +""" + +import requests + +coupons = {"SAVE10": 10, "HALF": 50} + + +class Cart: + """Класс, представляющий корзину покупок клиента.""" + + def __init__(self): + """Инициализация пустой корзины.""" + self.items = [] + + def add_item(self, name: str, price: float) -> None: + """Добавляет новый товар в корзину. Выбрасывает ошибку при отрицательной цене.""" + if price < 0: + raise ValueError("Цена не может быть отрицательной") + self.items.append({"name": name, "price": price}) + + def total(self) -> float: + """Вычисляет общую стоимость всех товаров в корзине.""" + return sum(item["price"] for item in self.items) + + def apply_discount(self, discount: float) -> None: + """Применяет процентную скидку ко всем товарам в корзине.""" + if discount < 0 or discount > 100: + raise ValueError("Скидка должна быть от 0 до 100 процентов") + + factor = (100 - discount) / 100 + for item in self.items: + item["price"] *= factor + + +def log_purchase(item: dict) -> None: + """Отправляет лог с информацией о покупке на удаленный сервер.""" + requests.post("https://example.com/log", json=item, timeout=10) + + +def apply_coupon(cart: Cart, coupon_code: str) -> None: + """Применяет скидку к корзине на основе переданного промокода.""" + if coupon_code in coupons: + cart.apply_discount(coupons[coupon_code]) + else: + raise ValueError("Invalid coupon") diff --git a/reports/Melnik/lab6/src/task3_indexOfDifference.py b/reports/Melnik/lab6/src/task3_indexOfDifference.py new file mode 100644 index 00000000..49298585 --- /dev/null +++ b/reports/Melnik/lab6/src/task3_indexOfDifference.py @@ -0,0 +1,54 @@ +# pylint: disable=invalid-name +""" +Модуль содержит реализацию функции indexOfDifference и тесты к ней. +Задание 3, вариант 5. +""" + +import pytest + + +def indexOfDifference(str1: str, str2: str) -> int: + """ + Возвращает индекс первой позиции, в которой строки различаются. + Если строки идентичны, возвращает -1. + Если передан None, выбрасывает TypeError. + """ + if str1 is None or str2 is None: + raise TypeError("Строки не могут быть None") + + if str1 == str2: + return -1 + + min_len = min(len(str1), len(str2)) + for i in range(min_len): + if str1[i] != str2[i]: + return i + + return min_len + + +@pytest.mark.parametrize( + "str1, str2, expected_index", + [ + ("", "", -1), + ("", "abc", 0), + ("abc", "", 0), + ("abc", "abc", -1), + ("ab", "abxyz", 2), + ("abcde", "abxyz", 2), + ("abcde", "xyz", 0), + ("i am a machine", "i am a robot", 7), + ], +) +def test_index_of_difference_logic(str1, str2, expected_index): + """Проверка функции indexOfDifference на корректных и граничных значениях.""" + assert indexOfDifference(str1, str2) == expected_index + + +def test_index_of_difference_type_error(): + """Проверка выброса TypeError при передаче None в качестве аргумента.""" + with pytest.raises(TypeError): + indexOfDifference(None, None) + + with pytest.raises(TypeError): + indexOfDifference("abc", None) diff --git a/reports/Melnik/lab6/src/test_cart.py b/reports/Melnik/lab6/src/test_cart.py new file mode 100644 index 00000000..f7a316b8 --- /dev/null +++ b/reports/Melnik/lab6/src/test_cart.py @@ -0,0 +1,98 @@ +# pylint: disable=invalid-name, redefined-outer-name, too-few-public-methods +""" +Модуль для тестирования мини-библиотеки покупок (shopping.py). +Включает тесты добавления товаров, применения скидок, купонов и логгирования. +""" + +import pytest +import requests +import shopping +from shopping import Cart, log_purchase, apply_coupon + + +@pytest.fixture +def empty_cart(): + """Фикстура для создания пустой корзины перед каждым тестом.""" + return Cart() + + +def test_add_item(empty_cart): + """Проверка успешного добавления товара в корзину.""" + empty_cart.add_item("Apple", 10.0) + assert len(empty_cart.items) == 1 + assert empty_cart.items[0]["name"] == "Apple" + assert empty_cart.items[0]["price"] == 10.0 + + +def test_negative_price(empty_cart): + """Проверка выброса исключения при добавлении товара с отрицательной ценой.""" + with pytest.raises(ValueError, match="Цена не может быть отрицательной"): + empty_cart.add_item("Banana", -5.0) + + +def test_total(empty_cart): + """Проверка корректного вычисления общей стоимости товаров в корзине.""" + empty_cart.add_item("Apple", 10.0) + empty_cart.add_item("Milk", 15.0) + assert empty_cart.total() == 25.0 + + +@pytest.mark.parametrize( + "discount, expected_total", [(0, 100.0), (50, 50.0), (100, 0.0)] +) +def test_apply_discount_valid(empty_cart, discount, expected_total): + """Проверка корректного применения валидных скидок (0, 50, 100 процентов).""" + empty_cart.add_item("Jacket", 100.0) + empty_cart.apply_discount(discount) + assert empty_cart.total() == expected_total + + +@pytest.mark.parametrize("invalid_discount", [-10, 150]) +def test_apply_discount_invalid(empty_cart, invalid_discount): + """Проверка выброса исключения при недопустимых значениях скидки.""" + empty_cart.add_item("Jacket", 100.0) + with pytest.raises(ValueError): + empty_cart.apply_discount(invalid_discount) + + +def test_log_purchase(monkeypatch): + """Тестирование функции логгирования покупок с использованием мокирования.""" + mock_data = {} + + def mock_post(url, json, timeout=10): + """Заглушка для имитации POST-запроса.""" + mock_data["url"] = url + mock_data["json"] = json + mock_data["timeout"] = timeout + + class MockResponse: + """Фейковый ответ сервера.""" + + status_code = 200 + + return MockResponse() + + monkeypatch.setattr(requests, "post", mock_post) + + item = {"name": "Laptop", "price": 1000.0} + log_purchase(item) + + assert mock_data["url"] == "https://example.com/log" + assert mock_data["json"] == item + assert mock_data["timeout"] == 10 + + +def test_apply_coupon_valid(empty_cart, monkeypatch): + """Проверка применения валидного купона с подменой словаря купонов.""" + monkeypatch.setattr(shopping, "coupons", {"TEST20": 20}) + + empty_cart.add_item("Shoes", 100.0) + apply_coupon(empty_cart, "TEST20") + + assert empty_cart.total() == 80.0 + + +def test_apply_coupon_invalid(empty_cart): + """Проверка выброса исключения при использовании несуществующего купона.""" + with pytest.raises(ValueError, match="Invalid coupon"): + apply_coupon(empty_cart, "WRONG_CODE") diff --git a/reports/Melnik/lab6/src/test_lab2.py b/reports/Melnik/lab6/src/test_lab2.py new file mode 100644 index 00000000..671a5e17 --- /dev/null +++ b/reports/Melnik/lab6/src/test_lab2.py @@ -0,0 +1,50 @@ +""" +lab2_tests +""" + +import pytest +from lab2 import BoundedIntSet + + +def test_create_bounded_set(): + """Тривиальный случай: создание множества и проверка базовых атрибутов.""" + s = BoundedIntSet(capacity=5, initial_elements=[1, 2, 3]) + assert s.capacity == 5 + assert s.elements == [1, 2, 3] + + +def test_negative_capacity(): + """Исключительная ситуация: отрицательная мощность.""" + with pytest.raises(ValueError): + BoundedIntSet(capacity=-1) + + +def test_add_duplicate_element(): + """Граничный случай: добавление дубликата игнорируется.""" + s = BoundedIntSet(capacity=3, initial_elements=[1]) + s.add(1) + assert len(s.elements) == 1 + + +def test_add_overflow(): + """Исключительная ситуация: превышение мощности (OverflowError).""" + s = BoundedIntSet(capacity=2, initial_elements=[1, 2]) + with pytest.raises(OverflowError): + s.add(3) + + +def test_remove_missing_element(): + """Исключительная ситуация: удаление несуществующего элемента.""" + s = BoundedIntSet(capacity=5, initial_elements=[1, 2]) + with pytest.raises(ValueError): + s.remove(99) + + +def test_set_union(): + """Тривиальный случай: объединение множеств.""" + set1 = BoundedIntSet(capacity=2, initial_elements=[1, 2]) + set2 = BoundedIntSet(capacity=2, initial_elements=[2, 3]) + + union_set = set1.union(set2) + assert union_set.capacity == 4 + assert sorted(union_set.elements) == [1, 2, 3]