diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..3d6f3e51 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + pull_request: + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install Ruff + run: pip install ruff + + - name: Ruff lint + run: ruff check . + + - name: Ruff format check + run: ruff format . --check \ No newline at end of file diff --git a/git/src/main.py b/git/src/main.py index 1822c7e9..5c920b33 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() @@ -94,24 +101,31 @@ def main(): # Получаем новый список с добавленной книгой new_books = add_book(books, title, author, year) 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): books = new_books save_books(books) + print("Книга удалена!") 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 +133,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/classes.py b/simple_backend/src/task_tracker/classes.py new file mode 100644 index 00000000..3f86a973 --- /dev/null +++ b/simple_backend/src/task_tracker/classes.py @@ -0,0 +1,50 @@ +import json +from pathlib import Path +import requests +from abc import ABC, abstractmethod +from settings import JSONBIN_API_KEY, JSONBIN_BIN_ID, CF_API_TOKEN, CF_ACCOUNT_ID, CF_MODEL + +JSONBIN_BASE = "https://api.jsonbin.io/v3/b" +CF_BASE = "https://api.cloudflare.com/client/v4" + +class BaseStorage(ABC): + @abstractmethod + def load(self): + pass + @abstractmethod + def save(self): + pass + +class TaskStorage(BaseStorage): + def __init__(self, path: str = "tasks.json"): + self.path = Path(path) + if not self.path.exists(): + self.save([]) + def load(self) -> list[dict]: + with self.path.open("r", encoding="utf-8") as f: + return json.load(f) + def save(self, data: list[dict]) -> None: + with self.path.open("w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + +class RemoteTaskStorage(BaseStorage): + def __init__(self): + self.bin_id = JSONBIN_BIN_ID + self.headers = {"X-Master-Key": JSONBIN_API_KEY, "Content-Type": "application/json"} + def load(self) -> list[dict]: + r = requests.get(f"{JSONBIN_BASE}/{self.bin_id}/latest", headers=self.headers, timeout=10) + r.raise_for_status() + return r.json()["record"] + def save(self, data: list[dict]) -> None: + r = requests.put(f"{JSONBIN_BASE}/{self.bin_id}", headers=self.headers, json=data, timeout=10) + r.raise_for_status() + +class CloudflareAIClient: + def __init__(self): + self.url = f"{CF_BASE}/accounts/{CF_ACCOUNT_ID}/ai/run/{CF_MODEL}" + self.headers = {"Authorization": f"Bearer {CF_API_TOKEN}"} + def explain(self, text: str) -> str: + r = requests.post(self.url, headers=self.headers, json={"prompt": text}, timeout=15) + r.raise_for_status() + return r.json()["result"]["response"] + diff --git a/simple_backend/src/task_tracker/data/task.json b/simple_backend/src/task_tracker/data/task.json new file mode 100644 index 00000000..e69de29b diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 3db98d0d..7b46beb7 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,19 +1,62 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException +from classes import RemoteTaskStorage, CloudflareAIClient app = FastAPI() +STATUS_LIST = ["Задача создана", "Задача в процессе выполнения", "Задача выполнена"] +DEFAULT_STATUS = STATUS_LIST[0] + +storage = RemoteTaskStorage() +ai = CloudflareAIClient() + +def next_id() -> int: + tasks = storage.load() + return max((t["task_id"] for t in tasks), default=0) + 1 + +class Task: + def __init__(self, name: str): + self.task_id = next_id() + self.name = name + self.status = DEFAULT_STATUS + def to_dict(self): + return {"task_id": self.task_id, "name": self.name, "status": self.status} + @app.get("/tasks") -def get_tasks(): - pass +def get_tasks() -> list[dict]: + return storage.load() @app.post("/tasks") -def create_task(task): - pass +def create_task(name: str): + tasks = storage.load() + try: + tip = ai.explain(f"Объясни пошагово, как решать вот эту задачу -> {name} ") + full_name = f"{name} \n\nПодсказка LLM:\n{tip}" + except Exception: + full_name = name + new_task = Task(full_name) + tasks.append(new_task.to_dict()) + storage.save(tasks) + return new_task.to_dict() @app.put("/tasks/{task_id}") def update_task(task_id: int): - pass + tasks = storage.load() + for t in tasks: + if t["task_id"] == task_id: + if t["status"] == DEFAULT_STATUS: + t["status"] = STATUS_LIST[1] + elif t["status"] == STATUS_LIST[1]: + t["status"] = STATUS_LIST[2] + storage.save(tasks) + return t + raise HTTPException(404, "ID не найден") @app.delete("/tasks/{task_id}") def delete_task(task_id: int): - pass + tasks = storage.load() + for i, t in enumerate(tasks): + if t["task_id"] == task_id: + tasks.pop(i) + storage.save(tasks) + return {"detail": "Удалено"} + raise HTTPException(404, "ID не найден") \ 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..3723cb2a --- /dev/null +++ b/simple_backend/src/task_tracker/readme.md @@ -0,0 +1,19 @@ +Хранение во внутреней памяти ++ легкая реализация +- отваливаются данные при перезапуске +Хранение в json ++ задачи сохраняются на диск и не пропадают +- все еще stateful плюс проблемы с одновременным сохранением и тд +Убрал ли я хранение состояния +Нет, я только перенес его из РАМ на диск +Варианты хранения +-Текстовые файлы (тхт, json, cvs) проблемы остаются стандартными для хранения на внутреней памяти +-Облачные сервисы, проблема гонки не исчезает но уже работает stateles подход +-Базы данных, вобще все круто все работает проблем никаких нет доллар по 30 и вкусное мороженное там же(я ничего не знаю про БД) +Хранени +Состояние гонки +Это ситуация, когда два клиента одновременно читают старое состояние и перезаписывают друг друга. - это термин из интернета +Как я понял: состояние гонки это когда один файл берут два человека и пытаются с ним работать(не получится - файл то один). +Если я правильно понимаю БД в таких случая обладаюст инструментами типа копирования и последующей синхронизации со склеиванием двух версий в одну. +Что то типа контроля версий гитхаба что ли. +Как подсказвает интернет состояние гонки можно решить оптимистичной блокировкой и генерацией уникальных айди через UUID(ксати я сначала так и делал) diff --git a/simple_backend/src/task_tracker/settings.py b/simple_backend/src/task_tracker/settings.py new file mode 100644 index 00000000..be6ffc1e --- /dev/null +++ b/simple_backend/src/task_tracker/settings.py @@ -0,0 +1,9 @@ +import os +from dotenv import load_dotenv +load_dotenv() + +JSONBIN_API_KEY = os.getenv("JSONBIN_API_KEY") +JSONBIN_BIN_ID = os.getenv("JSONBIN_BIN_ID") +CF_API_TOKEN = os.getenv("CF_API_TOKEN") +CF_ACCOUNT_ID = os.getenv("CF_ACCOUNT_ID") +CF_MODEL = os.getenv("CF_MODEL", "@cf/meta/llama-3.1-8b-instruct")