Skip to content
Open
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

latest образ кринж юзать, используй конкретную версию и по возможности слим

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install Ruff
run: pip install ruff

Choose a reason for hiding this comment

The 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
57 changes: 36 additions & 21 deletions git/src/main.py
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 []

Choose a reason for hiding this comment

The 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):
"""
Возвращает строку со списком всех книг.
Expand All @@ -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):
"""
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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()
50 changes: 50 additions & 0 deletions simple_backend/src/task_tracker/classes.py
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"

Choose a reason for hiding this comment

The 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]:

Choose a reason for hiding this comment

The 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)

Choose a reason for hiding this comment

The 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:

Choose a reason for hiding this comment

The 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"]

Empty file.
57 changes: 50 additions & 7 deletions simple_backend/src/task_tracker/main.py
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")

Choose a reason for hiding this comment

The 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")

Choose a reason for hiding this comment

The 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 не найден")
19 changes: 19 additions & 0 deletions simple_backend/src/task_tracker/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Хранение во внутреней памяти
+ легкая реализация
- отваливаются данные при перезапуске
Хранение в json
+ задачи сохраняются на диск и не пропадают
- все еще stateful плюс проблемы с одновременным сохранением и тд

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

не stateful. когда состояние хранится во внешнем хранилище, это уже стейтлесс

Убрал ли я хранение состояния
Нет, я только перенес его из РАМ на диск
Варианты хранения
-Текстовые файлы (тхт, json, cvs) проблемы остаются стандартными для хранения на внутреней памяти
-Облачные сервисы, проблема гонки не исчезает но уже работает stateles подход
-Базы данных, вобще все круто все работает проблем никаких нет доллар по 30 и вкусное мороженное там же(я ничего не знаю про БД)
Хранени
Состояние гонки
Это ситуация, когда два клиента одновременно читают старое состояние и перезаписывают друг друга. - это термин из интернета
Как я понял: состояние гонки это когда один файл берут два человека и пытаются с ним работать(не получится - файл то один).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

получится, но там данные могут быть записаны не в том порядке либо какие-то могут отсутствовать либо несогласованно

Если я правильно понимаю БД в таких случая обладаюст инструментами типа копирования и последующей синхронизации со склеиванием двух версий в одну.
Что то типа контроля версий гитхаба что ли.
Как подсказвает интернет состояние гонки можно решить оптимистичной блокировкой и генерацией уникальных айди через UUID(ксати я сначала так и делал)
9 changes: 9 additions & 0 deletions simple_backend/src/task_tracker/settings.py
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")

Choose a reason for hiding this comment

The 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")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

можно файл .env.example ещё пушить с пустыми переменными