~nora/fastapi-sandbox

2c5bd7482a023bec8f6cee931687fe5e178dd6ac — nora 6 months ago 4995cf6
Add templates
11 files changed, 188 insertions(+), 160 deletions(-)

R app/{users/database.py => database.py}
A app/items/__init__.py
A app/items/routers.py
A app/items/schemas.py
A app/items/templates/item.html
M app/main.py
M app/tests.py
R app/users/{crud.py => api.py}
M app/users/models.py
M app/users/routers.py
M app/users/schemas.py
R app/users/database.py => app/database.py +0 -0
A app/items/__init__.py => app/items/__init__.py +0 -0
A app/items/routers.py => app/items/routers.py +108 -0
@@ 0,0 1,108 @@
from datetime import datetime
from typing import Annotated

from fastapi import APIRouter, Request, Body, Cookie, File, Form, Header, Path, Query, UploadFile
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates

from .schemas import Item

templates = Jinja2Templates(directory="app/items/templates")

router = APIRouter(
    prefix="/items",
    tags=["items"],
)


@router.get("/{item_id}", response_class=HTMLResponse)
async def read_item(request: Request, item_id: int):
    """
    The function return type can be defined as follows:
    * builtin type
    * Pydantic model
    * Response
    * PlainTextResponse
    * HtmlResponse
    * JSONResponse
    * RedirectResponse

    Default response class JSONResponse can be overridden with:
    * app = FastAPI(default_response_class=HtmlResponse)

    The path decorator's parameter `response_class` define response class:
    * @app.get("/items/", response_class=HTMLResponse)

    The path decorator's parameter `response_model` define response model:
    * @app.get("/portal", response_model=None)
    * @app.get("/items/", response_model=list[Item])
    * @app.get("/user/", response_model=BaseUser, response_model_exclude_unset=True)

    The path decorator's parameter `status_code` define response status code:
    * @app.post("/items/", status_code=201)
    * @app.post("/items/", status_code=status.HTTP_201_CREATED)

    The path decorator's parameters `tags`, `summary` and `description` are used in interactive docs:
    * @app.get("/items/", tags=["items"], summary="Create an item", description="Create an item description")

    Exception can be defined as follows:
    * raise HTTPException(status_code=404, detail="Item not found")
    """
    return templates.TemplateResponse("item.html", {"request": request, "item_id": item_id})


@router.put("/{item_id}")
async def create_item(item_id: int, item: Item, item_query: str):
    """
    The function parameters will be recognized as follows:
    * If the parameter is also declared in the path, it will be used as a path parameter: `item_id`
    * If the parameter is of a builtin type, it will be interpreted as a query parameter: `item_query`
    * If the parameter is of a Pydantic model type, it will be interpreted as a request body (json string): `item`
    """
    result = {"item_id": item_id, **item.dict()}
    if item_query:
        result.update({"item_query": item_query})
    return result

'''
@router.patch("/{item_id}")
async def update_item(
        item_id: Annotated[int, Path(title="Item ID", gt=0, le=100)],
        item_query: Annotated[str | None, Query(alias="item-query", title="Query string",
                                                description="Query string for the items to search",
                                                max_length=10, regex=r"^\w+$", deprecated=True)] = None,
        item: Item = None,
        available: Annotated[bool, Body()] = None,
        date: Annotated[datetime, Cookie()] = datetime.now(),
        user_agent: Annotated[str | None, Header()] = None):
    """
    The function parameters can be defined as follows:
    * `Path`: a path parameter
    * `Query`: a query parameter
    * `Body`: a request body
    * `Cookie`: a cookie parameter
    * `Header`: a header parameter

    See also: [Dependency Injection](https://fastapi.tiangolo.com/tutorial/dependencies/) system (function or class).
    """
    result = {"item_id": item_id, "item": item, "available": available, "date": date, "User-Agent": user_agent}
    if item_query:
        result.update({"item_query": item_query})
    return result


@router.post("/file/", tags=["file"])
async def create_file(
        token: Annotated[str, Form()],
        file: Annotated[bytes | None, File()] = None,
        upload_file: UploadFile | None = None):
    """
    The function parameters can be defined as follows:
    * `Form`: a form parameter
    * `File`: a file parameter
    * `UploadFile`: a spooled file parameter

    You can declare multiple File and Form parameters, but you can't also declare Body fields.
    """
    return {"token": token, "file": file, "upload_file": upload_file}
'''

