Exercício Abrangente da Fase 1 — Construindo a API Básica do PriceTracker
Os conceitos abordados nas cinco aulas da Fase 1 são como cinco peças de quebra-cabeça. Agora é hora de juntá-las para formar uma imagem completa—a API Básica do PriceTracker—para que o front end do Bob possa realmente consumir os dados.
1. O Que Você Vai Aprender
- Projetar e implementar três conjuntos de endpoints centrais:
/products,/products/{id}, e/prices - O sistema de modelo Pydantic V2 completo:
ProductCreate/ProductResponse/PriceCreate/PriceResponse - Exemplos Práticos de Combinação de Parâmetros de Path, Parâmetros de Query e Corpos de Requisição
- Usar
response_modelpara implementar a lógica de negócios para "consulta pública com preços de atacado ocultos" - Bob (Integração Front-end): Confirmar que a documentação OpenAPI pode ser gerada automaticamente pelo front end para uso do SDK
2. A História Real da Alice
(1) Problema: Conhecimento fragmentado não pode ser montado em um produto
Alice terminou as primeiras cinco aulas, mas os exemplos em cada aula são trechos independentes—os exemplos sobre parâmetros de path e Pydantic não estão conectados. Bob não podia esperar mais: "Eu preciso de uma API funcional, não de um monte de demos espalhadas!" Alice precisa integrar design de roteamento, validação de parâmetros, validação de dados e filtragem de resposta em um único serviço de API funcional.
(2) Soluções Abrangentes para Problemas do Mundo Real
Esta aula integra todos os conceitos abordados nas primeiras cinco aulas na API Básica do PriceTracker, que inclui um conjunto completo de endpoints CRUD, um sistema de modelo Pydantic, uma cadeia de validação de parâmetros e lógica de filtragem de resposta.
# Estrutura básica completa da API do PriceTracker
from fastapi import FastAPI, Path, Query, HTTPException
from pydantic import BaseModel, Field
app = FastAPI(title="PriceTracker API", version="0.1.0")
# Modelos, rotas, validação — tudo integrado
(3) Resultado
Alice tem um serviço de API funcional, e Bob pode usar o Swagger UI para testar todos os endpoints; a documentação OpenAPI gerada automaticamente permite que SDKs front-end consumam a API diretamente.
3. Design Abrangente dos Endpoints da API
(1) Planejamento dos Endpoints da Fase 1
flowchart LR
Client[Client] -->|GET /products| List[List Products]
Client -->|POST /products| Create[Create Product]
Client -->|GET /products/id| Detail[Product Detail]
Client -->|PUT /products/id| Update[Update Product]
Client -->|DELETE /products/id| Delete[Delete Product]
Client -->|GET /prices| Search[Search Prices]
Client -->|POST /prices| AddPrice[Add Price]
List --> PM[ProductResponse]
Create --> PM
Detail --> PDP[ProductDetailResponse]
Search --> PRM[PriceResponse]
AddPrice --> PRM
| Endpoint | Método | Parâmetros de Path | Parâmetros de Query | Corpo da Requisição | Modelo de Resposta |
|---|---|---|---|---|---|
| Lista de Produtos | GET | - | category, sort, limit, offset | - | list[ProductResponse] |
| Criar Produto | POST | - | - | ProductCreate |
ProductResponse |
| Detalhes do Produto | GET | product_id | - | - | ProductDetailResponse |
| Atualizar Produto | PUT | product_id | - | ProductUpdate |
ProductResponse |
| Deletar Produto | DELETE | product_id | - | - | dict |
| Consulta de Preço | GET | - | product_id, min_price, max_price | - | list[PriceResponse] |
| Adicionar Preço | POST | - | - | PriceCreate |
PriceResponse |
(1) ▶ Exemplo: Sistema de Modelo Pydantic Completo
from pydantic import BaseModel, Field, field_validator, ConfigDict
from typing import Optional
from enum import Enum
from datetime import datetime
class Category(str, Enum):
electronics = "electronics"
clothing = "clothing"
food = "food"
books = "books"
class PriceInfo(BaseModel):
amount: float = Field(gt=0, description="Valor do preço em USD")
currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")
class ProductCreate(BaseModel):
name: str = Field(min_length=1, max_length=200)
category: Category
base_price: float = Field(gt=0, description="Preço base em USD")
description: Optional[str] = Field(None, max_length=2000)
class ProductUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=200)
category: Optional[Category] = None
base_price: Optional[float] = Field(None, gt=0)
description: Optional[str] = Field(None, max_length=2000)
class ProductResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
category: str
base_price: float
class ProductDetailResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
name: str
category: str
base_price: float
description: Optional[str] = None
created_at: datetime
class PriceCreate(BaseModel):
product_id: int = Field(gt=0)
price: float = Field(gt=0, description="Preço em USD")
currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")
source: str = Field(max_length=100)
@field_validator("price")
@classmethod
def round_price(cls, v: float) -> float:
return round(v, 2)
class PriceResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
product_id: int
price: float
currency: str
source: str
recorded_at: datetime
Saída:
# Função definida com sucesso
4. Ciclo de Vida Requisição-Resposta
(1) Fluxo de Trabalho Completo
sequenceDiagram
participant Client as Bob Frontend
participant FastAPI as FastAPI Router
participant Pydantic as Pydantic Validator
participant Handler as View Function
participant DB as In-Memory DB
Client->>FastAPI: POST /products (JSON body)
FastAPI->>Pydantic: Validate with ProductCreate
Pydantic-->>FastAPI: Instância do modelo validada
FastAPI->>Handler: create_product(data: ProductCreate)
Handler->>DB: Store product
DB-->>Handler: Stored record
Handler-->>FastAPI: Return full dict
FastAPI->>Pydantic: Filter with ProductResponse
Pydantic-->>Client: JSON response (filtrada)
(1) ▶ Exemplo: Implementando Endpoints CRUD de Produto
from fastapi import FastAPI, Path, Query, HTTPException
from datetime import datetime
app = FastAPI(title="PriceTracker API", version="0.1.0")
# Armazenamento em memória (será substituído por BD na Fase 2)
products_db: dict[int, dict] = {}
prices_db: list[dict] = []
_product_counter = 0
_price_counter = 0
@app.post("/products", response_model=ProductResponse, status_code=201)
async def create_product(product: ProductCreate):
global _product_counter
_product_counter += 1
record = {
"id": _product_counter,
"name": product.name,
"category": product.category.value,
"base_price": product.base_price,
"description": product.description,
"created_at": datetime.utcnow(),
}
products_db[_product_counter] = record
return record
@app.get("/products", response_model=list[ProductResponse])
async def list_products(
category: Optional[Category] = Query(None),
sort: str = Query("name", pattern="^(name|base_price)$"),
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
):
items = list(products_db.values())
if category:
items = [p for p in items if p["category"] == category.value]
items.sort(key=lambda p: p.get(sort, ""))
return items[offset : offset + limit]
@app.get("/products/{product_id}", response_model=ProductDetailResponse)
async def get_product(
product_id: int = Path(gt=0, description="Product ID"),
):
if product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
return products_db[product_id]
Saída:
# Função definida com sucesso
(2) ▶ Exemplo: Endpoints de Atualização e Exclusão
@app.put("/products/{product_id}", response_model=ProductResponse)
async def update_product(
product_id: int = Path(gt=0),
update: ProductUpdate = ...,
):
if product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
record = products_db[product_id]
update_data = update.model_dump(exclude_unset=True)
record.update(update_data)
return record
@app.delete("/products/{product_id}")
async def delete_product(product_id: int = Path(gt=0)):
if product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
del products_db[product_id]
return {"message": "Product deleted"}
Saída:
# Função definida com sucesso
5. Endpoints de Preço e Estratégias de Portfólio na Prática
(1) Combinação de Parâmetros de Query e Corpo da Requisição
(1) ▶ Exemplo: Endpoints de Consulta e Criação de Preço
@app.get("/prices", response_model=list[PriceResponse])
async def search_prices(
product_id: Optional[int] = Query(None, gt=0),
min_price: float = Query(0, ge=0, description="Preço mín em USD"),
max_price: float = Query(999999, ge=0, description="Preço máx em USD"),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
):
results = prices_db
if product_id:
results = [p for p in results if p["product_id"] == product_id]
results = [p for p in results if min_price <= p["price"] <= max_price]
return results[offset : offset + limit]
@app.post("/prices", response_model=PriceResponse, status_code=201)
async def create_price(price: PriceCreate):
global _price_counter
if price.product_id not in products_db:
raise HTTPException(status_code=404, detail="Product not found")
_price_counter += 1
record = {
"id": _price_counter,
"product_id": price.product_id,
"price": price.price,
"currency": price.currency,
"source": price.source,
"recorded_at": datetime.utcnow(),
}
prices_db.append(record)
return record
Saída:
# Função definida com sucesso
6. Guia Prático de Filtragem de Resposta: Versão Pública vs. Versão Admin
(1) Cenário de Negócios: Ocultando Preços de Atacado
Usuários de varejo do PriceTracker só podem visualizar preços de varejo, enquanto administradores podem visualizar preços de atacado e de custo.
(1) ▶ Exemplo: Modelo de Resposta Multi-Papel
class ProductPublicResponse(BaseModel):
"""Visualização pública - ocultar preços de atacado e custo"""
id: int
name: str
category: str
retail_price: float
class ProductAdminResponse(BaseModel):
"""Visualização admin - mostrar cadeia completa de preços"""
id: int
name: str
category: str
retail_price: float
wholesale_price: float
cost_price: float
margin_pct: float # Percentual de margem de lucro
# Armazenamento em memória estendido com níveis de preço
admin_products_db: dict[int, dict] = {}
@app.get("/products/{product_id}/public", response_model=ProductPublicResponse)
async def get_product_public(product_id: int = Path(gt=0)):
if product_id not in admin_products_db:
raise HTTPException(status_code=404, detail="Product not found")
return admin_products_db[product_id]
@app.get("/products/{product_id}/admin", response_model=ProductAdminResponse)
async def get_product_admin(product_id: int = Path(gt=0)):
if product_id not in admin_products_db:
raise HTTPException(status_code=404, detail="Product not found")
return admin_products_db[product_id]
Saída:
# Função definida com sucesso
❓ Perguntas Frequentes
ProductCreate e ProductResponse são separados?Create não inclui um ID (que é gerado no servidor), enquanto o modelo Response inclui um ID. Esta também é uma melhor prática de segurança—o cliente não deve especificar o ID.exclude_unset=True no endpoint PUT?None. Combinado com os campos opcionais em ProductUpdate, isso habilita atualizações parciais./openapi.json, use npx openapi-typescript para gerar tipos TypeScript, ou use Swagger Codegen para gerar um SDK.ProductBase(BaseModel), e faça ProductCreate(ProductBase) e ProductResponse(ProductBase) herdarem e os estenderem.📖 Resumo
- Fase 1: Completados três conjuntos de endpoints centrais para o PriceTracker: CRUD de produto, consulta/criação de preço, e visualização pública/administração
- Arquitetura de modelo Pydantic: Três camadas distintas—Create (validação de entrada), Update (atualizações parciais), e Response (filtragem de saída)
- Parâmetros de path, parâmetros de query e corpo da requisição são naturalmente combinados; o FastAPI os distingue automaticamente por tipo
response_modelalcançando "Mesmos Dados, Visualizações Diferentes"—A Versão Pública Oculta Preços de Atacado, Enquanto a Versão Admin Exibe a Cadeia Completa de Preços- Geração automática de documentação OpenAPI; desenvolvedores front-end podem usar Swagger UI para testar ou gerar SDKs diretamente
📝 Exercícios
- Exercício Básico (Dificuldade ⭐): Combine todo o código desta aula em um único
app/main.py, inicie o serviço, e use o Swagger UI para criar três produtos e consultar a lista. Dica:uvicorn app.main:app --reload - Exercício Avançado (Dificuldade ⭐⭐): Adicione o endpoint
/products/{id}/pricespara consultar todos os registros de preço de um produto especificado, suportando os parâmetros de querysort(date/price) elimit. Dica: parâmetro de pathproduct_id+ parâmetros de querysort/limit - Desafio (Dificuldade: ⭐⭐⭐): Implemente um sistema de visualização dupla usando
ProductAdmineProductPublic—crie dados de produto contendo a cadeia completa de preços. O endpoint público deve retornar apenas o preço de varejo, enquanto o endpoint administrativo deve retornar o preço de varejo, preço de atacado e margem de lucro. Dica: Duas instâncias deresponse_model+ mesma fonte de dados
---|



