Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d4c7fce
feat: create conflicts
Nov 19, 2024
edcbec4
fix: main
Nov 21, 2024
1ece50e
feat: changes
Feb 27, 2025
c9d3378
fixed conflict
maloyparser Sep 22, 2025
0eba75a
Task1
maloyparser Oct 4, 2025
dc27349
Merge branch 'second-branch' of https://github.com/maloyparser/fastap…
maloyparser Oct 4, 2025
169e70f
task2 backend with state storage
maloyparser Oct 4, 2025
269a8a7
storage class with json
maloyparser Oct 4, 2025
dc06153
Add files via upload
maloyparser Oct 4, 2025
efd0bb4
storage gist add
maloyparser Oct 4, 2025
1f1627c
Merge branch 'second-branch' of https://github.com/maloyparser/fastap…
maloyparser Oct 4, 2025
073cc99
Delete simple_backend/src/task_tracker/.env
maloyparser Oct 4, 2025
cafa301
Delete simple_backend/src/task_tracker/tasks.json
maloyparser Oct 4, 2025
128e409
Delete simple_backend/src/task_tracker/main.py
maloyparser Oct 4, 2025
d77a82e
Delete simple_backend/src/task_tracker/storage.py
maloyparser Oct 4, 2025
712ad57
Delete simple_backend/src/task_tracker/storage_gist.py
maloyparser Oct 4, 2025
2b800b3
del
maloyparser Oct 4, 2025
cd764d6
Merge branch 'second-branch' of https://github.com/maloyparser/fastap…
maloyparser Oct 4, 2025
35e423d
storage github gist add
maloyparser Oct 4, 2025
f6f5ead
Create README.md
maloyparser Oct 4, 2025
7eb183a
Update README.md
maloyparser Oct 4, 2025
443cd77
cludflare add
maloyparser Oct 4, 2025
26f94bc
Merge branch 'second-branch' of https://github.com/maloyparser/fastap…
maloyparser Oct 4, 2025
4943856
BaseHTTPClient add
maloyparser Oct 5, 2025
5dce8a4
add workflows
maloyparser Oct 9, 2025
4584deb
correct smth
maloyparser Oct 9, 2025
ec3de8d
Update ci.yml
maloyparser Oct 9, 2025
e291024
Update ci.yml
maloyparser Oct 9, 2025
f79221f
Create .ruff.toml
maloyparser Oct 9, 2025
46ef92e
Update .ruff.toml
maloyparser Oct 9, 2025
c152ca9
fix giststorage, put now works
maloyparser Oct 9, 2025
a6f9490
Merge branch 'second-branch' of https://github.com/maloyparser/fastap…
maloyparser Oct 9, 2025
6f9945f
fix gist storage
maloyparser Oct 9, 2025
9011e89
del comment httpclient
maloyparser Oct 9, 2025
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
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

Choose a reason for hiding this comment

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

название кнш не очень, на работе осмысленно называй


on:
push:
branches: [ main, second-branch ]
pull_request:
branches: [ main, second-branch ]

jobs:
lint:
runs-on: ubuntu-24.04

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12.3"

- name: Cache pip
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-ruff-v1

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff

Choose a reason for hiding this comment

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

можно ещё закэшировать установку ruff. и лучше указывать точную версию

- name: Auto-fix code with Ruff
run: |
ruff check . --fix --line-length 88 || true

Choose a reason for hiding this comment

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

мы в ci не запускаем фиксы через линтеры, потому что они так коммитом не фиксируются. запускаем только ruff check и ruff fix --check (чисто проверки без форматирования). Команды с форматированием можно в пре-коммите запустить

- name: Run Ruff linter
run: |
ruff check . --line-length 88
10 changes: 10 additions & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
line-length = 88
select = ["E", "F"]
ignore = ["F401", "W391", "E501"]
exclude = [
"venv",
"migrations",
".git",
"__pycache__",
"node_modules"
]
1 change: 1 addition & 0 deletions git/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def main():
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)
Expand Down
34 changes: 34 additions & 0 deletions simple_backend/src/task_tracker/BaseHTTPClient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import requests
from abc import ABC, abstractmethod

class BaseHTTPClient(ABC):

def __init__(self, base_url: str, headers: dict = None):

Choose a reason for hiding this comment

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