A app/items/schemas.py => app/items/schemas.py +38 -0
@@ 0,0 1,38 @@
from pydantic import BaseModel, Field, HttpUrl, validator


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = Field(title="Description", max_length=300, example="A description")
    price: float = Field(lt=100, description="The price must be lesser than 100")
    tags: set[str] = set()
    image: Image | None = None

    @validator("price")
    def price_must_be_positive(cls, value):
        if value <= 0:
            raise ValueError(f"The price must be greater than zero, received: {value}")
        return value

    class Config:
        schema_extra = {
            "example": {
                "name": "Foo",
                "description": "Foo description",
                "price": 42.0,
                "tags": [
                    "foo",
                    "bar",
                    "baz"
                ],
                "image": {
                    "url": "http://example.com/foo.jpg",
                    "name": "The Foo"
                }
            }
        }

A app/items/templates/item.html => app/items/templates/item.html +11 -0
@@ 0,0 1,11 @@
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Item Details</title>
    <link rel="stylesheet" href="{{ url_for('static', path='/simple.css') }}">
</head>
<body>
    <h1>Item ID: {{ item_id }}</h1>
</body>
</html>
\ No newline at end of file

M app/main.py => app/main.py +4 -141
@@ 1,10 1,7 @@
from datetime import datetime
from typing import Annotated

from fastapi import FastAPI, BackgroundTasks, Body, Cookie, File, Form, Header, Path, Query, UploadFile
from fastapi import FastAPI, BackgroundTasks
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel, EmailStr, Field, HttpUrl, validator

from .items.routers import router as items_router
from .users.routers import router as users_router

