poka_ijo/poka-ijo.org

288 lines
8.3 KiB
Org Mode

:PROPERTIES:
:ID: 20210918-pokaijo
:ROAM_ALIASES: "FastAPI Personal Inventory" "My Inventory Application"
:END:
#+TITLE: Poka Ijo
#+filetags: :Project:Programming:PokaIjo:
#+AUTO_TANGLE: t
Poka Ijo is a catalog of my nearby things. Poka Ijo is [[id:tokipona][Tokipona]] for "nearby things", and it's a simple FastAPI application designed to run on my laptop or server and be used on my phone to quickly capture things in to my inventory, and run queries to determine where things are, and to find things that I should replace, donate, or maintain.
* Frontmatter
:LOGBOOK:
CLOCK: [2021-09-18 Sat 16:33]--[2021-09-18 Sat 16:56] => 0:23
:END:
** =.gitignore= file
:PROPERTIES:
:ID: 20210918T181352.587601
:END:
#+begin_src shell :tangle .gitignore
__pycache__
env
poka-ijo.db
#+end_src
** =requirements.txt= and mach-nix setup
:PROPERTIES:
:ID: 20210918T172447.506306
:END:
#+begin_src txt :tangle requirements.txt :comments none
fastapi
uvicorn[standard]
sqlmodel
jinja2
#+end_src
#+begin_src nix -r :tangle mach.nix
{ ... }:
let
mach-nix = import (builtins.fetchGit {
url = "https://github.com/DavHau/mach-nix";
ref = "refs/tags/3.3.0";
}) {
pkgs = import <nixpkgs> {};
};
in
mach-nix.mkPythonShell {
requirements = builtins.readFile ./requirements.txt;
_.websockets.patchPhase = ""; # (ref:websockets)
}
#+end_src
in [[(websockets)]] reference, [[file:~/.nix-defexpr/channels/nixpkgs/pkgs/development/python-modules/websockets/default.nix::patchPhase = ''][patchPhase in websockets is broken...]]
[[https://github.com/DavHau/mach-nix/issues/324][sqlmodel]] is uninstallable in =mach-nix= right now, just use venv...
#+begin_src nix :tangle shell.nix
let
pkgs = import <nixpkgs> {};
in pkgs.mkShell {
packages = [
pkgs.python3
];
}
#+end_src
* FastAPI HTTP Server
:PROPERTIES:
:ID: 20210918T172533.031317
:END:
- Set up FastAPI; =uvicorn server:app= will use that FastAPI =app= object.
- Set up Jinja2: =templates= is a [[https://fastapi.tiangolo.com/advanced/templates/][FastAPI]] helper which can easily render things tangled in to [[file:./templates/][./templates]].
- [[id:20210918T172550.522530][Create the DB]] using metadata attached to the =SQLModel= class, make sure an =engine= is lying around for querying.
#+begin_src python :tangle server.py :noweb yes
from typing import Optional
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
import poka_ijo.models as models
app = FastAPI()
engine = models.create_db_and_tables()
templates = Jinja2Templates(directory="templates")
<<index-endpoints>>
#+end_src
** Some really basic app template:
#+begin_src jinja2 :tangle templates/base.html.j2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="manifest" href="/appmanifest.json">
<title>{% block title %}Poka Ijo: Nearby Things{% endblock %}</title>
{% block head %}{% endblock %}
</head>
<body>
<main role="main">
{% block content %}{% endblock %}
</main>
<footer>
{% block footer %}
<hr/>
<p class="text-center">
All content &copy; 02021 Ryan Rix &lt;<a href="mailto:site@whatthfuck.computer">site@whatthefuck.computer</a>&gt;
</p>
{% endblock %}
</footer>
</body>
</html>
#+end_src
** INPROGRESS index view
:LOGBOOK:
- State "INPROGRESS" from "NEXT" [2021-09-18 Sat 18:15]
:END:
This page will list all the items with some sort of pagination strategy.
#+begin_src python :noweb-ref index-endpoints
from fastapi.responses import HTMLResponse
from fastapi import Request
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
return templates.TemplateResponse("index.html.j2", {"request": request})
#+end_src
#+begin_src jinja2 :tangle templates/index.html.j2
{% extends "base.html.j2" %}
{% block title %}Index of Inventory{% endblock %}
{% block head %}{% endblock %}
{% block content %}
<h1>Index</h1>
<p class="important">
Welcome to my awesome homepage.
</p>
{% endblock %}
#+end_src
** NEXT mobile-friendly capture interface
- image upload, clean up with imagemagick or so
- fast metadata creation
- "insert into"
- "move to"
** NEXT search field
just a quick name -> thing search, maybe description, too
** NEXT dynamic autocomplete for object names, inline move-to, insert-into.
* Data Models
:LOGBOOK:
CLOCK: [2021-09-18 Sat 16:56]--[2021-09-18 Sat 16:58] => 0:02
:END:
#+begin_src python :tangle poka_ijo/models.py :noweb yes
from typing import Optional, List
from sqlmodel import Field, Relationship, SQLModel, create_engine
<<ThingTagLink>>
<<Thing>>
<<Tag>>
<<Attachment>>
<<create_db_and_tables>>
#+end_src
** =Thing= is a discrete object or collection in my Inventory
:PROPERTIES:
:ID: 20210918T172538.083873
:END:
#+NAME: thing-desc
God the definition of what a "thing" is loose-goosey and probably just gets figured out ad-hoc.
If i have "my aa-batteries", i want to be able to say how many i have left without doing something so insane as tracking each one; but a simple "count" field isn't quite so smart unless we also have a "split", which adds all sorts of struggle in the data model.
- name
- serial number
- description
- count of items in the collection
- Things have-many Attachments, usually images but maybe PDF documentation or receipt scans.
These can just go on my disk, in a [[id:cce/syncthing][Syncthing]] share.
- Things have Many-to-Many with Tags which are simple strings for queryin's sake.
#+begin_src python :noweb-ref Thing :noweb yes
class Thing(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
serial_no: str
description: str
count: int
attachments: List["Attachment"] = Relationship(back_populates="thing")
tags: List["Tag"] = Relationship(back_populates="thing", link_model=ThingTagLink)
#+end_src
** =Tag= is a simple string which can be joined against for a Good Time
:PROPERTIES:
:ID: 20210918T172541.240125
:END:
#+begin_src python :noweb-ref Tag
class Tag(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
tag: str
things: List["Thing"] = Relationship(back_populates="thing", link_model=ThingTagLink)
#+end_src
** =ThingTagLink= is a Many-to-Many link between Things and Tags
:PROPERTIES:
:ID: 20210918T172543.888246
:END:
[[https://sqlmodel.tiangolo.com/tutorial/many-to-many/create-models-with-link/][SQLModel docs]] on many-to-many:
#+begin_src python :noweb-ref ThingTagLink
class ThingTagLink(SQLModel, table=True):
thing_id: Optional[int] = Field(
default=None, foreign_key="thing.id", primary_key=True
)
tag_id: Optional[int] = Field(
default=None, foreign_key="tag.id", primary_key=True
)
#+end_src
** =Attachment= is a file that is relevant to a Thing
:PROPERTIES:
:ID: 20210918T172547.083917
:END:
They'll be pictures of the thing I am capturing in to the system, they'll be PDFs of documentation or scans of receipts and inserts.
make sure this is relative to a configurable root in configuration for [[id:cce/syncthing][Syncthing]]ability.
#+begin_src python :noweb-ref Attachment
class Attachment(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
name: str
path: str
thing_id: Optional[int] = Field(default=None, foreign_key="thing.id")
thing: Optional[Thing] = Relationship(back_populates="attachments")
#+end_src
** Model table creation
:PROPERTIES:
:ID: 20210918T172550.522530
:END:
SQLModel gives us this "for free". Eventually this will go in a configuration file.
#+begin_src python :noweb-ref create_db_and_tables
sqlite_file_name = "poka-ijo.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
_engine = create_engine(sqlite_url, echo=True)
def engine():
return _engine
def create_db_and_tables():
SQLModel.metadata.create_all(engine())
return engine()
#+end_src
* NEXT sqlite queries
these can just stay in the notebook and run against the (read-only!!!!!! I promise!!!!) database on my laptop, unless i have mobile usecases for them that can't be solved with a linux shell access on my phone? idonno.
* NEXT org exporter?
things exported with description with links inserted, maybe even local links to the images.