Add some SQL classes

This commit is contained in:
linuskmr 2021-12-22 09:06:08 +01:00
parent 906b8705b9
commit d381888f42
9 changed files with 132 additions and 91 deletions

View file

@ -34,13 +34,13 @@ Activate virtual environment (Windows):
$ .venv\Scripts\activate $ .venv\Scripts\activate
``` ```
Run `src/main.py`: Run inside `backend/`:
> Note: When executing, you must be in the `backend/` directory! Executing the program from another directory will > Note the `-m` here. This causes python to execute the program as a module, not as a script, which would result in
> fail, because Python then can not resolve imports correctly. Why? Don't know. > broken imports.
```bash ```bash
$ python src/main.py $ python -m src.main
``` ```
## Docker ## Docker

View file

@ -1,3 +1,4 @@
uvicorn~=0.15.0 uvicorn~=0.15.0
fastapi~=0.70.0 fastapi~=0.70.0
pydantic~=1.8.2 pydantic~=1.8.2
sqlmodel~=0.0.5

View file

@ -1,10 +1,3 @@
# Modify python's import path to include the current directory so that the imports get resolved correctly
import os
import sys
sys.path.extend([os.getcwd()])
import argparse import argparse
import uvicorn import uvicorn
@ -77,14 +70,14 @@ def start_dev_server(port: int):
"""Starts a development server with auto-reloading on port.""" """Starts a development server with auto-reloading on port."""
# Host="0.0.0.0" makes the application accessible from other IPs. Necessary when running inside docker # Host="0.0.0.0" makes the application accessible from other IPs. Necessary when running inside docker
uvicorn.run("main:app", reload=True, port=port, host="0.0.0.0") uvicorn.run("src.main:app", reload=True, port=port, host="0.0.0.0")
def start_production_server(port: int): def start_production_server(port: int):
"""Starts a production, i.e. performance-optimized server on port.""" """Starts a production, i.e. performance-optimized server on port."""
# Host="0.0.0.0" makes the application accessible from other IPs. Necessary when running inside docker # Host="0.0.0.0" makes the application accessible from other IPs. Necessary when running inside docker
uvicorn.run("main:app", port=port, host="0.0.0.0") uvicorn.run("src.main:app", port=port, host="0.0.0.0")
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,25 +1,38 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pydantic import BaseModel, Field from typing import Optional
from pydantic.json import timedelta_isoformat
from sqlmodel import SQLModel, Field
class ProjectNew(BaseModel): class ProjectBase(SQLModel):
"""Superclass model which all project classes have in common."""
name: str # = Field(description="Name of the project", example="Learning")
class ProjectCreate(ProjectBase):
"""Model used when a user creates a new project.""" """Model used when a user creates a new project."""
pass
name: str = Field(description="Name of the project", example="Learning")
class ProjectInfo(BaseModel): class Project(ProjectBase, table=True):
"""Model used inside the database."""
id: Optional[int] = Field(default=None, primary_key=True)
class ProjectRead(ProjectCreate):
"""Model used when querying information about a module.""" """Model used when querying information about a module."""
name: str = Field(description="Name of the project", example="Learning") id: int
start_date: datetime = Field(description="Project creation date", example=datetime.now()) name: str # = Field(description="Name of the project", example="Learning")
duration: timedelta = Field(description="Total tracked duration", example=timedelta(days=3, hours=5, minutes=47)) start_date: datetime # = Field(description="Project creation date", example=datetime.now())
records: int = Field(description="Total number of trackings/records", example=42) duration: timedelta # = Field(description="Total tracked duration", example=timedelta(days=3, hours=5, minutes=47))
records: int # = Field(description="Total number of trackings/records", example=42)
class Config: # TODO: Adding this config may be done with a decorator # class Config: # TODO: Adding this config may be done with a decorator
"""pydantic config""" # """pydantic config"""
json_encoders = { # json_encoders = {
# Serialize timedeltas as ISO 8601 and not as float seconds, which is the default # # Serialize timedeltas as ISO 8601 and not as float seconds, which is the default
timedelta: timedelta_isoformat, # timedelta: timedelta_isoformat,
} # }

