Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions simple_backend/src/task_tracker/clients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import requests


class CloudFlareClient:
def __init__(self, api_token, account_id):
self.api_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/"
self.headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
self.inputs = [

Choose a reason for hiding this comment

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

это можно просто не в init написать, а в классе над методом, потому что это все равно одинаково для всех объектов

{"role": "system", "content":
"Отвечай по-русски, кратко (1–2 предложения),"
"как лучше выполнить задачу из списка дел. Без приветствий и воды."
"Отвечай естественно, без вводных фраз вроде «Для выполнения задачи…», «Чтобы сделать…», «Следует…»."
"Сразу переходи к сути,"
"Если в задаче есть действие, пиши как"
" будто даёшь понятную инструкцию или совет, а не академическое объяснение"}
]

def generate_answer(self,task):
data = {"messages": self.inputs + [{"role": "user", "content": task}]}

Choose a reason for hiding this comment

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

в идеале такое в pydantic модель перегонять сразу после получения http ответа

response = requests.post(f"{self.api_url}@cf/meta/llama-3-8b-instruct", headers=self.headers, json=data)


if response.status_code != 200:

Choose a reason for hiding this comment

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

  1. Нужно поднимать ошибку через метод raise_for_status
  2. Успешный код ответа это не только 200, но и любой от 200 до 299
  3. print в проектах не искользуют, надо logging или loguru
  4. Если случилась какая-то ошибка её просто всегда прокидываем на самый верх и ловим в fastapi через exception handler

print("Ошибка запроса:", response.status_code, response.text)
return None

result = response.json()
return result.get("result", {}).get("response", "⚠️ Нет ответа в JSON")

Choose a reason for hiding this comment

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

тут опять же ошибку надо кидать если ниче нет, а не рандомный текст

38 changes: 31 additions & 7 deletions simple_backend/src/task_tracker/main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,43 @@
from fastapi import FastAPI
from fastapi import FastAPI, Body
from storage import JSONStorage, CloudJSONStorage
from models import Task, TaskCreate
import os
from dotenv import load_dotenv
from clients import CloudFlareClient

app = FastAPI()

load_dotenv()
BIN_ID = os.getenv('BIN_ID')
MASTER_KEY = os.getenv('MASTER_KEY')
storage = CloudJSONStorage(bin_id=BIN_ID, master_key=MASTER_KEY)

API_TOKEN_AI = os.getenv('API_TOKEN_AI')

Choose a reason for hiding this comment

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

  1. Вместо os щас моднее использовать starlette.config для работы с переменными окружения и pydantic.base_settings для класса конфига

Choose a reason for hiding this comment

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

Также все переменные в отдельном файле cpnfig.py пиши

ACCOUNT_ID_AI = os.getenv('ACCOUNT_ID_AI')
client_ai = CloudFlareClient(API_TOKEN_AI, ACCOUNT_ID_AI)


@app.get("/tasks")
def get_tasks():
pass
return storage.get_task()

@app.post("/tasks")
def create_task(task):
pass
def create_task(task_data: TaskCreate):
ai_reply = client_ai.generate_answer(task_data.title)
task_data.title = f"{task_data.title}{ai_reply}"
task = storage.create_task(task_data.model_dump())
return task

@app.put("/tasks/{task_id}")
def update_task(task_id: int):
pass
def update_task(task_id: int, task_data: TaskCreate = Body(...)):

Choose a reason for hiding this comment

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

Вместо Body и прочей шляпы от фастапи используй pydantic модели

ai_reply = client_ai.generate_answer(task_data.title)
task_data.title = f"{task_data.title}{ai_reply}"
is_update = storage.update_task(task_id, task_data.model_dump())
if is_update: return is_update
return None

@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
pass
is_delete = storage.delete_task(task_id)
if is_delete: return True
Copy link

@LilChichaaa LilChichaaa Nov 9, 2025

Choose a reason for hiding this comment

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

опять же, никаких true false, просто выкидывай ошибку в storage.delete_task, если не получилось что-то. мы не на языке go пишем, где исключения не опрокидываются. внешняя функция должна знать как можно меньше о той, которую использует внутри себя

return None
8 changes: 8 additions & 0 deletions simple_backend/src/task_tracker/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from pydantic import BaseModel

class TaskCreate(BaseModel):
title: str

Choose a reason for hiding this comment

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

нету параметров валидации никаких (например минимальная длина строки)

status: bool

class Task(TaskCreate):
id: int

Choose a reason for hiding this comment

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

ТЗ внимательно читай - У задачи должны быть параметры: id, название, статус задачи.

Binary file modified simple_backend/src/task_tracker/requirements.txt
Binary file not shown.
105 changes: 105 additions & 0 deletions simple_backend/src/task_tracker/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import json
from pathlib import Path
import requests

class IsMemoryStorage:

def __init__(self):
self.tasks = []

Choose a reason for hiding this comment

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

вообще можно было бы ещё общий интерфейс Storage сделать кстати

self.next_task = 1


def list_tasks(self):

Choose a reason for hiding this comment

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

нигде не типизируешь, что возвращается из функции

return self.tasks

def create_task(self, task_data):
task = {'id': self.next_task, **task_data}

Choose a reason for hiding this comment

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

pydantic возвращай, никаких диктов

self.tasks.append(task)
self.next_task += 1

return task

def get_task(self, task_id):
for task in self.tasks:
if task['id'] == task_id:
return task
return None

def update_task(self, task_id:int, task_data):
for task in self.tasks:
if task['id'] == task_id:
task.update(task_data)
return task
return None


def delete_task(self, task_id):
task = self.get_task(task_id)
if not task: return False
self.tasks.remove(task)
return True


class JSONStorage:
def __init__(self, filepath: str | None = "tasks.json"):
self.file = Path(filepath)
if not self.file.exists():
self.file.write_text("[]", encoding="utf-8")

def _load(self):
with self.file.open("r", encoding="utf-8") as f:
return json.load(f)

def _save(self, data):
with self.file.open("w", encoding="utf-8") as f:
json.dump(data, f, indent=4, ensure_ascii=False)

def get_task(self):
return self._load()

def create_task(self, task_data: dict):
tasks = self._load()
new_id = max([t["id"] for t in tasks], default=0) + 1
task = {"id": new_id, **task_data}
tasks.append(task)
self._save(tasks)
return task

def update_task(self, task_id: int, new_data: dict):
tasks = self._load()
for task in tasks:
if task["id"] == task_id:
task.update(new_data)
self._save(tasks)
return task
return None

def delete_task(self, task_id: int):
tasks = self._load()
for task in tasks:
if task["id"] == task_id:
tasks.remove(task)
self._save(tasks)
return task
return None

class CloudJSONStorage(JSONStorage):
def __init__(self, bin_id: str, master_key: str):
super().__init__(filepath="tasks.json")
self.base_url = f"https://api.jsonbin.io/v3/b/{bin_id}"
self.headers = {
"X-Master-Key": master_key,
"Content-Type": "application/json",
}

def _load(self):
res = requests.get(f"{self.base_url}/latest", headers=self.headers)
res.raise_for_status()
record = res.json()["record"]
return record.get("tasks", [])

def _save(self, tasks):
payload = {"tasks": tasks}
res = requests.put(self.base_url, headers=self.headers, json=payload)
res.raise_for_status()
return res.json()