Browse Source

♻ 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>
pull/13907/head
Esteban Maya 1 year ago
committed by GitHub
parent
commit
f41f4432fe
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 37
      .github/workflows/test.yaml
  2. 15
      src/backend/app/app/api/api_v1/endpoints/items.py
  3. 12
      src/backend/app/app/api/api_v1/endpoints/users.py
  4. 11
      src/backend/app/app/models.py
  5. 4
      src/backend/app/app/tests/api/api_v1/test_celery.py
  6. 5
      src/backend/app/app/tests/api/api_v1/test_users.py
  7. 2
      src/backend/app/pyproject.toml
  8. 28
      src/docker-compose.override.yml
  9. 20
      src/docker-compose.yml

37
.github/workflows/test.yaml

@ -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

15
src/backend/app/app/api/api_v1/endpoints/items.py

@ -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)

12
src/backend/app/app/api/api_v1/endpoints/users.py

@ -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(

11
src/backend/app/app/models.py

@ -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

4
src/backend/app/app/tests/api/api_v1/test_celery.py

@ -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"

5
src/backend/app/app/tests/api/api_v1/test_users.py

@ -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

2
src/backend/app/pyproject.toml

@ -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
src/docker-compose.override.yml

@ -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:

20
src/docker-compose.yml

@ -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:

Loading…
Cancel
Save