From 21f11fa97301624da83f8f549b4c574d53d93e07 Mon Sep 17 00:00:00 2001 From: Knokeros Date: Fri, 29 Aug 2025 13:54:03 +0300 Subject: [PATCH 01/10] Add Actions with Ruff --- .github/workflows/ruff.yml | 28 ++++++++++++++++++ git/src/main.py | 60 +++++++++++++++++++++++--------------- 2 files changed, 65 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/ruff.yml 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() From 90c6b406ee9798460430944c36a3577465ed963b Mon Sep 17 00:00:00 2001 From: Knokeros Date: Tue, 9 Sep 2025 10:35:28 +0300 Subject: [PATCH 02/10] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BB=20=D1=80=D0=B5=D0=B4=D0=BC=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D1=8E=20?= =?UTF-8?q?2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/main.py | 56 +++++++++++++++++++---- simple_backend/src/task_tracker/readme.md | 0 2 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 simple_backend/src/task_tracker/readme.md diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 3db98d0d..986370e1 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,19 +1,55 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from typing import List app = FastAPI() -@app.get("/tasks") + +class Task(BaseModel): + """Модель Задачи""" + + id: int + title: str + status: str + + +# Добаляю хранение в оперативной памяти +tasks = [] +current_id = 1 + + +@app.get("/tasks", response_model=List[Task]) def get_tasks(): - pass + """ "Получение всвех задач""" + return tasks + + +@app.post("/tasks", response_model=Task) +def create_task(task_data: dict): + """Создание новой задачи""" + global current_id + task = Task(id=current_id, **task_data) + tasks.append(task) + current_id += 1 + return task + -@app.post("/tasks") -def create_task(task): - pass +@app.put("/tasks/{task_id}", response_model=Task) +def update_task(task_id: int, task_data: dict): + """Обновление задачи""" + for task in tasks: + if task.id == task_id: + task.title = task_data.get("title", task.title) + task.status = task_data.get("status", task.status) + 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 + global tasks + for i, task in enumerate(tasks): + if task.id == task_id: + tasks.pop(i) + 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..e69de29b From cad8733c080f9c88fe4ce3e6778ac861df770c92 Mon Sep 17 00:00:00 2001 From: Knokeros Date: Tue, 9 Sep 2025 10:54:37 +0300 Subject: [PATCH 03/10] =?UTF-8?q?=D0=9E=D1=82=D0=B4=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B5=D1=82=D0=BA=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/main.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 986370e1..729d00ec 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -28,7 +28,11 @@ def get_tasks(): def create_task(task_data: dict): """Создание новой задачи""" global current_id - task = Task(id=current_id, **task_data) + task = Task( + id=current_id, + title=task_data.get("title"), + status=task_data.get("status"), + ) tasks.append(task) current_id += 1 return task @@ -47,6 +51,7 @@ def update_task(task_id: int, task_data: dict): @app.delete("/tasks/{task_id}") def delete_task(task_id: int): + """Удаление задачи""" global tasks for i, task in enumerate(tasks): if task.id == task_id: From f09cac6328c109294484020256c1a4aacb286be2 Mon Sep 17 00:00:00 2001 From: Knokeros Date: Tue, 9 Sep 2025 12:23:13 +0300 Subject: [PATCH 04/10] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BB=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B2=20json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/storage.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 simple_backend/src/task_tracker/storage.py 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) From 4440a21f6c9ab022420c7ad491851f56287d545c Mon Sep 17 00:00:00 2001 From: Knokeros Date: Tue, 9 Sep 2025 12:25:29 +0300 Subject: [PATCH 05/10] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B4=20json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/main.py | 38 +++++++++++++---------- simple_backend/src/task_tracker/readme.md | 34 ++++++++++++++++++++ 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 729d00ec..9d937cea 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,8 +1,10 @@ from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List +from storage import TaskStorage app = FastAPI() +storage = TaskStorage("tasks.json") class Task(BaseModel): @@ -13,38 +15,39 @@ class Task(BaseModel): status: str -# Добаляю хранение в оперативной памяти -tasks = [] -current_id = 1 +class TaskCreate(BaseModel): + """Модель для создания задачи""" + + title: str + status: str @app.get("/tasks", response_model=List[Task]) def get_tasks(): """ "Получение всвех задач""" + tasks = storage.load_tasks() return tasks @app.post("/tasks", response_model=Task) -def create_task(task_data: dict): +def create_task(task_data: TaskCreate): """Создание новой задачи""" - global current_id - task = Task( - id=current_id, - title=task_data.get("title"), - status=task_data.get("status"), - ) + tasks = storage.load_tasks() + new_id = max([task["id"] for task in tasks], default=0) + 1 + task = {"id": new_id, "title": task_data.title, "status": task_data.status} tasks.append(task) - current_id += 1 + storage.save_tasks(tasks) return task @app.put("/tasks/{task_id}", response_model=Task) -def update_task(task_id: int, task_data: dict): +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.get("title", task.title) - task.status = task_data.get("status", task.status) + if task["id"] == task_id: + task["title"] = task_data.title + task["status"] = task_data.status return task raise HTTPException(status_code=404, detail="Задача не найдена") @@ -52,9 +55,10 @@ def update_task(task_id: int, task_data: dict): @app.delete("/tasks/{task_id}") def delete_task(task_id: int): """Удаление задачи""" - global tasks + tasks = storage.load_tasks() for i, task in enumerate(tasks): - if task.id == task_id: + 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 index e69de29b..ee918cca 100644 --- a/simple_backend/src/task_tracker/readme.md +++ b/simple_backend/src/task_tracker/readme.md @@ -0,0 +1,34 @@ +# Задание 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): + + Не зависит от локальной машины. + - Есть задержки сети и лимиты бесплатных тарифов. From eee65a8b513a14aa873b280693c0bb4fb39d9eb3 Mon Sep 17 00:00:00 2001 From: Knokeros Date: Wed, 10 Sep 2025 11:17:43 +0300 Subject: [PATCH 06/10] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BB=D0=B0=D0=BA=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/task_tracker/cloud_storage.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 simple_backend/src/task_tracker/cloud_storage.py 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..f69c07e6 --- /dev/null +++ b/simple_backend/src/task_tracker/cloud_storage.py @@ -0,0 +1,24 @@ +import requests + + +class CloudTaskStorage: + """Класс для хранения в облаке jsonbin""" + + def __init__(self, bin_id: str, api_key: str): + self.base_url = f"https://api.jsonbin.io/v3/b/{bin_id}" + self.headers = {"X-Master-Key": api_key, "Content-Type": "application/json"} + + def load_tasks(self): + """Загрузка задач из облака""" + response = requests.get(self.base_url, headers=self.headers) + if response.status_code == 200: + return response.json()["record"]["tasks"] + raise Exception(f"Ошибка загрузки: {response.text}") + + def save_tasks(self, tasks): + """Сохранение задач в облаке""" + data = {"tasks": tasks} + response = requests.put(self.base_url, headers=self.headers, json=data) + if response.status_code == 200: + return True + raise Exception(f"Ошибка сохранения: {response.text}") From eadc3fafe1f8a4b387eab2f868d5680b81006da3 Mon Sep 17 00:00:00 2001 From: Knokeros Date: Wed, 10 Sep 2025 11:20:46 +0300 Subject: [PATCH 07/10] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BB=D0=B0=D0=BA=D0=BE=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/main.py | 28 +++++++++++++++---- simple_backend/src/task_tracker/readme.md | 18 +++++++++++- .../src/task_tracker/requirements.txt | 5 +++- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 9d937cea..500b1d30 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,14 +1,31 @@ +import os +from typing import List + from fastapi import FastAPI, HTTPException from pydantic import BaseModel -from typing import List -from storage import TaskStorage +from dotenv import load_dotenv + +from cloud_storage import CloudTaskStorage + + +# Загружаем .env +load_dotenv() app = FastAPI() -storage = TaskStorage("tasks.json") + +BIN_ID = os.getenv("JSONBIN_BIN_ID") +API_KEY = os.getenv("JSONBIN_API_KEY") + +if not BIN_ID or not API_KEY: + raise RuntimeError( + "Не найдены переменные окружения JSONBIN_BIN_ID или JSONBIN_API_KEY" + ) + +storage = CloudTaskStorage(BIN_ID, API_KEY) class Task(BaseModel): - """Модель Задачи""" + """Модель задачи""" id: int title: str @@ -24,7 +41,7 @@ class TaskCreate(BaseModel): @app.get("/tasks", response_model=List[Task]) def get_tasks(): - """ "Получение всвех задач""" + """Получение всех задач""" tasks = storage.load_tasks() return tasks @@ -48,6 +65,7 @@ def update_task(task_id: int, task_data: TaskCreate): 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="Задача не найдена") diff --git a/simple_backend/src/task_tracker/readme.md b/simple_backend/src/task_tracker/readme.md index ee918cca..201fcff3 100644 --- a/simple_backend/src/task_tracker/readme.md +++ b/simple_backend/src/task_tracker/readme.md @@ -17,7 +17,7 @@ ## Избавились ли мы от хранения состояния? Нет. Мы всё ещё храним состояние (теперь уже на диске, а не в памяти). -Чтобы сделать backend **stateless**, нужно вынести данные во внешний сервис (например, jsonbin.io, mockapi.io, Firebase). +Чтобы сделать backend stateless, нужно вынести данные во внешний сервис (например, jsonbin.io, mockapi.io, Firebase). ## Где ещё можно хранить задачи @@ -32,3 +32,19 @@ - 3.Облачный сервис (jsonbin.io, Firebase, mockapi.io): + Не зависит от локальной машины. - Есть задержки сети и лимиты бесплатных тарифов. + + +# Задание 2 - бэк на облаке +## Что изменилось при переходе в облако? +- Данные теперь хранятся не локально, а в облаке. +- Бэк стал stateless: можно запустить его на любой машине, и задачи не потеряются. +- Несколько серверов могут работать с одними и теми же задачами. + +## Избавились ли мы от хранения состояния? +Да, на стороне бека мы больше не храним задачи — они лежат в облаке. +Но полностью от состояния избавиться нельзя: оно просто переехало в jsonbin.io + +## Проблемы состояния гонки +- Если два клиента одновременно обновляют список задач, один может перезаписать изменения другого +- Решения: + - Перейти на базу данных или сервис, который поддерживает конкурентный доступ (PostgreSQL). 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 From e68db8deb533edb889cc271c878f8085a12ccaad Mon Sep 17 00:00:00 2001 From: Knokeros Date: Wed, 10 Sep 2025 13:04:42 +0300 Subject: [PATCH 08/10] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=9B=D0=9B=D0=9C=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=20=D1=81=20=D0=90=D0=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/task_tracker/cloud_flare.py | 52 +++++++++++++++++++ .../src/task_tracker/cloud_storage.py | 5 +- simple_backend/src/task_tracker/main.py | 52 ++++++++++++------- 3 files changed, 89 insertions(+), 20 deletions(-) create mode 100644 simple_backend/src/task_tracker/cloud_flare.py 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..9bec99b1 --- /dev/null +++ b/simple_backend/src/task_tracker/cloud_flare.py @@ -0,0 +1,52 @@ +import requests + + +class CloudflareAIClient: + """Клиент для работы с Cloudflare AI API""" + + def __init__(self, account_id: str, api_key: str): + self.base_url = ( + f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run" + ) + self.headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + 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: + response = requests.post( + f"{self.base_url}/@cf/meta/llama-3-8b-instruct", + headers=self.headers, + json=payload, + timeout=30, + ) + response.raise_for_status() + + result = response.json() + return result["result"]["response"] + + except requests.exceptions.RequestException as e: + print(f"Ошибка Cloudflare AI: {e}") + return "Не удалось получить рекомендации" diff --git a/simple_backend/src/task_tracker/cloud_storage.py b/simple_backend/src/task_tracker/cloud_storage.py index f69c07e6..62fb6f97 100644 --- a/simple_backend/src/task_tracker/cloud_storage.py +++ b/simple_backend/src/task_tracker/cloud_storage.py @@ -12,11 +12,14 @@ def load_tasks(self): """Загрузка задач из облака""" response = requests.get(self.base_url, headers=self.headers) if response.status_code == 200: - return response.json()["record"]["tasks"] + data = response.json()["record"] + # Поддержка старого формата (массив задач) и нового (объект с tasks) + return data.get("tasks", data) if isinstance(data, dict) else data raise Exception(f"Ошибка загрузки: {response.text}") def save_tasks(self, tasks): """Сохранение задач в облаке""" + # Сохраняем как объект с ключом tasks для consistency data = {"tasks": tasks} response = requests.put(self.base_url, headers=self.headers, json=data) if response.status_code == 200: diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 500b1d30..c12b1161 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,27 +1,28 @@ 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() +# Конфигурация BIN_ID = os.getenv("JSONBIN_BIN_ID") -API_KEY = os.getenv("JSONBIN_API_KEY") +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 BIN_ID or not API_KEY: - raise RuntimeError( - "Не найдены переменные окружения JSONBIN_BIN_ID или JSONBIN_API_KEY" - ) +if not all([BIN_ID, JSONBIN_API_KEY, CLOUDFLARE_AI_API_KEY, CLOUDFLARE_ACCOUNT_ID]): + raise RuntimeError("Не найдены необходимые переменные окружения") -storage = CloudTaskStorage(BIN_ID, API_KEY) +# Инициализация клиентов +storage = CloudTaskStorage(BIN_ID, JSONBIN_API_KEY) +ai_client = CloudflareAIClient(CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_AI_API_KEY) class Task(BaseModel): @@ -30,6 +31,7 @@ class Task(BaseModel): id: int title: str status: str + solution_advice: str = "" # Новое поле для советов по решению class TaskCreate(BaseModel): @@ -39,24 +41,36 @@ class TaskCreate(BaseModel): status: str -@app.get("/tasks", response_model=List[Task]) -def get_tasks(): - """Получение всех задач""" - tasks = storage.load_tasks() - return tasks - - @app.post("/tasks", response_model=Task) -def create_task(task_data: TaskCreate): - """Создание новой задачи""" +async def create_task(task_data: TaskCreate): + """Создание новой задачи с AI-анализом""" tasks = storage.load_tasks() new_id = max([task["id"] for task in tasks], default=0) + 1 - task = {"id": new_id, "title": task_data.title, "status": task_data.status} + + # Получаем советы от 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(): + """Получение всех задач""" + tasks = storage.load_tasks() + return tasks + + @app.put("/tasks/{task_id}", response_model=Task) def update_task(task_id: int, task_data: TaskCreate): """Обновление задачи""" From d1e5cecd63d855a88098d8ccf13f6109c9d8cb2f Mon Sep 17 00:00:00 2001 From: Knokeros Date: Thu, 25 Sep 2025 12:36:32 +0300 Subject: [PATCH 09/10] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BA?= =?UTF-8?q?=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= =?UTF-8?q?=D0=B0=204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/task_tracker/base_http_client.py | 41 +++++++++++++++++++ .../src/task_tracker/cloud_flare.py | 35 ++++++++-------- .../src/task_tracker/cloud_storage.py | 38 +++++++++-------- simple_backend/src/task_tracker/main.py | 1 + 4 files changed, 79 insertions(+), 36 deletions(-) create mode 100644 simple_backend/src/task_tracker/base_http_client.py 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 index 9bec99b1..1a26532b 100644 --- a/simple_backend/src/task_tracker/cloud_flare.py +++ b/simple_backend/src/task_tracker/cloud_flare.py @@ -1,17 +1,24 @@ -import requests +from base_http_client import BaseHTTPClient -class CloudflareAIClient: +class CloudflareAIClient(BaseHTTPClient): """Клиент для работы с Cloudflare AI API""" def __init__(self, account_id: str, api_key: str): - self.base_url = ( - f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run" - ) - self.headers = { + 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""" @@ -31,22 +38,14 @@ def get_task_solutions(self, task_title: str) -> str: }, {"role": "user", "content": prompt}, ], - "model": "@cf/meta/llama-3-8b-instruct", # Или другая модель + "model": "@cf/meta/llama-3-8b-instruct", "max_tokens": 500, } try: - response = requests.post( - f"{self.base_url}/@cf/meta/llama-3-8b-instruct", - headers=self.headers, - json=payload, - timeout=30, + result = self._make_request( + "POST", "/@cf/meta/llama-3-8b-instruct", json=payload ) - response.raise_for_status() - - result = response.json() return result["result"]["response"] - - except requests.exceptions.RequestException as e: - print(f"Ошибка Cloudflare AI: {e}") + except Exception: return "Не удалось получить рекомендации" diff --git a/simple_backend/src/task_tracker/cloud_storage.py b/simple_backend/src/task_tracker/cloud_storage.py index 62fb6f97..4c1fa88f 100644 --- a/simple_backend/src/task_tracker/cloud_storage.py +++ b/simple_backend/src/task_tracker/cloud_storage.py @@ -1,27 +1,29 @@ -import requests +from base_http_client import BaseHTTPClient -class CloudTaskStorage: - """Класс для хранения в облаке jsonbin""" +class CloudTaskStorage(BaseHTTPClient): + """Класс для хранения задач в jsonbin.io""" def __init__(self, bin_id: str, api_key: str): - self.base_url = f"https://api.jsonbin.io/v3/b/{bin_id}" - self.headers = {"X-Master-Key": api_key, "Content-Type": "application/json"} + 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_tasks(self): + def load_data(self) -> list: """Загрузка задач из облака""" - response = requests.get(self.base_url, headers=self.headers) - if response.status_code == 200: - data = response.json()["record"] - # Поддержка старого формата (массив задач) и нового (объект с tasks) - return data.get("tasks", data) if isinstance(data, dict) else data - raise Exception(f"Ошибка загрузки: {response.text}") + response_data = self._make_request("GET") + data = response_data["record"] + return data.get("tasks", data) if isinstance(data, dict) else data - def save_tasks(self, tasks): + def save_data(self, tasks: list) -> bool: """Сохранение задач в облаке""" - # Сохраняем как объект с ключом tasks для consistency data = {"tasks": tasks} - response = requests.put(self.base_url, headers=self.headers, json=data) - if response.status_code == 200: - return True - raise Exception(f"Ошибка сохранения: {response.text}") + 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 c12b1161..282e757c 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -3,6 +3,7 @@ from fastapi import FastAPI, HTTPException from pydantic import BaseModel from dotenv import load_dotenv + from cloud_storage import CloudTaskStorage from cloud_flare import CloudflareAIClient From 084256c5ff6de181a39d9e2c242db117871b8913 Mon Sep 17 00:00:00 2001 From: Knokeros Date: Thu, 25 Sep 2025 13:32:23 +0300 Subject: [PATCH 10/10] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=80=D0=B5=D0=B4=D0=B4=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/cloud_flare.py | 1 + simple_backend/src/task_tracker/readme.md | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/simple_backend/src/task_tracker/cloud_flare.py b/simple_backend/src/task_tracker/cloud_flare.py index 1a26532b..9144246b 100644 --- a/simple_backend/src/task_tracker/cloud_flare.py +++ b/simple_backend/src/task_tracker/cloud_flare.py @@ -1,3 +1,4 @@ +from typing import Any from base_http_client import BaseHTTPClient diff --git a/simple_backend/src/task_tracker/readme.md b/simple_backend/src/task_tracker/readme.md index 201fcff3..770bd4c5 100644 --- a/simple_backend/src/task_tracker/readme.md +++ b/simple_backend/src/task_tracker/readme.md @@ -34,7 +34,7 @@ - Есть задержки сети и лимиты бесплатных тарифов. -# Задание 2 - бэк на облаке +# Задание - бэк на облаке ## Что изменилось при переходе в облако? - Данные теперь хранятся не локально, а в облаке. - Бэк стал stateless: можно запустить его на любой машине, и задачи не потеряются. @@ -48,3 +48,18 @@ - Если два клиента одновременно обновляют список задач, один может перезаписать изменения другого - Решения: - Перейти на базу данных или сервис, который поддерживает конкурентный доступ (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!" + }