Filtrando dados
Agora que já podemos trocar pontos entre usuários vamos criar endpoints onde será possivel consultar as transações e saldos.
POST /transaction/{username}/
(feito) - adiciona transaçãoGET /transaction/
- Lista todas as transações- Se
superuser
exibe todas, caso contrário apenas as próprias. - permite filtros:
?from_user=username
,?user=username
- permite ordenação:
?order_by=from_user,user,value,date
- permite paginação:
?page=1&size=10
- Se
Antes de criarmos o endpoint precisamos criar um model de saida, TransactionResponse
para evitar o retorno do próprio model do banco de dados e se fizermos isso em dundie/models/transaction.py
teremos um problema de circular imports.
- # !!!! Exemplo em dundie/models/transaction.py
- from dundie.models.user import User # <- CIRCULAR IMPORT
Para contornar este problema de maneira simples, vamos agora criar um novo arquivo, desta forma isolamos o import e evitamos o import circular.
OBS Neste momento vamos colocar apenas o serializar para Transaction neste novo módulo mas futuramente podemos mover todos os serializers definidos em
models/user.py
emodels/transaction.py
para este mesmo módulo também.
Neste serializer vamos utilizar root_validator
para criar campos que são calculados no
momento da serialização.
CRIE o arquivo dundie/models/serializers.py
VocÊ pode criar usando
touch dundie/models/serializers.py
ou usando seu IDE ou navegador de arquivos.
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, root_validator
from sqlmodel import Session
from dundie.db import engine
from .user import User
class TransactionResponse(BaseModel, extra="allow"):
id: int
value: int
date: datetime
# These 2 fields will be calculated at response time.
user: Optional[str] = None
from_user: Optional[str] = None
@root_validator(pre=True)
def get_usernames(cls, values: dict):
with Session(engine) as session:
user = session.get(User, values["user_id"])
values["user"] = user and user.username
from_user = session.get(User, values["from_id"])
values["from_user"] = from_user and from_user.username
return values
Podemos testar no shell com:
$ docker compose exec api dundie shell
Auto imports: ['settings', 'engine', 'select', 'session', 'User',
'Transaction', 'Balance', 'add_transaction']
In [1]: from dundie.models.serializers import TransactionResponse
In [2]: t = session.get(Transaction, 1)
In [3]: TransactionResponse.parse_obj(t)
Out[3]: TransactionResponse(
value=100,
date=datetime.datetime(2023, 1, 6, 12, 21, 55, 30204),
user='bruno-rocha',
from_user='michael-scott',
user_id=2,
from_id=1,
id=1
)
Agora vamos EDITAR o arquivo dundie/routes/transaction.py
# No topo
from fastapi_pagination import Page, Params
from fastapi_pagination.ext.sqlmodel import paginate
from dundie.models import Transaction
from dundie.models.serializers import TransactionResponse
from sqlmodel import text
from sqlalchemy.orm import aliased
# No final do arquivo
@router.get("/", response_model=Page[TransactionResponse])
async def list_transactions(
*,
current_user: User = AuthenticatedUser,
session: Session = ActiveSession,
params: Params = Depends(),
from_user: Optional[str] = None,
user: Optional[str] = None,
order_by: Optional[str] = None,
):
query = select(Transaction)
# Optional `AND` filters
if user:
query = query.join(
User, Transaction.user_id == User.id
).where(User.username == user)
if from_user:
FromUser = aliased(User) # aliased needed to desambiguous the join
query = query.join(
FromUser, Transaction.from_id == FromUser.id
).where(FromUser.username == from_user)
# Mandatory access filter
# regular users can only see their own transactions
if not current_user.superuser:
query = query.where(
Transaction.user_id == current_user.id | Transaction.from_id == current_user.id
)
# Ordering based on &order_by=date (asc) or -date (desc)
if order_by:
order_text = text(
order_by.replace("-", "") + " " + ("desc" if "-" in order_by else "asc")
)
query = query.order_by(order_text)
# wrap response_model in a pagination object {"items": [], total, page, size }
return paginate(query=query, session=session, params=params)
Agora temos um novo endpoint listando todas as transactions e com os filtros que especificamos.
$ curl 'GET' -H 'Content-Type: application/json' \
-H 'Authorization: Bearer TOKEN_AQUI' \
-k 'http://localhost:8000/transaction/\
?page=1&size=2&from_user=michael-scott&user=bruno-rocha&order_by=-date'
#+RESPONSE
{
"items": [
{
"id": 12,
"value": 300,
"date": "2023-01-10T17:08:29.953452",
"user": "bruno-rocha",
"from_user": "michael-scott",
"from_id": 1,
"user_id": 2
},
{
"id": 11,
"value": 112,
"date": "2023-01-10T17:07:49.296277",
"user": "bruno-rocha",
"from_user": "michael-scott",
"from_id": 1,
"user_id": 2
}
],
"total": 6,
"page": 1,
"size": 2
}
#+END
Repare que como usamos o plugin fastapi_pagination agora o formato da resposta está diferente contendo items
, total
, page
e size
Revisão da API
Agora que já temos bastante funcionalidade na API vamos revisar e identificar o que está faltando.
Auth
- POST /token - login via formulário para gerar acccess token
- POST /refresh_token - Obter um novo token sem a necessidade de fazer login novamente
User
- GET /user/ - Lista todos os usuários
- GET /user/{username} - Lista um usuário específico
- POST🔒 /user/ - Cria um novo usuário
- PATCH🔒 /user/{username} - Altera informações do usuário
- POST /user/{username}/password - Altera a senha do usuário (?pwd_reset_token ou 🔒)
- POST /user/pwd_reset_token/ - Solicita um token via email para resetar a senha (?email)
Transaction
- POST🔒 /transaction/ - Cria uma nova transaction de
from_user para user
- GET🔒 /transaction/ - Lista transactions do usuário logado (ou todas em caso de superuser)
- filters:
user
,from_user
- sort:
order_by=date
(asc) ou-date
(desc) - pagination:
page
,size
- filters:
Mas ainda falta informação neste retorno, onde está o saldo total do usuário? vamos resolver -->