dict[Any, Any]

self.base_url = base_url.rstrip("/")
self.headers = headers or {}

def _get(self, endpoint: str = "", **kwargs):

Choose a reason for hiding this comment

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

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

url = f"{self.base_url}{endpoint}"
print(f"[GET] {url}")

Choose a reason for hiding this comment

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

никаких print. только logging или loguru

resp = requests.get(url, headers=self.headers, timeout=15, **kwargs)
resp.raise_for_status()
return resp.json()

def _post(self, endpoint: str = "", json_data=None, **kwargs):
url = f"{self.base_url}{endpoint}"
print(f"[POST] {url} | DATA: {json_data}")
resp = requests.post(url, headers=self.headers, json=json_data, timeout=15, **kwargs)
resp.raise_for_status()
return resp.json()

def _patch(self, endpoint: str = "", json_data=None, **kwargs):
url = f"{self.base_url}{endpoint}"
print(f"[PATCH] {url} | DATA: {json_data}")
resp = requests.patch(url, headers=self.headers, json=json_data, timeout=15, **kwargs)
resp.raise_for_status()
return resp.json()

@abstractmethod
def _make_request(self, *args, **kwargs):
pass

30 changes: 30 additions & 0 deletions simple_backend/src/task_tracker/Cloudflare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import os
import requests
from dotenv import load_dotenv
from BaseHTTPClient import BaseHTTPClient
load_dotenv()
class Cloudflare(BaseHTTPClient):
def __init__(self, api_key: str = None, acc_id: str = None):
self.api_key = api_key or os.getenv("CLOUDFLARE_API_KEY")
self.acc_id = acc_id or os.getenv("CLOUDFLARE_ACCOUNT_ID")

Choose a reason for hiding this comment

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

переменные окружения тянем в отдельном файле config.py и импортируем оттуда. для получения переменных можно тянуть их через starlette emvironment, а сами настройки хранить в pydantic base settings

base_url = f"https://api.cloudflare.com/client/v4/accounts/{self.acc_id}/ai/run/@cf/meta/llama-3-8b-instruct"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
super().__init__(base_url=base_url, headers=headers)
def _make_request(self, text: str):

Choose a reason for hiding this comment

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

не типизируешь возвращаемые значения

payload = {
"messages": [
{"role": "system", "content": "You are a helpful assistant that explains tasks clearly."},
{"role": "user", "content": text}
]
}
return self._post(json_data=payload)
def get_llm_response(self, text:str) -> str:
try:
data = self._make_request(text)
return data.get('result', {}).get('response', "No response")
except Exception as e:

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

print("Error getting LLM response:", e)
return "Error: wrong API answer"
26 changes: 26 additions & 0 deletions simple_backend/src/task_tracker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Хранение состояния

+ 1.Минусы метода хранения в оперативной памяти в том, что доступ к данным есть только во время одной сессии ,
после перезагрузки приложения, доступа к данным уже не будет.
+ 2.Отсутствие масштабируемости.
+ 3.Нет истории изменений
-----------------------------------------------------------------------------------------------------------------------------
Переход на json

+ 1.После того, как список оперативной памяти изменился на файл проекта, мы получаем доступ к данным хранилища даже после перезапуска приложения, данные не исчезают, а остаются в списке
+ 2.Можно открыть файл и редактировать его вручную
+ 3.Так как json файл является локальным, то при создании копий приложения, у них будут разные файлы tasks.json

Choose a reason for hiding this comment

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

к файлу может быть конкурентный доступ

-----------------------------------------------------------------------------------------------------------------------------
Хранилища задач

Задачи можно хранить в облачных хранилищах:

+ Плюсы: Доступ откуда угодно
+ Минусы: Обдачные хранилища зависимы от сторонних ресурсов
Задачи можно хранить в баазах данныхЖ

