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..e17d2423 --- /dev/null +++ b/simple_backend/src/task_tracker/base_http_client.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod +import requests +import json + +class BaseHTTPClient(ABC): + def __init__(self): + self.base_url = self.get_base_url() + self.headers = self.get_headers() + + @abstractmethod + def get_base_url(self) -> str: + + pass + + @abstractmethod + def get_headers(self) -> dict: + + pass + + def make_request(self, method: str, endpoint: str = "", json_data: dict = None, data: str = None, timeout: int = 30): + + url = f"{self.base_url}{endpoint}" + + try: + response = requests.request( + method=method, + url=url, + headers=self.headers, + json=json_data, + data=data, + timeout=timeout + ) + return response + except Exception as e: + raise Exception(f"Request error: {str(e)}") \ No newline at end of file diff --git a/simple_backend/src/task_tracker/cloudflare.py b/simple_backend/src/task_tracker/cloudflare.py new file mode 100644 index 00000000..b089d5cd --- /dev/null +++ b/simple_backend/src/task_tracker/cloudflare.py @@ -0,0 +1,50 @@ +import os +from base_http_client import BaseHTTPClient + +class CloudflareAI(BaseHTTPClient): + def __init__(self): + self.account_id = os.getenv('CLOUDFLARE_ACCOUNT_ID') + self.api_token = os.getenv('CLOUDFLARE_API_TOKEN') + self.model = '@cf/meta/llama-3-8b-instruct' + super().__init__() + + def get_base_url(self) -> str: + return f"https://api.cloudflare.com/client/v4/accounts/{self.account_id}/ai/run/{self.model}" + + def get_headers(self) -> dict: + return { + "Authorization": f"Bearer {self.api_token}", + "Content-Type": "application/json" + } + + def get_advice(self, task_text: str) -> str: + """Получает советы по решению задачи от LLM""" + if not self.account_id or not self.api_token: + return "AI не настроен" + + try: + payload = { + "messages": [ + { + "role": "system", + "content": "Ты - полезный ассистент. Отвечай ТОЛЬКО на русском языке. Давай четкие, структурированные ответы с нумерованными шагами." + }, + { + "role": "user", + "content": f"Дай подробные шаги для решения этой задачи на русском языке: {task_text}" + } + ] + } + + response = self.make_request("POST", json_data=payload, timeout=30) + + print(f"Cloudflare Status: {response.status_code}") + + if response.status_code == 200: + result = response.json() + return result.get('result', {}).get('response', 'Ответ получен') + else: + return f"Ошибка API: {response.status_code} - {response.text}" + + except Exception as e: + return f"Ошибка: {str(e)}" \ No newline at end of file diff --git a/simple_backend/src/task_tracker/gist_storage.py b/simple_backend/src/task_tracker/gist_storage.py new file mode 100644 index 00000000..79396e21 --- /dev/null +++ b/simple_backend/src/task_tracker/gist_storage.py @@ -0,0 +1,56 @@ +import json +import os +from base_http_client import BaseHTTPClient + +class GistStorage(BaseHTTPClient): + def __init__(self, filename='tasks.json'): + self.json_name = filename + self.gist_id = os.getenv('GIST_ID') + self.github_token = os.getenv('GITHUB_TOKEN') + super().__init__() + + def get_base_url(self) -> str: + return "https://api.github.com" + + def get_headers(self) -> dict: + return { + "Authorization": f"token {self.github_token}", + "Accept": "application/vnd.github.v3+json" + } + + def load_tasks(self): + if not self.gist_id or not self.github_token: + return [] + + try: + response = self.make_request("GET", f"/gists/{self.gist_id}") + + if response.status_code == 200: + gist_data = response.json() + tasks_content = gist_data['files'][self.json_name]['content'] + return json.loads(tasks_content) + return [] + except: + return [] + + def save_tasks(self, task_list): + if not self.gist_id or not self.github_token: + return False + + try: + gist_data = { + "files": { + self.json_name: { + "content": json.dumps(task_list, indent=2, ensure_ascii=False) + } + } + } + + response = self.make_request( + "PATCH", + f"/gists/{self.gist_id}", + data=json.dumps(gist_data) + ) + return response.status_code == 200 + except: + return False \ No newline at end of file diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 3db98d0d..8870d47a 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,19 +1,64 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from gist_storage import GistStorage +from cloudflare import CloudflareAI +from dotenv import load_dotenv + +load_dotenv() app = FastAPI() -@app.get("/tasks") +storage = GistStorage() +ai_client = CloudflareAI() + +class CreateTask(BaseModel): + task: str + status: str = 'No' + +class UpdateTask(BaseModel): + task: str + status: str + +@app.get("/tasks", tags=['Вывод всех задач']) def get_tasks(): - pass + return storage.load_tasks() -@app.post("/tasks") -def create_task(task): - pass +@app.post("/tasks", tags=['Добавление задачи']) +def create_task(new_task: CreateTask): + tasks = storage.load_tasks() + max_id = max(task['id'] for task in tasks) + 1 if tasks else 1 + + ai_advice = ai_client.get_advice(new_task.task) + enhanced_task = f"{new_task.task}\n\nСоветы по решению:\n{ai_advice}" + + tasks.append({ + 'id': max_id, + 'task': enhanced_task, + 'status': new_task.status + }) + + storage.save_tasks(tasks) + return {"success": True} -@app.put("/tasks/{task_id}") -def update_task(task_id: int): - pass +@app.put("/tasks/{task_id}", tags=['Обновление задачи']) +def update_task(task_id: int, update_task: UpdateTask): + tasks = storage.load_tasks() + for i in tasks: + if i['id'] == task_id: + i['task'] = update_task.task + i['status'] = update_task.status + storage.save_tasks(tasks) + return {"success": True} + + raise HTTPException(status_code=404, detail='Задача не найдена') -@app.delete("/tasks/{task_id}") +@app.delete("/tasks/{task_id}", tags=['Удаление задачи']) 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 {"success": True} + + raise HTTPException(status_code=404, detail='Задача не найдена') \ 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..b2b8a7a3 --- /dev/null +++ b/simple_backend/src/task_tracker/readme.md @@ -0,0 +1,63 @@ +**В чём минусы подхода с хранением задач в оперативной памяти (списке python)?** + +Минусы хранения задач в оперативной памяти (списке Python): +1 - Потеря данных при перезапуске +2 - Ограниченный объем памяти +3 - Данные существуют только во время работы программы + +## после перехода на json файл +**Что улучшилось после того, как список из оперативной памяти изменился на файл проекта?** + +Улучшения: +1. Сохраняемость данных - задачи не теряются при перезапуске сервера +2. Устойчивость к сбоям - данные сохраняются на диск, а не только в RAM +3. Прозрачность данных - можно открыть файл и посмотреть задачи вручную + +Остались проблемы: +1. Производительность - чтение/запись файла медленнее чем работа с памятью +2. Состояние гонки - при одновременных запросах возможна потеря данных + +**Избавились ли мы от хранения состояния?** +Нет, Backend остался STATEful - он по-прежнему хранит данные между запросами, просто теперь они сохраняются на диск. + +**Где еще можно хранить задачи и какие есть преимущества и недостатки этих подходов?** +### 1. Реляционные БД (PostgreSQL, MySQL) +Плюсы: +- ACID-транзакции +- Сложные запросы (JOIN, GROUP BY) +- Целостность данных +- Одновременный доступ + +Минусы: +- Сложность настройки +- Оверкилл для простых проектов +- Требует отдельного сервера БД + +### 2. Облачные хранилища +Плюсы: +- Доступность из любого места +- Автоматическое резервное копирование +- Не нужно настраивать сервер + +Минусы: +- Зависимость от внешнего сервиса +- Лимиты запросов +- Проблемы с доступностью + +**Прочитайте что такое "состояние гонки" и напишите в readme файле о том, какие проблемы остались в бекенде на данном этапе проекта. Есть ли у вас какое-то решение этой проблемы?** + +При одновременных запросах возможна потеря данных: +- Два пользователя добавляют задачи → одна задача теряется +- Редактирование и удаление одновременно → повреждение данных +- Изменения статусов конфликтуют + +## Решение проблемы + +mb Реализовать оптимистичную блокировку с повторными попытками: + +def update_with_retry(task_id, update_data, max_attempts=3): + for attempt in range(max_attempts): + tasks = load_tasks() + if save_tasks(tasks): + return True + return False # \ No newline at end of file diff --git a/simple_backend/src/task_tracker/storage.py b/simple_backend/src/task_tracker/storage.py new file mode 100644 index 00000000..afd14959 --- /dev/null +++ b/simple_backend/src/task_tracker/storage.py @@ -0,0 +1,20 @@ +import json +import os + +class FileStorage: + def __init__(self, filename='tasks.json'): + self.json_name = filename + + def load_tasks(self): + if not os.path.exists(self.json_name): + return [] + + try: + with open(self.json_name, 'r', encoding='utf-8') as file: + return json.load(file) + except (FileNotFoundError, json.JSONDecodeError): + return [] + + def save_tasks(self, task_list): + with open(self.json_name, 'w', encoding='utf-8') as file: + json.dump(task_list, file) \ No newline at end of file diff --git a/simple_backend/src/task_tracker/tasks.json b/simple_backend/src/task_tracker/tasks.json new file mode 100644 index 00000000..0e26473e --- /dev/null +++ b/simple_backend/src/task_tracker/tasks.json @@ -0,0 +1,14 @@ +[ + { + "id": 1, + "task": "Закончить простые бэки", + "status": "No" + }, + + { + "id": 2, + "task": "Хз", + "status": "Yes" + } + +] \ No newline at end of file