♻ 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:
37
.github/workflows/test.yaml
vendored
Normal file
37
.github/workflows/test.yaml
vendored
Normal 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
|
@@ -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)
|
||||||
|
@@ -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(
|
||||||
|
@@ -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
|
||||||
|
@@ -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"
|
||||||
|
@@ -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
|
||||||
|
@@ -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"
|
||||||
|
@@ -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:
|
||||||
|
@@ -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:
|
||||||
|
Reference in New Issue
Block a user