-
Notifications
You must be signed in to change notification settings - Fork 99
all tasks OOP #161
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?
all tasks OOP #161
Changes from all commits
d4c7fce
edcbec4
1ece50e
c9d3378
0eba75a
dc27349
169e70f
269a8a7
dc06153
efd0bb4
1f1627c
073cc99
cafa301
128e409
d77a82e
712ad57
2b800b3
cd764d6
35e423d
f6f5ead
7eb183a
443cd77
26f94bc
4943856
5dce8a4
4584deb
ec3de8d
e291024
f79221f
46ef92e
c152ca9
a6f9490
6f9945f
9011e89
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,39 @@ | ||
| name: CI | ||
|
|
||
| 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 | ||
|
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. можно ещё закэшировать установку ruff. и лучше указывать точную версию |
||
| - name: Auto-fix code with Ruff | ||
| run: | | ||
| ruff check . --fix --line-length 88 || true | ||
|
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. мы в ci не запускаем фиксы через линтеры, потому что они так коммитом не фиксируются. запускаем только ruff check и ruff fix --check (чисто проверки без форматирования). Команды с форматированием можно в пре-коммите запустить |
||
| - name: Run Ruff linter | ||
| run: | | ||
| ruff check . --line-length 88 | ||
| 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" | ||
| ] |
| 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): | ||
|
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. dict[Any, Any] |
||
| self.base_url = base_url.rstrip("/") | ||
| self.headers = headers or {} | ||
|
|
||
| def _get(self, endpoint: str = "", **kwargs): | ||
|
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 = f"{self.base_url}{endpoint}" | ||
| print(f"[GET] {url}") | ||
|
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. никаких 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 | ||
|
|
||
| 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") | ||
|
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. переменные окружения тянем в отдельном файле 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): | ||
|
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. не типизируешь возвращаемые значения |
||
| 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: | ||
|
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 |
||
| print("Error getting LLM response:", e) | ||
| return "Error: wrong API answer" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| Хранение состояния | ||
|
|
||
| + 1.Минусы метода хранения в оперативной памяти в том, что доступ к данным есть только во время одной сессии , | ||
| после перезагрузки приложения, доступа к данным уже не будет. | ||
| + 2.Отсутствие масштабируемости. | ||
| + 3.Нет истории изменений | ||
| ----------------------------------------------------------------------------------------------------------------------------- | ||
| Переход на json | ||
|
|
||
| + 1.После того, как список оперативной памяти изменился на файл проекта, мы получаем доступ к данным хранилища даже после перезапуска приложения, данные не исчезают, а остаются в списке | ||
| + 2.Можно открыть файл и редактировать его вручную | ||
| + 3.Так как json файл является локальным, то при создании копий приложения, у них будут разные файлы tasks.json | ||
|
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. к файлу может быть конкурентный доступ |
||
| ----------------------------------------------------------------------------------------------------------------------------- | ||
| Хранилища задач | ||
|
|
||
| Задачи можно хранить в облачных хранилищах: | ||
|
|
||
| + Плюсы: Доступ откуда угодно | ||
| + Минусы: Обдачные хранилища зависимы от сторонних ресурсов | ||
| Задачи можно хранить в баазах данныхЖ | ||
|
|
||
| + Плюсы: Поддержка транзакций, масштабирование и надежность | ||
| + Минусы: Сложная настройка | ||
| ----------------------------------------------------------------------------------------------------------------------------- | ||
| Состояние гонки, это когда два разработчика делают одно и то же в коде и возникает конфликт(данные одного разработчика перезаписывают данные другого. | ||
|
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. не два разработчика в коде что-то делают, а два процесса/потока/клиента одновременно обращаются к одним данным |
||
| Если перенести хранение на базу данных , то решится проблема с гонками, плюс данные лучше защищены и легче масштабировать | ||
| 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") | ||
|
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 модель. вообще вся работа в коде и особенно в эндпоинтах должна производиться через 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') | ||
| 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): | ||
|
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. типизацию куда потерял |
||
| with open(self.file, 'w', encoding = 'utf-8') as f: | ||
| json.dump(tasks, f) | ||
| 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): | ||
|
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. нарушение I из SOLID |
||
| pass | ||
| # loading task list from gist | ||
| 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 надо возвращать 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. ну и не только тут, а вообще везде где 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) | ||
| 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"}] | ||
|
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. лучше не пушить этот файл в гит. добавь в gitignore |
||
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.
название кнш не очень, на работе осмысленно называй