Практика 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(альтернативная документация).
Цель практики
- Установить FastAPI + uvicorn на Windows (PowerShell).
- Реализовать сервис «Книги» без БД (хранение в памяти).
- Покрыть популярные HTTP-методы: GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS.
- Научиться принимать и возвращать JSON.
- Протестировать через 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 pippip install fastapi uvicorn
# (опционально) зафиксировать зависимостиpip freeze > requirements.txt2) Код приложения — main.py
from typing import Optional, Listfrom uuid import uuid4, UUID
from fastapi import FastAPI, HTTPException, Path, Query, Body, Header, statusfrom 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/13Year = 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) Задания для самостоятельной работы
- Добавьте сортировку в
GET /books/: параметрыsort_by(price/year) иorder(asc/desc). - Добавьте проверку: если
price > 1_000_000→ вернуть 400 Bad Request сdetail. - Реализуйте
POST /books/bulk/— принимает массив книг и добавляет все сразу. Верните список созданных книг. - Реализуйте
GET /books/stats— посчитайтеcount,avg_price,in_stock_count. - Сгенерируйте минимум 5 книг, поиграйте с
q,min_price,max_price,in_stock, убедитесь, что фильтры работают.
Примечания
- Память очищается при перезапуске — это нормально для учебной практики без БД.
- В продакшене FastAPI часто запускают через gunicorn:
gunicorn -k uvicorn.workers.UvicornWorker main:app -w 4 - Эндпойнты можно тестировать и через браузер на
/docs— это удобно для демонстраций и проверки схемы.