From 3272ede7a64bafec4a125212573f46fccedbad88 Mon Sep 17 00:00:00 2001 From: Anton Shcherbak Date: Thu, 21 Nov 2024 23:08:05 +0300 Subject: [PATCH 01/25] fix: main --- git/src/main.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/git/src/main.py b/git/src/main.py index e3200827..f0fd22c8 100644 --- a/git/src/main.py +++ b/git/src/main.py @@ -6,6 +6,12 @@ class Order: def __init__(self, customer): self.customer = customer self.dishes = [] + + def remove_dish(self, dish): + if dish in self.dishes: + self.dishes.remove(dish) + else: + raise ValueError("Такого блюда нет в заказе.") def add_dish(self, dish): if isinstance(dish, Dish): @@ -13,12 +19,6 @@ def add_dish(self, dish): else: raise ValueError("Можно добавлять только объекты класса Dish.") - 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) @@ -36,6 +36,15 @@ 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 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 GroupOrder(Order): def __init__(self, customers): @@ -53,17 +62,6 @@ def __str__(self): 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"): From 6d52a1d80451eb8e43d7002d77f6e30e8d85dd03 Mon Sep 17 00:00:00 2001 From: Anton Shcherbak Date: Thu, 21 Nov 2024 23:12:57 +0300 Subject: [PATCH 02/25] fix: first-branch --- git/src/main.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/git/src/main.py b/git/src/main.py index f0fd22c8..54daeee7 100644 --- a/git/src/main.py +++ b/git/src/main.py @@ -7,24 +7,21 @@ def __init__(self, customer): self.customer = customer self.dishes = [] - def remove_dish(self, dish): - if dish in self.dishes: - self.dishes.remove(dish) - else: - raise ValueError("Такого блюда нет в заказе.") - 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) + 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() @@ -32,19 +29,14 @@ def final_total(self): final_total = total_with_tax * (1 + Order.SERVICE_CHARGE) return final_total + def apply_discount(self): + discount_rate = self.customer.get_discount() / 100 + return self.calculate_total() * (1 - discount_rate) + 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 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 GroupOrder(Order): def __init__(self, customers): @@ -61,7 +53,15 @@ 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"): From f32a7fae825d3a418a1a7e7ec141f5afa5073a80 Mon Sep 17 00:00:00 2001 From: Anton Shcherbak Date: Thu, 21 Nov 2024 23:15:30 +0300 Subject: [PATCH 03/25] fix: second task --- git/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/git/README.md b/git/README.md index 96f5d52d..1ff117ed 100644 --- a/git/README.md +++ b/git/README.md @@ -11,6 +11,7 @@ В пулл-реквесте по [ссылке](https://github.com/gardiys/fastapi-backend-course/pull/1) есть конфликт. Нужно: 1. Форкнуть к себе репозиторий 2. Разрешить конфликт в этой ветке, чтобы ветку можно было вмержить в мастер без потери логики +3. Проверить папку git/src с помощью линтера ruff на ошибки и несоответствия стандартам разработки на Python ## Задание 3: простой CI Нужно добавить конфигурацию CI с помощью GitHub Actions в свой репозиторий. From 5352f5e81bdb7898ff111346c9f0d0d86ddb32e5 Mon Sep 17 00:00:00 2001 From: Anton Shcherbak Date: Fri, 22 Nov 2024 00:29:30 +0300 Subject: [PATCH 04/25] fix: tasks --- simple_backend/README.md | 36 +++++++++++++++++-- simple_backend/src/task_tracker/main.py | 19 ++++++++++ .../src/task_tracker/requirements.txt | 2 ++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 simple_backend/src/task_tracker/main.py create mode 100644 simple_backend/src/task_tracker/requirements.txt diff --git a/simple_backend/README.md b/simple_backend/README.md index 856add4d..3aaa43de 100644 --- a/simple_backend/README.md +++ b/simple_backend/README.md @@ -1,3 +1,33 @@ -Задание 1 -- создайте приложение main.py на fastapi -- проверьте его на тестах \ No newline at end of file +# Задачи +## Задание 1 +Нужно локально запустить backend приложение. Пройдитесь по шагам: +1. Если еще не склонирован этот репозиторий, то склонируйте +2. Создайте в папке `simple_backend/src/task_tracker` виртуальное окружение +3. Активируйте `venv` +4. Установите записимости из `requirements.txt` +5. Запустите сервер с помощью команды `uvicorn main:app` +6. Перейдите по ссылке http://127.0.0.1:8000/docs и проверьте, что бэк работает +## Задание 2 +Вам нужно оживить бекенд из первой задачи и создать простой API для управления списком задач. Данные нужно хранить в оперативной памяти (например в списке Python). +В каждой из функций нужно прописать логику: +- get_tasks должен возвращать список всех задач +- create_task должен создавать новую задачу +- update_task должен обновлять информацию о задаче +- delete_task должен удалять задачу + +У задачи должны быть параметры: id, название, статус задачи. + +### Подзадачи +- Прочитайте, что такое "Хранение состояния", создайте в task_tracker readme.md файл и напишите в чём минусы подхода с хранением задач в оперативной памяти (списке python) +- Исправьте ситуацию и переделайте хранение информации о задачах в файле проекта. Информацию можно хранить например в формате json. +- Напишите в readme.md: + - что улучшилось после того, как список из оперативной памяти изменился на файл проекта? + - избавились ли мы таким способом от хранения состояния или нет? + - где еще можно хранить задачи и какие есть преимущества и недостатки этих подходов? +- Напишите класс для работы с файлом хранения задач в task_tracker и измените код проекта так, чтобы он работал с объектом этого класса. +- Сделайте свой backend - stateless с помощью интеграции с облачным сервисом (jsonbin.io, mockapi.io, github gist). Организуйте хранение и обновление json файла во внешнем сервисе. + +## Задание 3 + + +## Задание 4 diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py new file mode 100644 index 00000000..3db98d0d --- /dev/null +++ b/simple_backend/src/task_tracker/main.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/tasks") +def get_tasks(): + pass + +@app.post("/tasks") +def create_task(task): + pass + +@app.put("/tasks/{task_id}") +def update_task(task_id: int): + pass + +@app.delete("/tasks/{task_id}") +def delete_task(task_id: int): + pass diff --git a/simple_backend/src/task_tracker/requirements.txt b/simple_backend/src/task_tracker/requirements.txt new file mode 100644 index 00000000..8e0578a0 --- /dev/null +++ b/simple_backend/src/task_tracker/requirements.txt @@ -0,0 +1,2 @@ +fastapi +uvicorn[standard] \ No newline at end of file From de130486910bc328760a676a841a79ab06a7d3be Mon Sep 17 00:00:00 2001 From: Anton Shcherbak Date: Fri, 22 Nov 2024 00:37:44 +0300 Subject: [PATCH 05/25] fix: tasks --- simple_backend/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/simple_backend/README.md b/simple_backend/README.md index 3aaa43de..22c715aa 100644 --- a/simple_backend/README.md +++ b/simple_backend/README.md @@ -26,6 +26,7 @@ - где еще можно хранить задачи и какие есть преимущества и недостатки этих подходов? - Напишите класс для работы с файлом хранения задач в task_tracker и измените код проекта так, чтобы он работал с объектом этого класса. - Сделайте свой backend - stateless с помощью интеграции с облачным сервисом (jsonbin.io, mockapi.io, github gist). Организуйте хранение и обновление json файла во внешнем сервисе. +- Прочитайте что такое "состояние гонки" и напишите в readme файле о том, какие проблемы остались в бекенде на данном этапе проекта. Есть ли у вас какое-то решение этой пробелмы? ## Задание 3 From 8b2b7147a33d12ef421548c7dd411aa0270b3b0c Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 18 Feb 2025 00:27:32 +0300 Subject: [PATCH 06/25] feat: update with new lesson --- setting_environment/README.md | 39 ++++++++++++++++++++++++++--------- simple_backend/README.md | 7 +------ 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/setting_environment/README.md b/setting_environment/README.md index be432212..6fce2071 100644 --- a/setting_environment/README.md +++ b/setting_environment/README.md @@ -1,10 +1,29 @@ -Задание 1 -настройка Poetry -Задание 2 -Создание docker образа -Задание 3 -создание docker compose файла -Задание 4 -линтеры и pre-commit хуки -задание 5 -конфигурация проектов +# Задачи + +## Задание 1 +Переведите проект, полученный в разделе [Простой backend](/simple_backend/README.md) на Poetry. + +## Задание 2 +Установите в dev группу poetry линтер ruff. Настройте pre-commit хуки для ruff check и ruff format. +При каждом коммите у вас должны запускаться проверки с помощью линтеров. + +## Задание 3 +Заверните проект в Docker-образ. Критерии: +- Должен быть slim +- Внутри должны устанавливаться poetry зависимости +- Образ должен собираться +- С помощью образа можно запустить приложение на FastAPI + +## Задание 4 +Создайте docker-compose.yaml файл. Критерии: +- Должен быть сервис приложения +- Внутри должна быть команда запуска приложения + +## Задание 5 +Создайте makefile с основными командами для работы с вашим проектом. Критерии: +- Должны быть команды для запуска, перезагрузки, остановки проекта с помощью docker compose +- Должны быть команды для запуска pre-commit check + +## Задание 6 +Создайте файлы конфигурации `.env` и `.env.example`. `.env` должен быть в gitignore, а `.env.example` должен содержать структуру `.env` файла. +Подключите `.env` в docker compose с помощью `env_file:`. diff --git a/simple_backend/README.md b/simple_backend/README.md index 22c715aa..fcbc6778 100644 --- a/simple_backend/README.md +++ b/simple_backend/README.md @@ -26,9 +26,4 @@ - где еще можно хранить задачи и какие есть преимущества и недостатки этих подходов? - Напишите класс для работы с файлом хранения задач в task_tracker и измените код проекта так, чтобы он работал с объектом этого класса. - Сделайте свой backend - stateless с помощью интеграции с облачным сервисом (jsonbin.io, mockapi.io, github gist). Организуйте хранение и обновление json файла во внешнем сервисе. -- Прочитайте что такое "состояние гонки" и напишите в readme файле о том, какие проблемы остались в бекенде на данном этапе проекта. Есть ли у вас какое-то решение этой пробелмы? - -## Задание 3 - - -## Задание 4 +- Прочитайте что такое "состояние гонки" и напишите в readme файле о том, какие проблемы остались в бекенде на данном этапе проекта. Есть ли у вас какое-то решение этой проблемы? From 87afae5c1df02abe0e047ef38dc0377517522971 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 18 Feb 2025 00:56:30 +0300 Subject: [PATCH 07/25] feat: update tasks --- setting_environment/README.md | 1 + simple_backend/README.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/setting_environment/README.md b/setting_environment/README.md index 6fce2071..9495789e 100644 --- a/setting_environment/README.md +++ b/setting_environment/README.md @@ -27,3 +27,4 @@ ## Задание 6 Создайте файлы конфигурации `.env` и `.env.example`. `.env` должен быть в gitignore, а `.env.example` должен содержать структуру `.env` файла. Подключите `.env` в docker compose с помощью `env_file:`. +Вынесите все свои токены из проекта в `.env` \ No newline at end of file diff --git a/simple_backend/README.md b/simple_backend/README.md index fcbc6778..585135a1 100644 --- a/simple_backend/README.md +++ b/simple_backend/README.md @@ -27,3 +27,17 @@ - Напишите класс для работы с файлом хранения задач в task_tracker и измените код проекта так, чтобы он работал с объектом этого класса. - Сделайте свой backend - stateless с помощью интеграции с облачным сервисом (jsonbin.io, mockapi.io, github gist). Организуйте хранение и обновление json файла во внешнем сервисе. - Прочитайте что такое "состояние гонки" и напишите в readme файле о том, какие проблемы остались в бекенде на данном этапе проекта. Есть ли у вас какое-то решение этой проблемы? + + +## Задание 3 +Давайте прокачаем наш таск-треккер. Хочется, чтобы текст задачи заливался в LLM модель и она выдавала способы решения задачи и добавляла к её тексту. +Для того, чтобы это сделать: +- Настройте интеграцию с сервисом [Cloudflare](https://developers.cloudflare.com/workers-ai/get-started/rest-api/) через REST API. Для этого создайте новый класс для работы с этой API. +- При создании новой задачи отправляйте запрос с её текстом в LLM и просьбой объяснить как решать задачу +- Добавляйте полученный ответ в текст задачи + +## Задание 4 +Заметили, что в коде для работы с файлами и в коде для работы с LLM API есть похожие участки? Давайте избавимся от дублирования через наследование. +- Сделайте базовый класс BaseHTTPClient и вынесите в него общие функции и методы из двух классов +- Сделайте наследование от базового класса в клиентах +- С помощью абстрактных классов реализуйте абстрактные методы, которые должны быть в классах наследниках From adc836d287a494da8abaf52ccf4eb5e6cd113a3a Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 18 Feb 2025 00:59:26 +0300 Subject: [PATCH 08/25] fix: tasks --- README.md | 11 ++++++----- async_tasks/README.md | 0 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 async_tasks/README.md diff --git a/README.md b/README.md index 48de95e6..6c730951 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,9 @@ git clone git@github.com:gardiys/fastapi-backend-course.git - [ ] [Урок 3: Настройка окружения](./setting_environment/) - [ ] [Урок 4: Использование хранилищ](./using_storage/) - [ ] [Урок 5: Архитектура проекта](./project_architecture/) -- [ ] [Урок 6: Общение сервисов](./services_communication/) -- [ ] [Урок 7: Тестирование](./testing/) -- [ ] [Урок 8: Деплой сервисов](./services_deploy/) -- [ ] [Урок 9: Отказоустойчивость](./fault_tolerance/) -- [ ] [Урок 10: Мониторинг](./monitoring/) +- [ ] [Урок 6: Фоновые задачи](./async_tasks/) +- [ ] [Урок 7: Общение сервисов](./services_communication/) +- [ ] [Урок 8: Тестирование](./testing/) +- [ ] [Урок 9: Деплой сервисов](./services_deploy/) +- [ ] [Урок 10: Отказоустойчивость](./fault_tolerance/) +- [ ] [Урок 11: Мониторинг](./monitoring/) diff --git a/async_tasks/README.md b/async_tasks/README.md new file mode 100644 index 00000000..e69de29b From 2c9dd500a441e227d0e996cd973bc740a9ebb887 Mon Sep 17 00:00:00 2001 From: Anton Shcherbak <32383502+gardiys@users.noreply.github.com> Date: Tue, 25 Feb 2025 20:21:58 +0300 Subject: [PATCH 09/25] Update README.md --- git/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/README.md b/git/README.md index 1ff117ed..14f7c822 100644 --- a/git/README.md +++ b/git/README.md @@ -5,7 +5,7 @@ 3. Создать новую ветку с названием задачи "pull-request-task" и переключиться на неё 4. В проекте создать два новых файла main.py и config.py 5. Закоммитить изменения и запушить ветку в репозиторий -6. Затем создать пулл-реквест +6. Затем создать пулл-реквест с ветки pull-request-task на ветку main ## Задание 2: разрешение конфликтов В пулл-реквесте по [ссылке](https://github.com/gardiys/fastapi-backend-course/pull/1) есть конфликт. Нужно: From 20987c812685c460e1e3ead9593b267f51bf7dfb Mon Sep 17 00:00:00 2001 From: Anton Shcherbak <32383502+gardiys@users.noreply.github.com> Date: Thu, 27 Feb 2025 19:36:00 +0300 Subject: [PATCH 10/25] Update README.md --- simple_backend/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/simple_backend/README.md b/simple_backend/README.md index 585135a1..13bf0e54 100644 --- a/simple_backend/README.md +++ b/simple_backend/README.md @@ -1,4 +1,5 @@ # Задачи +**Во всех заданиях обязательно создание pull-request по аналогии из задачи блока git!** ## Задание 1 Нужно локально запустить backend приложение. Пройдитесь по шагам: 1. Если еще не склонирован этот репозиторий, то склонируйте From 0f4f66e523c5d8b706b9cb29276a814d7c461708 Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 27 Feb 2025 23:27:25 +0300 Subject: [PATCH 11/25] feat: change task --- git/src/main.py | 230 ++++++++++++++++++++++----------------- simple_backend/orders.py | 104 ++++++++++++++++++ 2 files changed, 232 insertions(+), 102 deletions(-) create mode 100644 simple_backend/orders.py diff --git a/git/src/main.py b/git/src/main.py index 54daeee7..1822c7e9 100644 --- a/git/src/main.py +++ b/git/src/main.py @@ -1,104 +1,130 @@ +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 save_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 # Обновляем переменную, чтобы сохранить изменения + save_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 + save_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 == '5': + print("Выход из программы.") + break -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.") - - 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 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 apply_discount(self): - discount_rate = self.customer.get_discount() / 100 - return self.calculate_total() * (1 - discount_rate) - - 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 + print("Некорректный ввод. Попробуйте ещё раз.") + +if __name__ == "__main__": + main() diff --git a/simple_backend/orders.py b/simple_backend/orders.py new file mode 100644 index 00000000..54daeee7 --- /dev/null +++ b/simple_backend/orders.py @@ -0,0 +1,104 @@ + +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.") + + 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 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 apply_discount(self): + discount_rate = self.customer.get_discount() / 100 + return self.calculate_total() * (1 - discount_rate) + + 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 From 4126798ad3f9520192dafdea6bf1f1cbdbc2da09 Mon Sep 17 00:00:00 2001 From: Anton Shcherbak <32383502+gardiys@users.noreply.github.com> Date: Fri, 7 Mar 2025 18:20:38 +0300 Subject: [PATCH 12/25] Update README.md --- simple_backend/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/simple_backend/README.md b/simple_backend/README.md index 13bf0e54..92622225 100644 --- a/simple_backend/README.md +++ b/simple_backend/README.md @@ -1,5 +1,6 @@ # Задачи **Во всех заданиях обязательно создание pull-request по аналогии из задачи блока git!** +**Ваш код обязательно должен запускаться и проверяться на работоспособность** ## Задание 1 Нужно локально запустить backend приложение. Пройдитесь по шагам: 1. Если еще не склонирован этот репозиторий, то склонируйте From bb5a993a30188858758002bf0d986e4dcb887d43 Mon Sep 17 00:00:00 2001 From: Anton Shcherbak <32383502+gardiys@users.noreply.github.com> Date: Fri, 7 Mar 2025 18:20:51 +0300 Subject: [PATCH 13/25] Update README.md --- simple_backend/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/simple_backend/README.md b/simple_backend/README.md index 92622225..fd203c5b 100644 --- a/simple_backend/README.md +++ b/simple_backend/README.md @@ -1,5 +1,6 @@ # Задачи **Во всех заданиях обязательно создание pull-request по аналогии из задачи блока git!** + **Ваш код обязательно должен запускаться и проверяться на работоспособность** ## Задание 1 Нужно локально запустить backend приложение. Пройдитесь по шагам: From 5d288a12cfe0a11fabb2a6b75d29d1bee9372750 Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Thu, 14 Aug 2025 03:18:28 +0300 Subject: [PATCH 14/25] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20CI=20=D0=B4=D0=BB=D1=8F=20Ruff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..5563a4bb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: CI +on: push +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + # Update output format to enable automatic inline annotations. + - name: Run Ruff + run: ruff check --output-format=github . \ No newline at end of file From 39ca49700d9a9cd74f692fbe204d87616ec4a7c4 Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Sat, 23 Aug 2025 19:00:03 +0300 Subject: [PATCH 15/25] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B8=D1=81=D0=BA=D0=BB=D1=8E=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B2=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 82f92755..75c9365b 100644 --- a/.gitignore +++ b/.gitignore @@ -129,6 +129,7 @@ venv/ ENV/ env.bak/ venv.bak/ +simple_backend/src/task_tracker/simple_backend_venv # Spyder project settings .spyderproject From 519a4d79b4a25e3569b2d104e2fbcb6bad5e2da6 Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Sat, 23 Aug 2025 19:02:52 +0300 Subject: [PATCH 16/25] =?UTF-8?q?=D0=9E=D0=B6=D0=B8=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=BA=D0=B5=D0=BD=D0=B4=20=D0=B8=D0=B7=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B2=D0=BE=D0=B9=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= =?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 | 0 simple_backend/src/task_tracker/main.py | 63 ++++++++++++++++++----- 2 files changed, 50 insertions(+), 13 deletions(-) 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..e69de29b diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 3db98d0d..442e974c 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,19 +1,56 @@ -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException, status +from typing import List, Optional +from pydantic import BaseModel +from itertools import count + app = FastAPI() -@app.get("/tasks") -def get_tasks(): - pass -@app.post("/tasks") -def create_task(task): - pass +class Task(BaseModel): + id: Optional[int] = None + title: str + status: str = "to do" + + +class TaskUpdate(BaseModel): + title: Optional[str] = None + status: Optional[str] = None + + +tasks_db: List[Task] = [] + +_id_seq = count(1) + + +@app.get("/tasks", response_model=List[Task]) +def get_tasks() -> List[Task]: + return tasks_db + +@app.post("/tasks", response_model=Task, status_code=status.HTTP_201_CREATED) +def create_task(task: Task) -> Task: + task.id = next(_id_seq) + tasks_db.append(task) + return task + +@app.put("/tasks/{task_id}", response_model=Task) +def update_task(task_id: int, task_update: TaskUpdate) -> Task: + for task in tasks_db: + if task.id == task_id: + if task_update.title is not None: + task.title = task_update.title + if task_update.status is not None: + task.status = task_update.status + + return task + raise HTTPException(status_code=404, detail="Task not found") + -@app.put("/tasks/{task_id}") -def update_task(task_id: int): - pass -@app.delete("/tasks/{task_id}") -def delete_task(task_id: int): - pass +@app.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_task(task_id: int) -> None: + for i, task in enumerate(tasks_db): + if task.id == task_id: + tasks_db.pop(i) + return + raise HTTPException(status_code=404, detail="Task not found") \ No newline at end of file From 6d1a3c61aa13714625e7ce8b5d38f55f7adf41b6 Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Sat, 23 Aug 2025 19:28:18 +0300 Subject: [PATCH 17/25] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлено описание минусов хранения задач в оперативной памяти. --- simple_backend/src/task_tracker/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/simple_backend/src/task_tracker/README.md b/simple_backend/src/task_tracker/README.md index e69de29b..85394fbb 100644 --- a/simple_backend/src/task_tracker/README.md +++ b/simple_backend/src/task_tracker/README.md @@ -0,0 +1,3 @@ +## Минусы хранения задач в оперативной памяти: +1. Список задач сбрасывается каждый раз после прекращения работы сервера. +2. При запуске двух серверов у каждого будет свой отдельный список задач. \ No newline at end of file From 84f468f3e8602cf8ed18ab3da770ddec88bb9f51 Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Sun, 24 Aug 2025 04:01:59 +0300 Subject: [PATCH 18/25] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=BE=20=D1=85=D1=80=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=20=D0=B2=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB=D0=B5=20data/tasks.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/task_tracker/data/tasks.json | 12 +++ simple_backend/src/task_tracker/main.py | 80 ++++++++++++++----- 2 files changed, 71 insertions(+), 21 deletions(-) create mode 100644 simple_backend/src/task_tracker/data/tasks.json diff --git a/simple_backend/src/task_tracker/data/tasks.json b/simple_backend/src/task_tracker/data/tasks.json new file mode 100644 index 00000000..b2e913b0 --- /dev/null +++ b/simple_backend/src/task_tracker/data/tasks.json @@ -0,0 +1,12 @@ +[ + { + "id": 1, + "title": "string", + "status": "to do" + }, + { + "id": 3, + "title": "string", + "status": "to do" + } +] \ 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 442e974c..8ee94b50 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,11 +1,14 @@ +import json from fastapi import FastAPI, HTTPException, status from typing import List, Optional from pydantic import BaseModel from itertools import count - +from pathlib import Path app = FastAPI() +DATA_FILE = Path("data/tasks.json") + class Task(BaseModel): id: Optional[int] = None @@ -18,39 +21,74 @@ class TaskUpdate(BaseModel): status: Optional[str] = None -tasks_db: List[Task] = [] +class JsonTasks: + def __init__(self, filename: Path): + self.file = filename + if not self.file.exists(): + self._write([]) + + tasks = self.get_all() + last_id = max((task.id or 0 for task in tasks), default=0) + self._id_counter = count(last_id + 1) + + def _write(self, tasks: List[Task]) -> None: + with self.file.open("w", encoding="utf-8") as file: + + json.dump([task.model_dump() for task in tasks], file, indent=2, ensure_ascii=False) + + def get_all(self) -> List[Task]: + with self.file.open("r", encoding="utf-8") as file: + tasks = json.load(file) + return [Task(**task) for task in tasks] + + def add(self, task: Task) -> Task: + tasks = self.get_all() + task.id = next(self._id_counter) + tasks.append(task) + self._write(tasks) + return task + + def update(self, task_id: int, task_update: TaskUpdate) -> Task: + tasks = self.get_all() + for i, task in enumerate(tasks): + if task.id == task_id: + if task_update.title is not None: + task.title = task_update.title + if task_update.status is not None: + task.status = task_update.status + tasks[i] = task + self._write(tasks) + return task + raise HTTPException(status_code=404, detail="Task not found") + + def delete(self, task_id: int) -> None: + tasks = self.get_all() + for i, task in enumerate(tasks): + if task.id == task_id: + tasks.pop(i) + self._write(tasks) + return + raise HTTPException(status_code=404, detail="Task not found") + + +tasks_db = JsonTasks(DATA_FILE) -_id_seq = count(1) @app.get("/tasks", response_model=List[Task]) def get_tasks() -> List[Task]: - return tasks_db + return tasks_db.get_all() @app.post("/tasks", response_model=Task, status_code=status.HTTP_201_CREATED) def create_task(task: Task) -> Task: - task.id = next(_id_seq) - tasks_db.append(task) - return task + return tasks_db.add(task) @app.put("/tasks/{task_id}", response_model=Task) def update_task(task_id: int, task_update: TaskUpdate) -> Task: - for task in tasks_db: - if task.id == task_id: - if task_update.title is not None: - task.title = task_update.title - if task_update.status is not None: - task.status = task_update.status - - return task - raise HTTPException(status_code=404, detail="Task not found") + return tasks_db.update(task_id, task_update) @app.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_task(task_id: int) -> None: - for i, task in enumerate(tasks_db): - if task.id == task_id: - tasks_db.pop(i) - return - raise HTTPException(status_code=404, detail="Task not found") \ No newline at end of file + tasks_db.delete(task_id) \ No newline at end of file From 50e6d5fc67b0d3b585b050e787f636a9daa80869 Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Sun, 24 Aug 2025 04:21:47 +0300 Subject: [PATCH 19/25] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/simple_backend/src/task_tracker/README.md b/simple_backend/src/task_tracker/README.md index 85394fbb..ef05d1d7 100644 --- a/simple_backend/src/task_tracker/README.md +++ b/simple_backend/src/task_tracker/README.md @@ -1,3 +1,16 @@ -## Минусы хранения задач в оперативной памяти: +### Минусы хранения задач в оперативной памяти: 1. Список задач сбрасывается каждый раз после прекращения работы сервера. -2. При запуске двух серверов у каждого будет свой отдельный список задач. \ No newline at end of file +2. При запуске двух серверов у каждого будет свой отдельный список задач. + + +### После реализации через JSON: +1. Улучшилось: +- Задачи не пропадают после прекращения работы сервера. +- Можно синхронизировать файл. + +2. Избавились ли от хранения состояния: +Нет, т.к. файл хранится локально и при каждом изменении читается и перезаписывается. + +3. Где еще можно хранить: +- БД: масштабируемость и скорость при больших объемах данных, но сложнее в реализации. +- Облачное хранилище: доступ к данным из любого места, высокая надежность, но задержки и зависимость от внешних сервисов. \ No newline at end of file From 1d122b45fd95f2059c7435f0639e5c5920388263 Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Sun, 24 Aug 2025 04:31:43 +0300 Subject: [PATCH 20/25] =?UTF-8?q?=D0=92=D1=81=D0=B5=20=D0=BA=D0=BB=D0=B0?= =?UTF-8?q?=D1=81=D1=81=D1=8B=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB=20storage.p?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/main.py | 71 +--------------------- simple_backend/src/task_tracker/storage.py | 66 ++++++++++++++++++++ 2 files changed, 69 insertions(+), 68 deletions(-) create mode 100644 simple_backend/src/task_tracker/storage.py diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 8ee94b50..59fc46f7 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,76 +1,11 @@ -import json from fastapi import FastAPI, HTTPException, status -from typing import List, Optional -from pydantic import BaseModel -from itertools import count +from typing import List from pathlib import Path -app = FastAPI() +from storage import JsonTasks, Task, TaskUpdate +app = FastAPI() DATA_FILE = Path("data/tasks.json") - - -class Task(BaseModel): - id: Optional[int] = None - title: str - status: str = "to do" - - -class TaskUpdate(BaseModel): - title: Optional[str] = None - status: Optional[str] = None - - -class JsonTasks: - def __init__(self, filename: Path): - self.file = filename - if not self.file.exists(): - self._write([]) - - tasks = self.get_all() - last_id = max((task.id or 0 for task in tasks), default=0) - self._id_counter = count(last_id + 1) - - def _write(self, tasks: List[Task]) -> None: - with self.file.open("w", encoding="utf-8") as file: - - json.dump([task.model_dump() for task in tasks], file, indent=2, ensure_ascii=False) - - def get_all(self) -> List[Task]: - with self.file.open("r", encoding="utf-8") as file: - tasks = json.load(file) - return [Task(**task) for task in tasks] - - def add(self, task: Task) -> Task: - tasks = self.get_all() - task.id = next(self._id_counter) - tasks.append(task) - self._write(tasks) - return task - - def update(self, task_id: int, task_update: TaskUpdate) -> Task: - tasks = self.get_all() - for i, task in enumerate(tasks): - if task.id == task_id: - if task_update.title is not None: - task.title = task_update.title - if task_update.status is not None: - task.status = task_update.status - tasks[i] = task - self._write(tasks) - return task - raise HTTPException(status_code=404, detail="Task not found") - - def delete(self, task_id: int) -> None: - tasks = self.get_all() - for i, task in enumerate(tasks): - if task.id == task_id: - tasks.pop(i) - self._write(tasks) - return - raise HTTPException(status_code=404, detail="Task not found") - - tasks_db = JsonTasks(DATA_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..2bebf1f8 --- /dev/null +++ b/simple_backend/src/task_tracker/storage.py @@ -0,0 +1,66 @@ +import json +from fastapi import HTTPException +from typing import List, Optional +from pydantic import BaseModel +from itertools import count +from pathlib import Path + +class Task(BaseModel): + id: Optional[int] = None + title: str + status: str = "to do" + + +class TaskUpdate(BaseModel): + title: Optional[str] = None + status: Optional[str] = None + + +class JsonTasks: + def __init__(self, filename: Path): + self.file = filename + if not self.file.exists(): + self._write([]) + + tasks = self.get_all() + last_id = max((task.id or 0 for task in tasks), default=0) + self._id_counter = count(last_id + 1) + + def _write(self, tasks: List[Task]) -> None: + with self.file.open("w", encoding="utf-8") as file: + + json.dump([task.model_dump() for task in tasks], file, indent=2, ensure_ascii=False) + + def get_all(self) -> List[Task]: + with self.file.open("r", encoding="utf-8") as file: + tasks = json.load(file) + return [Task(**task) for task in tasks] + + def add(self, task: Task) -> Task: + tasks = self.get_all() + task.id = next(self._id_counter) + tasks.append(task) + self._write(tasks) + return task + + def update(self, task_id: int, task_update: TaskUpdate) -> Task: + tasks = self.get_all() + for i, task in enumerate(tasks): + if task.id == task_id: + if task_update.title is not None: + task.title = task_update.title + if task_update.status is not None: + task.status = task_update.status + tasks[i] = task + self._write(tasks) + return task + raise HTTPException(status_code=404, detail="Task not found") + + def delete(self, task_id: int) -> None: + tasks = self.get_all() + for i, task in enumerate(tasks): + if task.id == task_id: + tasks.pop(i) + self._write(tasks) + return + raise HTTPException(status_code=404, detail="Task not found") From 3890deaef9d82ccb91d2abc6cbc891c1fb31da39 Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Sun, 24 Aug 2025 07:02:48 +0300 Subject: [PATCH 21/25] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=20ba?= =?UTF-8?q?ckend=20-=20stateless=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20mockap?= =?UTF-8?q?i.io?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/task_tracker/cloud_storage.py | 70 +++++++++++++++++++ simple_backend/src/task_tracker/main.py | 25 ++++--- .../src/task_tracker/requirements.txt | 3 +- 3 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 simple_backend/src/task_tracker/cloud_storage.py 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..3e1dd9ce --- /dev/null +++ b/simple_backend/src/task_tracker/cloud_storage.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass +from typing import List, Optional +from pydantic import BaseModel +import requests + + +class Task(BaseModel): + id: Optional[str] = None + title: str + status: str = "to do" + + +class TaskUpdate(BaseModel): + title: Optional[str] = None + status: Optional[str] = None + + +@dataclass +class MockApiConfig: + api_url: str + resource: str = "tasks" + + +class MockApiTasks: + def __init__(self, cfg: MockApiConfig, timeout_sec: float = 10.0): + self._cfg = cfg + self._timeout = timeout_sec + self._session = requests.Session() + + def _collections_url(self) -> str: + return f"{self._cfg.api_url.rstrip('/')}/{self._cfg.resource}" + + def _item_url(self, task_id: str) -> str: + return f"{self._collections_url().rstrip('/')}/{task_id}" + + def get_all(self) -> List[Task]: + response = self._session.get(self._collections_url(), timeout=self._timeout) + response.raise_for_status() + data = response.json() + return [Task(**task) for task in data] + + def get_by_id(self, task_id: str) -> Task: + response = self._session.get(self._item_url(task_id), timeout=self._timeout) + if response.status_code == 404: + raise KeyError("Task not found") + response.raise_for_status() + return Task(**response.json()) + + def add(self, task_in: Task) -> Task: + payload = task_in.model_dump(exclude={"id"}) + response = self._session.post(self._collections_url(), json=payload, timeout=self._timeout) + response.raise_for_status() + return Task(**response.json()) + + def update(self, task_id: str, task_update: TaskUpdate) -> Task: + payload = task_update.model_dump(exclude_none=True) + if not payload: + return self.get_by_id(task_id) + + response = self._session.put(self._item_url(task_id), json=payload, timeout=self._timeout) + if response.status_code == 404: + raise KeyError("Task not found") + response.raise_for_status() + return Task(**response.json()) + + def delete(self, task_id: str) -> None: + response = self._session.delete(self._item_url(task_id), timeout=self._timeout) + if response.status_code == 404: + raise KeyError("Task not found") + response.raise_for_status() \ 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 59fc46f7..75bd4a9d 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -2,28 +2,37 @@ from typing import List from pathlib import Path -from storage import JsonTasks, Task, TaskUpdate +from cloud_storage import MockApiTasks, MockApiConfig, Task, TaskUpdate app = FastAPI() -DATA_FILE = Path("data/tasks.json") -tasks_db = JsonTasks(DATA_FILE) +MOCKAPI_API_URL = "https://68aa779b909a5835049c516f.mockapi.io/" +MOCKAPI_RESOURCE = "tasks" + +tasks_db = MockApiTasks(MockApiConfig(api_url=MOCKAPI_API_URL, resource=MOCKAPI_RESOURCE)) @app.get("/tasks", response_model=List[Task]) def get_tasks() -> List[Task]: return tasks_db.get_all() + @app.post("/tasks", response_model=Task, status_code=status.HTTP_201_CREATED) def create_task(task: Task) -> Task: return tasks_db.add(task) -@app.put("/tasks/{task_id}", response_model=Task) -def update_task(task_id: int, task_update: TaskUpdate) -> Task: - return tasks_db.update(task_id, task_update) +@app.put("/tasks/{task_id}", response_model=Task) +def update_task(task_id: str, task_update: TaskUpdate) -> Task: + try: + return tasks_db.update(task_id, task_update) + except KeyError: + raise HTTPException(status_code=404, detail="Task not found") @app.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT) -def delete_task(task_id: int) -> None: - tasks_db.delete(task_id) \ No newline at end of file +def delete_task(task_id: str) -> None: + try: + tasks_db.delete(task_id) + except KeyError: + raise HTTPException(status_code=404, detail="Task not found") 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 From 564f2b9606dad47f1f58e3b11ea1c939f2b3aa7b Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Sun, 24 Aug 2025 07:11:14 +0300 Subject: [PATCH 22/25] =?UTF-8?q?=D0=98=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/simple_backend/src/task_tracker/README.md b/simple_backend/src/task_tracker/README.md index ef05d1d7..9aa93b42 100644 --- a/simple_backend/src/task_tracker/README.md +++ b/simple_backend/src/task_tracker/README.md @@ -13,4 +13,10 @@ 3. Где еще можно хранить: - БД: масштабируемость и скорость при больших объемах данных, но сложнее в реализации. -- Облачное хранилище: доступ к данным из любого места, высокая надежность, но задержки и зависимость от внешних сервисов. \ No newline at end of file +- Облачное хранилище: доступ к данным из любого места, высокая надежность, но задержки и зависимость от внешних сервисов. + + +### Состояние гонки: +1. Проблема параллельной запииси. +2. Решения: использовать БД или блокировку threading.Lock(). + From 433ef39ac73286004b3b5233480fbee6900b1105 Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Sun, 24 Aug 2025 16:41:40 +0300 Subject: [PATCH 23/25] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=203.=20=D0=98=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D1=81=20Clouflare=20AI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/task_tracker/llm_assistant.py | 23 +++++++++++++++++++ simple_backend/src/task_tracker/main.py | 8 +++++++ 2 files changed, 31 insertions(+) create mode 100644 simple_backend/src/task_tracker/llm_assistant.py diff --git a/simple_backend/src/task_tracker/llm_assistant.py b/simple_backend/src/task_tracker/llm_assistant.py new file mode 100644 index 00000000..72f80492 --- /dev/null +++ b/simple_backend/src/task_tracker/llm_assistant.py @@ -0,0 +1,23 @@ +import requests + + + +class LLMAssistant: + + def __init__(self, api_token: str, account_id: str, model: str = "@cf/meta/llama-3-8b-instruct"): + self.api_token = api_token + self.account_id = account_id + self.model = model + + self.base_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/" + self.headers = {"Authorization": f"Bearer {api_token}"} + + def task_llm(self, task_text: str) -> str: + input = {"messages": [ + {"role": "system", + "content": f"Напиши короткое решение задачи (до 150 символов): {task_text}"} + ]} + response = requests.post(f"{self.base_url}{self.model}", headers=self.headers, json=input) + if response.status_code == 200: + return response.json().get("result", {}).get("response", "") + return f"Ошибка LLM: {response.status_code} {response.text}" \ 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 75bd4a9d..1df41adc 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -3,12 +3,15 @@ from pathlib import Path from cloud_storage import MockApiTasks, MockApiConfig, Task, TaskUpdate +from llm_assistant import LLMAssistant app = FastAPI() MOCKAPI_API_URL = "https://68aa779b909a5835049c516f.mockapi.io/" MOCKAPI_RESOURCE = "tasks" +llm = LLMAssistant(api_token="API_TOKEN", account_id="ACCOUNT_ID") + tasks_db = MockApiTasks(MockApiConfig(api_url=MOCKAPI_API_URL, resource=MOCKAPI_RESOURCE)) @@ -19,6 +22,11 @@ def get_tasks() -> List[Task]: @app.post("/tasks", response_model=Task, status_code=status.HTTP_201_CREATED) def create_task(task: Task) -> Task: + answer = llm.task_llm(task.title) + if answer: + task.title = f"{task.title}\n\nРешение от AI: {answer}" + else: + task.title = f"{task.title}\n\Не удалось получить решение от AI" return tasks_db.add(task) From 1900552caac3d0a2ec8a58f2340be6130f7b9b2b Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Sun, 24 Aug 2025 23:28:44 +0300 Subject: [PATCH 24/25] =?UTF-8?q?=D0=97=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=204.=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=20BaseHTTPClient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/task_tracker/base_http_client.py | 59 +++++++++++++++++++ .../src/task_tracker/cloud_storage.py | 29 ++++----- .../src/task_tracker/llm_assistant.py | 16 +++-- 3 files changed, 84 insertions(+), 20 deletions(-) create mode 100644 simple_backend/src/task_tracker/base_http_client.py diff --git a/simple_backend/src/task_tracker/base_http_client.py b/simple_backend/src/task_tracker/base_http_client.py new file mode 100644 index 00000000..fe688296 --- /dev/null +++ b/simple_backend/src/task_tracker/base_http_client.py @@ -0,0 +1,59 @@ +import requests +from abc import ABC, abstractmethod +from typing import Dict, Optional, Any + +class BaseHTTPClient(ABC): + def __init__( + self, + api_url: str, + headers: Optional[Dict[str, str]] = None, + *, + default_timeout: float = 15.0): + self.api_url = api_url + self._headers = headers or {} + self._default_timeout = float(default_timeout) + self._session = requests.Session() + + @abstractmethod + def _default_headers(self) -> Dict[str, str]: + raise NotImplementedError + + def build_url(self, path: str = "") -> str: + return self.api_url + path.lstrip("/") + + def request(self, + method: str, + path: str = "", + *, + params: Optional[Dict[str, Any]] = None, + json: Optional[Any] = None, + data: Optional[Any] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Optional[float] = None) -> requests.Response: + merged_headers = {**self._headers, **(headers or {})} + response = self._session.request(method=method.upper(), + url=self.build_url(self.build_url(path)), + params=params, + json=json, + data=data, + headers=merged_headers, + timeout=self._default_timeout if timeout is None else float(timeout)) + return response + + def ensure_ok(self, resp: requests.Response) -> None: + try: + resp.raise_for_status() + except requests.HTTPError as exc: + raise requests.HTTPError(f"HTTP {resp.status_code}: {resp.text}") from exc + + def get(self, path: str = "", **kwargs) -> requests.Response: + return self.request("GET", path, **kwargs) + + def post(self, path: str = "", **kwargs) -> requests.Response: + return self.request("POST", path, **kwargs) + + def put(self, path: str = "", **kwargs) -> requests.Response: + return self.request("PUT", path, **kwargs) + + def delete(self, path: str = "", **kwargs) -> requests.Response: + return self.request("DELETE", path, **kwargs) \ 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 index 3e1dd9ce..9665a576 100644 --- a/simple_backend/src/task_tracker/cloud_storage.py +++ b/simple_backend/src/task_tracker/cloud_storage.py @@ -1,8 +1,7 @@ from dataclasses import dataclass -from typing import List, Optional +from typing import List, Optional, Dict from pydantic import BaseModel -import requests - +from base_http_client import BaseHTTPClient class Task(BaseModel): id: Optional[str] = None @@ -21,26 +20,28 @@ class MockApiConfig: resource: str = "tasks" -class MockApiTasks: - def __init__(self, cfg: MockApiConfig, timeout_sec: float = 10.0): +class MockApiTasks(BaseHTTPClient): + def __init__(self, cfg: MockApiConfig): self._cfg = cfg - self._timeout = timeout_sec - self._session = requests.Session() + super().__init__(api_url=cfg.api_url, default_timeout=20.0) + + def _default_headers(self) -> Dict[str, str]: + return {} def _collections_url(self) -> str: - return f"{self._cfg.api_url.rstrip('/')}/{self._cfg.resource}" + return self._cfg.resource def _item_url(self, task_id: str) -> str: - return f"{self._collections_url().rstrip('/')}/{task_id}" + return f"{self._collections_url()}/{task_id}" def get_all(self) -> List[Task]: - response = self._session.get(self._collections_url(), timeout=self._timeout) + response = self.get(self._collections_url()) response.raise_for_status() data = response.json() return [Task(**task) for task in data] def get_by_id(self, task_id: str) -> Task: - response = self._session.get(self._item_url(task_id), timeout=self._timeout) + response = self.get(self._item_url(task_id)) if response.status_code == 404: raise KeyError("Task not found") response.raise_for_status() @@ -48,7 +49,7 @@ def get_by_id(self, task_id: str) -> Task: def add(self, task_in: Task) -> Task: payload = task_in.model_dump(exclude={"id"}) - response = self._session.post(self._collections_url(), json=payload, timeout=self._timeout) + response = self.post(self._collections_url(), json=payload) response.raise_for_status() return Task(**response.json()) @@ -57,14 +58,14 @@ def update(self, task_id: str, task_update: TaskUpdate) -> Task: if not payload: return self.get_by_id(task_id) - response = self._session.put(self._item_url(task_id), json=payload, timeout=self._timeout) + response = self.put(self._item_url(task_id), json=payload) if response.status_code == 404: raise KeyError("Task not found") response.raise_for_status() return Task(**response.json()) def delete(self, task_id: str) -> None: - response = self._session.delete(self._item_url(task_id), timeout=self._timeout) + response = self.delete(self._item_url(task_id)) if response.status_code == 404: raise KeyError("Task not found") response.raise_for_status() \ No newline at end of file diff --git a/simple_backend/src/task_tracker/llm_assistant.py b/simple_backend/src/task_tracker/llm_assistant.py index 72f80492..00b405ef 100644 --- a/simple_backend/src/task_tracker/llm_assistant.py +++ b/simple_backend/src/task_tracker/llm_assistant.py @@ -1,23 +1,27 @@ -import requests +from typing import Dict, Any +from base_http_client import BaseHTTPClient - -class LLMAssistant: +class LLMAssistant(BaseHTTPClient): def __init__(self, api_token: str, account_id: str, model: str = "@cf/meta/llama-3-8b-instruct"): self.api_token = api_token self.account_id = account_id self.model = model + api_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/" + super().__init__(api_url=api_url, default_timeout=20.0) - self.base_url = f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/" - self.headers = {"Authorization": f"Bearer {api_token}"} + def _default_headers(self) -> Dict[str, str]: + return {"Authorization": f"Bearer {self.api_token}"} def task_llm(self, task_text: str) -> str: input = {"messages": [ {"role": "system", "content": f"Напиши короткое решение задачи (до 150 символов): {task_text}"} ]} - response = requests.post(f"{self.base_url}{self.model}", headers=self.headers, json=input) + + response = self.post(self.model, json=input) + if response.status_code == 200: return response.json().get("result", {}).get("response", "") return f"Ошибка LLM: {response.status_code} {response.text}" \ No newline at end of file From 593444c2d5471902d04ee77ebbd8632c4685eb2b Mon Sep 17 00:00:00 2001 From: Trofim Ivanov Date: Sun, 24 Aug 2025 23:33:52 +0300 Subject: [PATCH 25/25] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BD=D0=B5=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=B7=D1=83=D0=B5=D0=BC=D1=8B=D1=85=20=D0=B8=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple_backend/src/task_tracker/llm_assistant.py | 2 +- simple_backend/src/task_tracker/main.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/simple_backend/src/task_tracker/llm_assistant.py b/simple_backend/src/task_tracker/llm_assistant.py index 00b405ef..b71be1c1 100644 --- a/simple_backend/src/task_tracker/llm_assistant.py +++ b/simple_backend/src/task_tracker/llm_assistant.py @@ -1,4 +1,4 @@ -from typing import Dict, Any +from typing import Dict from base_http_client import BaseHTTPClient diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 1df41adc..d59cff0f 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,6 +1,5 @@ from fastapi import FastAPI, HTTPException, status from typing import List -from pathlib import Path from cloud_storage import MockApiTasks, MockApiConfig, Task, TaskUpdate from llm_assistant import LLMAssistant