Ir para o conteúdo

Aplicações Maiores - Múltiplos Arquivos

Se você está construindo uma aplicação ou uma API web, é raro que você possa colocar tudo em um único arquivo.

FastAPI oferece uma ferramenta conveniente para estruturar sua aplicação, mantendo toda a flexibilidade.

Informação

Se você vem do Flask, isso seria o equivalente aos Blueprints do Flask.

Um exemplo de estrutura de arquivos

Digamos que você tenha uma estrutura de arquivos como esta:

.
├── app
│   ├── __init__.py
│   ├── main.py
│   ├── dependencies.py
│   └── routers
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── internal
│       ├── __init__.py
│       └── admin.py

Dica

Existem vários arquivos __init__.py presentes em cada diretório ou subdiretório.

Isso permite a importação de código de um arquivo para outro.

Por exemplo, no arquivo app/main.py, você poderia ter uma linha como:

from app.routers import items
  • O diretório app contém todo o código da aplicação. Ele possui um arquivo app/__init__.py vazio, o que o torna um "pacote Python" (uma coleção de "módulos Python"): app.
  • Dentro dele, o arquivo app/main.py está localizado em um pacote Python (diretório com __init__.py). Portanto, ele é um "módulo" desse pacote: app.main.
  • Existem também um arquivo app/dependencies.py, assim como o app/main.py, ele é um "módulo": app.dependencies.
  • Há um subdiretório app/routers/ com outro arquivo __init__.py, então ele é um "subpacote Python": app.routers.
  • O arquivo app/routers/items.py está dentro de um pacote, app/routers/, portanto, é um "submódulo": app.routers.items.
  • O mesmo com app/routers/users.py, ele é outro submódulo: app.routers.users.
  • Há também um subdiretório app/internal/ com outro arquivo __init__.py, então ele é outro "subpacote Python":app.internal.
  • E o arquivo app/internal/admin.py é outro submódulo: app.internal.admin.

A mesma estrutura de arquivos com comentários:

.
├── app                  # "app" é um pacote Python
│   ├── __init__.py      # este arquivo torna "app" um "pacote Python"
│   ├── main.py          # "main" módulo, e.g. import app.main
│   ├── dependencies.py  # "dependencies" módulo, e.g. import app.dependencies
│   └── routers          # "routers" é um  "subpacote Python"
│   │   ├── __init__.py  # torna "routers" um "subpacote Python"
│   │   ├── items.py     # "items" submódulo, e.g. import app.routers.items
│   │   └── users.py     # "users" submódulo, e.g. import app.routers.users
│   └── internal         # "internal" é um  "subpacote Python"
│       ├── __init__.py  # torna "internal" um  "subpacote Python"
│       └── admin.py     # "admin" submódulo, e.g. import app.internal.admin

APIRouter

Vamos supor que o arquivo dedicado a lidar apenas com usuários seja o submódulo em /app/routers/users.py.

Você quer manter as operações de rota relacionadas aos seus usuários separadas do restante do código, para mantê-lo organizado.

Mas ele ainda faz parte da mesma aplicação/web API FastAPI (faz parte do mesmo "pacote Python").

Você pode criar as operações de rotas para esse módulo usando o APIRouter.

Importar APIRouter

você o importa e cria uma "instância" da mesma maneira que faria com a classe FastAPI:

app/routers/users.py
from fastapi import APIRouter

router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

Operações de Rota com APIRouter

E então você o utiliza para declarar suas operações de rota.

Utilize-o da mesma maneira que utilizaria a classe FastAPI:

app/routers/users.py
from fastapi import APIRouter

router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

Você pode pensar em APIRouter como uma classe "mini FastAPI".

Todas as mesmas opções são suportadas.

Todos os mesmos parameters, responses, dependencies, tags, etc.

Dica

Neste exemplo, a variável é chamada de router, mas você pode nomeá-la como quiser.

Vamos incluir este APIRouter na aplicação principal FastAPI, mas primeiro, vamos verificar as dependências e outro APIRouter.

Dependências