app = FastAPI(


@@ 15,88 12,10 @@ app = FastAPI(
    openapi_tags=[{"name": "items", "description": "Manage *items*."}]
)

app.mount("/static", StaticFiles(directory="app/static"), name="static")
app.include_router(items_router)
app.include_router(users_router)


class BaseUser(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserIn(BaseUser):
    password: str


@app.post("/user/")
async def create_user(user: UserIn) -> BaseUser:
    """
    The function return type can be defined as follows:
    * builtin type
    * Pydantic model
    * Response
    * PlainTextResponse
    * HtmlResponse
    * JSONResponse
    * RedirectResponse

    Default response class JSONResponse can be overridden with:
    * app = FastAPI(default_response_class=HtmlResponse)

    The path decorator's parameter `response_model` define response model:
    * @app.get("/portal", response_model=None)
    * @app.get("/items/", response_model=list[Item])
    * @app.get("/user/", response_model=BaseUser, response_model_exclude_unset=True)

    The path decorator's parameter `status_code` define response status code:
    * @app.post("/items/", status_code=201)
    * @app.post("/items/", status_code=status.HTTP_201_CREATED)

    The path decorator's parameters `tags`, `summary` and `description` are used in interactive docs:
    * @app.get("/items/", tags=["items"], summary="Create an item", description="Create an item description")

    Exception can be defined as follows:
    * raise HTTPException(status_code=404, detail="Item not found")
    """
    return user


class Image(BaseModel):
    url: HttpUrl
    name: str


class Item(BaseModel):
    name: str
    description: str | None = Field(title="Description", max_length=300, example="A description")
    price: float = Field(lt=100, description="The price must be lesser than 100")
    tags: set[str] = set()
    image: Image | None = None

    @validator("price")
    def price_must_be_positive(cls, value):
        if value <= 0:
            raise ValueError(f"The price must be greater than zero, received: {value}")
        return value

    class Config:
        schema_extra = {
            "example": {
                "name": "Foo",
                "description": "Foo description",
                "price": 42.0,
                "tags": [
                    "foo",
                    "bar",
                    "baz"
                ],
                "image": {
                    "url": "http://example.com/foo.jpg",
                    "name": "The Foo"
                }
            }
        }
app.mount("/static", StaticFiles(directory="app/static"), name="static")


@app.get("/")


@@ 104,62 23,6 @@ def root():
    return {"message": "Hello World!"}


@app.put("/items/create/{item_id}", tags=["items"])
async def create_item(item_id: int, item: Item, item_query: str):
    """
    The function parameters will be recognized as follows:
    * If the parameter is also declared in the path, it will be used as a path parameter: `item_id`
    * If the parameter is of a builtin type, it will be interpreted as a query parameter: `item_query`
    * If the parameter is of a Pydantic model type, it will be interpreted as a request body (json string): `item`
    """
    result = {"item_id": item_id, **item.dict()}
    if item_query:
        result.update({"item_query": item_query})
    return result


@app.patch("/items/update/{item_id}", tags=["items"])
async def update_item(
        item_id: Annotated[int, Path(title="Item ID", gt=0, le=100)],
        item_query: Annotated[str | None, Query(alias="item-query", title="Query string",
                                                description="Query string for the items to search",
                                                max_length=10, regex=r"^\w+$", deprecated=True)] = None,
        item: Item = None,
        available: Annotated[bool, Body()] = None,
        date: Annotated[datetime, Cookie()] = datetime.now(),
        user_agent: Annotated[str | None, Header()] = None):
    """
    The function parameters can be defined as follows:
    * `Path`: a path parameter
    * `Query`: a query parameter
    * `Body`: a request body
    * `Cookie`: a cookie parameter
    * `Header`: a header parameter

    See also: [Dependency Injection](https://fastapi.tiangolo.com/tutorial/dependencies/) system (function or class).
    """
    result = {"item_id": item_id, "item": item, "available": available, "date": date, "User-Agent": user_agent}
    if item_query:
        result.update({"item_query": item_query})
    return result


@app.post("/file/")
async def create_file(
        token: Annotated[str, Form()],
        file: Annotated[bytes | None, File()] = None,
        upload_file: UploadFile | None = None):
    """
    The function parameters can be defined as follows:
    * `Form`: a form parameter
    * `File`: a file parameter
    * `UploadFile`: a spooled file parameter

    You can declare multiple File and Form parameters, but you can't also declare Body fields.
    """
    return {"token": token, "file": file, "upload_file": upload_file}


def write_notification(email: str, message=""):
    print(f"notification for {email}: {message}")


M app/tests.py => app/tests.py +16 -9
@@ 6,18 6,25 @@ client = TestClient(app)


def test_root_endpoint():
    r = client.get("/")
    assert r.status_code == 200
    assert r.json() == {"message": "Hello World!"}
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello World!"}


def test_items_template():
    response = client.get("/items/1")
    assert response.status_code == 200
    assert response.template.name == 'item.html'
    assert "request" in response.context


def test_correct_user():
    json = {"username": "myuser", "email": "myuser@example.com", "password": "mypassword"}
    resp = client.post("/user/", json=json)
    assert resp.status_code == 200
    json = {"email": "myuser@example.com", "password": "mypassword"}
    response = client.post("/users/", json=json)
    assert response.status_code == 200


def test_wrong_user():
    json = {"username": "myuser", "email": "myuser@example.com"}
    resp = client.post("/user/", json=json)
    assert resp.status_code != 200
    json = {"email": "myuser@example.com"}
    response = client.post("/users/", json=json)
    assert response.status_code != 200

R app/users/crud.py => app/users/api.py +0 -0
M app/users/models.py => app/users/models.py +1 -1
@@ 1,7 1,7 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship

from .database import Base
from app.database import Base


class User(Base):

M app/users/routers.py => app/users/routers.py +8 -7
@@ 1,7 1,8 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from . import database, crud, models, schemas
from app import database
from . import api, models, schemas

models.Base.metadata.create_all(bind=database.engine)



@@ 22,21 23,21 @@ router = APIRouter(

@router.post("/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
    db_user = crud.get_user_by_email(db, email=user.email)
    db_user = api.get_user_by_email(db, email=user.email)
    if db_user:
        raise HTTPException(status_code=400, detail="Email already registered")
    return crud.create_user(db=db, user=user)
    return api.create_user(db=db, user=user)


@router.get("/", response_model=list[schemas.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    users = crud.get_users(db, skip=skip, limit=limit)
    users = api.get_users(db, skip=skip, limit=limit)
    return users


@router.get("/{user_id}", response_model=schemas.User)
def read_user(user_id: int, db: Session = Depends(get_db)):
    db_user = crud.get_user(db, user_id=user_id)
    db_user = api.get_user(db, user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user


@@ 44,10 45,10 @@ def read_user(user_id: int, db: Session = Depends(get_db)):

@router.post("/{user_id}/items/", response_model=schemas.Item)
def create_item_for_user(user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)):
    return crud.create_user_item(db=db, item=item, user_id=user_id)
    return api.create_user_item(db=db, item=item, user_id=user_id)


@router.get("/items/", response_model=list[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    items = crud.get_items(db, skip=skip, limit=limit)
    items = api.get_items(db, skip=skip, limit=limit)
    return items

M app/users/schemas.py => app/users/schemas.py +2 -2
@@ 1,4 1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, EmailStr


class ItemBase(BaseModel):


@@ 19,7 19,7 @@ class Item(ItemBase):


class UserBase(BaseModel):
    email: str
    email: EmailStr


class UserCreate(UserBase):