+ Плюсы: Поддержка транзакций, масштабирование и надежность
+ Минусы: Сложная настройка
-----------------------------------------------------------------------------------------------------------------------------
Состояние гонки, это когда два разработчика делают одно и то же в коде и возникает конфликт(данные одного разработчика перезаписывают данные другого.

Choose a reason for hiding this comment

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

не два разработчика в коде что-то делают, а два процесса/потока/клиента одновременно обращаются к одним данным

Если перенести хранение на базу данных , то решится проблема с гонками, плюс данные лучше защищены и легче масштабировать
47 changes: 35 additions & 12 deletions simple_backend/src/task_tracker/main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
from fastapi import FastAPI

'''backend'''
from fastapi import FastAPI, HTTPException
from storage_gist import GistStorage
from Cloudflare import Cloudflare
app = FastAPI()

storage = GistStorage()
llm = Cloudflare()
# return tasks list
@app.get("/tasks")
def get_tasks():
pass

return storage.load()
#create a new task with id, name, and status
@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 модель. вообще вся работа в коде и особенно в эндпоинтах должна производиться через pydantic модели. они везде принимаются и возвращаются, никаких json и dict

def create_task(task):
pass

def create_task(name: str, condition: str = 'new'):
tasks = storage.load()
new_id = len(tasks) + 1
llm_response = llm.get_llm_response(name)
task = {'id': new_id, 'name': name, 'condition': condition, 'description': f'LLM Suggestion: {llm_response}'}
tasks.append(task)
storage.save(tasks)
return task
#update task
@app.put("/tasks/{task_id}")
def update_task(task_id: int):
pass

def update_task(task_id: int, name: str, condition: str):
tasks = storage.load()
for task in tasks:
if task['id'] == task_id:
task['name'] = name
task['condition'] = condition
storage.save(tasks)
return task
raise HTTPException(status_code = 404, detail = 'task not found')
#delete task
@app.delete("/tasks/{task_id}")
def delete_task(task_id: int):
pass
tasks = storage.load()
for task in tasks:
if task['id'] == task_id:
tasks.remove(task)
storage.save(tasks)
return 'task deleted'
raise HTTPException(status_code = 404, detail = 'task not found')
13 changes: 13 additions & 0 deletions simple_backend/src/task_tracker/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import json
import os
class TaskStorage:
def __init__(self, filename: str = 'tasks.json'):
self.file = filename
if not os.path.exists(self.file): # if file does not exist, create file with empty list
self.save([])
def load(self):
with open(self.file, 'r', encoding = 'utf-8') as f:
return json.load(f)
def save(self, tasks):

Choose a reason for hiding this comment

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

типизацию куда потерял

with open(self.file, 'w', encoding = 'utf-8') as f:
json.dump(tasks, f)
44 changes: 44 additions & 0 deletions simple_backend/src/task_tracker/storage_gist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import os
import requests
import json
from dotenv import load_dotenv
from BaseHTTPClient import BaseHTTPClient
load_dotenv()
GITHUB_API = 'https://api.github.com'
class GistStorage(BaseHTTPClient):
def __init__(self, token: str = None, gist_id: str = None, filename: str = 'tasks.json'):
self.token: str = token or os.getenv('GITHUB_TOKEN')
self.gist_id: str = gist_id or os.getenv('GIST_ID')
self.filename: str = filename
base_url = f"{GITHUB_API}/gists/{self.gist_id}"
if not self.token or not self.gist_id:
raise RuntimeError('GITHUB_TOKEN and GIST_ID must be given')
headers: dict = {
'Authorization': f'token {self.token}',
'Accept': 'application/vnd.github+json'
}
super().__init__(base_url=base_url, headers=headers)
def _make_request(self, *args, **kwargs):

Choose a reason for hiding this comment

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

нарушение I из SOLID

pass
# loading task list from gist
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 надо возвращать

Choose a reason for hiding this comment

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

ну и не только тут, а вообще везде где dict юзаешь

gist_data = self._get()
files = gist_data.get("files", {})
if self.filename not in files:
return []
try:
content = files[self.filename]["content"]
data = json.loads(content)
return data.get("tasks", [])
except json.JSONDecodeError:
return []
# saving task list on gist
def save(self, tasks: list[dict]) -> None:
body: dict = {
'files': {
self.filename: {
'content': json.dumps({'tasks': tasks})
}
}
}
self._patch(json_data=body)
1 change: 1 addition & 0 deletions simple_backend/src/task_tracker/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"id": 1, "name": "finaltask", "condition": "updated"}, {"id": 2, "name": "totaltask", "condition": "updated"}, {"id": 3, "name": "Task3", "condition": "new"}]

Choose a reason for hiding this comment

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

лучше не пушить этот файл в гит. добавь в gitignore