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 arquivoapp/__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 oapp/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
:
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
:
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:
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")
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.
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ênciaX-Token
que criamos.
Então, em vez de adicionar tudo isso a cada operação de rota, podemos adicioná-lo ao APIRouter
.
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
comscopes
.
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:
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órioapp/routers/
)... - encontre o módulo
dependencies
(um arquivo imaginário emapp/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órioapp/routers/
)... - vá para o pacote pai (o diretório
app/
)... - e lá, encontre o módulo
dependencies
(o arquivo emapp/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órioapp/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 emapp/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:
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
:
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 APIRouter
s:
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órioapp/
)... - procure o subpacote
routers
(o diretório emapp/routers/
)... - e dele, importe o submódulo
items
(o arquivo emapp/routers/items.py
) eusers
(o arquivo emapp/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:
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 APIRouter
s para usuários
e itens
¶
Agora, vamos incluir os roteadores
dos submódulos usuários
e itens
:
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
:
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()
:
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 🤷:
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 APIRouter
s 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.