diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..4e1d476d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + + - name: Run ruff linter + run: ruff check . + + - name: Run ruff formatter check + run: ruff format --check . diff --git a/git/src/main.py b/git/src/main.py index 1822c7e9..0a9b5ffc 100644 --- a/git/src/main.py +++ b/git/src/main.py @@ -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 [] -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): """ Возвращает строку со списком всех книг. @@ -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): """ @@ -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 @@ -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() @@ -97,9 +104,11 @@ def main(): 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): @@ -109,9 +118,11 @@ def main(): 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Найденные книги:") @@ -119,12 +130,13 @@ def main(): else: print("Ничего не найдено.") - elif choice == '5': + elif choice == "5": print("Выход из программы.") break else: print("Некорректный ввод. Попробуйте ещё раз.") + if __name__ == "__main__": main() diff --git a/simple_backend/orders.py b/simple_backend/orders.py index 54daeee7..5e83bdbf 100644 --- a/simple_backend/orders.py +++ b/simple_backend/orders.py @@ -1,4 +1,3 @@ - class Order: TAX_RATE = 0.08 # 8% налог SERVICE_CHARGE = 0.05 # 5% сервисный сбор @@ -6,13 +5,13 @@ class Order: def __init__(self, customer): self.customer = customer self.dishes = [] - + def add_dish(self, dish): if isinstance(dish, Dish): self.dishes.append(dish) else: raise ValueError("Можно добавлять только объекты класса Dish.") - + def remove_dish(self, dish): if dish in self.dishes: self.dishes.remove(dish) @@ -22,7 +21,6 @@ def remove_dish(self, dish): def calculate_total(self): return sum(dish.price for dish in self.dishes) - def final_total(self): total_after_discount = self.apply_discount() total_with_tax = total_after_discount * (1 + Order.TAX_RATE) @@ -53,7 +51,8 @@ def __str__(self): customer_list = ", ".join([customer.name for customer in self.customers]) dish_list = "\n".join([str(dish) for dish in self.dishes]) return f"Group Order for {customer_list}:\n{dish_list}\nTotal: ${self.final_total():.2f}" - + + class Dish: def __init__(self, name, price, category): self.name = name @@ -63,6 +62,7 @@ def __init__(self, name, price, category): def __str__(self): return f"Dish: {self.name}, Category: {self.category}, Price: ${self.price:.2f}" + class Customer: def __init__(self, name, membership="Regular"): self.name = name @@ -75,6 +75,8 @@ def get_discount(self): def __str__(self): return f"Customer: {self.name}, Membership: {self.membership}" + + # Пример использования # Создаем блюда @@ -101,4 +103,4 @@ def __str__(self): group_order.add_dish(coffee) print(group_order) # Вывод информации о групповом заказе -print(f"Split Bill: ${group_order.split_bill():.2f} per person") # Стоимость на каждого \ No newline at end of file +print(f"Split Bill: ${group_order.split_bill():.2f} per person") # Стоимость на каждого diff --git a/simple_backend/src/task_tracker/README.md b/simple_backend/src/task_tracker/README.md new file mode 100644 index 00000000..41744502 --- /dev/null +++ b/simple_backend/src/task_tracker/README.md @@ -0,0 +1,80 @@ +# Task Tracker API + +API для управления задачами на FastAPI. + +## Как запустить + +1. Активировать виртуальное окружение: +``` +venv\Scripts\activate +``` + +2. Установить зависимости: +``` +pip install -r requirements.txt +``` + +3. Запустить сервер: +``` +uvicorn main:app --reload +``` + +4. Открыть http://127.0.0.1:8000/docs + +## Что умеет API + +- GET /tasks - получить все задачи +- POST /tasks - создать задачу +- PUT /tasks/{id} - обновить задачу +- DELETE /tasks/{id} - удалить задачу + +## Про хранение данных + +### Хранение в памяти (сейчас) + +Что такое хранение состояния - это когда сервер запоминает информацию между запросами. У нас список задач хранится в переменной tasks_db. + +Проблемы с хранением в памяти: +- При перезапуске сервера все задачи пропадают +- Нельзя запустить несколько серверов одновременно +- Если данных много, может не хватить памяти +- При сбое все данные теряются + +### Хранение в файле + +Когда мы переделали на хранение в файле, стало лучше: +- Задачи не пропадают при перезапуске +- Можно сделать резервную копию файла +- Легко посмотреть данные в файле + +Но проблемы остались: +- Файл все равно лежит на одном сервере +- Если запустить несколько серверов, у каждого будет свой файл + +### Другие способы хранения + +База данных - хорошо масштабируется, но сложно настроить +Облачные сервисы - доступно из любого места, но нужен интернет +Redis - очень быстро, но дорого по памяти +Файлы - просто, но не масштабируется + +### Stateless сервер + +Когда мы используем внешний сервис (jsonbin.io), сервер становится stateless: +- Сервер не хранит данные у себя +- Все данные в облаке +- Можно запустить много серверов + +### Состояние гонки + +Это когда несколько запросов одновременно меняют одни и те же данные, и получается путаница. + +Проблемы в нашем API: +- Два человека могут создать задачи с одинаковым ID +- Один может удалить задачу пока другой ее читает +- При одновременном обновлении изменения могут потеряться + +Как исправить: +- Использовать блокировки +- Использовать базу данных с транзакциями +- Обрабатывать запросы по очереди \ No newline at end of file diff --git a/simple_backend/src/task_tracker/base_client.py b/simple_backend/src/task_tracker/base_client.py new file mode 100644 index 00000000..eafd2bea --- /dev/null +++ b/simple_backend/src/task_tracker/base_client.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod +import requests +from typing import Any, Dict, Optional + +class BaseHTTPClient(ABC): + """Базовый класс для HTTP клиентов с общими функциями""" + + def __init__(self, base_url: str): + self.base_url = base_url + + def _make_request(self, method: str, endpoint: str, **kwargs) -> Optional[Dict[Any, Any]]: + + """Общий метод для выполнения HTTP запросов""" + url = f"{self.base_url}{endpoint}" + + try: + response = requests.request(method, url, **kwargs) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + print(f"Ошибка HTTP запроса: {e}") + return None + except Exception as e: + print(f"Неожиданная ошибка: {e}") + return None + + @abstractmethod + def load_tasks(self): + """Абстрактный метод для загрузки данных""" + pass + + @abstractmethod + def save_tasks(self, data): + """Абстрактный метод для сохранения данных""" + pass \ No newline at end of file diff --git a/simple_backend/src/task_tracker/cloud_storage.py b/simple_backend/src/task_tracker/cloud_storage.py new file mode 100644 index 00000000..8ac7907a --- /dev/null +++ b/simple_backend/src/task_tracker/cloud_storage.py @@ -0,0 +1,39 @@ +# cloud_storage.py +from typing import List, Dict, Any +from base_client import BaseHTTPClient + +class CloudStorage(BaseHTTPClient): + + def __init__(self, bin_id: str, api_key: str): + self.bin_id = bin_id + self.api_key = api_key + base_url = "https://api.jsonbin.io/v3" + super().__init__(base_url) + + self.headers = { + "X-Master-Key": api_key, + "Content-Type": "application/json" + } + + def load_tasks(self) -> List[Dict[str, Any]]: + + endpoint = f"/b/{self.bin_id}" + kwargs = {"headers": self.headers} + + response = self._make_request("GET", endpoint, **kwargs) + + if response: + data = response.get("record", []) + return data if isinstance(data, list) else [] + return [] + + def save_tasks(self, tasks: List[Dict[str, Any]]) -> bool: + + endpoint = f"/b/{self.bin_id}" + kwargs = { + "headers": self.headers, + "json": tasks + } + + response = self._make_request("PUT", endpoint, **kwargs) + return response is not None \ No newline at end of file diff --git a/simple_backend/src/task_tracker/llm_client.py b/simple_backend/src/task_tracker/llm_client.py new file mode 100644 index 00000000..fef5a640 --- /dev/null +++ b/simple_backend/src/task_tracker/llm_client.py @@ -0,0 +1,48 @@ +from typing import Optional +from base_client import BaseHTTPClient + +class CloudflareLLMClient(BaseHTTPClient): + """Клиент для работы с Cloudflare Workers AI API""" + + def __init__(self, api_token: str, account_id: str): + self.api_token = api_token + self.account_id = account_id + base_url = "https://api.cloudflare.com/client/v4" + super().__init__(base_url) + + self.headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json" + } + # Исправлено: используем self.account_id + self.model_endpoint = f"/accounts/{self.account_id}/ai/run/@cf/meta/llama-3.1-8b-instruct" + + def load_tasks(self): + """ + Этот метод не используется для LLM, но должен быть реализован + из-за абстрактного базового класса + """ + raise NotImplementedError("LLM клиент не поддерживает загрузку задач") + + def save_tasks(self, data): + """ + Этот метод не используется для LLM, но должен быть реализован + из-за абстрактного базового класса + """ + raise NotImplementedError("LLM клиент не поддерживает сохранение задач") + + def ask_llm(self, prompt: str) -> Optional[str]: + """ + Отправляет запрос к LLM Cloudflare и возвращает ответ + """ + kwargs = { + "headers": self.headers, + "json": {"prompt": prompt}, + "timeout": 30 + } + + response = self._make_request("POST", self.model_endpoint, **kwargs) + + if response: + return response.get("result", {}).get("response") + return None \ No newline at end of file diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 3db98d0d..7109589c 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,19 +1,87 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from typing import List +import os + +from storage import FileStorage + +from llm_client import CloudflareLLMClient app = FastAPI() -@app.get("/tasks") +storage = FileStorage() + +API_TOKEN = os.getenv("CLOUDFLARE_API_TOKEN") +ACCOUNT_ID = os.getenv("CLOUDFLARE_ACCOUNT_ID") + +try: + if API_TOKEN and ACCOUNT_ID: + llm_client = CloudflareLLMClient(API_TOKEN, ACCOUNT_ID) + llm_enabled = True + else: + llm_client = None + llm_enabled = False + print("Cloudflare API ключи не найдены. LLM функциональность отключена.") +except Exception as e: + llm_client = None + llm_enabled = False + print(f"Не удалось инициализировать LLM клиент: {e}") + +class Task(BaseModel): + id: str + title: str + status: str + +@app.get("/") +def read_root(): + return {"message": "Task Tracker API"} + +@app.get("/tasks", response_model=List[Task]) def get_tasks(): - pass + tasks_data = storage.load_tasks() + return [Task(**task) for task in tasks_data] -@app.post("/tasks") -def create_task(task): - pass +@app.post("/tasks", response_model=Task) +def create_task(task: Task): + if llm_enabled and llm_client: + prompt = f"Объясни, как решить следующую задачу: {task.title}. Предложи пошаговое решение." + + llm_response = llm_client.ask_llm(prompt) + + if llm_response: + task.title = f"{task.title}\n\nРешение от AI:\n{llm_response}" + else: + task.title = f"{task.title}\n\n(Не удалось получить решение от AI)" + else: + task.title = f"{task.title}\n\n(AI функциональность недоступна - проверьте настройки API)" + + tasks_data = storage.load_tasks() + task_dict = task.model_dump() + tasks_data.append(task_dict) + storage.save_tasks(tasks_data) + + return task -@app.put("/tasks/{task_id}") -def update_task(task_id: int): - pass +@app.put("/tasks/{task_id}", response_model=Task) +def update_task(task_id: str, updated_task: Task): + tasks_data = storage.load_tasks() + task_dict = updated_task.model_dump() + for i, task in enumerate(tasks_data): + if task["id"] == task_id: + tasks_data[i] = task_dict + storage.save_tasks(tasks_data) + return updated_task + raise HTTPException(status_code=404, detail="Task not found") @app.delete("/tasks/{task_id}") -def delete_task(task_id: int): - pass +def delete_task(task_id: str): + tasks_data = storage.load_tasks() + original_count = len(tasks_data) + tasks_data = [task for task in tasks_data if task["id"] != task_id] + + if len(tasks_data) == original_count: + raise HTTPException(status_code=404, detail="Task not found") + + storage.save_tasks(tasks_data) + return {"message": "Task deleted"} + diff --git a/simple_backend/src/task_tracker/requirements.txt b/simple_backend/src/task_tracker/requirements.txt index 8e0578a0..4bf6b8c4 100644 --- a/simple_backend/src/task_tracker/requirements.txt +++ b/simple_backend/src/task_tracker/requirements.txt @@ -1,2 +1,3 @@ fastapi -uvicorn[standard] \ No newline at end of file +uvicorn[standard] +requests \ 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..73a5df08 --- /dev/null +++ b/simple_backend/src/task_tracker/storage.py @@ -0,0 +1,40 @@ +import json +import os +from typing import List, Dict, Any +from base_client import BaseHTTPClient + +class FileStorage: + + def __init__(self, data_file: str = "tasks.json"): + self.data_file = data_file + + def load_tasks(self) -> List[Dict[str, Any]]: + if not os.path.exists(self.data_file): + return [] + try: + with open(self.data_file, 'r', encoding='utf-8') as f: + data = json.load(f) + return data if isinstance(data, list) else [] + except (json.JSONDecodeError, FileNotFoundError): + return [] + + def save_tasks(self, tasks: List[Dict[str, Any]]) -> bool: + try: + with open(self.data_file, 'w', encoding='utf-8') as f: + json.dump(tasks, f, ensure_ascii=False, indent=2) + return True + except Exception as e: + print(f"Ошибка сохранения в файл: {e}") + return False + +class FileStorageAdapter(BaseHTTPClient): + + def __init__(self, data_file: str = "tasks.json"): + super().__init__("file://localhost") + self.storage = FileStorage(data_file) + + def load_tasks(self): + return self.storage.load_tasks() + + def save_tasks(self, tasks): + return self.storage.save_tasks(tasks) \ 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..22c0cd23 --- /dev/null +++ b/simple_backend/src/task_tracker/tasks.json @@ -0,0 +1,7 @@ +[ + { + "id": "string", + "title": "string", + "status": "string" + } +] \ No newline at end of file