View file

@ -1,27 +1,49 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pydantic import BaseModel, Field from typing import Optional
from pydantic.json import timedelta_isoformat
import pydantic
from sqlmodel import SQLModel, Field
class RecordStart(BaseModel): class RecordBase(SQLModel):
"""Superclass model that all record classes have in common."""
project: str # = Field(description="Name of the project that should be tracked", example="Uni")
tags: list[str] # = Field(description="List of tags by name", example=["Listening Lectures",
# "Not concentrated"])
class RecordStartCreate(RecordBase):
"""Model used when a user starts a record.""" """Model used when a user starts a record."""
pass
start: datetime = Field(
description="Start date of record",
example=datetime.now() - timedelta(hours=3)
)
project: str = Field(description="Name of the project that should be tracked", example="Uni")
tags: list[str] = Field(description="List of tags by name", example=["Listening Lectures", "Not be concentrated"])
class RecordInfo(BaseModel): class RecordCreate(RecordBase):
start: datetime = Field( """Model used when a user creates a complete record."""
description="Start date of record",
example=datetime.now() - timedelta(hours=3) start: datetime
) end: datetime
end: datetime = Field(
description="End date of record",
example=datetime.now() class Record(RecordBase, table=True):
) """Model used inside the database."""
project: str = Field(description="Name of the project that should be tracked", example="Uni")
tags: list[str] = Field(description="List of tags by name", example=["Listening Lectures", "Not be concentrated"]) id: Optional[int] = Field(default=None, primary_key=True)
class RecordRead(RecordBase):
"""Model used when a user queries a record."""
id: int
start: datetime # = Field(
# description="Start date of record",
# example=datetime.now() - timedelta(hours=3)
# )
end: Optional[datetime] # = Field(
# default=None,
# description="End date of record",
# example=datetime.now()
# )
project: str # = Field(description="Name of the project that should be tracked", example="Uni")
tags: list[str] # = Field(description="List of tags by name", example=["Listening Lectures",
# "Not be concentrated"])

View file

