Перейти к содержимому

Практика 1. Практическая работа #1: **Python + FastAPI** (Windows) — сервис «Книги»

Теория

Что такое FastAPI

  • FastAPI — современный фреймворк для веб-API на Python поверх ASGI (обычно работает с uvicorn).
  • Преимущества: типы Python → автоматическая валидация и документация (OpenAPI/Swagger), высокая скорость, простота.

ASGI и uvicorn

  • ASGI — протокол для асинхронных Python-сервисов (замена WSGI).
  • uvicorn — быстрый ASGI-сервер (на базе uvloop/httptools). Мы запускаем приложение командой uvicorn main:app.

HTTP-методы (что будем тренировать)

  • GET — чтение ресурса (коллекция/элемент).
  • POST — создание ресурса (тело запроса — JSON).
  • PUT — полная замена ресурса (все поля).
  • PATCH — частичное обновление ресурса (только изменяемые поля).
  • DELETE — удаление ресурса.
  • HEAD — как GET, но без тела (проверка наличия/метаданных).
  • OPTIONS — какие методы поддерживаются для эндпойнта.

Модели данных и валидация

  • Используем Pydantic v2 (идёт вместе с FastAPI).
  • Описываем модели запроса/ответа → FastAPI валидирует входные данные, сериализует ответ и генерирует документацию.
  • В примере у книги есть поля: title, author, year, price, isbn?, in_stock, rating?.
  • Валидация через типы и ограничители (conint, confloat, constr).

Документация

  • Swagger UI: /docs (тестировать эндпойнты прямо в браузере).
  • ReDoc: /redoc (альтернативная документация).

Цель практики

  1. Установить FastAPI + uvicorn на Windows (PowerShell).
  2. Реализовать сервис «Книги» без БД (хранение в памяти).
  3. Покрыть популярные HTTP-методы: GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS.
  4. Научиться принимать и возвращать JSON.
  5. Протестировать через PowerShell (Invoke-RestMethod) и через браузер (/docs).

Требования

  • Windows 10/11, установлен Python 3.10+ (обычно доступен как py и python).
  • PowerShell.

1) Установка (Windows, PowerShell)

Окно терминала
# Создаём папку проекта
mkdir fastapi-books
# Переходим папку проекта
cd fastapi-books
# создаём виртуальное окружение
python -m venv .venv
# активируем
.\.venv\Scripts\Activate.ps1
# Обновляем pip и ставим зависимости
python -m pip install --upgrade pip
pip install fastapi uvicorn
# (опционально) зафиксировать зависимости
pip freeze > requirements.txt

2) Код приложения — main.py

