Add some SQL classes
This commit is contained in:
parent
906b8705b9
commit
d381888f42
9 changed files with 132 additions and 91 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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__":
|
||||||
|
|
|
@ -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,
|
||||||
}
|
# }
|
||||||
|
|
|
@ -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"])
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
Loading…
Reference in a new issue