Compare commits

..

12 commits

11 changed files with 116 additions and 25 deletions

View file

@ -1,6 +1,6 @@
/.dockerignore
/.gitignore
.*
/inventory.sqlite3
/README.md
*.pyc
Dockerfile
/contrib/

View file

@ -1,4 +1,4 @@
FROM python:3.10-alpine
FROM python:3.12-alpine3.19
WORKDIR /app

47
contrib/bulk_create.py Executable file
View file

@ -0,0 +1,47 @@
#!/usr/bin/env python3
import sys
import requests
BASE = "https://in.ccc.ac"
COUNT = 0
START_ID = 1
session = requests.Session()
# session.auth = (":)", ":)")
res = session.get(f"{BASE}/api/items")
res.raise_for_status()
ids = res.json().keys()
kiste_ids = {int(i.lstrip("K")) for i in ids if i.startswith("K") and i != "K"}
print("existing ids:", repr(kiste_ids), file=sys.stderr)
current_id = START_ID
new_ids = []
while len(new_ids) < COUNT:
if current_id not in kiste_ids:
new_ids.append(current_id)
current_id += 1
print("created:", file=sys.stderr)
if len(new_ids) == 0:
print("(none)", file=sys.stderr)
for i in new_ids:
j = {
"id": f"K{i}",
"is_in": None,
"coords_bl": None,
"coords_tr": None,
"type": "Kiste",
"name": None,
"content": None,
"note": "Bitte Name, Ort, Inhalt eintragen",
"hidden": False,
}
res = session.put(f"{BASE}/api/items/{j['id']}", json=j)
res.raise_for_status()
print(f"{j['id']},{BASE}/form.html?id={j['id']}")

24
crud.py
View file

@ -1,18 +1,32 @@
from sqlalchemy.orm import Session
from sqlalchemy import DECIMAL, cast
from enum import Enum
from sqlalchemy import DECIMAL, cast
from sqlalchemy.orm import Session
import models
import schemas
import models, schemas
def get_items(db: Session) -> list[models.Item]:
return db.query(models.Item).all()
def put_item(db: Session, id: str, item: schemas.Item):
updated = bool(db.query(models.Item).filter(models.Item.id == id).update(item.dict()))
updated = bool(
db.query(models.Item).filter(models.Item.id == id).update(item.dict())
)
if not updated:
db.add(models.Item(**item.dict()))
db.commit()
return updated
if updated:
return PutItemResult.UPDATED
else:
return PutItemResult.ADDED
class PutItemResult(Enum):
ADDED = 1
UPDATED = 2
def delete_item(db: Session, id: str):

View file

@ -5,9 +5,7 @@ from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./inventory.sqlite3"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args = {"check_same_thread": False},
echo=True
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}, echo=True
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

35
main.py
View file

@ -1,9 +1,12 @@
import re
from calendar import timegm
from datetime import date
from fastapi import Depends, FastAPI, Response
from fastapi.staticfiles import StaticFiles
from sqlalchemy import event
from sqlalchemy.orm import Session
from sqlalchemy.engine import Engine
import re
from sqlalchemy.orm import Session
import crud
from database import SessionLocal, engine
@ -14,6 +17,7 @@ Base.metadata.create_all(bind=engine)
app = FastAPI()
def get_db():
db = SessionLocal()
try:
@ -21,6 +25,7 @@ def get_db():
finally:
db.close()
@event.listens_for(Engine, "connect")
def _set_sqlite_pragma(conn, _):
cursor = conn.cursor()
@ -31,21 +36,35 @@ def _set_sqlite_pragma(conn, _):
@app.get("/api/items", response_model=dict[str, Item])
async def list_items(db: Session = Depends(get_db)):
# natural sort by id
natsort = lambda item: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', item.id)]
natsort = lambda item: [
int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", item.id)
]
items = crud.get_items(db)
items = sorted(items, key=natsort)
return {i.id:i for i in items}
return {i.id: i for i in items}
@app.put("/api/items/{id}")
@app.put(
"/api/items/{id}",
status_code=201,
responses={201: {"description": "created"}, 204: {"description": "updated"}},
)
async def put_item(id: str, item: Item, db: Session = Depends(get_db)):
if crud.put_item(db, id, item):
return Response(b'', status_code=204)
return Response(b'', status_code=201)
if item.last_updated is None:
item.last_updated = month_timestamp()
if crud.put_item(db, id, item) == crud.PutItemResult.UPDATED:
return Response(b"", status_code=204)
return Response(b"", status_code=201)
@app.delete("/api/items/{id}", status_code=204)
async def delete_item(id: str, db: Session = Depends(get_db)):
crud.delete_item(db, id)
def month_timestamp() -> int:
"""Provides the timestamp of the current month's beginning (for improved privacy)"""
return timegm(date.today().replace(day=1).timetuple())
app.mount("/", StaticFiles(directory="static", html=True), name="static")

View file

@ -1,7 +1,8 @@
from sqlalchemy import Column, ForeignKey, String, Text, Boolean
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text
from database import Base
class Item(Base):
__tablename__ = "items"
@ -14,3 +15,4 @@ class Item(Base):
content = Column(Text)
note = Column(Text)
hidden = Column(Boolean, nullable=False)
last_updated = Column(Integer)

View file

@ -1,3 +1,3 @@
fastapi>=0.79.0
sqlalchemy>=1.4.39
uvicorn>=0.17.6
fastapi>=0.109.0
sqlalchemy>=2.0.25
uvicorn>=0.27.0.post1

View file

@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, Field
class Item(BaseModel):
@ -11,6 +11,7 @@ class Item(BaseModel):
content: str | None
note: str | None
hidden: bool
last_updated: int | None = Field(None)
class Config:
orm_mode = True
from_attributes = True

View file

@ -43,6 +43,9 @@
<label for="hidden">Hide by default</label>
<input type="checkbox" id="hidden">
<label for="last_updated">Last updated</label>
<input id="last_updated" type="text" disabled>
</form>
<button id="delete" class="btn red" autocomplete="off">Delete</button>

View file

@ -39,6 +39,7 @@ function fillForm() {
document.getElementById('content').value = item.content;
document.getElementById('note').value = item.note;
document.getElementById('hidden').checked = item.hidden;
document.getElementById('last_updated').value = formatTimestamp(item.last_updated);
formCoordsToMap();
}
@ -172,3 +173,9 @@ function formCoordsToMap() {
coordsToMap(coords_bl, coords_tr);
}
}
function formatTimestamp(ts) {
const date = new Date(ts * 1000);
// using Swedish format as a hack to get an iso formatted date
return date.toLocaleDateString("sv", {timeZone: "UTC"}).replace(/\-\d+$/,'')
}