from typing import Optional, List
from uuid import uuid4, UUID
from fastapi import FastAPI, HTTPException, Path, Query, Body, Header, status
from pydantic import BaseModel, Field, constr, conint, confloat
app = FastAPI(title="Books API (Practice)")
# ---------- Pydantic-модели ----------
ISBN = constr(strip_whitespace=True, min_length=10, max_length=17) # ISBN-10/13
Year = conint(ge=1400, le=2100)
Price = confloat(gt=0)
Rating = confloat(ge=0, le=5)
class BookCreate(BaseModel):
title: str = Field(..., min_length=1, description="Название книги")
author: str = Field(..., min_length=1, description="Автор")
year: Year = Field(..., description="Год издания")
price: Price = Field(..., description="Цена, > 0")
isbn: Optional[ISBN] = Field(None, description="ISBN-10/13")
in_stock: bool = Field(default=True, description="В наличии?")
rating: Optional[Rating] = Field(None, description="Оценка 0..5")
class Book(BookCreate):
id: UUID
class BookUpdate(BaseModel):
title: Optional[str] = None
author: Optional[str] = None
year: Optional[Year] = None
price: Optional[Price] = None
isbn: Optional[ISBN] = None
in_stock: Optional[bool] = None
rating: Optional[Rating] = None
# ---------- Память вместо БД ----------
DB: dict[UUID, Book] = {}
# ---------- Endpoints ----------
@app.get("/", tags=["root"])
def root():
return {"msg": "Books API. Откройте /docs для Swagger UI."}
# Список книг + фильтры
@app.get("/books/", response_model=List[Book], tags=["books"])
def list_books(
limit: int = Query(10, ge=1, le=100),
q: Optional[str] = Query(None, description="Поиск по названию/автору"),
min_price: Optional[float] = Query(None, ge=0),
max_price: Optional[float] = Query(None, ge=0),
in_stock: Optional[bool] = None,
):
items = list(DB.values())
if q:
ql = q.lower()
items = [b for b in items if ql in b.title.lower() or ql in b.author.lower()]
if min_price is not None:
items = [b for b in items if b.price >= min_price]
if max_price is not None:
items = [b for b in items if b.price <= max_price]
if in_stock is not None:
items = [b for b in items if b.in_stock == in_stock]
return items[:limit]
# Получить книгу по id
@app.get("/books/{book_id}", response_model=Book, tags=["books"])
def get_book(book_id: UUID = Path(...)):
book = DB.get(book_id)
if not book:
raise HTTPException(status_code=404, detail="Book not found")
return book
# HEAD
@app.head("/books/{book_id}", tags=["books"])
def head_book(book_id: UUID = Path(...)):
if book_id not in DB:
raise HTTPException(status_code=404, detail="Not found")
return
# OPTIONS
@app.options("/books/", tags=["books"])
def options_books():
return {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]}
# Создать книгу
@app.post("/books/", response_model=Book, status_code=status.HTTP_201_CREATED, tags=["books"])
def create_book(payload: BookCreate = Body(...)):
new_id = uuid4()
book = Book(id=new_id, **payload.model_dump())
DB[new_id] = book
return book
# Полная замена книги
@app.put("/books/{book_id}", response_model=Book, tags=["books"])
def replace_book(book_id: UUID, payload: BookCreate = Body(...)):
if book_id not in DB:
raise HTTPException(status_code=404, detail="Book not found")
new_book = Book(id=book_id, **payload.model_dump())
DB[book_id] = new_book
return new_book
# Частичное обновление
@app.patch("/books/{book_id}", response_model=Book, tags=["books"])
def update_book(book_id: UUID, payload: BookUpdate = Body(...)):
existing = DB.get(book_id)
if not existing:
raise HTTPException(status_code=404, detail="Book not found")
updated_data = existing.model_dump()
for k, v in payload.model_dump(exclude_unset=True).items():
updated_data[k] = v
updated_book = Book(**updated_data)
DB[book_id] = updated_book
return updated_book
# Удаление
@app.delete("/books/{book_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["books"])
def delete_book(book_id: UUID):
if book_id not in DB:
raise HTTPException(status_code=404, detail="Book not found")
del DB[book_id]
return
# Эхо-пример
@app.post("/echo/", tags=["utils"])
def echo(payload: dict = Body(...), x_request_id: Optional[str] = Header(None)):
return {"received": payload, "x-request-id": x_request_id}

3) Запуск сервера

Окно терминала
uvicorn main:app --reload --host 127.0.0.1 --port 8000

Откройте:

  • Swagger UI: http://127.0.0.1:8000/docs
  • ReDoc: http://127.0.0.1:8000/redoc

4) Задания для самостоятельной работы

  1. Добавьте сортировку в GET /books/: параметры sort_by (price/year) и order (asc/desc).
  2. Добавьте проверку: если price > 1_000_000 → вернуть 400 Bad Request с detail.
  3. Реализуйте POST /books/bulk/ — принимает массив книг и добавляет все сразу. Верните список созданных книг.
  4. Реализуйте GET /books/stats — посчитайте count, avg_price, in_stock_count.
  5. Сгенерируйте минимум 5 книг, поиграйте с q, min_price, max_price, in_stock, убедитесь, что фильтры работают.

Примечания

  • Память очищается при перезапуске — это нормально для учебной практики без БД.
  • В продакшене FastAPI часто запускают через gunicorn:
    gunicorn -k uvicorn.workers.UvicornWorker main:app -w 4
  • Эндпойнты можно тестировать и через браузер на /docs — это удобно для демонстраций и проверки схемы.