Vemos que precisaremos de algumas dependências usadas em vários lugares da aplicação.

Então, as colocamos em seu próprio módulo de dependencies (app/dependencies.py).

Agora usaremos uma dependência simples para ler um cabeçalho X-Token personalizado:

app/dependencies.py
from typing import Annotated

from fastapi import Header, HTTPException


async def get_token_header(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")
app/dependencies.py
from fastapi import Header, HTTPException
from typing_extensions import Annotated


async def get_token_header(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")

Dica

Prefira usar a versão Annotated se possível.

app/dependencies.py
from fastapi import Header, HTTPException


async def get_token_header(x_token: str = Header()):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")

Dica

Estamos usando um cabeçalho inventado para simplificar este exemplo.

Mas em casos reais, você obterá melhores resultados usando os Utilitários de Segurança integrados.

Outro módulo com APIRouter

Digamos que você também tenha os endpoints dedicados a manipular "itens" do seu aplicativo no módulo em app/routers/items.py.

Você tem operações de rota para:

  • /items/
  • /items/{item_id}

É tudo a mesma estrutura de app/routers/users.py.

Mas queremos ser mais inteligentes e simplificar um pouco o código.

Sabemos que todas as operações de rota neste módulo têm o mesmo:

  • Path prefix: /items.
  • tags: (apenas uma tag: items).
  • Extra responses.
  • dependências: todas elas precisam da dependência X-Token que criamos.

Então, em vez de adicionar tudo isso a cada operação de rota, podemos adicioná-lo ao APIRouter.

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

Como o caminho de cada operação de rota deve começar com /, como em:

@router.get("/{item_id}")
async def read_item(item_id: str):
    ...

...o prefixo não deve incluir um / final.

Então, o prefixo neste caso é /items.

Também podemos adicionar uma lista de tags e responses extras que serão aplicadas a todas as operações de rota incluídas neste roteador.

E podemos adicionar uma lista de dependencies que serão adicionadas a todas as operações de rota no roteador e serão executadas/resolvidas para cada solicitação feita a elas.

Dica

Observe que, assim como dependências em decoradores de operação de rota, nenhum valor será passado para sua função de operação de rota.

O resultado final é que os caminhos dos itens agora são:

  • /items/
  • /items/{item_id}

...como pretendíamos.

  • Elas serão marcadas com uma lista de tags que contêm uma única string "items".
    • Essas "tags" são especialmente úteis para os sistemas de documentação interativa automática (usando OpenAPI).
  • Todas elas incluirão as responses predefinidas.
  • Todas essas operações de rota terão a lista de dependencies avaliada/executada antes delas.
    • Se você também declarar dependências em uma operação de rota específica, elas também serão executadas.
    • As dependências do roteador são executadas primeiro, depois as dependencies no decorador e, em seguida, as dependências de parâmetros normais.
    • Você também pode adicionar dependências de Segurança com scopes.

Dica

Ter dependências no APIRouter pode ser usado, por exemplo, para exigir autenticação para um grupo inteiro de operações de rota. Mesmo que as dependências não sejam adicionadas individualmente a cada uma delas.

Check

Os parâmetros prefix, tags, responses e dependencies são (como em muitos outros casos) apenas um recurso do FastAPI para ajudar a evitar duplicação de código.

Importar as dependências

Este código reside no módulo app.routers.items, o arquivo app/routers/items.py.

E precisamos obter a função de dependência do módulo app.dependencies, o arquivo app/dependencies.py.

Então usamos uma importação relativa com .. para as dependências:

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

Como funcionam as importações relativas

Dica

Se você sabe perfeitamente como funcionam as importações, continue para a próxima seção abaixo.

Um único ponto ., como em:

from .dependencies import get_token_header

significaria:

  • Começando no mesmo pacote em que este módulo (o arquivo app/routers/items.py) vive (o diretório app/routers/)...
  • encontre o módulo dependencies (um arquivo imaginário em app/routers/dependencies.py)...
  • e dele, importe a função get_token_header.

Mas esse arquivo não existe, nossas dependências estão em um arquivo em app/dependencies.py.

Lembre-se de como nossa estrutura app/file se parece:


Os dois pontos .., como em:

from ..dependencies import get_token_header

significa:

  • Começando no mesmo pacote em que este módulo (o arquivo app/routers/items.py) reside (o diretório app/routers/)...
  • vá para o pacote pai (o diretório app/)...
  • e lá, encontre o módulo dependencies (o arquivo em app/dependencies.py)...
  • e dele, importe a função get_token_header.

Isso funciona corretamente! 🎉


Da mesma forma, se tivéssemos usado três pontos ..., como em:

from ...dependencies import get_token_header

isso significaria:

  • Começando no mesmo pacote em que este módulo (o arquivo app/routers/items.py) vive (o diretório app/routers/)...
  • vá para o pacote pai (o diretório app/)...
  • então vá para o pai daquele pacote (não há pacote pai, app é o nível superior 😱)...
  • e lá, encontre o módulo dependencies (o arquivo em app/dependencies.py)...
  • e dele, importe a função get_token_header.

Isso se referiria a algum pacote acima de app/, com seu próprio arquivo __init__.py, etc. Mas não temos isso. Então, isso geraria um erro em nosso exemplo. 🚨

Mas agora você sabe como funciona, então você pode usar importações relativas em seus próprios aplicativos, não importa o quão complexos eles sejam. 🤓

Adicione algumas tags, respostas e dependências personalizadas

Não estamos adicionando o prefixo /items nem tags=["items"] a cada operação de rota porque os adicionamos ao APIRouter.

Mas ainda podemos adicionar mais tags que serão aplicadas a uma operação de rota específica, e também algumas respostas extras específicas para essa operação de rota:

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

Dica

Esta última operação de caminho terá a combinação de tags: ["items", "custom"].

E também terá ambas as respostas na documentação, uma para 404 e uma para 403.

O principal FastAPI

Agora, vamos ver o módulo em app/main.py.

Aqui é onde você importa e usa a classe FastAPI.

Este será o arquivo principal em seu aplicativo que une tudo.

E como a maior parte de sua lógica agora viverá em seu próprio módulo específico, o arquivo principal será bem simples.

Importar FastAPI

Você importa e cria uma classe FastAPI normalmente.

E podemos até declarar dependências globais que serão combinadas com as dependências para cada APIRouter:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Importe o APIRouter

Agora importamos os outros submódulos que possuem APIRouters:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Como os arquivos app/routers/users.py e app/routers/items.py são submódulos que fazem parte do mesmo pacote Python app, podemos usar um único ponto . para importá-los usando "importações relativas".

Como funciona a importação

A seção:

from .routers import items, users

significa:

  • Começando no mesmo pacote em que este módulo (o arquivo app/main.py) reside (o diretório app/)...
  • procure o subpacote routers (o diretório em app/routers/)...
  • e dele, importe o submódulo items (o arquivo em app/routers/items.py) e users (o arquivo em app/routers/users.py)...

O módulo items terá uma variável router (items.router). Esta é a mesma que criamos no arquivo app/routers/items.py, é um objeto APIRouter.

E então fazemos o mesmo para o módulo users.

Também poderíamos importá-los como:

from app.routers import items, users

Informação

A primeira versão é uma "importação relativa":

from .routers import items, users

A segunda versão é uma "importação absoluta":

from app.routers import items, users

Para saber mais sobre pacotes e módulos Python, leia a documentação oficial do Python sobre módulos.

Evite colisões de nomes

Estamos importando o submódulo items diretamente, em vez de importar apenas sua variável router.

Isso ocorre porque também temos outra variável chamada router no submódulo users.

Se tivéssemos importado um após o outro, como:

from .routers.items import router
from .routers.users import router

o router de users sobrescreveria o de items e não poderíamos usá-los ao mesmo tempo.

Então, para poder usar ambos no mesmo arquivo, importamos os submódulos diretamente:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Incluir o APIRouters para usuários e itens

Agora, vamos incluir os roteadores dos submódulos usuários e itens:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Informação

users.router contém o APIRouter dentro do arquivo app/routers/users.py.

E items.router contém o APIRouter dentro do arquivo app/routers/items.py.

Com app.include_router() podemos adicionar cada APIRouter ao aplicativo principal FastAPI.

Ele incluirá todas as rotas daquele roteador como parte dele.

Detalhe Técnico

Na verdade, ele criará internamente uma operação de rota para cada operação de rota que foi declarada no APIRouter.

Então, nos bastidores, ele realmente funcionará como se tudo fosse o mesmo aplicativo único.

Check

Você não precisa se preocupar com desempenho ao incluir roteadores.

Isso levará microssegundos e só acontecerá na inicialização.

Então não afetará o desempenho. ⚡

Incluir um APIRouter com um prefix personalizado, tags, responses e dependencies

Agora, vamos imaginar que sua organização lhe deu o arquivo app/internal/admin.py.

Ele contém um APIRouter com algumas operações de rota de administração que sua organização compartilha entre vários projetos.

Para este exemplo, será super simples. Mas digamos que, como ele é compartilhado com outros projetos na organização, não podemos modificá-lo e adicionar um prefix, dependencies, tags, etc. diretamente ao APIRouter:

app/internal/admin.py
from fastapi import APIRouter

router = APIRouter()


@router.post("/")
async def update_admin():
    return {"message": "Admin getting schwifty"}

Mas ainda queremos definir um prefixo personalizado ao incluir o APIRouter para que todas as suas operações de rota comecem com /admin, queremos protegê-lo com as dependências que já temos para este projeto e queremos incluir tags e responses.

Podemos declarar tudo isso sem precisar modificar o APIRouter original passando esses parâmetros para app.include_router():

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Dessa forma, o APIRouter original permanecerá inalterado, para que possamos compartilhar o mesmo arquivo app/internal/admin.py com outros projetos na organização.

O resultado é que em nosso aplicativo, cada uma das operações de rota do módulo admin terá:

  • O prefixo /admin.
  • A tag admin.
  • A dependência get_token_header.
  • A resposta 418. 🍵

Mas isso afetará apenas o APIRouter em nosso aplicativo, e não em nenhum outro código que o utilize.

Assim, por exemplo, outros projetos poderiam usar o mesmo APIRouter com um método de autenticação diferente.

Incluir uma operação de rota

Também podemos adicionar operações de rota diretamente ao aplicativo FastAPI.

Aqui fazemos isso... só para mostrar que podemos 🤷:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

e funcionará corretamente, junto com todas as outras operações de rota adicionadas com app.include_router().

Detalhes Técnicos

Observação: este é um detalhe muito técnico que você provavelmente pode simplesmente pular.


Os APIRouters não são "montados", eles não são isolados do resto do aplicativo.

Isso ocorre porque queremos incluir suas operações de rota no esquema OpenAPI e nas interfaces de usuário.

Como não podemos simplesmente isolá-los e "montá-los" independentemente do resto, as operações de rota são "clonadas" (recriadas), não incluídas diretamente.

Verifique a documentação automática da API

Agora, execute uvicorn, usando o módulo app.main e a variável app:

$ uvicorn app.main:app --reload

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

E abra os documentos em http://127.0.0.1:8000/docs.

Você verá a documentação automática da API, incluindo os caminhos de todos os submódulos, usando os caminhos (e prefixos) corretos e as tags corretas:

Incluir o mesmo roteador várias vezes com prefixos diferentes

Você também pode usar .include_router() várias vezes com o mesmo roteador usando prefixos diferentes.

Isso pode ser útil, por exemplo, para expor a mesma API sob prefixos diferentes, por exemplo, /api/v1 e /api/latest.

Esse é um uso avançado que você pode não precisar, mas está lá caso precise.

Incluir um APIRouter em outro

Da mesma forma que você pode incluir um APIRouter em um aplicativo FastAPI, você pode incluir um APIRouter em outro APIRouter usando:

router.include_router(other_router)

Certifique-se de fazer isso antes de incluir router no aplicativo FastAPI, para que as operações de rota de other_router também sejam incluídas.