diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 00000000..86415209 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,28 @@ +name: CI with Ruff + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + + - name: Run Ruff + run: | + ruff check . diff --git a/git/src/main.py b/git/src/main.py index 1822c7e9..7088ab99 100644 --- a/git/src/main.py +++ b/git/src/main.py @@ -1,26 +1,29 @@ import json import os -def load_books(filename='library.json'): + +def load_books(filename="library.json"): """ Загрузка списка книг из JSON-файла. Возвращает список книг (каждая книга - это словарь). """ if not os.path.isfile(filename): return [] - with open(filename, 'r', encoding='utf-8') as file: + with open(filename, "r", encoding="utf-8") as file: try: return json.load(file) except json.JSONDecodeError: return [] -def save_books(books, filename='library.json'): + +def save_books(books, filename="library.json"): """ Сохранение списка книг в JSON-файл. """ - with open(filename, 'w', encoding='utf-8') as file: + with open(filename, "w", encoding="utf-8") as file: json.dump(books, file, ensure_ascii=False, indent=4) + def list_books(books): """ Возвращает строку со списком всех книг. @@ -29,29 +32,31 @@ def list_books(books): return "Библиотека пуста." result_lines = [] for idx, book in enumerate(books, start=1): - result_lines.append(f"{idx}. {book['title']} | {book['author']} | {book['year']}") + result_lines.append( + f"{idx}. {book['title']} | {book['author']} | {book['year']}" + ) return "\n".join(result_lines) + def add_book(books, title, author, year): """ Принимает текущий список книг и данные о новой книге. Возвращает новый список, в котором добавлена новая книга. """ - new_book = { - 'title': title, - 'author': author, - 'year': year - } + new_book = {"title": title, "author": author, "year": year} # Создаём НОВЫЙ список, добавляя new_book return books + [new_book] + def remove_book(books, title): """ Принимает текущий список книг и название книги для удаления. Возвращает новый список без книги, у которой совпадает название. """ - # Фильтруем список: оставляем только те книги, у которых название не совпадает с переданным - return [book for book in books if book['title'].lower() != title.lower()] + # Фильтруем список: оставляем только те книги, + # у которых название не совпадает с переданным + return [book for book in books if book["title"].lower() != title.lower()] + def search_books(books, keyword): """ @@ -60,13 +65,16 @@ def search_books(books, keyword): """ keyword_lower = keyword.lower() return [ - book for book in books - if keyword_lower in book['title'].lower() or keyword_lower in book['author'].lower() + book + for book in books + if keyword_lower in book["title"].lower() + or keyword_lower in book["author"].lower() ] + def main(): """ - Точка входа в программу: здесь мы загружаем книги, + Точка входа в программу: здесь мы загружаем книги, показываем меню и обрабатываем ввод пользователя. """ books = load_books() # Загрузили список книг из JSON @@ -81,11 +89,11 @@ def main(): choice = input("Выберите действие (1-5): ").strip() - if choice == '1': + if choice == "1": print("\nСписок книг:") print(list_books(books)) - elif choice == '2': + elif choice == "2": print("\nДобавление новой книги:") title = input("Введите название: ").strip() author = input("Введите автора: ").strip() @@ -93,13 +101,16 @@ def main(): # Получаем новый список с добавленной книгой new_books = add_book(books, title, author, year) - books = new_books # Обновляем переменную, чтобы сохранить изменения + # Обновляем переменную, чтобы сохранить изменения + books = new_books save_books(books) # Сразу сохраняем в файл print("Книга добавлена!") - elif choice == '3': + elif choice == "3": print("\nУдаление книги:") - title_to_remove = input("Введите название книги, которую хотите удалить: ").strip() + title_to_remove = input( + "Введите название книги, которую хотите удалить: " + ).strip() new_books = remove_book(books, title_to_remove) if len(new_books) < len(books): @@ -109,9 +120,11 @@ def main(): else: print("Книга с таким названием не найдена.") - elif choice == '4': + elif choice == "4": print("\nПоиск книг:") - keyword = input("Введите ключевое слово для поиска (в названии или авторе): ").strip() + keyword = input( + "Введите ключевое слово для поиска (в названии или авторе): " + ).strip() found_books = search_books(books, keyword) if found_books: print("\nНайденные книги:") @@ -119,12 +132,13 @@ def main(): else: print("Ничего не найдено.") - elif choice == '5': + elif choice == "5": print("Выход из программы.") break else: print("Некорректный ввод. Попробуйте ещё раз.") + if __name__ == "__main__": main() diff --git a/simple_backend/src/task_tracker/base_http_client.py b/simple_backend/src/task_tracker/base_http_client.py new file mode 100644 index 00000000..d6fbe135 --- /dev/null +++ b/simple_backend/src/task_tracker/base_http_client.py @@ -0,0 +1,41 @@ +from abc import ABC, abstractmethod +import requests +from typing import Dict, Any, Optional + + +class BaseHTTPClient(ABC): + """Базовый класс для HTTP клиентов""" + + def __init__(self, base_url: str, headers: Optional[Dict[str, str]] = None): + self.base_url = base_url + self.headers = headers or {} + self.timeout = 30 + + def _make_request( + self, method: str, endpoint: str = "", **kwargs + ) -> Dict[str, Any]: + """Общий метод для выполнения HTTP запросов""" + url = f"{self.base_url}{endpoint}" + + try: + response = requests.request( + method=method, + url=url, + headers=self.headers, + timeout=self.timeout, + **kwargs, + ) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise Exception(f"Ошибка {method} запроса к {url}: {e}") + + @abstractmethod + def load_data(self) -> Any: + """Абстрактный метод для загрузки данных""" + pass + + @abstractmethod + def save_data(self, data: Any) -> bool: + """Абстрактный метод для сохранения данных""" + pass diff --git a/simple_backend/src/task_tracker/cloud_flare.py b/simple_backend/src/task_tracker/cloud_flare.py new file mode 100644 index 00000000..9144246b --- /dev/null +++ b/simple_backend/src/task_tracker/cloud_flare.py @@ -0,0 +1,52 @@ +from typing import Any +from base_http_client import BaseHTTPClient + + +class CloudflareAIClient(BaseHTTPClient): + """Клиент для работы с Cloudflare AI API""" + + def __init__(self, account_id: str, api_key: str): + base_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + super().__init__(base_url, headers) + + def load_data(self) -> Any: + """Не используется для AI клиента, но требуется абстрактным методом""" + raise NotImplementedError("Метод load_data не реализован для AI клиента") + + def save_data(self, data: Any) -> bool: + """Не используется для AI клиента, но требуется абстрактным методом""" + raise NotImplementedError("Метод save_data не реализован для AI клиента") + + def get_task_solutions(self, task_title: str) -> str: + """Получить способы решения задачи от LLM""" + prompt = f""" + Задача: {task_title} + + Проанализируй эту задачу и предложи 3-5 конкретных способов её решения. + Ответ должен быть кратким, практичным и на русском языке. + Формат: маркированный список. + """ + + payload = { + "messages": [ + { + "role": "system", + "content": "Ты помощник по решению задач. Давай практичные советы.", + }, + {"role": "user", "content": prompt}, + ], + "model": "@cf/meta/llama-3-8b-instruct", + "max_tokens": 500, + } + + try: + result = self._make_request( + "POST", "/@cf/meta/llama-3-8b-instruct", json=payload + ) + return result["result"]["response"] + except Exception: + return "Не удалось получить рекомендации" diff --git a/simple_backend/src/task_tracker/cloud_storage.py b/simple_backend/src/task_tracker/cloud_storage.py new file mode 100644 index 00000000..4c1fa88f --- /dev/null +++ b/simple_backend/src/task_tracker/cloud_storage.py @@ -0,0 +1,29 @@ +from base_http_client import BaseHTTPClient + + +class CloudTaskStorage(BaseHTTPClient): + """Класс для хранения задач в jsonbin.io""" + + def __init__(self, bin_id: str, api_key: str): + base_url = f"https://api.jsonbin.io/v3/b/{bin_id}" + headers = {"X-Master-Key": api_key, "Content-Type": "application/json"} + super().__init__(base_url, headers) + + def load_data(self) -> list: + """Загрузка задач из облака""" + response_data = self._make_request("GET") + data = response_data["record"] + return data.get("tasks", data) if isinstance(data, dict) else data + + def save_data(self, tasks: list) -> bool: + """Сохранение задач в облаке""" + data = {"tasks": tasks} + self._make_request("PUT", json=data) + return True + + # Для обратной совместимости + def load_tasks(self): + return self.load_data() + + def save_tasks(self, tasks): + return self.save_data(tasks) diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 3db98d0d..282e757c 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,19 +1,97 @@ -from fastapi import FastAPI +import os +from typing import List +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from dotenv import load_dotenv + +from cloud_storage import CloudTaskStorage +from cloud_flare import CloudflareAIClient + +# Загружаем .env +load_dotenv() app = FastAPI() -@app.get("/tasks") +# Конфигурация +BIN_ID = os.getenv("JSONBIN_BIN_ID") +JSONBIN_API_KEY = os.getenv("JSONBIN_API_KEY") +CLOUDFLARE_AI_API_KEY = os.getenv("CLOUDFLARE_AI_API_KEY") +CLOUDFLARE_ACCOUNT_ID = os.getenv("CLOUDFLARE_ACCOUNT_ID") + +if not all([BIN_ID, JSONBIN_API_KEY, CLOUDFLARE_AI_API_KEY, CLOUDFLARE_ACCOUNT_ID]): + raise RuntimeError("Не найдены необходимые переменные окружения") + +# Инициализация клиентов +storage = CloudTaskStorage(BIN_ID, JSONBIN_API_KEY) +ai_client = CloudflareAIClient(CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_AI_API_KEY) + + +class Task(BaseModel): + """Модель задачи""" + + id: int + title: str + status: str + solution_advice: str = "" # Новое поле для советов по решению + + +class TaskCreate(BaseModel): + """Модель для создания задачи""" + + title: str + status: str + + +@app.post("/tasks", response_model=Task) +async def create_task(task_data: TaskCreate): + """Создание новой задачи с AI-анализом""" + tasks = storage.load_tasks() + new_id = max([task["id"] for task in tasks], default=0) + 1 + + # Получаем советы от AI + solution_advice = ai_client.get_task_solutions(task_data.title) + + # Создаем задачу с советами + task = { + "id": new_id, + "title": task_data.title, + "status": task_data.status, + "solution_advice": solution_advice, + } + + tasks.append(task) + storage.save_tasks(tasks) + return task + + +# Остальные endpoints остаются без изменений +@app.get("/tasks", response_model=List[Task]) def get_tasks(): - pass + """Получение всех задач""" + tasks = storage.load_tasks() + return tasks + -@app.post("/tasks") -def create_task(task): - pass +@app.put("/tasks/{task_id}", response_model=Task) +def update_task(task_id: int, task_data: TaskCreate): + """Обновление задачи""" + tasks = storage.load_tasks() + for task in tasks: + if task["id"] == task_id: + task["title"] = task_data.title + task["status"] = task_data.status + storage.save_tasks(tasks) + return task + raise HTTPException(status_code=404, detail="Задача не найдена") -@app.put("/tasks/{task_id}") -def update_task(task_id: int): - pass @app.delete("/tasks/{task_id}") def delete_task(task_id: int): - pass + """Удаление задачи""" + tasks = storage.load_tasks() + for i, task in enumerate(tasks): + if task["id"] == task_id: + tasks.pop(i) + storage.save_tasks(tasks) + return {"message": "Задача удалена"} + raise HTTPException(status_code=404, detail="Задача не найдена") diff --git a/simple_backend/src/task_tracker/readme.md b/simple_backend/src/task_tracker/readme.md new file mode 100644 index 00000000..770bd4c5 --- /dev/null +++ b/simple_backend/src/task_tracker/readme.md @@ -0,0 +1,65 @@ +# Задание 2 + +Хранениен в оперативной памяти(список Python) + +## Минусы + +1. Если выключить сервер – все данные потеряются. +2. Ограничение по объему (можно забить всю RAM). +3. Разные приложения не могуть иметь общие данные. + +## Что улучшилось при переходе на json + +1. Данные сохраняются между перезапусками +2. Можно открыть файл и отредактировать вручную +3. Разные приложения могут читать один и тот же файл + +## Избавились ли мы от хранения состояния? + +Нет. Мы всё ещё храним состояние (теперь уже на диске, а не в памяти). +Чтобы сделать backend stateless, нужно вынести данные во внешний сервис (например, jsonbin.io, mockapi.io, Firebase). + +## Где ещё можно хранить задачи + +- 1. База данных (SQLite, PostgreSQL): + + Быстро, надёжно, поддержка транзакций. + - Нужно больше настроек и сервер. + +- 2. Файл YAML/CSV: + + Просто. + - Меньше возможностей, чем JSON. + +- 3.Облачный сервис (jsonbin.io, Firebase, mockapi.io): + + Не зависит от локальной машины. + - Есть задержки сети и лимиты бесплатных тарифов. + + +# Задание - бэк на облаке +## Что изменилось при переходе в облако? +- Данные теперь хранятся не локально, а в облаке. +- Бэк стал stateless: можно запустить его на любой машине, и задачи не потеряются. +- Несколько серверов могут работать с одними и теми же задачами. + +## Избавились ли мы от хранения состояния? +Да, на стороне бека мы больше не храним задачи — они лежат в облаке. +Но полностью от состояния избавиться нельзя: оно просто переехало в jsonbin.io + +## Проблемы состояния гонки +- Если два клиента одновременно обновляют список задач, один может перезаписать изменения другого +- Решения: + - Перейти на базу данных или сервис, который поддерживает конкурентный доступ (PostgreSQL). + +# Задание 3 +## Ответы ИИ. + { + "id": 5, + "title": "Как сварить яйцо", + "status": "тест1?", + "solution_advice": "**Как сварить яйцо: 5 способов**\n\n1. **Варить в кипящей воде**: поместите яйцо в кипящую воду, варите 10-12 минут, затем слейте воду и опустите яйцо в ледяную воду для остановки приготовления.\n2. **Варить в пароварке**: поместите яйцо в пароварку и варите 10-12 минут, затем откройте пароварку и проверьте яйцо на готовность.\n3. **Варить в микроволновке**: поместите яйцо в микроволновку на 30-45 секунд, затем проверьте яйцо на готовность и, если необходимо, продолжайте варить в интервалах 15 секунд.\n4. **Варить в мультиварке**: поместите яйцо в мультиварку, залейте водой на 1-2 см выше яйца, установите режим варки и варите 10-12 минут.\n5. **Варить в скороварке**: поместите яйцо в скороварку, залейте водой на 1-2 см выше яйца, установите режим варки и варите 5-7 минут.\n\nОбратите внимание на степень готовности яйца: желток должен быть твердым, а белок - жидким." + }, + { + "id": 6, + "title": "Как ускорить медленный запрос в SQL", + "status": "тест2?", + "solution_advice": "**Советы по ускорению медленного запроса в SQL**\n\n1. **Оптимизируйте индексирование**: Проверьте, есть ли у вас необходимые индексы на столбцы, которые используются в WHERE, JOIN и ORDER BY запросах. Создайте индексы, если они отсутствуют.\n2. **Упростите запрос**: Если ваш запрос сложен, попробуйте упростить его, удалив излишние операции и использование функций. Это может помочь ускорить выполнение запроса.\n3. **Используйте производительные функции**: Используйте производительные функции, такие как `SUM` вместо `SELECT`, или `EXISTS` вместо `IN`.\n4. **Ограничьте количество данных**: Если ваш запрос работает с большим количеством данных, попробуйте ограничить количество данных, используя LIMIT или OFFSET.\n5. **Используйте кэширование**: Если ваш запрос часто повторяется, используйте кэширование результатов, чтобы ускорить выполнение запроса.\n\nНадеюсь, эти советы помогут вам ускорить медленный запрос в SQL!" + } diff --git a/simple_backend/src/task_tracker/requirements.txt b/simple_backend/src/task_tracker/requirements.txt index 8e0578a0..6e63147a 100644 --- a/simple_backend/src/task_tracker/requirements.txt +++ b/simple_backend/src/task_tracker/requirements.txt @@ -1,2 +1,5 @@ fastapi -uvicorn[standard] \ No newline at end of file +uvicorn[standard] +requests +python-dotenv +pydantic diff --git a/simple_backend/src/task_tracker/storage.py b/simple_backend/src/task_tracker/storage.py new file mode 100644 index 00000000..59700a15 --- /dev/null +++ b/simple_backend/src/task_tracker/storage.py @@ -0,0 +1,22 @@ +import json +import os + + +class TaskStorage: + """Класс для работы с файлом задач""" + + def __init__(self, filename="tasks.json"): + self.filename = filename + if not os.path.exists(self.filename): + with open(self.filename, "w") as f: + json.dump([], f) + + def load_tasks(self): + """Загрузка задач из файла""" + with open(self.filename, "r") as f: + return json.load(f) + + def save_tasks(self, tasks): + """Сохранение задач в файл""" + with open(self.filename, "w") as f: + json.dump(tasks, f, ensure_ascii=False, indent=4)