@ -1,16 +1,28 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pydantic import BaseModel, Field from typing import Optional
from pydantic.json import timedelta_isoformat
from sqlmodel import SQLModel, Field
class TagNew(BaseModel): class TagBase(SQLModel):
"""Model used when a user creates a new tag.""" """Superclass model which all tag classes have in common."""
name: str = Field(description="Name of the tag", example="Listening Lectures") name: str # = Field(description="Name of the tag", example="Listening Lectures")
class TagInfo(BaseModel): class TagCreate(TagBase):
"""Model used when creating a new model."""
pass
class Tag(TagBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
class TagRead(TagBase):
"""Model used when querying information about a tag.""" """Model used when querying information about a tag."""
name: str = Field(description="Name of the tag", example="Listening Lectures") id: int
duration: timedelta = Field(description="Total tracked duration", example=timedelta(days=3, hours=5, minutes=47)) name: str # = Field(description="Name of the tag", example="Listening Lectures")
duration: timedelta # = Field(description="Total tracked duration", example=timedelta(days=3, hours=5, minutes=47))

View file

@ -2,29 +2,29 @@ import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from fastapi import APIRouter, status, Path from fastapi import APIRouter, status, Path
from src.models.project import ProjectNew, ProjectInfo from src.models.project import ProjectCreate, ProjectRead
router = APIRouter(prefix='/projects', tags=["Projects"]) router = APIRouter(prefix='/projects', tags=["Projects"])
@router.get("/", response_model=list[ProjectInfo], summary="Get a list of all projects") @router.get("/", response_model=list[ProjectRead], summary="Get a list of all projects")
async def all_projects() -> list[ProjectInfo]: async def all_projects() -> list[ProjectRead]:
"""Returns a list of all projects""" """Returns a list of all projects"""
return [ return [
ProjectInfo( ProjectRead(
name="Learning", name="Learning",
start_date=datetime.now(), start_date=datetime.now(),
duration=timedelta(days=3, hours=5, minutes=47), duration=timedelta(days=3, hours=5, minutes=47),
records=42 records=42
), ),
ProjectInfo( ProjectRead(
name="Sports", name="Sports",
start_date=datetime.now(), start_date=datetime.now(),
duration=timedelta(hours=14, minutes=10), duration=timedelta(hours=14, minutes=10),
records=10 records=10
), ),
ProjectInfo( ProjectRead(
name="Gaming", name="Gaming",
start_date=datetime.now(), start_date=datetime.now(),
duration=timedelta(hours=3, minutes=2), duration=timedelta(hours=3, minutes=2),
@ -34,17 +34,17 @@ async def all_projects() -> list[ProjectInfo]:
@router.post("/", status_code=status.HTTP_201_CREATED, summary="Add a project") @router.post("/", status_code=status.HTTP_201_CREATED, summary="Add a project")
async def add_project(project: ProjectNew): async def add_project(project: ProjectCreate):
"""Add a project.""" """Add a project."""
print('adding project', project) print('adding project', project)
@router.get("/{name}", response_model=ProjectInfo, summary="Get a project by name") @router.get("/{name}", response_model=ProjectRead, summary="Get a project by name")
async def get_project(name: str = Path(..., title="Name of the module")) -> ProjectInfo: async def get_project(name: str = Path(..., title="Name of the module")) -> ProjectRead:
"""Fetch a project by name.""" """Fetch a project by name."""
return ProjectInfo( return ProjectRead(
name=name, name=name,
start_date=datetime.now(), start_date=datetime.now(),
duration=timedelta(hours=3, minutes=2), duration=timedelta(hours=3, minutes=2),
@ -60,7 +60,7 @@ async def delete_project(name: str = Path(..., title="Name of the module")):
@router.patch("/{name}", summary="Apply partial updates to a project by name") @router.patch("/{name}", summary="Apply partial updates to a project by name")
async def patch_project(project: ProjectNew, name: str = Path(..., title="Name of the module")): async def patch_project(project: ProjectCreate, name: str = Path(..., title="Name of the module")):
"""Apply partial updates to a project.""" """Apply partial updates to a project."""
logging.debug(f"Patching project {name} with {project}") logging.debug(f"Patching project {name} with {project}")

View file

@ -2,23 +2,23 @@ import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from fastapi import APIRouter, status, Path from fastapi import APIRouter, status, Path
from src.models.record import RecordStart, RecordInfo from src.models.record import RecordStartCreate, RecordRead, RecordCreate
router = APIRouter(prefix="/records", tags=["Records"]) router = APIRouter(prefix="/records", tags=["Records"])
@router.get("/", response_model=list[RecordInfo], summary="Get a list of all records") @router.get("/", response_model=list[RecordRead], summary="Get a list of all records")
async def all_tags() -> list[RecordInfo]: async def all_tags() -> list[RecordRead]:
"""Returns a list of all records.""" """Returns a list of all records."""
return [ return [
RecordInfo( RecordRead(
start=datetime.now() - timedelta(hours=3), start=datetime.now() - timedelta(hours=3),
end=datetime.now(), end=datetime.now(),
project="Uni", project="Uni",
tags=["Listening Lectures", "Not be concentrated"] tags=["Listening Lectures", "Not be concentrated"]
), ),
RecordInfo( RecordRead(
start=datetime.now() - timedelta(hours=7), start=datetime.now() - timedelta(hours=7),
end=datetime.now() - timedelta(hours=4), end=datetime.now() - timedelta(hours=4),
project="Uni", project="Uni",
@ -28,24 +28,24 @@ async def all_tags() -> list[RecordInfo]:
@router.post("/start", status_code=status.HTTP_201_CREATED, summary="Start a record") @router.post("/start", status_code=status.HTTP_201_CREATED, summary="Start a record")
async def start_record(record: RecordStart): async def start_record(record: RecordStartCreate):
"""Start a record.""" """Start a record."""
print('starting record', record) print('starting record', record)
@router.post("/end", status_code=status.HTTP_204_NO_CONTENT, summary="End a record") @router.post("/end", status_code=status.HTTP_204_NO_CONTENT, summary="End a record")
async def end_record(record: RecordStart): async def end_record(record: RecordStartCreate):
"""End a record.""" """End a record."""
print('end record', record) print('end record', record)
@router.get("/{id}", response_model=RecordInfo, summary="Get a record by id") @router.get("/{id}", response_model=RecordRead, summary="Get a record by id")
async def get_record(id: str = Path(..., title="ID of the record")) -> RecordInfo: async def get_record(id: str = Path(..., title="ID of the record")) -> RecordRead:
"""Fetch a record by id.""" """Fetch a record by id."""
return RecordInfo( return RecordRead(
start=datetime.now() - timedelta(hours=3), start=datetime.now() - timedelta(hours=3),
end=datetime.now(), end=datetime.now(),
project="Uni", project="Uni",
@ -61,7 +61,7 @@ async def delete_record(id: str = Path(..., title="ID of the record")):
@router.patch("/{id}", summary="Apply partial updates to a record by id") @router.patch("/{id}", summary="Apply partial updates to a record by id")
async def patch_record(record: RecordInfo, id: str = Path(..., title="ID of the record")): async def patch_record(record: RecordCreate, id: str = Path(..., title="ID of the record")):
"""Apply partial updates to a record.""" """Apply partial updates to a record."""
logging.debug(f"Patching record {id} with {record}") logging.debug(f"Patching record {id} with {record}")

View file

@ -2,21 +2,21 @@ import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from fastapi import APIRouter, status, Path from fastapi import APIRouter, status, Path
from src.models.tag import TagNew, TagInfo from src.models.tag import TagRead, TagCreate
router = APIRouter(prefix="/tags", tags=["Tags"]) router = APIRouter(prefix="/tags", tags=["Tags"])
@router.get("/", response_model=list[TagInfo], summary="Get a list of all tags") @router.get("/", response_model=list[TagRead], summary="Get a list of all tags")
async def all_tags() -> list[TagInfo]: async def all_tags() -> list[TagRead]:
"""Returns a list of all tags.""" """Returns a list of all tags."""
return [ return [
TagInfo( TagRead(
name="Listening Lectures", name="Listening Lectures",
duration=timedelta(days=3, hours=5, minutes=47), duration=timedelta(days=3, hours=5, minutes=47),
), ),
TagInfo( TagRead(
name="Doing homework", name="Doing homework",
duration=timedelta(hours=14, minutes=10), duration=timedelta(hours=14, minutes=10),
) )
@ -24,17 +24,17 @@ async def all_tags() -> list[TagInfo]:
@router.post("/", status_code=status.HTTP_201_CREATED, summary="Add a tag") @router.post("/", status_code=status.HTTP_201_CREATED, summary="Add a tag")
async def add_tag(tag: TagNew): async def add_tag(tag: TagCreate):
"""Add a tag.""" """Add a tag."""
print('adding tag', tag) print('adding tag', tag)
@router.get("/{name}", response_model=TagInfo, summary="Get a project by name") @router.get("/{name}", response_model=TagRead, summary="Get a project by name")
async def get_tag(name: str = Path(..., title="Name of the tag")) -> TagInfo: async def get_tag(name: str = Path(..., title="Name of the tag")) -> TagRead:
"""Fetch a project by name.""" """Fetch a project by name."""
return TagInfo( return TagRead(
name=name, name=name,
duration=timedelta(hours=3, minutes=2), duration=timedelta(hours=3, minutes=2),
) )
@ -48,7 +48,7 @@ async def delete_tag(name: str = Path(..., title="Name of the tag")):
@router.patch("/{name}", summary="Apply partial updates to a tag by name") @router.patch("/{name}", summary="Apply partial updates to a tag by name")
async def patch_tag(tag: TagNew, name: str = Path(..., title="Name of the tag")): async def patch_tag(tag: TagCreate, name: str = Path(..., title="Name of the tag")):
"""Apply partial updates to a tag.""" """Apply partial updates to a tag."""
logging.debug(f"Patching tag {name} with {tag}") logging.debug(f"Patching tag {name} with {tag}")