Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
O FastAPI é uma framework web moderna em Python para construir APIs. Combinado com mssql-python, pode construir APIs REST de alto desempenho apoiadas por Microsoft SQL e Base de Dados SQL do Azure.
Pré-requisitos
- Python 3.10 ou posterior.
- Os pacotes
mssql-python,fastapi,uvicorn,pydanticePyJWT. Instale tudo compip install fastapi uvicorn mssql-python pydantic pyjwt. - Instale pré-requisitos que devem ser instalados uma única vez específicos do sistema operacional. Os utilizadores do Windows podem saltar este passo. Para detalhes completos da plataforma, consulte Instalar mssql-python.
Criar um banco de dados SQL
Criar ou ligar-se a uma base de dados SQL numa das seguintes plataformas:
Os exemplos neste artigo utilizam a base de dados de exemplos AdventureWorksLT , especificamente a SalesLT.Product tabela. Se não tiver o AdventureWorksLT instalado, consulte as bases de dados de exemplo do AdventureWorks.
Configuração do projeto
Criar um ambiente virtual
Crie e ative um ambiente virtual para que os pacotes deste projeto fiquem isolados de outras instalações em Python. Este passo também evita o problema comum de instalar pacotes num intérprete enquanto executa a sua aplicação ou os testes com outro.
py -m venv .venv
.\.venv\Scripts\Activate.ps1
Depois de ativar o ambiente, python, pip e pytest apontam todos para o mesmo interpretador. Execute os comandos restantes neste artigo a partir do ambiente ativado.
Note
No Windows para Arm, crie o ambiente com uma compilação Arm64 do Python para que mssql-python e as suas dependências sejam instalados a partir de wheels pré-compilados. Numa máquina com mais de uma versão do Python, py -m venv pode selecionar uma versão ou arquitetura diferente da esperada, por isso, verifique com python -c "import sys, sysconfig; print(sys.version, sysconfig.get_platform())" depois de ativar. Se pip tentar compilar cryptography a partir do código-fonte (um erro do conjunto de ferramentas do Rust e do OpenSSL), instale primeiro uma versão baseada em wheel com pip install --only-binary=:all: cryptography e, em seguida, instale o restante.
Instalar dependências
Instale os pacotes necessários com pip:
pip install fastapi uvicorn mssql-python pydantic pyjwt
Estrutura do projeto
Organize o seu projeto com módulos separados para bases de dados, esquemas e operações CRUD:
my_api/
├── main.py
├── database.py
├── models.py
├── schemas.py
├── crud.py
├── test_api.py
└── routers/
└── products.py
Gestão de ligações à base de dados
O FastAPI utiliza injeção de dependências para fornecer recursos como ligações a bases de dados para manipuladores de rotas. O padrão nesta secção cria um gestor de contexto que abre uma ligação, devolve um cursor e trata automaticamente da confirmação, da reversão e do fecho.
Cria database.py
A get_connection_string() função constrói a cadeia de ligação ODBC a partir dos valores de configuração. O gestor de contexto get_db() e o gerador get_db_dependency() seguem ambos o mesmo padrão: abrir uma conexão, devolver um cursor, confirmar a transação em caso de sucesso, anulá-la em caso de erro e fechar sempre no fim. O FastAPI chama Depends()get_db_dependency() uma vez por pedido e gere o seu ciclo de vida.
# database.py
import mssql_python
from contextlib import contextmanager
from typing import Generator
# Configuration
DATABASE_CONFIG = {
"server": "<server>.database.windows.net",
"database": "<database>",
}
def get_connection_string() -> str:
"""Build connection string from config."""
return (
f"Server={DATABASE_CONFIG['server']};"
f"Database={DATABASE_CONFIG['database']};"
"Authentication=ActiveDirectoryDefault;"
"Encrypt=yes"
)
Note
ActiveDirectoryDefault utiliza DefaultAzureCredential, que tenta vários provedores de credenciais em sequência. A primeira conexão pode ser lenta porque o SDK percorre a cadeia até encontrar um provedor funcional. Em produção, se souber que tipo de credencial o seu ambiente utiliza, especifique-o diretamente (por exemplo, ActiveDirectoryMSI para identidade gerida) para evitar o chain walk. Para obter mais informações, consulte Autenticação do Microsoft Entra.
@contextmanager
def get_db() -> Generator:
"""Database connection context manager for FastAPI dependency injection."""
conn = mssql_python.connect(get_connection_string())
cursor = conn.cursor()
try:
yield cursor
conn.commit()
except Exception:
conn.rollback()
raise
finally:
cursor.close()
conn.close()
def get_db_dependency():
"""FastAPI dependency for database cursor."""
conn = mssql_python.connect(get_connection_string())
cursor = conn.cursor()
try:
yield cursor
conn.commit()
except Exception:
conn.rollback()
raise
finally:
cursor.close()
conn.close()
Modelos pidânticos
Os modelos pidânticos definem as regras de forma e validação para dados de pedido e resposta. O FastAPI utiliza estes modelos para analisar JSON recebido, validar restrições de campo e gerar documentação OpenAPI automaticamente.
Cria schemas.py
Separe os esquemas em Base, Create, Update, e variantes de resposta. O Base esquema contém campos partilhados, Create herda deles para operações de inserção e Update torna todos os campos opcionais para atualizações parciais.
# schemas.py
from pydantic import BaseModel, ConfigDict, EmailStr, Field
from typing import Optional
from datetime import datetime
# Product schemas
class ProductBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
product_number: str = Field(..., min_length=1, max_length=25)
price: float = Field(..., gt=0)
color: Optional[str] = Field(None, max_length=50)
size: Optional[str] = Field(None, max_length=50)
category_id: Optional[int] = None
class ProductCreate(ProductBase):
pass
class ProductUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
product_number: Optional[str] = Field(None, min_length=1, max_length=25)
price: Optional[float] = Field(None, gt=0)
color: Optional[str] = Field(None, max_length=50)
size: Optional[str] = Field(None, max_length=50)
category_id: Optional[int] = None
class Product(ProductBase):
id: int
model_config = ConfigDict(from_attributes=True)
# Pagination
class PaginatedResponse(BaseModel):
items: list
total: int
page: int
page_size: int
pages: int
Operações CRUD
Encapsular as consultas à base de dados numa classe dedicada para manter os gestores de rotas limitados. Cada método estático recebe um cursor (injetado pelo FastAPI) e gere uma operação usando consultas parametrizadas (%(name)s marcadores de posição com um dicionário de valores) para evitar a injeção SQL. Esta separação torna a lógica de negócio mais fácil de testar e reutilizar.
Cria crud.py
# crud.py
from typing import Optional, List
from schemas import ProductCreate, ProductUpdate, Product
class ProductCRUD:
"""CRUD operations for products."""
@staticmethod
def get(cursor, product_id: int) -> Optional[dict]:
cursor.execute("""
SELECT ProductID, Name, ProductNumber, ListPrice, Color, Size
FROM SalesLT.Product
WHERE ProductID = %(id)s
""", {"id": product_id})
row = cursor.fetchone()
if row:
return {
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"size": row.Size
}
return None
@staticmethod
def get_all(cursor, skip: int = 0, limit: int = 100) -> List[dict]:
cursor.execute("""
SELECT ProductID, Name, ProductNumber, ListPrice, Color, Size
FROM SalesLT.Product
ORDER BY ProductID
OFFSET %(skip)s ROWS
FETCH NEXT %(limit)s ROWS ONLY
""", {"skip": skip, "limit": limit})
return [{
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"size": row.Size
} for row in cursor.fetchall()]
@staticmethod
def count(cursor) -> int:
cursor.execute("SELECT COUNT(*) FROM SalesLT.Product")
return cursor.fetchval()
@staticmethod
def create(cursor, product: ProductCreate) -> dict:
cursor.execute("""
INSERT INTO SalesLT.Product (Name, ProductNumber, ListPrice, Color, Size, ProductCategoryID, StandardCost, SellStartDate)
OUTPUT INSERTED.ProductID, INSERTED.Name, INSERTED.ProductNumber,
INSERTED.ListPrice, INSERTED.Color, INSERTED.Size
VALUES (%(name)s, %(product_number)s, %(price)s, %(color)s, %(size)s, %(category_id)s, 0, GETDATE())
""", {
"name": product.name,
"product_number": product.product_number,
"price": product.price,
"color": product.color,
"size": product.size,
"category_id": product.category_id
})
row = cursor.fetchone()
return {
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"size": row.Size
}
@staticmethod
def update(cursor, product_id: int, product: ProductUpdate) -> Optional[dict]:
# Build dynamic update
updates = []
params = {"id": product_id}
if product.name is not None:
updates.append("Name = %(name)s")
params["name"] = product.name
if product.product_number is not None:
updates.append("ProductNumber = %(product_number)s")
params["product_number"] = product.product_number
if product.price is not None:
updates.append("ListPrice = %(price)s")
params["price"] = product.price
if product.category_id is not None:
updates.append("ProductCategoryID = %(category_id)s")
params["category_id"] = product.category_id
if not updates:
return ProductCRUD.get(cursor, product_id)
cursor.execute(f"""
UPDATE SalesLT.Product SET {', '.join(updates)}
OUTPUT INSERTED.ProductID, INSERTED.Name, INSERTED.ProductNumber,
INSERTED.ListPrice, INSERTED.Color, INSERTED.Size
WHERE ProductID = %(id)s
""", params)
row = cursor.fetchone()
if row:
return {
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"size": row.Size
}
return None
@staticmethod
def delete(cursor, product_id: int) -> bool:
cursor.execute("""
DELETE FROM SalesLT.Product WHERE ProductID = %(id)s
""", {"id": product_id})
return cursor.rowcount > 0
@staticmethod
def search(cursor, query: str, skip: int = 0, limit: int = 100) -> List[dict]:
cursor.execute("""
SELECT ProductID, Name, ProductNumber, ListPrice, Color, Size
FROM SalesLT.Product
WHERE Name LIKE %(query)s OR ProductNumber LIKE %(query)s
ORDER BY ProductID
OFFSET %(skip)s ROWS
FETCH NEXT %(limit)s ROWS ONLY
""", {"query": f"%{query}%", "skip": skip, "limit": limit})
return [{
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"size": row.Size
} for row in cursor.fetchall()]
Aplicação FastAPI
Cria main.py
O módulo principal liga tudo. Cada rota declara cursor = Depends(get_db_dependency), o que indica ao FastAPI para chamar o gerador, passar o cursor produzido ao processador e fazer a limpeza no final. O FastAPI também valida os corpos dos pedidos de acordo com os seus esquemas Pydantic antes de a função de processamento ser executada.
# main.py
from fastapi import FastAPI, HTTPException, Depends, Query
from typing import List
from database import get_db_dependency
from schemas import Product, ProductCreate, ProductUpdate, PaginatedResponse
from crud import ProductCRUD
app = FastAPI(
title="Product API",
description="REST API for products using mssql-python",
version="1.0.0"
)
@app.get("/")
def root():
return {"message": "Product API", "docs": "/docs"}
@app.get("/products", response_model=PaginatedResponse)
def list_products(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
cursor = Depends(get_db_dependency)
):
"""List all products with pagination."""
skip = (page - 1) * page_size
items = ProductCRUD.get_all(cursor, skip=skip, limit=page_size)
total = ProductCRUD.count(cursor)
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size,
"pages": (total + page_size - 1) // page_size
}
@app.get("/products/{product_id}", response_model=Product)
def get_product(product_id: int, cursor = Depends(get_db_dependency)):
"""Get a specific product by ID."""
product = ProductCRUD.get(cursor, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return product
@app.post("/products", response_model=Product, status_code=201)
def create_product(product: ProductCreate, cursor = Depends(get_db_dependency)):
"""Create a new product."""
return ProductCRUD.create(cursor, product)
@app.put("/products/{product_id}", response_model=Product)
def update_product(
product_id: int,
product: ProductUpdate,
cursor = Depends(get_db_dependency)
):
"""Update an existing product."""
updated = ProductCRUD.update(cursor, product_id, product)
if not updated:
raise HTTPException(status_code=404, detail="Product not found")
return updated
@app.delete("/products/{product_id}", status_code=204)
def delete_product(product_id: int, cursor = Depends(get_db_dependency)):
"""Delete a product."""
if not ProductCRUD.delete(cursor, product_id):
raise HTTPException(status_code=404, detail="Product not found")
@app.get("/products/search/", response_model=List[Product])
def search_products(
q: str = Query(..., min_length=1),
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
cursor = Depends(get_db_dependency)
):
"""Search products by name or product number."""
skip = (page - 1) * page_size
return ProductCRUD.search(cursor, q, skip=skip, limit=page_size)
# Health check endpoint
@app.get("/health")
def health_check(cursor = Depends(get_db_dependency)):
"""Check database connectivity."""
try:
cursor.execute("SELECT 1")
return {"status": "healthy", "database": "connected"}
except Exception as e:
raise HTTPException(status_code=503, detail=f"Database unhealthy: {str(e)}")
Execute o aplicativo
uvicorn main:app --reload --host 0.0.0.0 --port 8000
Tratamento de erros
O FastAPI permite registar manipuladores globais de exceções para tipos específicos de exceções. Quando detecta mssql_python.DatabaseError e mssql_python.IntegrityError, o FastAPI devolve erros JSON estruturados com códigos de estado HTTP apropriados em vez de respostas genéricas 500.
Tratador global de exceções
Adicione estes tratadores a main.py, logo após a app = FastAPI(...) linha. O FastAPI executa o handler correspondente sempre que uma rota eleva esse tipo de exceção, por isso não precisas de um try/except bloco em cada rota.
# main.py
from fastapi import Request
from fastapi.responses import JSONResponse
import mssql_python
@app.exception_handler(mssql_python.DatabaseError)
async def database_exception_handler(request: Request, exc: mssql_python.DatabaseError):
"""Handle database errors globally."""
return JSONResponse(
status_code=500,
content={"detail": "Database error occurred", "type": "database_error"}
)
@app.exception_handler(mssql_python.IntegrityError)
async def integrity_exception_handler(request: Request, exc: mssql_python.IntegrityError):
"""Handle integrity constraint violations."""
error_msg = str(exc)
if "UNIQUE" in error_msg:
return JSONResponse(
status_code=409,
content={"detail": "Resource already exists", "type": "duplicate_error"}
)
elif "FOREIGN KEY" in error_msg:
return JSONResponse(
status_code=400,
content={"detail": "Referenced resource not found", "type": "reference_error"}
)
return JSONResponse(
status_code=400,
content={"detail": "Data integrity error", "type": "integrity_error"}
)
Note
Eliminar um produto que ainda é referenciado por outras linhas gera mssql_python.IntegrityError devido à restrição de chave estrangeira, e o manipulador retorna um 400 em vez de remover a linha. No exemplo AdventureWorksLT, a maioria dos produtos de SalesLT.Product são referenciados por SalesLT.SalesOrderDetail, pelo que DELETE falha propositadamente para esses produtos. Para testar uma eliminação bem-sucedida, crie um produto com POST /products e apague esse, ou remova primeiro as linhas de referência.
Agrupamento de conexões
Sem agrupamento de ligações, cada solicitação abre e fecha uma ligação TCP ao Microsoft SQL, o que acrescenta latência. O agrupamento de conexões mantém um conjunto de conexões inativas pronto para reutilização. Chame mssql_python.pooling() uma vez no arranque. Com o pooling ativado, conn.close() em get_db_dependency() devolve a conexão ao pool em vez de a fechar realmente.
Módulo de base de dados melhorado
Ative o pooling chamando mssql_python.pooling() no arranque e configure-o com o tamanho máximo e as definições de timeout apropriadas:
# database.py with connection pooling
import mssql_python
from contextlib import contextmanager
import os
# Configure pool
mssql_python.pooling(max_size=20, idle_timeout=300)
DATABASE_URL = os.getenv(
"DATABASE_URL",
"Server=<server>.database.windows.net;Database=<database>;"
"Authentication=ActiveDirectoryDefault;Encrypt=yes"
)
def get_db_dependency():
"""FastAPI dependency with connection pooling."""
conn = mssql_python.connect(DATABASE_URL)
cursor = conn.cursor()
try:
yield cursor
conn.commit()
except Exception:
conn.rollback()
raise
finally:
cursor.close()
conn.close() # Returns to pool
Middleware de autenticação
Pode combinar o acesso à base de dados com autenticação encadeando dependências do FastAPI. O exemplo seguinte valida um token portador JWT, pesquisa o registo de pessoa correspondente na base de dados de exemplos AdventureWorksLT e disponibiliza o resultado para rotas protegidas.
# auth.py
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
security = HTTPBearer()
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
cursor = Depends(get_db_dependency)
):
"""Validate JWT and return the matching AdventureWorksLT person."""
try:
token = credentials.credentials
# Replace with a strong secret loaded from environment variables
payload = jwt.decode(token, "your-secret-key", algorithms=["HS256"])
person_id = int(payload.get("sub"))
if not person_id:
raise HTTPException(status_code=401, detail="Invalid token")
cursor.execute("""
SELECT BusinessEntityID, FirstName, LastName
FROM Person.Person
WHERE BusinessEntityID = %(id)s
""", {"id": person_id})
person = cursor.fetchone()
if not person:
raise HTTPException(status_code=401, detail="User not found")
return {
"id": person.BusinessEntityID,
"first_name": person.FirstName,
"last_name": person.LastName
}
except (TypeError, ValueError):
raise HTTPException(status_code=401, detail="Invalid token subject")
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
# Protected endpoint
@app.get("/me")
def get_me(current_user: dict = Depends(get_current_user)):
return current_user
Testing
O FastAPI fornece um TestClient baseado em httpx que envia requisições para a sua aplicação sem iniciar um servidor HTTP real. Escreva testes com pytest para verificar rotas, códigos de estado e estruturas de resposta.
Antes de executar os testes desta secção, instale as dependências de teste:
pip install pytest httpx
Note
Se estiver a usar o Starlette mais recente ou a montar um novo ambiente, prefira httpx2 em vez de httpx. As versões recentes do Starlette usam httpx2 para TestClient e emitem um aviso de preterição quando está instalado apenas httpx. Instale-o com pip install pytest httpx2.
Configuração dos testes
Crie um ficheiro de teste que utilize TestClient para verificar o comportamento das rotas e os esquemas de resposta:
# test_api.py
from fastapi.testclient import TestClient
from main import app
import uuid
import pytest
client = TestClient(app)
def test_list_products():
response = client.get("/products")
assert response.status_code == 200
data = response.json()
assert "items" in data
assert "total" in data
def test_create_product():
suffix = uuid.uuid4().hex[:8]
name = f"Test Product {suffix}"
product_data = {
"name": name,
"product_number": f"TEST-{suffix}",
"price": 19.99,
"color": "Red",
"size": "M",
"category_id": 1
}
response = client.post("/products", json=product_data)
assert response.status_code == 201
data = response.json()
assert data["name"] == name
assert data["price"] == 19.99
def test_get_product_not_found():
response = client.get("/products/99999")
assert response.status_code == 404
def test_health_check():
response = client.get("/health")
assert response.status_code == 200
assert response.json()["status"] == "healthy"
Execute os testes com pytest na raiz do projeto, o mesmo diretório onde está main.py:
pytest
Estes testes correm contra a sua base de dados em tempo real em vez de mocks, por isso test_create_product insere uma linha real em SalesLT.Product. No AdventureWorksLT, tanto Name como ProductNumber têm restrições únicas, pelo que o teste gera um valor único para cada uma em cada execução. Se codificares esses valores de forma fixa, o teste falha com um conflito na segunda execução, a menos que apagues a linha primeiro.
Configuração de implantação
Utiliza o BaseSettings do Pydantic para carregar a configuração a partir de variáveis de ambiente e de ficheiros .env. Esta abordagem mantém segredos fora do código-fonte e facilita a alternância entre ambientes. Instale o pacote de definições com pip install pydantic-settings.
Variáveis ambientais
Crie um módulo de definições que carregue a configuração a partir das variáveis do ambiente, permitindo-lhe gerir segredos e valores específicos de implementação fora do seu código:
# config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
database_server: str = "<server>.database.windows.net"
database_name: str = "<database>"
pool_size: int = 10
model_config = SettingsConfigDict(env_file=".env")
settings = Settings()
def get_connection_string() -> str:
return (
f"Server={settings.database_server};"
f"Database={settings.database_name};"
"Authentication=ActiveDirectoryDefault;"
"Encrypt=yes"
)
Depois atualizo database.py para importar get_connection_string a partir config em vez de definir a sua própria cópia. Ao remover a função duplicada, garante que a aplicação lê as definições de ligação de uma única fonte.
# database.py
from config import get_connection_string