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