-
Notifications
You must be signed in to change notification settings - Fork 99
Git homework #128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Git homework #128
Changes from all commits
d4c7fce
edcbec4
1ece50e
df4e0be
eee4e56
01ee253
b9273db
18f73e2
b0560cb
23cf4e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. тут лучше тоже конкретную версию ставить + закэшировать можно установку |
||
|
|
||
| - name: Ruff lint | ||
| run: ruff check . | ||
|
|
||
| - name: Ruff format check | ||
| run: ruff format . --check | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 [] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. try except можно в такой ситуации не юзать, а просто на самый верх прокидывать ошибку и если че в fastapi exception handler ловить исключения |
||
|
|
||
| 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,37 +101,45 @@ 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Найденные книги:") | ||
| print(list_books(found_books)) | ||
| else: | ||
| print("Ничего не найдено.") | ||
|
|
||
| elif choice == '5': | ||
| elif choice == "5": | ||
| print("Выход из программы.") | ||
| break | ||
|
|
||
| else: | ||
| print("Некорректный ввод. Попробуйте ещё раз.") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. переменные с url лучше тянуть из env через pydanric base settings или starlette и хранить в отдельном файле с конфигом |
||
|
|
||
| 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]: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. у дикта можно типы внутренностей указать. а ещё лучше юзать pydantic модели вместо дикта |
||
| 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. длиннющая строка, можно url в отдельную переменную выше записать |
||
| 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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ты мог сделать методы post, get и тд вместо explain, save и тд, и вынести их в общий родительский класс |
||
| r = requests.post(self.url, headers=self.headers, json={"prompt": text}, timeout=15) | ||
| r.raise_for_status() | ||
| return r.json()["result"]["response"] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. коннекти эндпоинты к роутеру , а не к приложению фастапи |
||
| def get_tasks(): | ||
| pass | ||
| def get_tasks() -> list[dict]: | ||
| return storage.load() | ||
|
|
||
| @app.post("/tasks") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. нужно указывать response_model= и это должна быть pydantic модель |
||
| 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 не найден") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| Хранение во внутреней памяти | ||
| + легкая реализация | ||
| - отваливаются данные при перезапуске | ||
| Хранение в json | ||
| + задачи сохраняются на диск и не пропадают | ||
| - все еще stateful плюс проблемы с одновременным сохранением и тд | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. не stateful. когда состояние хранится во внешнем хранилище, это уже стейтлесс |
||
| Убрал ли я хранение состояния | ||
| Нет, я только перенес его из РАМ на диск | ||
| Варианты хранения | ||
| -Текстовые файлы (тхт, json, cvs) проблемы остаются стандартными для хранения на внутреней памяти | ||
| -Облачные сервисы, проблема гонки не исчезает но уже работает stateles подход | ||
| -Базы данных, вобще все круто все работает проблем никаких нет доллар по 30 и вкусное мороженное там же(я ничего не знаю про БД) | ||
| Хранени | ||
| Состояние гонки | ||
| Это ситуация, когда два клиента одновременно читают старое состояние и перезаписывают друг друга. - это термин из интернета | ||
| Как я понял: состояние гонки это когда один файл берут два человека и пытаются с ним работать(не получится - файл то один). | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. получится, но там данные могут быть записаны не в том порядке либо какие-то могут отсутствовать либо несогласованно |
||
| Если я правильно понимаю БД в таких случая обладаюст инструментами типа копирования и последующей синхронизации со склеиванием двух версий в одну. | ||
| Что то типа контроля версий гитхаба что ли. | ||
| Как подсказвает интернет состояние гонки можно решить оптимистичной блокировкой и генерацией уникальных айди через UUID(ксати я сначала так и делал) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. нормас, но можно юзать starlette config либо pydantic base settings, так чище и моднее |
||
| CF_ACCOUNT_ID = os.getenv("CF_ACCOUNT_ID") | ||
| CF_MODEL = os.getenv("CF_MODEL", "@cf/meta/llama-3.1-8b-instruct") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. можно файл .env.example ещё пушить с пустыми переменными |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
latest образ кринж юзать, используй конкретную версию и по возможности слим