From d381888f4245327cf0d19468933f912232bf0c86 Mon Sep 17 00:00:00 2001 From: linuskmr Date: Wed, 22 Dec 2021 09:06:08 +0100 Subject: [PATCH] Add some SQL classes --- backend/docs/installation.md | 8 ++--- backend/requirements.txt | 3 +- backend/src/main.py | 11 ++---- backend/src/models/project.py | 45 +++++++++++++++--------- backend/src/models/record.py | 64 +++++++++++++++++++++++----------- backend/src/models/tag.py | 28 ++++++++++----- backend/src/routes/projects.py | 22 ++++++------ backend/src/routes/records.py | 22 ++++++------ backend/src/routes/tags.py | 20 +++++------ 9 files changed, 132 insertions(+), 91 deletions(-) diff --git a/backend/docs/installation.md b/backend/docs/installation.md index f3606fc..2a7c9af 100644 --- a/backend/docs/installation.md +++ b/backend/docs/installation.md @@ -34,13 +34,13 @@ Activate virtual environment (Windows): $ .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 -> fail, because Python then can not resolve imports correctly. Why? Don't know. +> Note the `-m` here. This causes python to execute the program as a module, not as a script, which would result in +> broken imports. ```bash -$ python src/main.py +$ python -m src.main ``` ## Docker diff --git a/backend/requirements.txt b/backend/requirements.txt index ff6f180..540ec02 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,3 +1,4 @@ uvicorn~=0.15.0 fastapi~=0.70.0 -pydantic~=1.8.2 \ No newline at end of file +pydantic~=1.8.2 +sqlmodel~=0.0.5 \ No newline at end of file diff --git a/backend/src/main.py b/backend/src/main.py index 36803bb..f59cb38 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -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 uvicorn @@ -77,14 +70,14 @@ def start_dev_server(port: int): """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 - 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): """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 - 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__": diff --git a/backend/src/models/project.py b/backend/src/models/project.py index 4ad63fd..9cca0cc 100644 --- a/backend/src/models/project.py +++ b/backend/src/models/project.py @@ -1,25 +1,38 @@ from datetime import datetime, timedelta -from pydantic import BaseModel, Field -from pydantic.json import timedelta_isoformat +from typing import Optional + +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.""" - - name: str = Field(description="Name of the project", example="Learning") + pass -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.""" - name: str = Field(description="Name of the project", example="Learning") - start_date: datetime = Field(description="Project creation date", example=datetime.now()) - 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) + id: int + name: str # = Field(description="Name of the project", example="Learning") + start_date: datetime # = Field(description="Project creation date", example=datetime.now()) + 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 - """pydantic config""" - json_encoders = { - # Serialize timedeltas as ISO 8601 and not as float seconds, which is the default - timedelta: timedelta_isoformat, - } + # class Config: # TODO: Adding this config may be done with a decorator + # """pydantic config""" + # json_encoders = { + # # Serialize timedeltas as ISO 8601 and not as float seconds, which is the default + # timedelta: timedelta_isoformat, + # } diff --git a/backend/src/models/record.py b/backend/src/models/record.py index 1d98c65..600e2f4 100644 --- a/backend/src/models/record.py +++ b/backend/src/models/record.py @@ -1,27 +1,49 @@ from datetime import datetime, timedelta -from pydantic import BaseModel, Field -from pydantic.json import timedelta_isoformat +from typing import Optional + +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.""" - - 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"]) + pass -class RecordInfo(BaseModel): - start: datetime = Field( - description="Start date of record", - example=datetime.now() - timedelta(hours=3) - ) - end: datetime = Field( - 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"]) +class RecordCreate(RecordBase): + """Model used when a user creates a complete record.""" + + start: datetime + end: datetime + + +class Record(RecordBase, table=True): + """Model used inside the database.""" + + 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"]) diff --git a/backend/src/models/tag.py b/backend/src/models/tag.py index c217e64..fbfe369 100644 --- a/backend/src/models/tag.py +++ b/backend/src/models/tag.py @@ -1,16 +1,28 @@ from datetime import datetime, timedelta -from pydantic import BaseModel, Field -from pydantic.json import timedelta_isoformat +from typing import Optional + +from sqlmodel import SQLModel, Field -class TagNew(BaseModel): - """Model used when a user creates a new tag.""" +class TagBase(SQLModel): + """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.""" - 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)) + id: int + 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)) diff --git a/backend/src/routes/projects.py b/backend/src/routes/projects.py index 787498c..a97c773 100644 --- a/backend/src/routes/projects.py +++ b/backend/src/routes/projects.py @@ -2,29 +2,29 @@ import logging from datetime import datetime, timedelta 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.get("/", response_model=list[ProjectInfo], summary="Get a list of all projects") -async def all_projects() -> list[ProjectInfo]: +@router.get("/", response_model=list[ProjectRead], summary="Get a list of all projects") +async def all_projects() -> list[ProjectRead]: """Returns a list of all projects""" return [ - ProjectInfo( + ProjectRead( name="Learning", start_date=datetime.now(), duration=timedelta(days=3, hours=5, minutes=47), records=42 ), - ProjectInfo( + ProjectRead( name="Sports", start_date=datetime.now(), duration=timedelta(hours=14, minutes=10), records=10 ), - ProjectInfo( + ProjectRead( name="Gaming", start_date=datetime.now(), 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") -async def add_project(project: ProjectNew): +async def add_project(project: ProjectCreate): """Add a project.""" print('adding project', project) -@router.get("/{name}", response_model=ProjectInfo, summary="Get a project by name") -async def get_project(name: str = Path(..., title="Name of the module")) -> ProjectInfo: +@router.get("/{name}", response_model=ProjectRead, summary="Get a project by name") +async def get_project(name: str = Path(..., title="Name of the module")) -> ProjectRead: """Fetch a project by name.""" - return ProjectInfo( + return ProjectRead( name=name, start_date=datetime.now(), 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") -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.""" logging.debug(f"Patching project {name} with {project}") diff --git a/backend/src/routes/records.py b/backend/src/routes/records.py index 3afa7dd..0bb3553 100644 --- a/backend/src/routes/records.py +++ b/backend/src/routes/records.py @@ -2,23 +2,23 @@ import logging from datetime import datetime, timedelta 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.get("/", response_model=list[RecordInfo], summary="Get a list of all records") -async def all_tags() -> list[RecordInfo]: +@router.get("/", response_model=list[RecordRead], summary="Get a list of all records") +async def all_tags() -> list[RecordRead]: """Returns a list of all records.""" return [ - RecordInfo( + RecordRead( start=datetime.now() - timedelta(hours=3), end=datetime.now(), project="Uni", tags=["Listening Lectures", "Not be concentrated"] ), - RecordInfo( + RecordRead( start=datetime.now() - timedelta(hours=7), end=datetime.now() - timedelta(hours=4), 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") -async def start_record(record: RecordStart): +async def start_record(record: RecordStartCreate): """Start a record.""" print('starting record', 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.""" print('end record', record) -@router.get("/{id}", response_model=RecordInfo, summary="Get a record by id") -async def get_record(id: str = Path(..., title="ID of the record")) -> RecordInfo: +@router.get("/{id}", response_model=RecordRead, summary="Get a record by id") +async def get_record(id: str = Path(..., title="ID of the record")) -> RecordRead: """Fetch a record by id.""" - return RecordInfo( + return RecordRead( start=datetime.now() - timedelta(hours=3), end=datetime.now(), 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") -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.""" logging.debug(f"Patching record {id} with {record}") diff --git a/backend/src/routes/tags.py b/backend/src/routes/tags.py index 165d657..c785590 100644 --- a/backend/src/routes/tags.py +++ b/backend/src/routes/tags.py @@ -2,21 +2,21 @@ import logging from datetime import datetime, timedelta 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.get("/", response_model=list[TagInfo], summary="Get a list of all tags") -async def all_tags() -> list[TagInfo]: +@router.get("/", response_model=list[TagRead], summary="Get a list of all tags") +async def all_tags() -> list[TagRead]: """Returns a list of all tags.""" return [ - TagInfo( + TagRead( name="Listening Lectures", duration=timedelta(days=3, hours=5, minutes=47), ), - TagInfo( + TagRead( name="Doing homework", 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") -async def add_tag(tag: TagNew): +async def add_tag(tag: TagCreate): """Add a tag.""" print('adding tag', tag) -@router.get("/{name}", response_model=TagInfo, summary="Get a project by name") -async def get_tag(name: str = Path(..., title="Name of the tag")) -> TagInfo: +@router.get("/{name}", response_model=TagRead, summary="Get a project by name") +async def get_tag(name: str = Path(..., title="Name of the tag")) -> TagRead: """Fetch a project by name.""" - return TagInfo( + return TagRead( name=name, 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") -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.""" logging.debug(f"Patching tag {name} with {tag}")