diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..b72adceb --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 + + - name: Auto-fix code with Ruff + run: | + ruff check . --fix --line-length 88 || true + + - name: Run Ruff linter + run: | + ruff check . --line-length 88 diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..aa31313b --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,10 @@ +line-length = 88 +select = ["E", "F"] +ignore = ["F401", "W391", "E501"] +exclude = [ + "venv", + "migrations", + ".git", + "__pycache__", + "node_modules" +] diff --git a/git/src/main.py b/git/src/main.py index 1822c7e9..704ed1cf 100644 --- a/git/src/main.py +++ b/git/src/main.py @@ -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) diff --git a/simple_backend/src/task_tracker/BaseHTTPClient.py b/simple_backend/src/task_tracker/BaseHTTPClient.py new file mode 100644 index 00000000..d5f328e2 --- /dev/null +++ b/simple_backend/src/task_tracker/BaseHTTPClient.py @@ -0,0 +1,34 @@ +import requests +from abc import ABC, abstractmethod + +class BaseHTTPClient(ABC): + + def __init__(self, base_url: str, headers: dict = None): + self.base_url = base_url.rstrip("/") + self.headers = headers or {} + + def _get(self, endpoint: str = "", **kwargs): + url = f"{self.base_url}{endpoint}" + print(f"[GET] {url}") + 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 + \ No newline at end of file diff --git a/simple_backend/src/task_tracker/Cloudflare.py b/simple_backend/src/task_tracker/Cloudflare.py new file mode 100644 index 00000000..5bea74e0 --- /dev/null +++ b/simple_backend/src/task_tracker/Cloudflare.py @@ -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") + 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): + 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: + print("Error getting LLM response:", e) + return "Error: wrong API answer" \ No newline at end of file diff --git a/simple_backend/src/task_tracker/README.md b/simple_backend/src/task_tracker/README.md new file mode 100644 index 00000000..765f273c --- /dev/null +++ b/simple_backend/src/task_tracker/README.md @@ -0,0 +1,26 @@ +Хранение состояния + ++ 1.Минусы метода хранения в оперативной памяти в том, что доступ к данным есть только во время одной сессии , + после перезагрузки приложения, доступа к данным уже не будет. ++ 2.Отсутствие масштабируемости. ++ 3.Нет истории изменений +----------------------------------------------------------------------------------------------------------------------------- +Переход на json + ++ 1.После того, как список оперативной памяти изменился на файл проекта, мы получаем доступ к данным хранилища даже после перезапуска приложения, данные не исчезают, а остаются в списке ++ 2.Можно открыть файл и редактировать его вручную ++ 3.Так как json файл является локальным, то при создании копий приложения, у них будут разные файлы tasks.json +----------------------------------------------------------------------------------------------------------------------------- +Хранилища задач + +Задачи можно хранить в облачных хранилищах: + ++ Плюсы: Доступ откуда угодно ++ Минусы: Обдачные хранилища зависимы от сторонних ресурсов +Задачи можно хранить в баазах данныхЖ + ++ Плюсы: Поддержка транзакций, масштабирование и надежность ++ Минусы: Сложная настройка +----------------------------------------------------------------------------------------------------------------------------- +Состояние гонки, это когда два разработчика делают одно и то же в коде и возникает конфликт(данные одного разработчика перезаписывают данные другого. +Если перенести хранение на базу данных , то решится проблема с гонками, плюс данные лучше защищены и легче масштабировать diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 3db98d0d..cd272e3f 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -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") -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') \ No newline at end of file diff --git a/simple_backend/src/task_tracker/storage.py b/simple_backend/src/task_tracker/storage.py new file mode 100644 index 00000000..ce237692 --- /dev/null +++ b/simple_backend/src/task_tracker/storage.py @@ -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): + with open(self.file, 'w', encoding = 'utf-8') as f: + json.dump(tasks, f) \ No newline at end of file diff --git a/simple_backend/src/task_tracker/storage_gist.py b/simple_backend/src/task_tracker/storage_gist.py new file mode 100644 index 00000000..34c9972a --- /dev/null +++ b/simple_backend/src/task_tracker/storage_gist.py @@ -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): + pass + # loading task list from gist + def load(self) -> list[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) \ No newline at end of file diff --git a/simple_backend/src/task_tracker/tasks.json b/simple_backend/src/task_tracker/tasks.json new file mode 100644 index 00000000..80c933c8 --- /dev/null +++ b/simple_backend/src/task_tracker/tasks.json @@ -0,0 +1 @@ +[{"id": 1, "name": "finaltask", "condition": "updated"}, {"id": 2, "name": "totaltask", "condition": "updated"}, {"id": 3, "name": "Task3", "condition": "new"}] \ No newline at end of file