diff --git a/.github/workflows/ruff-ci.yml b/.github/workflows/ruff-ci.yml new file mode 100644 index 00000000..c729213d --- /dev/null +++ b/.github/workflows/ruff-ci.yml @@ -0,0 +1,29 @@ +name: Ruff Code Quality + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install ruff + run: pip install ruff + + - name: Run ruff check + run: ruff check git/src/ + + - name: Check formatting + run: ruff format git/src/ --check diff --git a/git/src/main.py b/git/src/main.py index 1822c7e9..1135ea5a 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,30 @@ 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 +64,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 +88,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() @@ -97,9 +104,11 @@ def main(): 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 +118,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 +130,14 @@ def main(): else: print("Ничего не найдено.") - elif choice == '5': + elif choice == "5": print("Выход из программы.") break else: print("Некорректный ввод. Попробуйте ещё раз.") + if __name__ == "__main__": main() +# Test comment diff --git a/simple_backend/src/task_tracker/.gitignore b/simple_backend/src/task_tracker/.gitignore new file mode 100644 index 00000000..c1443be6 --- /dev/null +++ b/simple_backend/src/task_tracker/.gitignore @@ -0,0 +1,4 @@ +.venv/ +venv/ +__pycache__/ +*.pyc.env diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 3db98d0d..d7828bec 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,19 +1,223 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException, BackgroundTasks +from pydantic import BaseModel +import requests +import json +import os +from typing import List, Any, Dict +import httpx +from dotenv import load_dotenv + +load_dotenv() app = FastAPI() +# Конфигурация JSONBin.io +JSONBIN_API_KEY = "$2a$10$pcwqDUhDIMDVaHFqqn4gCui92e3cFwdyhwt5gxmglbF.Y/FJAHDJG" +JSONBIN_BIN_ID = "68d56971d0ea881f408ac1f5" + +CLOUDFLARE_API_TOKEN = os.getenv("CLOUDFLARE_API_TOKEN") +CLOUDFLARE_ACCOUNT_ID = os.getenv("CLOUDFLARE_ACCOUNT_ID") + + +class BaseHTTPClient: + """Базовый класс для HTTP клиентов""" + + def __init__(self, base_url: str, headers: Dict[str, str] = None): + self.base_url = base_url + self.headers = headers or {} + + def _request_sync(self, method: str, endpoint: str = "", **kwargs) -> Any: + """Синхронный HTTP запрос""" + url = f"{self.base_url}{endpoint}" + try: + response = requests.request(method, url, headers=self.headers, **kwargs) + print(f"HTTP {method} {url} - Status: {response.status_code}") + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"Ошибка HTTP запроса: {e}") + raise HTTPException(status_code=500, detail=f"Ошибка HTTP: {str(e)}") + + async def _request_async(self, method: str, endpoint: str = "", **kwargs) -> Any: + """Асинхронный HTTP запрос""" + url = f"{self.base_url}{endpoint}" + try: + async with httpx.AsyncClient() as client: + response = await client.request(method, url, headers=self.headers, **kwargs) + print(f"Async HTTP {method} {url} - Status: {response.status_code}") + response.raise_for_status() + return response.json() + except httpx.RequestError as e: + print(f"Ошибка асинхронного HTTP запроса: {e}") + raise HTTPException(status_code=500, detail=f"Ошибка HTTP: {str(e)}") + +class Task(BaseModel): + id: int + title: str + completed: bool = False + ai_help: str = '' + +class CloudTaskManager(BaseHTTPClient): + + def __init__(self, api_key: str, bin_id: str): + base_url = f"https://api.jsonbin.io/v3/b/{bin_id}" + + headers = { + "Content-Type": "application/json", + "X-Master-Key": api_key + } + + super().__init__(base_url, headers) + + self.api_key = api_key + self.bin_id = bin_id + + def load_tasks(self) -> List[Task]: + """Загрузить задачи из JSONBin.io""" + + try: + data = self._request_sync('GET') + + tasks_data = data['record'] + return [Task(**task) for task in tasks_data] + except HTTPException: + print("Бин не найден, создаем начальные задачи") + return self._create_initial_tasks() + + + + + def save_tasks(self, tasks: List[Task]) -> None: + """Сохранить задачи в JSONBin.io""" + data = [task.model_dump() for task in tasks] + self._request_sync("PUT", json=data) + print("Данные успешно сохранены") + + def _create_initial_tasks(self) -> List[Task]: + """Создать начальные задачи если бин не существует""" + print("Создаем начальные задачи...") + initial_tasks_data = [ + {"id": 1, "title": "купить молоко", "completed": False, "ai_help": ""}, + {"id": 2, "title": "позавтракать", "completed": True, "ai_help": ""}, + {"id": 3, "title": "позвонить дедушке", "completed": False, "ai_help": ""} + ] + + initial_tasks = [Task(**task) for task in initial_tasks_data] + self.save_tasks(initial_tasks) + return initial_tasks + + def get_next_id(self, tasks: List[Task]) -> int: + """Получить следующий ID для новой задачи""" + if not tasks: + return 1 + return max(task.id for task in tasks) + 1 + +class CloudflareAI(BaseHTTPClient): + """Клиент для Cloudflare AI""" + + def __init__(self, api_token: str, account_id: str, model: str = "@cf/meta/llama-3.1-8b-instruct"): + base_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run" + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json" + } + super().__init__(base_url, headers) + self.model = model + + + async def get_ai_help(self, task_title: str) -> str: + """Получить AI помощь для задачи""" + try: + print(f"Отправка запроса к Cloudflare AI...") + print(f"URL: {self.base_url}/{self.model}") # ← Используем base_url из базового класса + print(f"Токен: {self.headers['Authorization'][:10]}...") # ← Токен из headers + data = await self._request_async( + "POST", + endpoint=f"/{self.model}", + json={ + "messages": [ + { + "role": "user", + "content": f"Кратко объясни как решить задачу: {task_title}" + } + ] + }, + timeout=30.0 + ) + + return data.get("result", {}).get("response", "Не удалось получить ответ") + + + except Exception as e: + print(f"Ошибка AI: {e}") + return f"Ошибка подключения: {str(e)}" + + +# Создаем менеджер задач +task_manager = CloudTaskManager(JSONBIN_API_KEY, JSONBIN_BIN_ID) +ai_service = CloudflareAI(CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID) + + +async def add_ai_to_task(task_id: int, task_title: str): + try: + ai_help = await ai_service.get_ai_help(task_title) + + tasks = task_manager.load_tasks() + for task in tasks: + if task.id == task_id: + task.ai_help = ai_help + task_manager.save_tasks(tasks) + print(f"Добавлена AI помощь для задачи {task_id}") + break + except Exception as e: + print(f"Ошибка при добавлении AI помощи: {e}") + +# Эндпоинты @app.get("/tasks") def get_tasks(): - pass + return task_manager.load_tasks() @app.post("/tasks") -def create_task(task): - pass +async def create_task(title: str, background_tasks: BackgroundTasks): + tasks = task_manager.load_tasks() + new_id = task_manager.get_next_id(tasks) + new_task = Task(id=new_id, title=title, completed=False) + tasks.append(new_task) + task_manager.save_tasks(tasks) + + background_tasks.add_task(add_ai_to_task, new_id, title) + + return new_task @app.put("/tasks/{task_id}") -def update_task(task_id: int): - pass +def update_task(task_id: int, background_tasks: BackgroundTasks, title: str = None, completed: bool = None): + tasks = task_manager.load_tasks() + for task in tasks: + if task.id == task_id: + if title is not None: + task.title = title + if completed is not None: + task.completed = completed + task_manager.save_tasks(tasks) + + background_tasks.add_task(add_ai_to_task, task_id, title) + + return task + return {"error": "Задача не найдена"} @app.delete("/tasks/{task_id}") def delete_task(task_id: int): - pass + tasks = task_manager.load_tasks() + for i, task in enumerate(tasks): + if task.id == task_id: + deleted_task = tasks.pop(i) + task_manager.save_tasks(tasks) + return {"message": f"Задача '{deleted_task.title}' удалена"} + return {"error": "Задача не найдена"} + +@app.on_event("startup") +async def startup_event(): + """Проверяем соединение при запуске""" + print("Проверка соединения с JSONBin.io...") + tasks = task_manager.load_tasks() + print(f"Загружено задач: {len(tasks)}") \ No newline at end of file diff --git a/simple_backend/src/task_tracker/readme.md b/simple_backend/src/task_tracker/readme.md new file mode 100644 index 00000000..313141ef --- /dev/null +++ b/simple_backend/src/task_tracker/readme.md @@ -0,0 +1,203 @@ +Минусы этого подхода: +1. Потеря данных при перезапуске +Все данные удаляются при остановке сервера + +При деплое или обновлении приложения задачи теряются + +При сбое сервера все данные исчезают + +2. Проблемы с масштабированием +Нельзя запустить несколько экземпляров приложения (балансировка нагрузки) + +Каждый сервер будет иметь свою копию данных + +Нет синхронизации между разными процессами + +3. Ограничение памяти +Данные хранятся в RAM, что ограничено размером памяти сервера + +При большом количестве задач может закончиться память + +Нет эффективного управления памятью + +4. Нет персистентности +Невозможно сделать бэкап данных + +Нет истории изменений + +Невозможно восстановить данные после сбоя + +5. Проблемы с производительностью +Поиск по большому списку медленный (O(n)) + +Нет индексов для оптимизации запросов + +Нет кэширования сложных запросов + +6. Отсутствие транзакций +Нет ACID-свойств (атомарность, согласованность, изолированность, долговечность) + +Возможны race conditions при параллельных запросах + +Нет механизмов блокировок + +Когда использовать такой подход? +Только для: + +Прототипирования и разработки + +Тестирования функциональности + +Демонстрационных проектов + +Учебных целей (как сейчас у нас) + +Альтернативы для продакшена: +Базы данных: PostgreSQL, MySQL, SQLite + +In-memory DB: Redis, Memcached + +Файловое хранилище: JSON, CSV файлы + +Облачные решения: Firebase, AWS DynamoDB + +## Переход от хранения списка задач в оперативной памяти к файлу проекта + +### Улучшения после перехода: + +1. **Сохраняемость данных** - задачи сохраняются между перезапусками приложения +2. **Надежность хранения** - данные устойчивы к внезапным завершениям работы программы +3. **Прозрачность данных** - возможность прямого просмотра и редактирования файла +4. **Версионный контроль** - интеграция с системами контроля версий (Git) +5. **Переносимость** - простое копирование и перенос между устройствами + +### Состояние приложения: анализ изменений + +**Состояние не устранено, а перенесено** из временного хранилища в постоянное: + +- **Предыдущий подход**: состояние в оперативной памяти (временное, volatile) +- **Текущий подход**: состояние в файловой системе (постоянное, persistent) + +Приложение продолжает хранить состояние, но теперь обеспечивает его сохранность между сеансами работы. + +## Альтернативные подходы к хранению задач + +### 1. Оперативная память (исходный подход) +**Преимущества:** +- Максимальная производительность +- Минимальная сложность реализации +- Отсутствие накладных расходов + +**Недостатки:** +- Потеря данных при перезапуске +- Отсутствие персистентности +- Ограниченный доступ к данным + +### 2. Текстовый файл (текущая реализация) +**Преимущества:** +- Простота реализации и обслуживания +- Человеко-читаемый формат +- Прямое редактирование без приложения +- Совместимость с системами контроля версий + +**Недостатки:** +- Снижение производительности при больших объемах +- Риск повреждения формата файла +- Ограниченные возможности запросов и фильтрации + +### 3. База данных SQLite +**Преимущества:** +- Оптимизированная производительность +- Гарантированная целостность данных +- Расширенные возможности запросов +- Поддержка конкурентного доступа + +**Недостатки:** +- Усложнение архитектуры приложения +- Избыточность для простых случаев использования +- Бинарный формат затрудняет ручное редактирование + +### 4. Структурированные файлы (JSON/YAML) +**Преимущества:** +- Стандартизированный машинно-читаемый формат +- Широкая инструментальная поддержка +- Поддержка сложных структур данных +- Сохранение человеко-читаемости + +**Недостатки:** +- Увеличенный объем хранения +- Потенциальные проблемы с валидацией формата +- Снижение производительности по сравнению с бинарными форматами + +### 5. Облачные хранилища +**Преимущества:** +- Кросс-платформенная доступность +- Автоматическая синхронизация +- Встроенное резервное копирование +- Горизонтальная масштабируемость + +**Недостатки:** +- Зависимость от сетевого соединения +- Потенциальные затраты на обслуживание +- Зависимость от сторонних сервисов +- Вопросы конфиденциальности данных + +## Критерии выбора стратегии хранения + +**Оперативная память**: прототипирование, тестовые среды +**Текстовые файлы**: персональные приложения, простые случаи использования +**SQLite**: многопользовательские приложения, сложные запросы +**JSON/YAML**: конфигурационные данные, сложные структуры +**Облачные хранилища**: мобильные и веб-приложения, распределенные команды + +Текущая реализация с текстовым файлом обеспечивает оптимальный баланс для персонального использования, сочетая простоту реализации с достаточной функциональностью. + + +# Stateless Backend с облачным хранилищем + +## Архитектура +Backend теперь является stateless - все данные хранятся в облачном сервисе jsonbin.io. При каждом запросе данные загружаются и сохраняются в облачное хранилище. + +## Проблема состояния гонки (Race Condition) + +### Что это такое? +Состояние гонки возникает, когда несколько процессов или потоков одновременно пытаются изменить общие данные, и конечный результат зависит от порядка выполнения операций. + +### Проблемы в текущей реализации: + +1. **Чтение-Модификация-Запись (Read-Modify-Write)** + - Два запроса одновременно читают данные + - Оба модифицируют свою копию + - Последний запрос перезаписывает изменения первого + +2. **Конкретный сценарий:** + - Запрос A читает задачи: `[{id: 1, title: "Task 1"}]` + - Запрос B читает задачи: `[{id: 1, title: "Task 1"}]` + - Запрос A обновляет title на "Task 1 Updated" и сохраняет + - Запрос B обновляет title на "Task 1 Modified" и сохраняет + - **Результат**: Изменения запроса A теряются + +### Решение проблемы + +#### Вариант 1: Оптимистичная блокировка +```python +# Добавить версию данных +async def update_task(task_id: int, updated_task: Task, version: int): + current_data = await cloud_storage.get_tasks_with_version() + if current_data["version"] != version: + raise HTTPException(409, "Data was modified by another request") + # Обновить данные и увеличить версию + +Вариант 2: Пессимистичная блокировка +Внешний сервис блокировок + +Redis-based мьютексы + +Блокировка на время операции + +Вариант 3: База данных с транзакциями +Переход на PostgreSQL/MongoDB + +ACID-транзакции + +Атомарные операции \ No newline at end of file diff --git a/simple_backend/src/task_tracker/requirements.txt b/simple_backend/src/task_tracker/requirements.txt index 8e0578a0..535c39c8 100644 --- a/simple_backend/src/task_tracker/requirements.txt +++ b/simple_backend/src/task_tracker/requirements.txt @@ -1,2 +1,6 @@ -fastapi -uvicorn[standard] \ No newline at end of file +fastapi>=0.104.1 +uvicorn>=0.24.0 +requests>=2.31.0 +pydantic>=2.5.0 +httpx +python-dotenv \ No newline at end of file