♻ Refactor items and services endpoints to return count and data, and add CI tests (#599)

Co-authored-by: Esteban Maya Cadavid <emaya@trueblue.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
This commit is contained in:
Esteban Maya
2024-02-25 10:04:47 -05:00
committed by GitHub
parent 176b6fb1c9
commit f41f4432fe
9 changed files with 104 additions and 30 deletions

37
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Test
on:
push:
branches:
- master
pull_request:
types:
- opened
- synchronize
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Docker Compose build
run: docker compose build
- name: Docker Compose remove old containers and volumes
run: docker compose down -v --remove-orphans
- name: Docker Compose up
run: docker compose up -d
- name: Docker Compose run tests
run: docker compose exec -T backend bash /app/tests-start.sh
- name: Docker Compose cleanup
run: docker compose down -v --remove-orphans

View File

@@ -1,15 +1,15 @@
from typing import Any from typing import Any
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from sqlmodel import select from sqlmodel import select, func
from app.api.deps import CurrentUser, SessionDep from app.api.deps import CurrentUser, SessionDep
from app.models import Item, ItemCreate, ItemOut, ItemUpdate, Message from app.models import Item, ItemCreate, ItemOut, ItemUpdate, Message, ItemsOut
router = APIRouter() router = APIRouter()
@router.get("/", response_model=list[ItemOut]) @router.get("/", response_model=ItemsOut)
def read_items( def read_items(
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100 session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
) -> Any: ) -> Any:
@@ -17,9 +17,12 @@ def read_items(
Retrieve items. Retrieve items.
""" """
statment = select(func.count()).select_from(Item)
count = session.exec(statment).one()
if current_user.is_superuser: if current_user.is_superuser:
statement = select(Item).offset(skip).limit(limit) statement = select(Item).offset(skip).limit(limit)
return session.exec(statement).all() items = session.exec(statement).all()
else: else:
statement = ( statement = (
select(Item) select(Item)
@@ -27,7 +30,9 @@ def read_items(
.offset(skip) .offset(skip)
.limit(limit) .limit(limit)
) )
return session.exec(statement).all() items = session.exec(statement).all()
return ItemsOut(data=items, count=count)
@router.get("/{id}", response_model=ItemOut) @router.get("/{id}", response_model=ItemOut)

View File

@@ -1,7 +1,7 @@
from typing import Any, List from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import select from sqlmodel import select, func
from app import crud from app import crud
from app.api.deps import ( from app.api.deps import (
@@ -18,6 +18,7 @@ from app.models import (
UserCreate, UserCreate,
UserCreateOpen, UserCreateOpen,
UserOut, UserOut,
UsersOut,
UserUpdate, UserUpdate,
UserUpdateMe, UserUpdateMe,
) )
@@ -29,15 +30,20 @@ router = APIRouter()
@router.get( @router.get(
"/", "/",
dependencies=[Depends(get_current_active_superuser)], dependencies=[Depends(get_current_active_superuser)],
response_model=List[UserOut], response_model=UsersOut
) )
def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any: def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
""" """
Retrieve users. Retrieve users.
""" """
statment = select(func.count()).select_from(User)
count = session.exec(statment).one()
statement = select(User).offset(skip).limit(limit) statement = select(User).offset(skip).limit(limit)
users = session.exec(statement).all() users = session.exec(statement).all()
return users
return UsersOut(data=users, count=count)
@router.post( @router.post(

View File

@@ -51,6 +51,11 @@ class UserOut(UserBase):
id: int id: int
class UsersOut(SQLModel):
data: list[UserOut]
count: int
# Shared properties # Shared properties
class ItemBase(SQLModel): class ItemBase(SQLModel):
title: str title: str
@@ -80,6 +85,12 @@ class Item(ItemBase, table=True):
# Properties to return via API, id is always required # Properties to return via API, id is always required
class ItemOut(ItemBase): class ItemOut(ItemBase):
id: int id: int
owner_id: int
class ItemsOut(SQLModel):
data: list[ItemOut]
count: int
# Generic message # Generic message

View File

@@ -8,11 +8,11 @@ from app.core.config import settings
def test_celery_worker_test( def test_celery_worker_test(
client: TestClient, superuser_token_headers: Dict[str, str] client: TestClient, superuser_token_headers: Dict[str, str]
) -> None: ) -> None:
data = {"msg": "test"} data = {"message": "test"}
r = client.post( r = client.post(
f"{settings.API_V1_STR}/utils/test-celery/", f"{settings.API_V1_STR}/utils/test-celery/",
json=data, json=data,
headers=superuser_token_headers, headers=superuser_token_headers,
) )
response = r.json() response = r.json()
assert response["msg"] == "Word received" assert response["message"] == "Word received"

View File

@@ -110,6 +110,7 @@ def test_retrieve_users(
r = client.get(f"{settings.API_V1_STR}/users/", headers=superuser_token_headers) r = client.get(f"{settings.API_V1_STR}/users/", headers=superuser_token_headers)
all_users = r.json() all_users = r.json()
assert len(all_users) > 1 assert len(all_users["data"]) > 1
for item in all_users: assert "count" in all_users
for item in all_users["data"]:
assert "email" in item assert "email" in item

View File

@@ -23,6 +23,8 @@ python-jose = {extras = ["cryptography"], version = "^3.3.0"}
httpx = "^0.25.1" httpx = "^0.25.1"
psycopg = {extras = ["binary"], version = "^3.1.13"} psycopg = {extras = ["binary"], version = "^3.1.13"}
sqlmodel = "^0.0.16" sqlmodel = "^0.0.16"
# Pin bcrypt until passlib supports the latest
bcrypt = "4.0.1"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
mypy = "^1.7.0" mypy = "^1.7.0"

View File

@@ -28,10 +28,22 @@ services:
- traefik.http.routers.${STACK_NAME?Variable not set}-traefik-public-http.rule=Host(`${DOMAIN?Variable not set}`) - traefik.http.routers.${STACK_NAME?Variable not set}-traefik-public-http.rule=Host(`${DOMAIN?Variable not set}`)
- traefik.http.services.${STACK_NAME?Variable not set}-traefik-public.loadbalancer.server.port=80 - traefik.http.services.${STACK_NAME?Variable not set}-traefik-public.loadbalancer.server.port=80
db:
ports:
- "5432:5432"
pgadmin: pgadmin:
ports: ports:
- "5050:5050" - "5050:5050"
# Uncomment the section below to be able to debug locally
# queue:
# ports:
# - "5671:5671"
# - "5672:5672"
# - "15672:15672"
# - "15671:15671"
flower: flower:
ports: ports:
- "5555:5555" - "5555:5555"
@@ -84,14 +96,14 @@ services:
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=Host(`old-frontend.localhost.tiangolo.com`) - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=Host(`old-frontend.localhost.tiangolo.com`)
- traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80 - traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80
new-frontend: # new-frontend:
build: # build:
context: ./new-frontend # context: ./new-frontend
labels: # labels:
- traefik.enable=true # - traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set} # - traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-new-frontend-http.rule=PathPrefix(`/`) # - traefik.http.routers.${STACK_NAME?Variable not set}-new-frontend-http.rule=PathPrefix(`/`)
- traefik.http.services.${STACK_NAME?Variable not set}-new-frontend.loadbalancer.server.port=80 # - traefik.http.services.${STACK_NAME?Variable not set}-new-frontend.loadbalancer.server.port=80
networks: networks:
traefik-public: traefik-public:

View File

@@ -191,16 +191,16 @@ services:
- traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=PathPrefix(`/`) - traefik.http.routers.${STACK_NAME?Variable not set}-frontend-http.rule=PathPrefix(`/`)
- traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80 - traefik.http.services.${STACK_NAME?Variable not set}-frontend.loadbalancer.server.port=80
new-frontend: # new-frontend:
image: '${DOCKER_IMAGE_NEW_FRONTEND?Variable not set}:${TAG-latest}' # image: '${DOCKER_IMAGE_NEW_FRONTEND?Variable not set}:${TAG-latest}'
build: # build:
context: ./new-frontend # context: ./new-frontend
deploy: # deploy:
labels: # labels:
- traefik.enable=true # - traefik.enable=true
- traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set} # - traefik.constraint-label-stack=${TRAEFIK_TAG?Variable not set}
- traefik.http.routers.${STACK_NAME?Variable not set}-new-frontend-http.rule=PathPrefix(`/`) # - traefik.http.routers.${STACK_NAME?Variable not set}-new-frontend-http.rule=PathPrefix(`/`)
- traefik.http.services.${STACK_NAME?Variable not set}-new-frontend.loadbalancer.server.port=80 # - traefik.http.services.${STACK_NAME?Variable not set}-new-frontend.loadbalancer.server.port=80
volumes: volumes: