From d4c7fceb050423794915eb43a7b1d15de891135d Mon Sep 17 00:00:00 2001 From: Anton Shcherbak Date: Tue, 19 Nov 2024 22:14:00 +0300 Subject: [PATCH 01/14] feat: create conflicts --- .../src/main.py" | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git "a/\320\243\321\200\320\276\320\272 1: Git/src/main.py" "b/\320\243\321\200\320\276\320\272 1: Git/src/main.py" index 13580259..377a73b2 100644 --- "a/\320\243\321\200\320\276\320\272 1: Git/src/main.py" +++ "b/\320\243\321\200\320\276\320\272 1: Git/src/main.py" @@ -22,6 +22,22 @@ def __str__(self): return f"Customer: {self.name}, Membership: {self.membership}" +class GroupOrder("Order"): + def __init__(self, customers): + super().__init__(customer=None) # Групповой заказ не привязан к одному клиенту + self.customers = customers + + def split_bill(self): + if not self.customers: + raise ValueError("Нет клиентов для разделения счета.") + total = self.final_total() + return total / len(self.customers) + + 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 Order: TAX_RATE = 0.08 # 8% налог SERVICE_CHARGE = 0.05 # 5% сервисный сбор @@ -60,23 +76,6 @@ def __str__(self): return f"Order for {self.customer.name}:\n{dish_list}\nTotal: ${self.final_total():.2f}" -class GroupOrder(Order): - def __init__(self, customers): - super().__init__(customer=None) # Групповой заказ не привязан к одному клиенту - self.customers = customers - - def split_bill(self): - if not self.customers: - raise ValueError("Нет клиентов для разделения счета.") - total = self.final_total() - return total / len(self.customers) - - 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}" - - # Пример использования # Создаем блюда From 1ece50e5412787288409cd8cc18d38b2fbdb1ba0 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 27 Feb 2025 23:32:06 +0300 Subject: [PATCH 02/14] feat: changes --- git/src/main.py | 250 +++++++++++++++++++++++++----------------------- 1 file changed, 132 insertions(+), 118 deletions(-) diff --git a/git/src/main.py b/git/src/main.py index 8a11b44b..ca0839c3 100644 --- a/git/src/main.py +++ b/git/src/main.py @@ -1,121 +1,135 @@ +import json +import os + +def load_books(filename='library.json'): + """ + Загрузка списка книг из JSON-файла. + Возвращает список книг (каждая книга - это словарь). + """ + if not os.path.isfile(filename): + return [] + with open(filename, 'r', encoding='utf-8') as file: + try: + return json.load(file) + except json.JSONDecodeError: + return [] + +def saving_books(books, filename='library.json'): + """ + Сохранение списка книг в JSON-файл. + """ + with open(filename, 'w', encoding='utf-8') as file: + json.dump(books, file, ensure_ascii=False, indent=4) + +def list_books(books): + """ + Возвращает строку со списком всех книг. + """ + if not books: + return "Библиотека пуста." + result_lines = [] + for idx, book in enumerate(books, start=1): + 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 + return books + [new_book] + +def remove_book(books, title): + """ + Принимает текущий список книг и название книги для удаления. + Возвращает новый список без книги, у которой совпадает название. + """ + # Фильтруем список: оставляем только те книги, у которых название не совпадает с переданным + return [book for book in books if book['title'].lower() != title.lower()] + +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() + ] + +def main(): + """ + Точка входа в программу: здесь мы загружаем книги, + показываем меню и обрабатываем ввод пользователя. + """ + books = load_books() # Загрузили список книг из JSON + + while True: + print("\n=== Управление онлайн-библиотекой ===") + print("1. Показать все книги") + print("2. Добавить книгу") + print("3. Удалить книгу") + print("4. Поиск книг") + print("5. Выйти") + + choice = input("Выберите действие (1-5): ").strip() + + if choice == '1': + print("\nСписок книг:") + print(list_books(books)) + + elif choice == '2': + print("\nДобавление новой книги:") + title = input("Введите название: ").strip() + author = input("Введите автора: ").strip() + year = input("Введите год издания: ").strip() + + # Получаем новый список с добавленной книгой + new_books = add_book(books, title, author, year) + books = new_books # Обновляем переменную, чтобы сохранить изменения + saving_books(books) # Сразу сохраняем в файл + print("Книга добавлена!") + + elif choice == '3': + print("\nУдаление книги:") + title_to_remove = input("Введите название книги, которую хотите удалить: ").strip() + + new_books = remove_book(books, title_to_remove) + if len(new_books) > len(books): + books = new_books + saving_books(books) + print("Книга удалена!") + else: + print("Книга с таким названием не найдена.") + + elif choice == '4': + print("\nПоиск книг:") + keyword = input("Введите ключевое слово для поиска (в названии или авторе): ").strip() + found_books = search_books(books, keyword) + if found_books: + print("\nНайденные книги:") + print(list_books(found_books)) + else: + print("Ничего не найдено.") + + elif choice == '6': + print("Выход из программы.") + break -class GroupOrder("Order"): - def __init__(self, customers): - super().__init__(customer=None) # Групповой заказ не привязан к одному клиенту - self.customers = customers - - def split_bill(self): - if not self.customers: - raise ValueError("Нет клиентов для разделения счета.") - total = self.final_total() - return total / len(self.customers) - - 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 Order: - TAX_RATE = 0.08 # 8% налог - SERVICE_CHARGE = 0.05 # 5% сервисный сбор - - 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.") + print("Некорректный ввод. Попробуйте ещё раз.") - def remove_dish(self, dish): - if dish in self.dishes: - self.dishes.remove(dish) - else: - raise ValueError("Такого блюда нет в заказе.") - - def calculate_total(self): - return sum(dish.price for dish in self.dishes) - - def apply_discount(self): - discount_rate = self.customer.get_discount() / 100 - return self.calculate_total() * (1 - discount_rate) - - def final_total(self): - total_after_discount = self.apply_discount() - total_with_tax = total_after_discount * (1 + Order.TAX_RATE) - final_total = total_with_tax * (1 + Order.SERVICE_CHARGE) - return final_total - - def __str__(self): - dish_list = "\n".join([str(dish) for dish in self.dishes]) - return f"Order for {self.customer.name}:\n{dish_list}\nTotal: ${self.final_total():.2f}" - - -class GroupOrder(Order): - def __init__(self, customers): - super().__init__(customer=None) # Групповой заказ не привязан к одному клиенту - self.customers = customers - - def split_bill(self): - if not self.customers: - raise ValueError("Нет клиентов для разделения счета.") - total = self.final_total() - return total / len(self.customers) - - 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 - self.price = price - self.category = 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 - self.membership = membership - - def get_discount(self): - if self.membership == "VIP": - return 10 # VIP клиенты получают 10% скидки - return 0 # Обычные клиенты не получают скидки - - def __str__(self): - return f"Customer: {self.name}, Membership: {self.membership}" - -# Пример использования - -# Создаем блюда -pizza = Dish("Pizza", 12, "Main Course") -ice_cream = Dish("Ice Cream", 5, "Dessert") -coffee = Dish("Coffee", 3, "Drink") - -# Создаем клиентов -regular_customer = Customer("Alice", "Regular") -vip_customer = Customer("Bob", "VIP") - -# Индивидуальный заказ -order1 = Order(regular_customer) -order1.add_dish(pizza) -order1.add_dish(ice_cream) - -print(order1) # Вывод информации о заказе -print(f"Final Total: ${order1.final_total():.2f}") # Итоговая стоимость - -# Групповой заказ -group_order = GroupOrder([regular_customer, vip_customer]) -group_order.add_dish(pizza) -group_order.add_dish(ice_cream) -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 + + + + + +if __name__ == "__main__": + main() \ No newline at end of file From db7055aa25960f5bfc2843fe04d3e7f9086d0db5 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 23 Sep 2025 17:59:15 +0200 Subject: [PATCH 03/14] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20GitHub=20Actions=20CI=20with=20ruff=20linter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ruff-ci.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/ruff-ci.yml diff --git a/.github/workflows/ruff-ci.yml b/.github/workflows/ruff-ci.yml new file mode 100644 index 00000000..c729213d --- /dev/null +++ b/.github/workflows/ruff-ci.yml @@ -0,0 +1,29 @@ +name: Ruff Code Quality + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install ruff + run: pip install ruff + + - name: Run ruff check + run: ruff check git/src/ + + - name: Check formatting + run: ruff format git/src/ --check From 6c7cd32252fb8a775a7a7288403e783c5ca870f6 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 23 Sep 2025 18:05:38 +0200 Subject: [PATCH 04/14] test: trigger CI --- git/src/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/git/src/main.py b/git/src/main.py index 1822c7e9..9132bf58 100644 --- a/git/src/main.py +++ b/git/src/main.py @@ -128,3 +128,4 @@ def main(): if __name__ == "__main__": main() +# Test comment From a81ace0dbb3ac0a92efb4badee3a3f4b89bdd2f7 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 23 Sep 2025 18:14:27 +0200 Subject: [PATCH 05/14] style: format code with ruff --- git/src/main.py | 54 ++++++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/git/src/main.py b/git/src/main.py index 9132bf58..1135ea5a 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,13 +130,14 @@ def main(): else: print("Ничего не найдено.") - elif choice == '5': + elif choice == "5": print("Выход из программы.") break else: print("Некорректный ввод. Попробуйте ещё раз.") + if __name__ == "__main__": main() # Test comment From ff3406f598c48b30a6a33a77ea82b2efd4a6ab95 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 23 Sep 2025 23:32:29 +0200 Subject: [PATCH 06/14] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=201:=20=D0=9D=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B8=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D0=BA=20FastAP?= =?UTF-8?q?I=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/.gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 simple_backend/src/task_tracker/.gitignore diff --git a/simple_backend/src/task_tracker/.gitignore b/simple_backend/src/task_tracker/.gitignore new file mode 100644 index 00000000..6a7d2761 --- /dev/null +++ b/simple_backend/src/task_tracker/.gitignore @@ -0,0 +1,4 @@ +.venv/ +venv/ +__pycache__/ +*.pyc \ No newline at end of file From b467010b0ebb0ac84f1c543a5bc9afbcc97bae2f Mon Sep 17 00:00:00 2001 From: vlad Date: Wed, 24 Sep 2025 10:39:59 +0200 Subject: [PATCH 07/14] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B2=20main.py(get,=20post,=20put,=20del)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/main.py | 42 +++++++++++++++++++++---- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 3db98d0d..2ee6e995 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,19 +1,49 @@ from fastapi import FastAPI +from pydantic import BaseModel app = FastAPI() +class Task(BaseModel): + id: int + title: str + completed: bool = False + +tasks = [ + Task(id=1, title="купить молоко", completed=False), + Task(id=2, title="позавтракать", completed=True), + Task(id=3, title="позвонить дедушке", completed=False) +] + +# GET /tasks - получить все задачи @app.get("/tasks") def get_tasks(): - pass + return tasks +# POST /tasks - создать новую задачу @app.post("/tasks") -def create_task(task): - pass +def create_task(title: str): + new_id = max(task.id for task in tasks) + 1 + new_task = Task(id=new_id, title=title, completed=False) + tasks.append(new_task) + return new_task +# PUT /tasks/{id} - обновить задачу @app.put("/tasks/{task_id}") -def update_task(task_id: int): - pass +def update_task(task_id: int, title: str = None, completed: bool = None): + for task in tasks: + if task.id == task_id: + if title is not None: + task.title = title + if completed is not None: + task.completed = completed + return task + return {"error": "Задача не найдена"} +# DELETE /tasks/{id} - удалить задачу @app.delete("/tasks/{task_id}") def delete_task(task_id: int): - pass + for i, task in enumerate(tasks): + if task.id == task_id: + deleted_task = tasks.pop(i) + return {"message": f"Задача '{deleted_task.title}' удалена"} + return {"error": "Задача не найдена"} \ No newline at end of file From 5940bd6be0f948bf9703ae302a78a6f180996b39 Mon Sep 17 00:00:00 2001 From: vlad Date: Thu, 25 Sep 2025 17:07:36 +0200 Subject: [PATCH 08/14] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=20=D0=B2=D0=B0=D1=80?= =?UTF-8?q?=D0=B8=D0=B0=D0=BD=D1=82=D1=8B=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/readme.md | 153 ++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 simple_backend/src/task_tracker/readme.md diff --git a/simple_backend/src/task_tracker/readme.md b/simple_backend/src/task_tracker/readme.md new file mode 100644 index 00000000..ef29249c --- /dev/null +++ b/simple_backend/src/task_tracker/readme.md @@ -0,0 +1,153 @@ +Минусы этого подхода: +1. Потеря данных при перезапуске +Все данные удаляются при остановке сервера + +При деплое или обновлении приложения задачи теряются + +При сбое сервера все данные исчезают + +2. Проблемы с масштабированием +Нельзя запустить несколько экземпляров приложения (балансировка нагрузки) + +Каждый сервер будет иметь свою копию данных + +Нет синхронизации между разными процессами + +3. Ограничение памяти +Данные хранятся в RAM, что ограничено размером памяти сервера + +При большом количестве задач может закончиться память + +Нет эффективного управления памятью + +4. Нет персистентности +Невозможно сделать бэкап данных + +Нет истории изменений + +Невозможно восстановить данные после сбоя + +5. Проблемы с производительностью +Поиск по большому списку медленный (O(n)) + +Нет индексов для оптимизации запросов + +Нет кэширования сложных запросов + +6. Отсутствие транзакций +Нет ACID-свойств (атомарность, согласованность, изолированность, долговечность) + +Возможны race conditions при параллельных запросах + +Нет механизмов блокировок + +Когда использовать такой подход? +Только для: + +Прототипирования и разработки + +Тестирования функциональности + +Демонстрационных проектов + +Учебных целей (как сейчас у нас) + +Альтернативы для продакшена: +Базы данных: PostgreSQL, MySQL, SQLite + +In-memory DB: Redis, Memcached + +Файловое хранилище: JSON, CSV файлы + +Облачные решения: Firebase, AWS DynamoDB + +## Переход от хранения списка задач в оперативной памяти к файлу проекта + +### Улучшения после перехода: + +1. **Сохраняемость данных** - задачи сохраняются между перезапусками приложения +2. **Надежность хранения** - данные устойчивы к внезапным завершениям работы программы +3. **Прозрачность данных** - возможность прямого просмотра и редактирования файла +4. **Версионный контроль** - интеграция с системами контроля версий (Git) +5. **Переносимость** - простое копирование и перенос между устройствами + +### Состояние приложения: анализ изменений + +**Состояние не устранено, а перенесено** из временного хранилища в постоянное: + +- **Предыдущий подход**: состояние в оперативной памяти (временное, volatile) +- **Текущий подход**: состояние в файловой системе (постоянное, persistent) + +Приложение продолжает хранить состояние, но теперь обеспечивает его сохранность между сеансами работы. + +## Альтернативные подходы к хранению задач + +### 1. Оперативная память (исходный подход) +**Преимущества:** +- Максимальная производительность +- Минимальная сложность реализации +- Отсутствие накладных расходов + +**Недостатки:** +- Потеря данных при перезапуске +- Отсутствие персистентности +- Ограниченный доступ к данным + +### 2. Текстовый файл (текущая реализация) +**Преимущества:** +- Простота реализации и обслуживания +- Человеко-читаемый формат +- Прямое редактирование без приложения +- Совместимость с системами контроля версий + +**Недостатки:** +- Снижение производительности при больших объемах +- Риск повреждения формата файла +- Ограниченные возможности запросов и фильтрации + +### 3. База данных SQLite +**Преимущества:** +- Оптимизированная производительность +- Гарантированная целостность данных +- Расширенные возможности запросов +- Поддержка конкурентного доступа + +**Недостатки:** +- Усложнение архитектуры приложения +- Избыточность для простых случаев использования +- Бинарный формат затрудняет ручное редактирование + +### 4. Структурированные файлы (JSON/YAML) +**Преимущества:** +- Стандартизированный машинно-читаемый формат +- Широкая инструментальная поддержка +- Поддержка сложных структур данных +- Сохранение человеко-читаемости + +**Недостатки:** +- Увеличенный объем хранения +- Потенциальные проблемы с валидацией формата +- Снижение производительности по сравнению с бинарными форматами + +### 5. Облачные хранилища +**Преимущества:** +- Кросс-платформенная доступность +- Автоматическая синхронизация +- Встроенное резервное копирование +- Горизонтальная масштабируемость + +**Недостатки:** +- Зависимость от сетевого соединения +- Потенциальные затраты на обслуживание +- Зависимость от сторонних сервисов +- Вопросы конфиденциальности данных + +## Критерии выбора стратегии хранения + +**Оперативная память**: прототипирование, тестовые среды +**Текстовые файлы**: персональные приложения, простые случаи использования +**SQLite**: многопользовательские приложения, сложные запросы +**JSON/YAML**: конфигурационные данные, сложные структуры +**Облачные хранилища**: мобильные и веб-приложения, распределенные команды + +Текущая реализация с текстовым файлом обеспечивает оптимальный баланс для персонального использования, сочетая простоту реализации с достаточной функциональностью. \ No newline at end of file From 74410bb5dc0ccf675323242c8471bffefeb44124 Mon Sep 17 00:00:00 2001 From: vlad Date: Thu, 25 Sep 2025 17:11:39 +0200 Subject: [PATCH 09/14] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B0=D0=BD=D0=BE=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=BE=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B0?= =?UTF-8?q?=D1=85=20=D1=81=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=82=D0=B8?= =?UTF-8?q?=D0=B2=D0=BD=D0=BE=D0=B9=20=D0=BF=D0=B0=D0=BC=D1=8F=D1=82=D0=B8?= =?UTF-8?q?=20=D0=BD=D0=B0=20json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/main.py | 35 ++++++++++++++++++---- simple_backend/src/task_tracker/tasks.json | 17 +++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 simple_backend/src/task_tracker/tasks.json diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 2ee6e995..bc10d2be 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,5 +1,6 @@ from fastapi import FastAPI from pydantic import BaseModel +import json app = FastAPI() @@ -8,42 +9,64 @@ class Task(BaseModel): title: str completed: bool = False -tasks = [ - Task(id=1, title="купить молоко", completed=False), - Task(id=2, title="позавтракать", completed=True), - Task(id=3, title="позвонить дедушке", completed=False) -] +# Функции для работы с файлом +def load_tasks(): + try: + with open("tasks.json", "r", encoding="utf-8") as f: + data = json.load(f) + return [Task(**task) for task in data] + except: + # Если файла нет, создаем начальные задачи + initial_tasks = [ + {"id": 1, "title": "купить молоко", "completed": False}, + {"id": 2, "title": "позавтракать", "completed": True}, + {"id": 3, "title": "позвонить дедушке", "completed": False} + ] + with open("tasks.json", "w", encoding="utf-8") as f: + json.dump(initial_tasks, f, ensure_ascii=False, indent=2) + return [Task(**task) for task in initial_tasks] + +def save_tasks(tasks): + with open("tasks.json", "w", encoding="utf-8") as f: + data = [task.dict() for task in tasks] + json.dump(data, f, ensure_ascii=False, indent=2) # GET /tasks - получить все задачи @app.get("/tasks") def get_tasks(): - return tasks + return load_tasks() # POST /tasks - создать новую задачу @app.post("/tasks") def create_task(title: str): + tasks = load_tasks() new_id = max(task.id for task in tasks) + 1 new_task = Task(id=new_id, title=title, completed=False) tasks.append(new_task) + save_tasks(tasks) return new_task # PUT /tasks/{id} - обновить задачу @app.put("/tasks/{task_id}") def update_task(task_id: int, title: str = None, completed: bool = None): + tasks = load_tasks() for task in tasks: if task.id == task_id: if title is not None: task.title = title if completed is not None: task.completed = completed + save_tasks(tasks) return task return {"error": "Задача не найдена"} # DELETE /tasks/{id} - удалить задачу @app.delete("/tasks/{task_id}") def delete_task(task_id: int): + tasks = load_tasks() for i, task in enumerate(tasks): if task.id == task_id: deleted_task = tasks.pop(i) + save_tasks(tasks) return {"message": f"Задача '{deleted_task.title}' удалена"} return {"error": "Задача не найдена"} \ 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..6f6efe17 --- /dev/null +++ b/simple_backend/src/task_tracker/tasks.json @@ -0,0 +1,17 @@ +[ + { + "id": 2, + "title": "позавтракать", + "completed": true + }, + { + "id": 3, + "title": "позвонить дедушке", + "completed": false + }, + { + "id": 4, + "title": "медитация+расслабление", + "completed": false + } +] \ No newline at end of file From fc3c7e4ae7deeab77846f86818f4cf60efd3572a Mon Sep 17 00:00:00 2001 From: vlad Date: Thu, 25 Sep 2025 17:16:09 +0200 Subject: [PATCH 10/14] =?UTF-8?q?=D0=BD=D0=B0=D0=BF=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=BE=D0=BC=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/main.py | 74 +++++++++++++++---------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index bc10d2be..fbfe18fa 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from pydantic import BaseModel import json +from typing import List app = FastAPI() @@ -9,64 +10,81 @@ class Task(BaseModel): title: str completed: bool = False -# Функции для работы с файлом -def load_tasks(): - try: - with open("tasks.json", "r", encoding="utf-8") as f: - data = json.load(f) - return [Task(**task) for task in data] - except: - # Если файла нет, создаем начальные задачи - initial_tasks = [ - {"id": 1, "title": "купить молоко", "completed": False}, - {"id": 2, "title": "позавтракать", "completed": True}, - {"id": 3, "title": "позвонить дедушке", "completed": False} - ] - with open("tasks.json", "w", encoding="utf-8") as f: - json.dump(initial_tasks, f, ensure_ascii=False, indent=2) - return [Task(**task) for task in initial_tasks] +class TaskManager: + def __init__(self, filename: str = 'tasks.json'): + self.filename = filename + + def load_tasks(self) -> List[Task]: + """Загрузить задачи из файла""" + try: + with open(self.filename, 'r', encoding='utf-8') as f: + data = json.load(f) + return [Task(**task) for task in data] + except (FileNotFoundError, json.JSONDecodeError): + # Создаем начальные задачи если файл не существует или поврежден + initial_tasks = [ + {"id": 1, "title": "купить молоко", "completed": False}, + {"id": 2, "title": "позавтракать", "completed": True}, + {"id": 3, "title": "позвонить дедушке", "completed": False} + ] + self._save_tasks_data(initial_tasks) + return [Task(**task) for task in initial_tasks] + + def save_tasks(self, tasks: List[Task]) -> None: + """Сохранить задачи в файл""" + data = [task.model_dump() for task in tasks] + self._save_tasks_data(data) + + def _save_tasks_data(self, data: list) -> None: + """Внутренний метод для сохранения данных""" + with open(self.filename, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def get_next_id(self, tasks: List[Task]) -> int: + """Получить следующий ID для новой задачи""" + if not tasks: + return 1 + return max(task.id for task in tasks) + 1 -def save_tasks(tasks): - with open("tasks.json", "w", encoding="utf-8") as f: - data = [task.dict() for task in tasks] - json.dump(data, f, ensure_ascii=False, indent=2) +# Создаем экземпляр менеджера задач +task_manager = TaskManager() # GET /tasks - получить все задачи @app.get("/tasks") def get_tasks(): - return load_tasks() + return task_manager.load_tasks() # POST /tasks - создать новую задачу @app.post("/tasks") def create_task(title: str): - tasks = load_tasks() - new_id = max(task.id for task in tasks) + 1 + tasks = task_manager.load_tasks() + new_id = task_manager.get_next_id(tasks) new_task = Task(id=new_id, title=title, completed=False) tasks.append(new_task) - save_tasks(tasks) + task_manager.save_tasks(tasks) return new_task # PUT /tasks/{id} - обновить задачу @app.put("/tasks/{task_id}") def update_task(task_id: int, title: str = None, completed: bool = None): - tasks = load_tasks() + tasks = task_manager.load_tasks() for task in tasks: if task.id == task_id: if title is not None: task.title = title if completed is not None: task.completed = completed - save_tasks(tasks) + task_manager.save_tasks(tasks) return task return {"error": "Задача не найдена"} # DELETE /tasks/{id} - удалить задачу @app.delete("/tasks/{task_id}") def delete_task(task_id: int): - tasks = load_tasks() + tasks = task_manager.load_tasks() for i, task in enumerate(tasks): if task.id == task_id: deleted_task = tasks.pop(i) - save_tasks(tasks) + task_manager.save_tasks(tasks) return {"message": f"Задача '{deleted_task.title}' удалена"} return {"error": "Задача не найдена"} \ No newline at end of file From 2f8dae65f83e517cc10ad5ba2f4ac6e81ef62838 Mon Sep 17 00:00:00 2001 From: vlad Date: Sat, 27 Sep 2025 13:40:35 +0200 Subject: [PATCH 11/14] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=20=D0=BD=D0=B0=20=D0=BE=D0=B1=D0=BB=D0=B0=D1=87=D0=BD?= =?UTF-8?q?=D0=BE=D0=B5=20=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D0=BB=D0=B8=D1=89?= =?UTF-8?q?=D0=B5,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BD=D0=BE=D0=B2=D1=8B=D1=85=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/main.py | 100 ++++++++++++------ .../src/task_tracker/requirements.txt | 6 +- simple_backend/src/task_tracker/tasks.json | 17 --- 3 files changed, 72 insertions(+), 51 deletions(-) delete mode 100644 simple_backend/src/task_tracker/tasks.json diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index fbfe18fa..08980071 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,44 +1,76 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from pydantic import BaseModel +import requests import json from typing import List app = FastAPI() +# Конфигурация JSONBin.io +JSONBIN_API_KEY = "$2a$10$pcwqDUhDIMDVaHFqqn4gCui92e3cFwdyhwt5gxmglbF.Y/FJAHDJG" +JSONBIN_BIN_ID = "68d56971d0ea881f408ac1f5" + class Task(BaseModel): id: int title: str completed: bool = False -class TaskManager: - def __init__(self, filename: str = 'tasks.json'): - self.filename = filename +class CloudTaskManager: + def __init__(self, api_key: str, bin_id: str): + self.api_key = api_key + self.bin_id = bin_id + self.url = f"https://api.jsonbin.io/v3/b/{bin_id}" + self.headers = { + "Content-Type": "application/json", + "X-Master-Key": api_key + } def load_tasks(self) -> List[Task]: - """Загрузить задачи из файла""" - try: - with open(self.filename, 'r', encoding='utf-8') as f: - data = json.load(f) - return [Task(**task) for task in data] - except (FileNotFoundError, json.JSONDecodeError): - # Создаем начальные задачи если файл не существует или поврежден - initial_tasks = [ - {"id": 1, "title": "купить молоко", "completed": False}, - {"id": 2, "title": "позавтракать", "completed": True}, - {"id": 3, "title": "позвонить дедушке", "completed": False} - ] - self._save_tasks_data(initial_tasks) - return [Task(**task) for task in initial_tasks] + """Загрузить задачи из JSONBin.io""" + response = requests.get(self.url, headers=self.headers) + print(f"Статус ответа: {response.status_code}") # Для отладки + + if response.status_code == 200: + data = response.json() + print(f"Полученные данные: {data}") # Для отладки + tasks_data = data['record'] + return [Task(**task) for task in tasks_data] + elif response.status_code == 404: + print("Бин не найден, создаем начальные задачи") + return self._create_initial_tasks() + else: + print(f"Ошибка HTTP: {response.status_code}") + return self._create_initial_tasks() + def save_tasks(self, tasks: List[Task]) -> None: - """Сохранить задачи в файл""" - data = [task.model_dump() for task in tasks] - self._save_tasks_data(data) + """Сохранить задачи в JSONBin.io""" + try: + data = [task.model_dump() for task in tasks] + response = requests.put(self.url, json=data, headers=self.headers) + + if response.status_code == 200: + print("Данные успешно сохранены") + else: + print(f"Ошибка сохранения: {response.status_code} - {response.text}") + raise HTTPException(status_code=500, detail="Ошибка сохранения в облаке") + + except Exception as e: + print(f"Ошибка: {e}") + raise HTTPException(status_code=500, detail="Ошибка сохранения в облаке") - def _save_tasks_data(self, data: list) -> None: - """Внутренний метод для сохранения данных""" - with open(self.filename, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=2) + def _create_initial_tasks(self) -> List[Task]: + """Создать начальные задачи если бин не существует""" + print("Создаем начальные задачи...") + initial_tasks_data = [ + {"id": 1, "title": "купить молоко", "completed": False}, + {"id": 2, "title": "позавтракать", "completed": True}, + {"id": 3, "title": "позвонить дедушке", "completed": False} + ] + + initial_tasks = [Task(**task) for task in initial_tasks_data] + self.save_tasks(initial_tasks) + return initial_tasks def get_next_id(self, tasks: List[Task]) -> int: """Получить следующий ID для новой задачи""" @@ -46,15 +78,14 @@ def get_next_id(self, tasks: List[Task]) -> int: return 1 return max(task.id for task in tasks) + 1 -# Создаем экземпляр менеджера задач -task_manager = TaskManager() +# Создаем менеджер задач +task_manager = CloudTaskManager(JSONBIN_API_KEY, JSONBIN_BIN_ID) -# GET /tasks - получить все задачи +# Эндпоинты @app.get("/tasks") def get_tasks(): return task_manager.load_tasks() -# POST /tasks - создать новую задачу @app.post("/tasks") def create_task(title: str): tasks = task_manager.load_tasks() @@ -64,7 +95,6 @@ def create_task(title: str): task_manager.save_tasks(tasks) return new_task -# PUT /tasks/{id} - обновить задачу @app.put("/tasks/{task_id}") def update_task(task_id: int, title: str = None, completed: bool = None): tasks = task_manager.load_tasks() @@ -78,7 +108,6 @@ def update_task(task_id: int, title: str = None, completed: bool = None): return task return {"error": "Задача не найдена"} -# DELETE /tasks/{id} - удалить задачу @app.delete("/tasks/{task_id}") def delete_task(task_id: int): tasks = task_manager.load_tasks() @@ -87,4 +116,11 @@ def delete_task(task_id: int): deleted_task = tasks.pop(i) task_manager.save_tasks(tasks) return {"message": f"Задача '{deleted_task.title}' удалена"} - return {"error": "Задача не найдена"} \ No newline at end of file + return {"error": "Задача не найдена"} + +@app.on_event("startup") +async def startup_event(): + """Проверяем соединение при запуске""" + print("Проверка соединения с JSONBin.io...") + tasks = task_manager.load_tasks() + print(f"Загружено задач: {len(tasks)}") \ No newline at end of file diff --git a/simple_backend/src/task_tracker/requirements.txt b/simple_backend/src/task_tracker/requirements.txt index 8e0578a0..1c4eea45 100644 --- a/simple_backend/src/task_tracker/requirements.txt +++ b/simple_backend/src/task_tracker/requirements.txt @@ -1,2 +1,4 @@ -fastapi -uvicorn[standard] \ No newline at end of file +fastapi>=0.104.1 +uvicorn>=0.24.0 +requests>=2.31.0 +pydantic>=2.5.0 \ No newline at end of file diff --git a/simple_backend/src/task_tracker/tasks.json b/simple_backend/src/task_tracker/tasks.json deleted file mode 100644 index 6f6efe17..00000000 --- a/simple_backend/src/task_tracker/tasks.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - { - "id": 2, - "title": "позавтракать", - "completed": true - }, - { - "id": 3, - "title": "позвонить дедушке", - "completed": false - }, - { - "id": 4, - "title": "медитация+расслабление", - "completed": false - } -] \ No newline at end of file From 4c3f3de2f2d2a7549b779b2aec3bc97919206308 Mon Sep 17 00:00:00 2001 From: vlad Date: Sat, 27 Sep 2025 13:56:45 +0200 Subject: [PATCH 12/14] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE=20=D1=81=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B5=20=D0=B3=D0=BE=D0=BD=D0=BA?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/readme.md | 52 ++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/simple_backend/src/task_tracker/readme.md b/simple_backend/src/task_tracker/readme.md index ef29249c..313141ef 100644 --- a/simple_backend/src/task_tracker/readme.md +++ b/simple_backend/src/task_tracker/readme.md @@ -150,4 +150,54 @@ In-memory DB: Redis, Memcached **JSON/YAML**: конфигурационные данные, сложные структуры **Облачные хранилища**: мобильные и веб-приложения, распределенные команды -Текущая реализация с текстовым файлом обеспечивает оптимальный баланс для персонального использования, сочетая простоту реализации с достаточной функциональностью. \ No newline at end of file +Текущая реализация с текстовым файлом обеспечивает оптимальный баланс для персонального использования, сочетая простоту реализации с достаточной функциональностью. + + +# Stateless Backend с облачным хранилищем + +## Архитектура +Backend теперь является stateless - все данные хранятся в облачном сервисе jsonbin.io. При каждом запросе данные загружаются и сохраняются в облачное хранилище. + +## Проблема состояния гонки (Race Condition) + +### Что это такое? +Состояние гонки возникает, когда несколько процессов или потоков одновременно пытаются изменить общие данные, и конечный результат зависит от порядка выполнения операций. + +### Проблемы в текущей реализации: + +1. **Чтение-Модификация-Запись (Read-Modify-Write)** + - Два запроса одновременно читают данные + - Оба модифицируют свою копию + - Последний запрос перезаписывает изменения первого + +2. **Конкретный сценарий:** + - Запрос A читает задачи: `[{id: 1, title: "Task 1"}]` + - Запрос B читает задачи: `[{id: 1, title: "Task 1"}]` + - Запрос A обновляет title на "Task 1 Updated" и сохраняет + - Запрос B обновляет title на "Task 1 Modified" и сохраняет + - **Результат**: Изменения запроса A теряются + +### Решение проблемы + +#### Вариант 1: Оптимистичная блокировка +```python +# Добавить версию данных +async def update_task(task_id: int, updated_task: Task, version: int): + current_data = await cloud_storage.get_tasks_with_version() + if current_data["version"] != version: + raise HTTPException(409, "Data was modified by another request") + # Обновить данные и увеличить версию + +Вариант 2: Пессимистичная блокировка +Внешний сервис блокировок + +Redis-based мьютексы + +Блокировка на время операции + +Вариант 3: База данных с транзакциями +Переход на PostgreSQL/MongoDB + +ACID-транзакции + +Атомарные операции \ No newline at end of file From 23b7fcea207ca6595c74265b510b4acbc13ba332 Mon Sep 17 00:00:00 2001 From: vlad Date: Sat, 27 Sep 2025 17:06:41 +0200 Subject: [PATCH 13/14] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20Cloudflare?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/.gitignore | 2 +- simple_backend/src/task_tracker/main.py | 89 +++++++++++++++++-- .../src/task_tracker/requirements.txt | 4 +- 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/simple_backend/src/task_tracker/.gitignore b/simple_backend/src/task_tracker/.gitignore index 6a7d2761..c1443be6 100644 --- a/simple_backend/src/task_tracker/.gitignore +++ b/simple_backend/src/task_tracker/.gitignore @@ -1,4 +1,4 @@ .venv/ venv/ __pycache__/ -*.pyc \ No newline at end of file +*.pyc.env diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 08980071..5420d193 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,8 +1,13 @@ -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, BackgroundTasks from pydantic import BaseModel import requests import json +import os from typing import List +import httpx +from dotenv import load_dotenv + +load_dotenv() app = FastAPI() @@ -10,10 +15,14 @@ JSONBIN_API_KEY = "$2a$10$pcwqDUhDIMDVaHFqqn4gCui92e3cFwdyhwt5gxmglbF.Y/FJAHDJG" JSONBIN_BIN_ID = "68d56971d0ea881f408ac1f5" +CLOUDFLARE_API_TOKEN = os.getenv("CLOUDFLARE_API_TOKEN") +CLOUDFLARE_ACCOUNT_ID = os.getenv("CLOUDFLARE_ACCOUNT_ID") + class Task(BaseModel): id: int title: str completed: bool = False + ai_help: str = '' class CloudTaskManager: def __init__(self, api_key: str, bin_id: str): @@ -63,9 +72,9 @@ def _create_initial_tasks(self) -> List[Task]: """Создать начальные задачи если бин не существует""" print("Создаем начальные задачи...") initial_tasks_data = [ - {"id": 1, "title": "купить молоко", "completed": False}, - {"id": 2, "title": "позавтракать", "completed": True}, - {"id": 3, "title": "позвонить дедушке", "completed": False} + {"id": 1, "title": "купить молоко", "completed": False, "ai_help": ""}, + {"id": 2, "title": "позавтракать", "completed": True, "ai_help": ""}, + {"id": 3, "title": "позвонить дедушке", "completed": False, "ai_help": ""} ] initial_tasks = [Task(**task) for task in initial_tasks_data] @@ -77,9 +86,71 @@ def get_next_id(self, tasks: List[Task]) -> int: if not tasks: return 1 return max(task.id for task in tasks) + 1 + +class CloudflareAI: + def __init__(self): + self.token = CLOUDFLARE_API_TOKEN + self.account_id = CLOUDFLARE_ACCOUNT_ID + self.url = f"https://api.cloudflare.com/client/v4/accounts/{self.account_id}/ai/run/@cf/meta/llama-3.1-8b-instruct" + + + async def get_ai_help(self, task_title: str) -> str: + """Получить AI помощь для задачи""" + try: + print(f"Отправка запроса к Cloudflare AI...") + print(f"URL: {self.url}") + print(f"Токен: {self.token[:10]}...") # Логируем только начало токена + print(f"Account ID: {self.account_id}") + + async with httpx.AsyncClient() as client: + response = await client.post( + self.url, + headers={"Authorization": f"Bearer {self.token}"}, + json={ + "messages": [ + { + "role": "user", + "content": f"Кратко объясни как решить задачу: {task_title}" + } + ] + }, + timeout=30.0 + ) + + print(f"Статус ответа: {response.status_code}") + print(f"Текст ответа: {response.text}") + + if response.status_code == 200: + result = response.json() + print(f"Полный ответ AI: {result}") + return result.get("result", {}).get("response", "Не удалось получить ответ") + else: + print(f"Ошибка API: {response.status_code} - {response.text}") + return f"Ошибка API: {response.status_code}" + + except Exception as e: + print(f"Ошибка AI: {e}") + return f"Ошибка подключения: {str(e)}" + # Создаем менеджер задач task_manager = CloudTaskManager(JSONBIN_API_KEY, JSONBIN_BIN_ID) +ai_service = CloudflareAI() + + +async def add_ai_to_task(task_id: int, task_title: str): + try: + ai_help = await ai_service.get_ai_help(task_title) + + tasks = task_manager.load_tasks() + for task in tasks: + if task.id == task_id: + task.ai_help = ai_help + task_manager.save_tasks(tasks) + print(f"Добавлена AI помощь для задачи {task_id}") + break + except Exception as e: + print(f"Ошибка при добавлении AI помощи: {e}") # Эндпоинты @app.get("/tasks") @@ -87,16 +158,19 @@ def get_tasks(): return task_manager.load_tasks() @app.post("/tasks") -def create_task(title: str): +async def create_task(title: str, background_tasks: BackgroundTasks): tasks = task_manager.load_tasks() new_id = task_manager.get_next_id(tasks) new_task = Task(id=new_id, title=title, completed=False) tasks.append(new_task) task_manager.save_tasks(tasks) + + background_tasks.add_task(add_ai_to_task, new_id, title) + return new_task @app.put("/tasks/{task_id}") -def update_task(task_id: int, title: str = None, completed: bool = None): +def update_task(task_id: int, background_tasks: BackgroundTasks, title: str = None, completed: bool = None): tasks = task_manager.load_tasks() for task in tasks: if task.id == task_id: @@ -105,6 +179,9 @@ def update_task(task_id: int, title: str = None, completed: bool = None): if completed is not None: task.completed = completed task_manager.save_tasks(tasks) + + background_tasks.add_task(add_ai_to_task, task_id, title) + return task return {"error": "Задача не найдена"} diff --git a/simple_backend/src/task_tracker/requirements.txt b/simple_backend/src/task_tracker/requirements.txt index 1c4eea45..535c39c8 100644 --- a/simple_backend/src/task_tracker/requirements.txt +++ b/simple_backend/src/task_tracker/requirements.txt @@ -1,4 +1,6 @@ fastapi>=0.104.1 uvicorn>=0.24.0 requests>=2.31.0 -pydantic>=2.5.0 \ No newline at end of file +pydantic>=2.5.0 +httpx +python-dotenv \ No newline at end of file From 8696428a4a82354677ec8da899d6d77fa55af078 Mon Sep 17 00:00:00 2001 From: vlad Date: Sat, 27 Sep 2025 19:31:45 +0200 Subject: [PATCH 14/14] =?UTF-8?q?=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=20Ba?= =?UTF-8?q?seHTTPClient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/main.py | 146 ++++++++++++++---------- 1 file changed, 83 insertions(+), 63 deletions(-) diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 5420d193..d7828bec 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -3,7 +3,7 @@ import requests import json import os -from typing import List +from typing import List, Any, Dict import httpx from dotenv import load_dotenv @@ -18,55 +18,80 @@ CLOUDFLARE_API_TOKEN = os.getenv("CLOUDFLARE_API_TOKEN") CLOUDFLARE_ACCOUNT_ID = os.getenv("CLOUDFLARE_ACCOUNT_ID") + +class BaseHTTPClient: + """Базовый класс для HTTP клиентов""" + + def __init__(self, base_url: str, headers: Dict[str, str] = None): + self.base_url = base_url + self.headers = headers or {} + + def _request_sync(self, method: str, endpoint: str = "", **kwargs) -> Any: + """Синхронный HTTP запрос""" + url = f"{self.base_url}{endpoint}" + try: + response = requests.request(method, url, headers=self.headers, **kwargs) + print(f"HTTP {method} {url} - Status: {response.status_code}") + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + print(f"Ошибка HTTP запроса: {e}") + raise HTTPException(status_code=500, detail=f"Ошибка HTTP: {str(e)}") + + async def _request_async(self, method: str, endpoint: str = "", **kwargs) -> Any: + """Асинхронный HTTP запрос""" + url = f"{self.base_url}{endpoint}" + try: + async with httpx.AsyncClient() as client: + response = await client.request(method, url, headers=self.headers, **kwargs) + print(f"Async HTTP {method} {url} - Status: {response.status_code}") + response.raise_for_status() + return response.json() + except httpx.RequestError as e: + print(f"Ошибка асинхронного HTTP запроса: {e}") + raise HTTPException(status_code=500, detail=f"Ошибка HTTP: {str(e)}") + class Task(BaseModel): id: int title: str completed: bool = False ai_help: str = '' -class CloudTaskManager: +class CloudTaskManager(BaseHTTPClient): + def __init__(self, api_key: str, bin_id: str): - self.api_key = api_key - self.bin_id = bin_id - self.url = f"https://api.jsonbin.io/v3/b/{bin_id}" - self.headers = { + base_url = f"https://api.jsonbin.io/v3/b/{bin_id}" + + headers = { "Content-Type": "application/json", "X-Master-Key": api_key } + + super().__init__(base_url, headers) + + self.api_key = api_key + self.bin_id = bin_id def load_tasks(self) -> List[Task]: """Загрузить задачи из JSONBin.io""" - response = requests.get(self.url, headers=self.headers) - print(f"Статус ответа: {response.status_code}") # Для отладки - if response.status_code == 200: - data = response.json() - print(f"Полученные данные: {data}") # Для отладки + try: + data = self._request_sync('GET') + tasks_data = data['record'] - return [Task(**task) for task in tasks_data] - elif response.status_code == 404: + return [Task(**task) for task in tasks_data] + except HTTPException: print("Бин не найден, создаем начальные задачи") return self._create_initial_tasks() - else: - print(f"Ошибка HTTP: {response.status_code}") - return self._create_initial_tasks() + + def save_tasks(self, tasks: List[Task]) -> None: """Сохранить задачи в JSONBin.io""" - try: - data = [task.model_dump() for task in tasks] - response = requests.put(self.url, json=data, headers=self.headers) - - if response.status_code == 200: - print("Данные успешно сохранены") - else: - print(f"Ошибка сохранения: {response.status_code} - {response.text}") - raise HTTPException(status_code=500, detail="Ошибка сохранения в облаке") - - except Exception as e: - print(f"Ошибка: {e}") - raise HTTPException(status_code=500, detail="Ошибка сохранения в облаке") + data = [task.model_dump() for task in tasks] + self._request_sync("PUT", json=data) + print("Данные успешно сохранены") def _create_initial_tasks(self) -> List[Task]: """Создать начальные задачи если бин не существует""" @@ -87,46 +112,41 @@ def get_next_id(self, tasks: List[Task]) -> int: return 1 return max(task.id for task in tasks) + 1 -class CloudflareAI: - def __init__(self): - self.token = CLOUDFLARE_API_TOKEN - self.account_id = CLOUDFLARE_ACCOUNT_ID - self.url = f"https://api.cloudflare.com/client/v4/accounts/{self.account_id}/ai/run/@cf/meta/llama-3.1-8b-instruct" +class CloudflareAI(BaseHTTPClient): + """Клиент для Cloudflare AI""" + def __init__(self, api_token: str, account_id: str, model: str = "@cf/meta/llama-3.1-8b-instruct"): + base_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run" + headers = { + "Authorization": f"Bearer {api_token}", + "Content-Type": "application/json" + } + super().__init__(base_url, headers) + self.model = model + async def get_ai_help(self, task_title: str) -> str: """Получить AI помощь для задачи""" try: print(f"Отправка запроса к Cloudflare AI...") - print(f"URL: {self.url}") - print(f"Токен: {self.token[:10]}...") # Логируем только начало токена - print(f"Account ID: {self.account_id}") + print(f"URL: {self.base_url}/{self.model}") # ← Используем base_url из базового класса + print(f"Токен: {self.headers['Authorization'][:10]}...") # ← Токен из headers + data = await self._request_async( + "POST", + endpoint=f"/{self.model}", + json={ + "messages": [ + { + "role": "user", + "content": f"Кратко объясни как решить задачу: {task_title}" + } + ] + }, + timeout=30.0 + ) - async with httpx.AsyncClient() as client: - response = await client.post( - self.url, - headers={"Authorization": f"Bearer {self.token}"}, - json={ - "messages": [ - { - "role": "user", - "content": f"Кратко объясни как решить задачу: {task_title}" - } - ] - }, - timeout=30.0 - ) - - print(f"Статус ответа: {response.status_code}") - print(f"Текст ответа: {response.text}") - - if response.status_code == 200: - result = response.json() - print(f"Полный ответ AI: {result}") - return result.get("result", {}).get("response", "Не удалось получить ответ") - else: - print(f"Ошибка API: {response.status_code} - {response.text}") - return f"Ошибка API: {response.status_code}" + return data.get("result", {}).get("response", "Не удалось получить ответ") + except Exception as e: print(f"Ошибка AI: {e}") @@ -135,7 +155,7 @@ async def get_ai_help(self, task_title: str) -> str: # Создаем менеджер задач task_manager = CloudTaskManager(JSONBIN_API_KEY, JSONBIN_BIN_ID) -ai_service = CloudflareAI() +ai_service = CloudflareAI(CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID) async def add_ai_to_task(task_id: int, task_title: str):