From 4e9fd3d394b1ef22a9ebd930356a902a730f571d Mon Sep 17 00:00:00 2001 From: Bernhard Radermacher Date: Thu, 28 Aug 2025 16:01:44 +0000 Subject: [PATCH] initial wip --- .idea/runConfigurations/latest.xml | 13 +++++++++++ Dockerfile | 19 +++++++++++++++ main.py | 33 ++++++++++++++++++++++++++ pyproject.toml | 12 ++++++++++ src/__init__.py | 0 src/model/__init__.py | 4 ++++ src/model/contact.py | 9 ++++++++ src/model/status.py | 7 ++++++ src/model/util.py | 29 +++++++++++++++++++++++ src/populate.py | 37 ++++++++++++++++++++++++++++++ src/routers/__init__.py | 1 + src/routers/status.py | 26 +++++++++++++++++++++ test_main.http | 26 +++++++++++++++++++++ 13 files changed, 216 insertions(+) create mode 100644 .idea/runConfigurations/latest.xml create mode 100644 Dockerfile create mode 100644 main.py create mode 100644 pyproject.toml create mode 100644 src/__init__.py create mode 100644 src/model/__init__.py create mode 100644 src/model/contact.py create mode 100644 src/model/status.py create mode 100644 src/model/util.py create mode 100644 src/populate.py create mode 100644 src/routers/__init__.py create mode 100644 src/routers/status.py create mode 100644 test_main.http diff --git a/.idea/runConfigurations/latest.xml b/.idea/runConfigurations/latest.xml new file mode 100644 index 0000000..23410cf --- /dev/null +++ b/.idea/runConfigurations/latest.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f48db23 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3 + +LABEL authors="Bernhard Radermacher" + +RUN pip install mariadb pymysql sqlmodel uvicorn fastapi + +ENV DB_USER="must be set" \ + DB_PASSWORD="must be set" \ + DB_HOST="mariadb" \ + DB_PORT=3306 \ + DB_DATABASE="vpsx" + +COPY src /src + +COPY main.py / + + +ENTRYPOINT ["/usr/local/bin/uvicorn", "main:app", "--host", "0.0.0.0"] + diff --git a/main.py b/main.py new file mode 100644 index 0000000..2dbeb37 --- /dev/null +++ b/main.py @@ -0,0 +1,33 @@ +import datetime +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from src.populate import populate +import src.routers as routers + +@asynccontextmanager +async def lifespan(app: FastAPI): + populate() + yield + +updated = datetime.datetime.now(tz=datetime.timezone.utc) + +app = FastAPI(lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(routers.status) + +# +# @app.get("/info/updated", tags=["info"]) +# async def get_timestamp_of_last_update(): +# """Timestamp of last update (YYYY-MM-DD HH:MM:SS UTC).""" +# return updated.strftime("%Y-%m-%d %H:%M:%S") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..157b7c0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "vpsx-db" +version = "0.1.0" +description = "Add your description here" +requires-python = ">=3.13" +dependencies = [ + "fastapi>=0.116.1", + "mariadb>=1.1.13", + "pymysql>=1.1.2", + "sqlmodel>=0.0.24", + "uvicorn>=0.35.0", +] diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/model/__init__.py b/src/model/__init__.py new file mode 100644 index 0000000..3ff5c46 --- /dev/null +++ b/src/model/__init__.py @@ -0,0 +1,4 @@ +from .contact import Contact +from .status import Status + +from .util import get_session, count_rows \ No newline at end of file diff --git a/src/model/contact.py b/src/model/contact.py new file mode 100644 index 0000000..444cad7 --- /dev/null +++ b/src/model/contact.py @@ -0,0 +1,9 @@ +from sqlmodel import Field, SQLModel, Text + + +class Contact(SQLModel, table=True): + id: int = Field(primary_key=True) + address: str = Field(max_length=256, unique=True) + description: str = Field(max_length=512) + notes: Text + status_id: str = Field(foreign_key="status.id") diff --git a/src/model/status.py b/src/model/status.py new file mode 100644 index 0000000..68db33c --- /dev/null +++ b/src/model/status.py @@ -0,0 +1,7 @@ +from sqlmodel import SQLModel, Field + + +class Status(SQLModel, table=True): + id: str = Field(max_length=3, primary_key=True) + name: str = Field(max_length=30, unique=True) + diff --git a/src/model/util.py b/src/model/util.py new file mode 100644 index 0000000..428d32f --- /dev/null +++ b/src/model/util.py @@ -0,0 +1,29 @@ +from sqlalchemy import func, select, URL + +import os + +from sqlmodel import create_engine, SQLModel, Session + + +def get_engine(): + engine_url = URL.create( + drivername="mariadb+pymysql", + username=os.getenv("DB_USER"), + password=os.getenv("DB_PASSWORD"), + host=os.getenv("DB_HOST"), + port=int(os.getenv("DB_PORT")), + database=os.getenv("DB_DATABASE"), + query={'charset': 'utf8mb4'}, + ) + engine = create_engine(engine_url) + SQLModel.metadata.create_all(engine) + return engine + +engine = get_engine() + +def get_session(): + with Session(engine) as session: + yield session + +def count_rows(session, cls) -> int: + return session.execute(select(func.count()).select_from(cls)).scalar() diff --git a/src/populate.py b/src/populate.py new file mode 100644 index 0000000..5c45179 --- /dev/null +++ b/src/populate.py @@ -0,0 +1,37 @@ +from sqlmodel import Session + +from .model.util import engine +from src.model import count_rows, Status, Contact + + +def populate_contact(session): + if count_rows(session, Contact) == 0: + for kwargs in ( + dict(address="Active"), + dict(id="I", name="Inactive"), + dict(id="N", name="New"), + dict(id="P", name="Prepared"), + dict(id="X", name="eXcluded"), + ): + session.add(Status(**kwargs)) + + + +def populate_status(session): + if count_rows(session, Status) == 0: + for kwargs in ( + dict(id="A", name="Active"), + dict(id="I", name="Inactive"), + dict(id="N", name="New"), + dict(id="P", name="Prepared"), + dict(id="X", name="eXcluded"), + ): + session.add(Status(**kwargs)) + + +def populate(): + with Session(engine) as session: + + populate_status(session) + + session.commit() diff --git a/src/routers/__init__.py b/src/routers/__init__.py new file mode 100644 index 0000000..71a5c0a --- /dev/null +++ b/src/routers/__init__.py @@ -0,0 +1 @@ +from .status import router as status diff --git a/src/routers/status.py b/src/routers/status.py new file mode 100644 index 0000000..9743c0a --- /dev/null +++ b/src/routers/status.py @@ -0,0 +1,26 @@ + + +from fastapi import APIRouter, Depends, HTTPException, Path, Query +from sqlmodel import select + +from ..model import Status, get_session + +router = APIRouter( + prefix="/status", + tags=["status"], +) + +@router.get("/", response_model=list[Status]) +async def get_statuses(session=Depends(get_session)): + """List of Statuses""" + return session.exec(select(Status)).all() + +@router.get("/{status}", response_model=Status, responses={404: {"description": "Not found"}}) +async def get_status( + status: str, + session=Depends(get_session)): + """Get a Status""" + result = session.get(Status, status) + if not result: + raise HTTPException(status_code=404, detail=f"Status {status!r} not found") + return result diff --git a/test_main.http b/test_main.http new file mode 100644 index 0000000..8195c08 --- /dev/null +++ b/test_main.http @@ -0,0 +1,26 @@ +# Test your FastAPI endpoints + +GET http://localhost:8888/info/updated +Accept: application/json + +### + +#GET http://localhost:8888/calendar +#Accept: application/json +# +#### +# +#GET http://localhost:8888/calendar/1/dates/ +#Accept: application/json +# +#### +# +#GET http://localhost:8888/calendar/1/dates?offset=1 +#Accept: application/json +# +#### +# +#GET http://localhost:8888/calendar/1/dates?offset=-1 +#Accept: application/json +# +#### \ No newline at end of file