Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3272ede
fix: main
Nov 21, 2024
6d52a1d
fix: first-branch
Nov 21, 2024
f32a7fa
fix: second task
Nov 21, 2024
5352f5e
fix: tasks
Nov 21, 2024
de13048
fix: tasks
Nov 21, 2024
8b2b714
feat: update with new lesson
Feb 17, 2025
87afae5
feat: update tasks
Feb 17, 2025
adc836d
fix: tasks
Feb 17, 2025
2c9dd50
Update README.md
gardiys Feb 25, 2025
20987c8
Update README.md
gardiys Feb 27, 2025
0f4f66e
feat: change task
Feb 27, 2025
cd298cc
Merge branch 'main' of github.com:gardiys/fastapi-backend-course
Feb 27, 2025
4126798
Update README.md
gardiys Mar 7, 2025
bb5a993
Update README.md
gardiys Mar 7, 2025
e74a79d
Слияние second-branch с main
dark1exnt Aug 14, 2025
5d288a1
Добавлен CI для Ruff
dark1exnt Aug 14, 2025
39ca497
Добавлены исключения в .gitignore
dark1exnt Aug 23, 2025
519a4d7
Оживил бекенд из первой задачи
dark1exnt Aug 23, 2025
6d1a3c6
Изменен README.md
dark1exnt Aug 23, 2025
84f468f
Реализовано хранение задач в файле data/tasks.json
dark1exnt Aug 24, 2025
50e6d5f
Изменен README.md
dark1exnt Aug 24, 2025
1d122b4
Все классы перенесены в отдельный файл storage.py
dark1exnt Aug 24, 2025
3890dea
Сделан backend - stateless через mockapi.io
dark1exnt Aug 24, 2025
564f2b9
Изменен README.md
dark1exnt Aug 24, 2025
433ef39
Задание 3. Интеграция с Clouflare AI
dark1exnt Aug 24, 2025
1900552
Задание 4. Реализован класс BaseHTTPClient
dark1exnt Aug 24, 2025
593444c
Удаление неиспользуемых импортов
dark1exnt Aug 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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 .
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ venv/
ENV/
env.bak/
venv.bak/
simple_backend/src/task_tracker/simple_backend_venv

# Spyder project settings
.spyderproject
Expand Down
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ git clone [email protected]: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/)
Empty file added async_tasks/README.md
Empty file.
3 changes: 2 additions & 1 deletion git/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
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) есть конфликт. Нужно:
1. Форкнуть к себе репозиторий
2. Разрешить конфликт в этой ветке, чтобы ветку можно было вмержить в мастер без потери логики
3. Проверить папку git/src с помощью линтера ruff на ошибки и несоответствия стандартам разработки на Python

## Задание 3: простой CI
Нужно добавить конфигурацию CI с помощью GitHub Actions в свой репозиторий.
Expand Down
17 changes: 6 additions & 11 deletions git/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def load_books(filename='library.json'):
except json.JSONDecodeError:
return []

def saving_books(books, filename='library.json'):
def save_books(books, filename='library.json'):
"""
Сохранение списка книг в JSON-файл.
"""
Expand Down Expand Up @@ -94,17 +94,17 @@ def main():
# Получаем новый список с добавленной книгой
new_books = add_book(books, title, author, year)
books = new_books # Обновляем переменную, чтобы сохранить изменения
saving_books(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):
if len(new_books) < len(books):
books = new_books
saving_books(books)
save_books(books)
print("Книга удалена!")
else:
print("Книга с таким названием не найдена.")
Expand All @@ -119,17 +119,12 @@ def main():
else:
print("Ничего не найдено.")

elif choice == '6':
elif choice == '5':
print("Выход из программы.")
break

else:
print("Некорректный ввод. Попробуйте ещё раз.")






if __name__ == "__main__":
main()
main()
40 changes: 30 additions & 10 deletions setting_environment/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
Задание 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:`.
Вынесите все свои токены из проекта в `.env`
49 changes: 46 additions & 3 deletions simple_backend/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,46 @@
Задание 1
- создайте приложение main.py на fastapi
- проверьте его на тестах
# Задачи
**Во всех заданиях обязательно создание pull-request по аналогии из задачи блока git!**

**Ваш код обязательно должен запускаться и проверяться на работоспособность**
## Задание 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 файла во внешнем сервисе.
- Прочитайте что такое "состояние гонки" и напишите в readme файле о том, какие проблемы остались в бекенде на данном этапе проекта. Есть ли у вас какое-то решение этой проблемы?


## Задание 3
Давайте прокачаем наш таск-треккер. Хочется, чтобы текст задачи заливался в LLM модель и она выдавала способы решения задачи и добавляла к её тексту.
Для того, чтобы это сделать:
- Настройте интеграцию с сервисом [Cloudflare](https://developers.cloudflare.com/workers-ai/get-started/rest-api/) через REST API. Для этого создайте новый класс для работы с этой API.
- При создании новой задачи отправляйте запрос с её текстом в LLM и просьбой объяснить как решать задачу
- Добавляйте полученный ответ в текст задачи

## Задание 4
Заметили, что в коде для работы с файлами и в коде для работы с LLM API есть похожие участки? Давайте избавимся от дублирования через наследование.
- Сделайте базовый класс BaseHTTPClient и вынесите в него общие функции и методы из двух классов
- Сделайте наследование от базового класса в клиентах
- С помощью абстрактных классов реализуйте абстрактные методы, которые должны быть в классах наследниках
104 changes: 104 additions & 0 deletions simple_backend/orders.py
Original file line number Diff line number Diff line change
@@ -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") # Стоимость на каждого
22 changes: 22 additions & 0 deletions simple_backend/src/task_tracker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
### Минусы хранения задач в оперативной памяти:
1. Список задач сбрасывается каждый раз после прекращения работы сервера.
2. При запуске двух серверов у каждого будет свой отдельный список задач.


### После реализации через JSON:
1. Улучшилось:
- Задачи не пропадают после прекращения работы сервера.
- Можно синхронизировать файл.

2. Избавились ли от хранения состояния:
Нет, т.к. файл хранится локально и при каждом изменении читается и перезаписывается.

3. Где еще можно хранить:
- БД: масштабируемость и скорость при больших объемах данных, но сложнее в реализации.
- Облачное хранилище: доступ к данным из любого места, высокая надежность, но задержки и зависимость от внешних сервисов.


### Состояние гонки:
1. Проблема параллельной запииси.
2. Решения: использовать БД или блокировку threading.Lock().

59 changes: 59 additions & 0 deletions simple_backend/src/task_tracker/base_http_client.py
Original file line number Diff line number Diff line change
@@ -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)
Loading