From 3272ede7a64bafec4a125212573f46fccedbad88 Mon Sep 17 00:00:00 2001 From: Anton Shcherbak Date: Thu, 21 Nov 2024 23:08:05 +0300 Subject: [PATCH 01/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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/38] 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 0eba75a0472781bf8c70cc82df7dc14a54aad688 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 15:34:39 +0300 Subject: [PATCH 14/38] Task1 --- simple_backend/src/task_tracker/main.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 simple_backend/src/task_tracker/main.py diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py new file mode 100644 index 00000000..0301c7c5 --- /dev/null +++ b/simple_backend/src/task_tracker/main.py @@ -0,0 +1,20 @@ +'''backend''' +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 From 169e70f9fd82a61593b8b40357bb329f6378e223 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 16:23:37 +0300 Subject: [PATCH 15/38] task2 backend with state storage --- simple_backend/src/task_tracker/main.py | 37 +++++++++++++++++-------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 0301c7c5..178f03da 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,20 +1,35 @@ '''backend''' -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException app = FastAPI() - +tasks = [] +task_id_counter = 1 +# return tasks list @app.get("/tasks") def get_tasks(): - pass - + return tasks +#create a new task with id, name, and status @app.post("/tasks") -def create_task(task): - pass - +def create_task(name: str, condition: str = 'new'): + global task_id_counter + task = {'id': task_id_counter, 'name': name, 'condition': condition} + tasks.append(task) + task_id_counter += 1 + return task +#update task @app.put("/tasks/{task_id}") -def update_task(task_id: int): - pass - +def update_task(task_id: int, name: str, condition: str): + for task in tasks: + if task['id'] == task_id: + task['name'] = name + task['condition'] = condition + return task + raise HTTPException(status_code = 404, detail = 'task not found') +#delete task @app.delete("/tasks/{task_id}") def delete_task(task_id: int): - pass + for task in tasks: + if task['id'] == task_id: + tasks.remove(task) + return 'task deleted' + \ No newline at end of file From 269a8a71a862dc48a44ccc73835555a80229a55a Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 16:39:03 +0300 Subject: [PATCH 16/38] storage class with json --- simple_backend/src/task_tracker/main.py | 19 ++++++++++++------- simple_backend/src/task_tracker/storage.py | 13 +++++++++++++ simple_backend/src/task_tracker/tasks.json | 1 + 3 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 simple_backend/src/task_tracker/storage.py 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 178f03da..403953a5 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,35 +1,40 @@ '''backend''' from fastapi import FastAPI, HTTPException - +from storage import TaskStorage app = FastAPI() -tasks = [] -task_id_counter = 1 +storage = TaskStorage() # return tasks list @app.get("/tasks") def get_tasks(): - return tasks + return storage.load() #create a new task with id, name, and status @app.post("/tasks") def create_task(name: str, condition: str = 'new'): - global task_id_counter - task = {'id': task_id_counter, 'name': name, 'condition': condition} + tasks = storage.load() + new_id = len(tasks) + 1 + task = {'id': new_id, 'name': name, 'condition': condition} tasks.append(task) - task_id_counter += 1 + storage.save(tasks) return task #update task @app.put("/tasks/{task_id}") def update_task(task_id: int, name: str, condition: str): + tasks = storage.load() for task in tasks: if task['id'] == task_id: task['name'] = name task['condition'] = condition + storage.save(tasks) return task raise HTTPException(status_code = 404, detail = 'task not found') #delete task @app.delete("/tasks/{task_id}") def delete_task(task_id: int): + tasks = storage.load() for task in tasks: if task['id'] == task_id: tasks.remove(task) + storage.save(tasks) return 'task deleted' + raise HTTPException(status_code = 404, detail = 'task not found') \ No newline at end of file diff --git a/simple_backend/src/task_tracker/storage.py b/simple_backend/src/task_tracker/storage.py new file mode 100644 index 00000000..ce237692 --- /dev/null +++ b/simple_backend/src/task_tracker/storage.py @@ -0,0 +1,13 @@ +import json +import os +class TaskStorage: + def __init__(self, filename: str = 'tasks.json'): + self.file = filename + if not os.path.exists(self.file): # if file does not exist, create file with empty list + self.save([]) + def load(self): + with open(self.file, 'r', encoding = 'utf-8') as f: + return json.load(f) + def save(self, tasks): + with open(self.file, 'w', encoding = 'utf-8') as f: + json.dump(tasks, f) \ No newline at end of file diff --git a/simple_backend/src/task_tracker/tasks.json b/simple_backend/src/task_tracker/tasks.json new file mode 100644 index 00000000..80c933c8 --- /dev/null +++ b/simple_backend/src/task_tracker/tasks.json @@ -0,0 +1 @@ +[{"id": 1, "name": "finaltask", "condition": "updated"}, {"id": 2, "name": "totaltask", "condition": "updated"}, {"id": 3, "name": "Task3", "condition": "new"}] \ No newline at end of file From dc061538269a54b358cf77000e9bae0affba3240 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 18:34:59 +0300 Subject: [PATCH 17/38] Add files via upload --- simple_backend/src/task_tracker/.env | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 simple_backend/src/task_tracker/.env diff --git a/simple_backend/src/task_tracker/.env b/simple_backend/src/task_tracker/.env new file mode 100644 index 00000000..7ca83ca8 --- /dev/null +++ b/simple_backend/src/task_tracker/.env @@ -0,0 +1,2 @@ +GITHUB_TOKEN=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 +GIST_ID=69fc2b19cd06bf212c1107481b1bad6f From efd0bb46723c80cf86d3b6bef56771a02f51f982 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 18:36:41 +0300 Subject: [PATCH 18/38] storage gist add --- simple_backend/src/task_tracker/main.py | 7 ++- .../src/task_tracker/storage_gist.py | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 simple_backend/src/task_tracker/storage_gist.py diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index 403953a5..6209f4dd 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,8 +1,8 @@ '''backend''' from fastapi import FastAPI, HTTPException -from storage import TaskStorage +from storage_gist import GistStorage app = FastAPI() -storage = TaskStorage() +storage = GistStorage() # return tasks list @app.get("/tasks") def get_tasks(): @@ -36,5 +36,4 @@ def delete_task(task_id: int): tasks.remove(task) storage.save(tasks) return 'task deleted' - raise HTTPException(status_code = 404, detail = 'task not found') - \ No newline at end of file + raise HTTPException(status_code = 404, detail = 'task not found') \ No newline at end of file diff --git a/simple_backend/src/task_tracker/storage_gist.py b/simple_backend/src/task_tracker/storage_gist.py new file mode 100644 index 00000000..6f548389 --- /dev/null +++ b/simple_backend/src/task_tracker/storage_gist.py @@ -0,0 +1,45 @@ +import os +import requests +import json +from dotenv import load_dotenv +load_dotenv() +GITHUB_API = 'https://api.github.com' +class GistStorage: + def __init__(self, token: str = None, gist_id: str = None, filename: str = 'tasks.json'): + self.token: str = token or os.getenv('GITHUB_TOKEN') + self.gist_id: str = gist_id or os.getenv('GIST_ID') + self.filename: str = filename + if not self.token or not self.gist_id: + raise RuntimeError('GITHUB_TOKEN and GIST_ID must be given') + self.headers: dict = { + 'Authorization': f'token {self.token}', + 'Accept': 'application/vnd.github+json' + } + + def _gist_url(self) -> str: + return f'{GITHUB_API}/gists/{self.gist_id}' + # loading task list from gist + def load(self) -> list[dict]: + r = requests.get(self._gist_url(), headers = self.headers, timeout = 10) + r.raise_for_status() + gist_data: dict = r.json() + files: dict = gist_data.get('files', {}) + if self.filename not in files: + return [] + content: str = files[self.filename]['content'] + try: + data: dict = json.loads(content) + return data.get('tasks', []) + except json.JSONDecodeError: + return [] + # saving task list on gist + def save(self, tasks: list[dict]) -> None: + body: dict = { + 'files': { + self.filename: { + 'content': json.dumps({'tasks': tasks}) + } + } + } + r = requests.patch(self._gist_url(), headers = self.headers, json = body, timeout = 10) + r.raise_for_status() From 073cc99665c6b236f7734575b8a766b76274a43e Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 19:12:53 +0300 Subject: [PATCH 19/38] Delete simple_backend/src/task_tracker/.env --- simple_backend/src/task_tracker/.env | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 simple_backend/src/task_tracker/.env diff --git a/simple_backend/src/task_tracker/.env b/simple_backend/src/task_tracker/.env deleted file mode 100644 index 7ca83ca8..00000000 --- a/simple_backend/src/task_tracker/.env +++ /dev/null @@ -1,2 +0,0 @@ -GITHUB_TOKEN=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 -GIST_ID=69fc2b19cd06bf212c1107481b1bad6f From cafa301bea0a62f28813535a5f7f2830bc254a17 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 19:55:59 +0300 Subject: [PATCH 20/38] Delete simple_backend/src/task_tracker/tasks.json --- simple_backend/src/task_tracker/tasks.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 simple_backend/src/task_tracker/tasks.json diff --git a/simple_backend/src/task_tracker/tasks.json b/simple_backend/src/task_tracker/tasks.json deleted file mode 100644 index 80c933c8..00000000 --- a/simple_backend/src/task_tracker/tasks.json +++ /dev/null @@ -1 +0,0 @@ -[{"id": 1, "name": "finaltask", "condition": "updated"}, {"id": 2, "name": "totaltask", "condition": "updated"}, {"id": 3, "name": "Task3", "condition": "new"}] \ No newline at end of file From 128e409943ea157f9f70f04c3423bc31fb8a20f0 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 19:58:39 +0300 Subject: [PATCH 21/38] Delete simple_backend/src/task_tracker/main.py --- simple_backend/src/task_tracker/main.py | 39 ------------------------- 1 file changed, 39 deletions(-) delete mode 100644 simple_backend/src/task_tracker/main.py diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py deleted file mode 100644 index 6209f4dd..00000000 --- a/simple_backend/src/task_tracker/main.py +++ /dev/null @@ -1,39 +0,0 @@ -'''backend''' -from fastapi import FastAPI, HTTPException -from storage_gist import GistStorage -app = FastAPI() -storage = GistStorage() -# return tasks list -@app.get("/tasks") -def get_tasks(): - return storage.load() -#create a new task with id, name, and status -@app.post("/tasks") -def create_task(name: str, condition: str = 'new'): - tasks = storage.load() - new_id = len(tasks) + 1 - task = {'id': new_id, 'name': name, 'condition': condition} - tasks.append(task) - storage.save(tasks) - return task -#update task -@app.put("/tasks/{task_id}") -def update_task(task_id: int, name: str, condition: str): - tasks = storage.load() - for task in tasks: - if task['id'] == task_id: - task['name'] = name - task['condition'] = condition - storage.save(tasks) - return task - raise HTTPException(status_code = 404, detail = 'task not found') -#delete task -@app.delete("/tasks/{task_id}") -def delete_task(task_id: int): - tasks = storage.load() - for task in tasks: - if task['id'] == task_id: - tasks.remove(task) - storage.save(tasks) - return 'task deleted' - raise HTTPException(status_code = 404, detail = 'task not found') \ No newline at end of file From d77a82efe97cf73d46d94049753db66de47cbab4 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 19:58:47 +0300 Subject: [PATCH 22/38] Delete simple_backend/src/task_tracker/storage.py --- simple_backend/src/task_tracker/storage.py | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 simple_backend/src/task_tracker/storage.py diff --git a/simple_backend/src/task_tracker/storage.py b/simple_backend/src/task_tracker/storage.py deleted file mode 100644 index ce237692..00000000 --- a/simple_backend/src/task_tracker/storage.py +++ /dev/null @@ -1,13 +0,0 @@ -import json -import os -class TaskStorage: - def __init__(self, filename: str = 'tasks.json'): - self.file = filename - if not os.path.exists(self.file): # if file does not exist, create file with empty list - self.save([]) - def load(self): - with open(self.file, 'r', encoding = 'utf-8') as f: - return json.load(f) - def save(self, tasks): - with open(self.file, 'w', encoding = 'utf-8') as f: - json.dump(tasks, f) \ No newline at end of file From 712ad577dea4eb62ebe39f6421d0c32b67e4469e Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 19:58:54 +0300 Subject: [PATCH 23/38] Delete simple_backend/src/task_tracker/storage_gist.py --- .../src/task_tracker/storage_gist.py | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 simple_backend/src/task_tracker/storage_gist.py diff --git a/simple_backend/src/task_tracker/storage_gist.py b/simple_backend/src/task_tracker/storage_gist.py deleted file mode 100644 index 6f548389..00000000 --- a/simple_backend/src/task_tracker/storage_gist.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -import requests -import json -from dotenv import load_dotenv -load_dotenv() -GITHUB_API = 'https://api.github.com' -class GistStorage: - def __init__(self, token: str = None, gist_id: str = None, filename: str = 'tasks.json'): - self.token: str = token or os.getenv('GITHUB_TOKEN') - self.gist_id: str = gist_id or os.getenv('GIST_ID') - self.filename: str = filename - if not self.token or not self.gist_id: - raise RuntimeError('GITHUB_TOKEN and GIST_ID must be given') - self.headers: dict = { - 'Authorization': f'token {self.token}', - 'Accept': 'application/vnd.github+json' - } - - def _gist_url(self) -> str: - return f'{GITHUB_API}/gists/{self.gist_id}' - # loading task list from gist - def load(self) -> list[dict]: - r = requests.get(self._gist_url(), headers = self.headers, timeout = 10) - r.raise_for_status() - gist_data: dict = r.json() - files: dict = gist_data.get('files', {}) - if self.filename not in files: - return [] - content: str = files[self.filename]['content'] - try: - data: dict = json.loads(content) - return data.get('tasks', []) - except json.JSONDecodeError: - return [] - # saving task list on gist - def save(self, tasks: list[dict]) -> None: - body: dict = { - 'files': { - self.filename: { - 'content': json.dumps({'tasks': tasks}) - } - } - } - r = requests.patch(self._gist_url(), headers = self.headers, json = body, timeout = 10) - r.raise_for_status() From 2b800b352d48e62c6af41f3581a664b1f89abafb Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 20:11:52 +0300 Subject: [PATCH 24/38] del --- simple_backend/src/task_tracker/main.py | 39 ---------------- simple_backend/src/task_tracker/storage.py | 13 ------ .../src/task_tracker/storage_gist.py | 45 ------------------- simple_backend/src/task_tracker/tasks.json | 1 - 4 files changed, 98 deletions(-) delete mode 100644 simple_backend/src/task_tracker/main.py delete mode 100644 simple_backend/src/task_tracker/storage.py delete mode 100644 simple_backend/src/task_tracker/storage_gist.py 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 deleted file mode 100644 index 6209f4dd..00000000 --- a/simple_backend/src/task_tracker/main.py +++ /dev/null @@ -1,39 +0,0 @@ -'''backend''' -from fastapi import FastAPI, HTTPException -from storage_gist import GistStorage -app = FastAPI() -storage = GistStorage() -# return tasks list -@app.get("/tasks") -def get_tasks(): - return storage.load() -#create a new task with id, name, and status -@app.post("/tasks") -def create_task(name: str, condition: str = 'new'): - tasks = storage.load() - new_id = len(tasks) + 1 - task = {'id': new_id, 'name': name, 'condition': condition} - tasks.append(task) - storage.save(tasks) - return task -#update task -@app.put("/tasks/{task_id}") -def update_task(task_id: int, name: str, condition: str): - tasks = storage.load() - for task in tasks: - if task['id'] == task_id: - task['name'] = name - task['condition'] = condition - storage.save(tasks) - return task - raise HTTPException(status_code = 404, detail = 'task not found') -#delete task -@app.delete("/tasks/{task_id}") -def delete_task(task_id: int): - tasks = storage.load() - for task in tasks: - if task['id'] == task_id: - tasks.remove(task) - storage.save(tasks) - return 'task deleted' - raise HTTPException(status_code = 404, detail = 'task not found') \ No newline at end of file diff --git a/simple_backend/src/task_tracker/storage.py b/simple_backend/src/task_tracker/storage.py deleted file mode 100644 index ce237692..00000000 --- a/simple_backend/src/task_tracker/storage.py +++ /dev/null @@ -1,13 +0,0 @@ -import json -import os -class TaskStorage: - def __init__(self, filename: str = 'tasks.json'): - self.file = filename - if not os.path.exists(self.file): # if file does not exist, create file with empty list - self.save([]) - def load(self): - with open(self.file, 'r', encoding = 'utf-8') as f: - return json.load(f) - def save(self, tasks): - with open(self.file, 'w', encoding = 'utf-8') as f: - json.dump(tasks, f) \ No newline at end of file diff --git a/simple_backend/src/task_tracker/storage_gist.py b/simple_backend/src/task_tracker/storage_gist.py deleted file mode 100644 index 6f548389..00000000 --- a/simple_backend/src/task_tracker/storage_gist.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -import requests -import json -from dotenv import load_dotenv -load_dotenv() -GITHUB_API = 'https://api.github.com' -class GistStorage: - def __init__(self, token: str = None, gist_id: str = None, filename: str = 'tasks.json'): - self.token: str = token or os.getenv('GITHUB_TOKEN') - self.gist_id: str = gist_id or os.getenv('GIST_ID') - self.filename: str = filename - if not self.token or not self.gist_id: - raise RuntimeError('GITHUB_TOKEN and GIST_ID must be given') - self.headers: dict = { - 'Authorization': f'token {self.token}', - 'Accept': 'application/vnd.github+json' - } - - def _gist_url(self) -> str: - return f'{GITHUB_API}/gists/{self.gist_id}' - # loading task list from gist - def load(self) -> list[dict]: - r = requests.get(self._gist_url(), headers = self.headers, timeout = 10) - r.raise_for_status() - gist_data: dict = r.json() - files: dict = gist_data.get('files', {}) - if self.filename not in files: - return [] - content: str = files[self.filename]['content'] - try: - data: dict = json.loads(content) - return data.get('tasks', []) - except json.JSONDecodeError: - return [] - # saving task list on gist - def save(self, tasks: list[dict]) -> None: - body: dict = { - 'files': { - self.filename: { - 'content': json.dumps({'tasks': tasks}) - } - } - } - r = requests.patch(self._gist_url(), headers = self.headers, json = body, timeout = 10) - r.raise_for_status() diff --git a/simple_backend/src/task_tracker/tasks.json b/simple_backend/src/task_tracker/tasks.json deleted file mode 100644 index 80c933c8..00000000 --- a/simple_backend/src/task_tracker/tasks.json +++ /dev/null @@ -1 +0,0 @@ -[{"id": 1, "name": "finaltask", "condition": "updated"}, {"id": 2, "name": "totaltask", "condition": "updated"}, {"id": 3, "name": "Task3", "condition": "new"}] \ No newline at end of file From 35e423dc900815c0746d53acdb0f29794077e79a Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 20:17:11 +0300 Subject: [PATCH 25/38] storage github gist add --- simple_backend/src/task_tracker/main.py | 39 ++++++++++++++++ simple_backend/src/task_tracker/storage.py | 13 ++++++ .../src/task_tracker/storage_gist.py | 45 +++++++++++++++++++ simple_backend/src/task_tracker/tasks.json | 1 + 4 files changed, 98 insertions(+) create mode 100644 simple_backend/src/task_tracker/main.py create mode 100644 simple_backend/src/task_tracker/storage.py create mode 100644 simple_backend/src/task_tracker/storage_gist.py 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 new file mode 100644 index 00000000..6209f4dd --- /dev/null +++ b/simple_backend/src/task_tracker/main.py @@ -0,0 +1,39 @@ +'''backend''' +from fastapi import FastAPI, HTTPException +from storage_gist import GistStorage +app = FastAPI() +storage = GistStorage() +# return tasks list +@app.get("/tasks") +def get_tasks(): + return storage.load() +#create a new task with id, name, and status +@app.post("/tasks") +def create_task(name: str, condition: str = 'new'): + tasks = storage.load() + new_id = len(tasks) + 1 + task = {'id': new_id, 'name': name, 'condition': condition} + tasks.append(task) + storage.save(tasks) + return task +#update task +@app.put("/tasks/{task_id}") +def update_task(task_id: int, name: str, condition: str): + tasks = storage.load() + for task in tasks: + if task['id'] == task_id: + task['name'] = name + task['condition'] = condition + storage.save(tasks) + return task + raise HTTPException(status_code = 404, detail = 'task not found') +#delete task +@app.delete("/tasks/{task_id}") +def delete_task(task_id: int): + tasks = storage.load() + for task in tasks: + if task['id'] == task_id: + tasks.remove(task) + storage.save(tasks) + return 'task deleted' + raise HTTPException(status_code = 404, detail = 'task not found') \ No newline at end of file diff --git a/simple_backend/src/task_tracker/storage.py b/simple_backend/src/task_tracker/storage.py new file mode 100644 index 00000000..ce237692 --- /dev/null +++ b/simple_backend/src/task_tracker/storage.py @@ -0,0 +1,13 @@ +import json +import os +class TaskStorage: + def __init__(self, filename: str = 'tasks.json'): + self.file = filename + if not os.path.exists(self.file): # if file does not exist, create file with empty list + self.save([]) + def load(self): + with open(self.file, 'r', encoding = 'utf-8') as f: + return json.load(f) + def save(self, tasks): + with open(self.file, 'w', encoding = 'utf-8') as f: + json.dump(tasks, f) \ No newline at end of file diff --git a/simple_backend/src/task_tracker/storage_gist.py b/simple_backend/src/task_tracker/storage_gist.py new file mode 100644 index 00000000..6f548389 --- /dev/null +++ b/simple_backend/src/task_tracker/storage_gist.py @@ -0,0 +1,45 @@ +import os +import requests +import json +from dotenv import load_dotenv +load_dotenv() +GITHUB_API = 'https://api.github.com' +class GistStorage: + def __init__(self, token: str = None, gist_id: str = None, filename: str = 'tasks.json'): + self.token: str = token or os.getenv('GITHUB_TOKEN') + self.gist_id: str = gist_id or os.getenv('GIST_ID') + self.filename: str = filename + if not self.token or not self.gist_id: + raise RuntimeError('GITHUB_TOKEN and GIST_ID must be given') + self.headers: dict = { + 'Authorization': f'token {self.token}', + 'Accept': 'application/vnd.github+json' + } + + def _gist_url(self) -> str: + return f'{GITHUB_API}/gists/{self.gist_id}' + # loading task list from gist + def load(self) -> list[dict]: + r = requests.get(self._gist_url(), headers = self.headers, timeout = 10) + r.raise_for_status() + gist_data: dict = r.json() + files: dict = gist_data.get('files', {}) + if self.filename not in files: + return [] + content: str = files[self.filename]['content'] + try: + data: dict = json.loads(content) + return data.get('tasks', []) + except json.JSONDecodeError: + return [] + # saving task list on gist + def save(self, tasks: list[dict]) -> None: + body: dict = { + 'files': { + self.filename: { + 'content': json.dumps({'tasks': tasks}) + } + } + } + r = requests.patch(self._gist_url(), headers = self.headers, json = body, timeout = 10) + r.raise_for_status() diff --git a/simple_backend/src/task_tracker/tasks.json b/simple_backend/src/task_tracker/tasks.json new file mode 100644 index 00000000..80c933c8 --- /dev/null +++ b/simple_backend/src/task_tracker/tasks.json @@ -0,0 +1 @@ +[{"id": 1, "name": "finaltask", "condition": "updated"}, {"id": 2, "name": "totaltask", "condition": "updated"}, {"id": 3, "name": "Task3", "condition": "new"}] \ No newline at end of file From f6f5ead90bed830f0d6198d7df8a50263c108520 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 20:21:41 +0300 Subject: [PATCH 26/38] Create README.md --- simple_backend/src/task_tracker/README.md | 24 +++++++++++++++++++++++ 1 file changed, 24 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..c786bbe7 --- /dev/null +++ b/simple_backend/src/task_tracker/README.md @@ -0,0 +1,24 @@ +Хранение состояния + ++ 1.Минусы метода хранения в оперативной памяти в том, что доступ к данным есть только во время одной сессии , + после перезагрузки приложения, доступа к данным уже не будет. ++ 2.Отсутствие масштабируемости. ++ 3.Нет истории изменений +----------------------------------------------------------------------------------------------------------------------------- +Переход на json + ++ 1.После того, как список оперативной памяти изменился на файл проекта, мы получаем доступ к данным хранилища даже после перезапуска приложения, данные не исчезают, а остаются в списке ++ 2.Можно открыть файл и редактировать его вручную ++ 3.Так как json файл является локальным, то при создании копий приложения, у них будут разные файлы tasks.json +----------------------------------------------------------------------------------------------------------------------------- +Хранилища задач + +Задачи можно хранить в облачных хранилищах: + ++ Плюсы: Доступ откуда угодно ++ Минусы: Обдачные хранилища зависимы от сторонних ресурсов +Задачи можно хранить в баазах данныхЖ + ++ Плюсы: Поддержка транзакций, масштабирование и надежность ++ Минусы: Сложная настройка +----------------------------------------------------------------------------------------------------------------------------- From 7eb183acc83bd4afa8957d6492ae0fbc08eda4e9 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 20:43:57 +0300 Subject: [PATCH 27/38] Update README.md --- simple_backend/src/task_tracker/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/simple_backend/src/task_tracker/README.md b/simple_backend/src/task_tracker/README.md index c786bbe7..765f273c 100644 --- a/simple_backend/src/task_tracker/README.md +++ b/simple_backend/src/task_tracker/README.md @@ -22,3 +22,5 @@ + Плюсы: Поддержка транзакций, масштабирование и надежность + Минусы: Сложная настройка ----------------------------------------------------------------------------------------------------------------------------- +Состояние гонки, это когда два разработчика делают одно и то же в коде и возникает конфликт(данные одного разработчика перезаписывают данные другого. +Если перенести хранение на базу данных , то решится проблема с гонками, плюс данные лучше защищены и легче масштабировать From 443cd7721caeb9864e16710f17422c620a69b75c Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sat, 4 Oct 2025 23:55:23 +0300 Subject: [PATCH 28/38] cludflare add --- simple_backend/src/task_tracker/Cloudflare.py | 30 +++++++++++++++++++ simple_backend/src/task_tracker/main.py | 6 +++- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 simple_backend/src/task_tracker/Cloudflare.py diff --git a/simple_backend/src/task_tracker/Cloudflare.py b/simple_backend/src/task_tracker/Cloudflare.py new file mode 100644 index 00000000..3c2c208d --- /dev/null +++ b/simple_backend/src/task_tracker/Cloudflare.py @@ -0,0 +1,30 @@ +import os +import requests +from dotenv import load_dotenv +load_dotenv() +class Cloudflare: + def __init__(self, api_key: str = None, acc_id: str = None): + self.api_key = api_key or os.getenv("CLOUDFLARE_API_KEY") + self.acc_id = acc_id or os.getenv("CLOUDFLARE_ACCOUNT_ID") + self.url = f"https://api.cloudflare.com/client/v4/accounts/{self.acc_id}/ai/run/@cf/meta/llama-3-8b-instruct" + + def get_llm_response(self, text:str) -> str: + headers = { + "Authorization": f'Bearer {self.api_key}', + "Content-type": "application/json" + } + pyload = { + 'messages': [ + {'role': 'system', 'content': 'I will help you with somethimg'}, + {'role': 'user', 'content': text} + ] + } + + response = requests.post(self.url, headers=headers, json=pyload, timeout=15) + print("CLOUDFLARE RAW RESPONSE:") + print(response.text) + try: + data = response.json() + return data['result']['response'] + except (KeyError, IndexError): + return 'Error: wrong API answer' \ 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 6209f4dd..fd29a564 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,8 +1,11 @@ '''backend''' from fastapi import FastAPI, HTTPException +from pydantic import BaseModel from storage_gist import GistStorage +from Cloudflare import Cloudflare app = FastAPI() storage = GistStorage() +llm = Cloudflare() # return tasks list @app.get("/tasks") def get_tasks(): @@ -12,7 +15,8 @@ def get_tasks(): def create_task(name: str, condition: str = 'new'): tasks = storage.load() new_id = len(tasks) + 1 - task = {'id': new_id, 'name': name, 'condition': condition} + llm_response = llm.get_llm_response(name) + task = {'id': new_id, 'name': name, 'condition': condition, 'description': f'LLM Suggestion: {llm_response}'} tasks.append(task) storage.save(tasks) return task From 4943856850ad474e45760e054cd397edbd7b20f9 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Sun, 5 Oct 2025 03:09:57 +0300 Subject: [PATCH 29/38] BaseHTTPClient add --- .../src/task_tracker/BaseHTTPClient.py | 34 +++++++++ simple_backend/src/task_tracker/Cloudflare.py | 36 +++++----- .../src/task_tracker/storage_gist.py | 69 +++++++++++++++---- 3 files changed, 107 insertions(+), 32 deletions(-) create mode 100644 simple_backend/src/task_tracker/BaseHTTPClient.py diff --git a/simple_backend/src/task_tracker/BaseHTTPClient.py b/simple_backend/src/task_tracker/BaseHTTPClient.py new file mode 100644 index 00000000..3c766281 --- /dev/null +++ b/simple_backend/src/task_tracker/BaseHTTPClient.py @@ -0,0 +1,34 @@ +import requests +from abc import ABC, abstractmethod + +class BaseHTTPClient(ABC): + + def __init__(self, base_url: str, headers: dict = None): + self.base_url = base_url.rstrip("/") # убираем лишний слэш + self.headers = headers or {} + + def _get(self, endpoint: str = "", **kwargs): + url = f"{self.base_url}{endpoint}" + print(f"[GET] {url}") + resp = requests.get(url, headers=self.headers, timeout=15, **kwargs) + resp.raise_for_status() + return resp.json() + + def _post(self, endpoint: str = "", json_data=None, **kwargs): + url = f"{self.base_url}{endpoint}" + print(f"[POST] {url} | DATA: {json_data}") + resp = requests.post(url, headers=self.headers, json=json_data, timeout=15, **kwargs) + resp.raise_for_status() + return resp.json() + + def _patch(self, endpoint: str = "", json_data=None, **kwargs): + url = f"{self.base_url}{endpoint}" + print(f"[PATCH] {url} | DATA: {json_data}") + resp = requests.patch(url, headers=self.headers, json=json_data, timeout=15, **kwargs) + resp.raise_for_status() + return resp.json() + + @abstractmethod + def _make_request(self, *args, **kwargs): + pass + \ No newline at end of file diff --git a/simple_backend/src/task_tracker/Cloudflare.py b/simple_backend/src/task_tracker/Cloudflare.py index 3c2c208d..5bea74e0 100644 --- a/simple_backend/src/task_tracker/Cloudflare.py +++ b/simple_backend/src/task_tracker/Cloudflare.py @@ -1,30 +1,30 @@ import os import requests from dotenv import load_dotenv +from BaseHTTPClient import BaseHTTPClient load_dotenv() -class Cloudflare: +class Cloudflare(BaseHTTPClient): def __init__(self, api_key: str = None, acc_id: str = None): self.api_key = api_key or os.getenv("CLOUDFLARE_API_KEY") self.acc_id = acc_id or os.getenv("CLOUDFLARE_ACCOUNT_ID") - self.url = f"https://api.cloudflare.com/client/v4/accounts/{self.acc_id}/ai/run/@cf/meta/llama-3-8b-instruct" - - def get_llm_response(self, text:str) -> str: + base_url = f"https://api.cloudflare.com/client/v4/accounts/{self.acc_id}/ai/run/@cf/meta/llama-3-8b-instruct" headers = { - "Authorization": f'Bearer {self.api_key}', - "Content-type": "application/json" + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json" } - pyload = { - 'messages': [ - {'role': 'system', 'content': 'I will help you with somethimg'}, - {'role': 'user', 'content': text} + super().__init__(base_url=base_url, headers=headers) + def _make_request(self, text: str): + payload = { + "messages": [ + {"role": "system", "content": "You are a helpful assistant that explains tasks clearly."}, + {"role": "user", "content": text} ] } - - response = requests.post(self.url, headers=headers, json=pyload, timeout=15) - print("CLOUDFLARE RAW RESPONSE:") - print(response.text) + return self._post(json_data=payload) + def get_llm_response(self, text:str) -> str: try: - data = response.json() - return data['result']['response'] - except (KeyError, IndexError): - return 'Error: wrong API answer' \ No newline at end of file + data = self._make_request(text) + return data.get('result', {}).get('response', "No response") + except Exception as e: + print("Error getting LLM response:", e) + return "Error: wrong API answer" \ No newline at end of file diff --git a/simple_backend/src/task_tracker/storage_gist.py b/simple_backend/src/task_tracker/storage_gist.py index 6f548389..10a08f3a 100644 --- a/simple_backend/src/task_tracker/storage_gist.py +++ b/simple_backend/src/task_tracker/storage_gist.py @@ -2,34 +2,76 @@ import requests import json from dotenv import load_dotenv +from BaseHTTPClient import BaseHTTPClient load_dotenv() GITHUB_API = 'https://api.github.com' -class GistStorage: +class GistStorage(BaseHTTPClient): def __init__(self, token: str = None, gist_id: str = None, filename: str = 'tasks.json'): self.token: str = token or os.getenv('GITHUB_TOKEN') self.gist_id: str = gist_id or os.getenv('GIST_ID') self.filename: str = filename + base_url = f"{GITHUB_API}/gists/{self.gist_id}" if not self.token or not self.gist_id: raise RuntimeError('GITHUB_TOKEN and GIST_ID must be given') - self.headers: dict = { + headers: dict = { 'Authorization': f'token {self.token}', 'Accept': 'application/vnd.github+json' } - - def _gist_url(self) -> str: - return f'{GITHUB_API}/gists/{self.gist_id}' + super().__init__(base_url=base_url, headers=headers) + import os +import requests +import json +from dotenv import load_dotenv +from BaseHTTPClient import BaseHTTPClient +load_dotenv() +GITHUB_API = 'https://api.github.com' +class GistStorage(BaseHTTPClient): + def __init__(self, token: str = None, gist_id: str = None, filename: str = 'tasks.json'): + self.token: str = token or os.getenv('GITHUB_TOKEN') + self.gist_id: str = gist_id or os.getenv('GIST_ID') + self.filename: str = filename + base_url = f"{GITHUB_API}/gists/{self.gist_id}" + if not self.token or not self.gist_id: + raise RuntimeError('GITHUB_TOKEN and GIST_ID must be given') + headers: dict = { + 'Authorization': f'token {self.token}', + 'Accept': 'application/vnd.github+json' + } + super().__init__(base_url=base_url, headers=headers) + def _make_request(self, *args, **kwargs): + pass + # loading task list from gist + def load(self) -> list[dict]: + gist_data = self._get() + files = gist_data.get("files", {}) + if self.filename not in files: + return [] + try: + content = files[self.filename]["content"] + data = json.loads(content) + return data.get("tasks", []) + except json.JSONDecodeError: + return [] + # saving task list on gist + def save(self, tasks: list[dict]) -> None: + body: dict = { + 'files': { + self.filename: { + 'content': json.dumps({'tasks': tasks}) + } + } + } + self._patch(json_data=body) # loading task list from gist def load(self) -> list[dict]: - r = requests.get(self._gist_url(), headers = self.headers, timeout = 10) - r.raise_for_status() - gist_data: dict = r.json() - files: dict = gist_data.get('files', {}) + gist_data = self._get() + files = gist_data.get("files", {}) if self.filename not in files: return [] - content: str = files[self.filename]['content'] try: - data: dict = json.loads(content) - return data.get('tasks', []) + content = files[self.filename]["content"] + data = json.loads(content) + return data.get("tasks", []) except json.JSONDecodeError: return [] # saving task list on gist @@ -41,5 +83,4 @@ def save(self, tasks: list[dict]) -> None: } } } - r = requests.patch(self._gist_url(), headers = self.headers, json = body, timeout = 10) - r.raise_for_status() + self._patch(json_data=body) \ No newline at end of file From 5dce8a4a60e298da3a0d0cdc25975094379ab9b3 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Thu, 9 Oct 2025 19:35:51 +0300 Subject: [PATCH 30/38] add workflows --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 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..6b2cde7d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12.3" + + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-ruff-v1 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + + - name: Auto-fix code with Ruff + run: | + ruff format . --fix --line-length 88 --exclude migrations,venv || true + + - name: Run Ruff linter + run: | + ruff check . --select ALL --ignore F401,W391 --line-length 88 From 4584deb12787da77b3b0d305efa9706a4e8d0a8e Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Thu, 9 Oct 2025 23:31:30 +0300 Subject: [PATCH 31/38] correct smth --- simple_backend/src/task_tracker/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/simple_backend/src/task_tracker/main.py b/simple_backend/src/task_tracker/main.py index fd29a564..cd272e3f 100644 --- a/simple_backend/src/task_tracker/main.py +++ b/simple_backend/src/task_tracker/main.py @@ -1,6 +1,5 @@ '''backend''' from fastapi import FastAPI, HTTPException -from pydantic import BaseModel from storage_gist import GistStorage from Cloudflare import Cloudflare app = FastAPI() From ec3de8dde413d5ef839ed428d3e9bbb7170eeeb0 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Thu, 9 Oct 2025 23:32:49 +0300 Subject: [PATCH 32/38] Update ci.yml --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b2cde7d..8ee302b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: [ main, second-branch ] pull_request: - branches: [ main ] + branches: [ main, second-branch ] jobs: lint: @@ -23,7 +23,9 @@ jobs: uses: actions/cache@v3 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-ruff-v1 + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- - name: Install dependencies run: | From e2910243a98b6035fc6dfecaab51923ed5e7bf67 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Thu, 9 Oct 2025 23:37:11 +0300 Subject: [PATCH 33/38] Update ci.yml --- .github/workflows/ci.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ee302b8..b72adceb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,9 +23,7 @@ jobs: uses: actions/cache@v3 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- + key: ${{ runner.os }}-pip-ruff-v1 - name: Install dependencies run: | @@ -34,8 +32,8 @@ jobs: - name: Auto-fix code with Ruff run: | - ruff format . --fix --line-length 88 --exclude migrations,venv || true + ruff check . --fix --line-length 88 || true - name: Run Ruff linter run: | - ruff check . --select ALL --ignore F401,W391 --line-length 88 + ruff check . --line-length 88 From f79221fae03c094d19cad6077bb9ca9969b8e720 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Thu, 9 Oct 2025 23:39:18 +0300 Subject: [PATCH 34/38] Create .ruff.toml --- .ruff.toml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .ruff.toml diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 00000000..a38af153 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,11 @@ +[tool.ruff] +line-length = 88 +select = ["E", "F"] +ignore = ["F401", "W391", "E501"] +exclude = [ + "venv", + "migrations", + ".git", + "__pycache__", + "node_modules" +] From 46ef92e39cff6c22550dd1bb932578d6fc202441 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Thu, 9 Oct 2025 23:41:01 +0300 Subject: [PATCH 35/38] Update .ruff.toml --- .ruff.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/.ruff.toml b/.ruff.toml index a38af153..aa31313b 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,4 +1,3 @@ -[tool.ruff] line-length = 88 select = ["E", "F"] ignore = ["F401", "W391", "E501"] From c152ca941d63f6f0c9ac42990395461f376090a1 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Thu, 9 Oct 2025 23:54:53 +0300 Subject: [PATCH 36/38] fix giststorage, put now works --- .../src/task_tracker/storage_gist.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/simple_backend/src/task_tracker/storage_gist.py b/simple_backend/src/task_tracker/storage_gist.py index 10a08f3a..237852e3 100644 --- a/simple_backend/src/task_tracker/storage_gist.py +++ b/simple_backend/src/task_tracker/storage_gist.py @@ -5,26 +5,6 @@ from BaseHTTPClient import BaseHTTPClient load_dotenv() GITHUB_API = 'https://api.github.com' -class GistStorage(BaseHTTPClient): - def __init__(self, token: str = None, gist_id: str = None, filename: str = 'tasks.json'): - self.token: str = token or os.getenv('GITHUB_TOKEN') - self.gist_id: str = gist_id or os.getenv('GIST_ID') - self.filename: str = filename - base_url = f"{GITHUB_API}/gists/{self.gist_id}" - if not self.token or not self.gist_id: - raise RuntimeError('GITHUB_TOKEN and GIST_ID must be given') - headers: dict = { - 'Authorization': f'token {self.token}', - 'Accept': 'application/vnd.github+json' - } - super().__init__(base_url=base_url, headers=headers) - import os -import requests -import json -from dotenv import load_dotenv -from BaseHTTPClient import BaseHTTPClient -load_dotenv() -GITHUB_API = 'https://api.github.com' class GistStorage(BaseHTTPClient): def __init__(self, token: str = None, gist_id: str = None, filename: str = 'tasks.json'): self.token: str = token or os.getenv('GITHUB_TOKEN') From 6f9945fc3830c05821b3091a0a32b6de53120426 Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Thu, 9 Oct 2025 23:57:58 +0300 Subject: [PATCH 37/38] fix gist storage --- .../src/task_tracker/storage_gist.py | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/simple_backend/src/task_tracker/storage_gist.py b/simple_backend/src/task_tracker/storage_gist.py index 237852e3..34c9972a 100644 --- a/simple_backend/src/task_tracker/storage_gist.py +++ b/simple_backend/src/task_tracker/storage_gist.py @@ -33,28 +33,6 @@ def load(self) -> list[dict]: except json.JSONDecodeError: return [] # saving task list on gist - def save(self, tasks: list[dict]) -> None: - body: dict = { - 'files': { - self.filename: { - 'content': json.dumps({'tasks': tasks}) - } - } - } - self._patch(json_data=body) - # loading task list from gist - def load(self) -> list[dict]: - gist_data = self._get() - files = gist_data.get("files", {}) - if self.filename not in files: - return [] - try: - content = files[self.filename]["content"] - data = json.loads(content) - return data.get("tasks", []) - except json.JSONDecodeError: - return [] - # saving task list on gist def save(self, tasks: list[dict]) -> None: body: dict = { 'files': { From 9011e890b1eb4f89d6ac1e23efa985ccdcace7ff Mon Sep 17 00:00:00 2001 From: Roman Andreev Date: Fri, 10 Oct 2025 00:38:18 +0300 Subject: [PATCH 38/38] del comment httpclient --- simple_backend/src/task_tracker/BaseHTTPClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple_backend/src/task_tracker/BaseHTTPClient.py b/simple_backend/src/task_tracker/BaseHTTPClient.py index 3c766281..d5f328e2 100644 --- a/simple_backend/src/task_tracker/BaseHTTPClient.py +++ b/simple_backend/src/task_tracker/BaseHTTPClient.py @@ -4,7 +4,7 @@ class BaseHTTPClient(ABC): def __init__(self, base_url: str, headers: dict = None): - self.base_url = base_url.rstrip("/") # убираем лишний слэш + self.base_url = base_url.rstrip("/") self.headers = headers or {} def _get(self, endpoint: str = "", **kwargs):