|
|
|
@ -0,0 +1,177 @@
|
|
|
|
|
:PROPERTIES:
|
|
|
|
|
:ID: 20210918-pokaijo
|
|
|
|
|
:ROAM_ALIASES: "FastAPI Personal Inventory" "My Inventory Application"
|
|
|
|
|
:END:
|
|
|
|
|
#+TITLE: Poka Ijo
|
|
|
|
|
#+filetags: :Project:Programming:
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
|
|
|
|
** =requirements.txt= and mach-nix setup
|
|
|
|
|
|
|
|
|
|
#+begin_src txt :tangle requirements.txt :comments none
|
|
|
|
|
fastapi
|
|
|
|
|
uvicorn[standard]
|
|
|
|
|
sqlmodel
|
|
|
|
|
#+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 Server initialization
|
|
|
|
|
|
|
|
|
|
#+begin_src :tangle server.py
|
|
|
|
|
|
|
|
|
|
#+end_src
|
|
|
|
|
* 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
|
|
|
|
|
from sqlmodel import Field, List, Relationship, SQLModel
|
|
|
|
|
|
|
|
|
|
<<ThingTagLink>>
|
|
|
|
|
<<Thing>>
|
|
|
|
|
<<Tag>>
|
|
|
|
|
<<Attachment>>
|
|
|
|
|
|
|
|
|
|
<<create_db_and_tables>>
|
|
|
|
|
#+end_src
|
|
|
|
|
|
|
|
|
|
** =Thing= is a discrete object or collection in my Inventory
|
|
|
|
|
|
|
|
|
|
#+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
|
|
|
|
|
|
|
|
|
|
#+begin_src python :noweb-ref Tag
|
|
|
|
|
class Tag(SQLModel, table=True):
|
|
|
|
|
id: Optiona[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
|
|
|
|
|
|
|
|
|
|
[[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
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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 create_db_and_tables():
|
|
|
|
|
SQLModel.metadata.create_all(engine)
|
|
|
|
|
#+end_src
|
|
|
|
|
|
|
|
|
|
* NEXT mobile-friendly capture interface
|
|
|
|
|
|
|
|
|
|
- image upload
|
|
|
|
|
- fast metadata creation
|
|
|
|
|
- "insert into"
|
|
|
|
|
- "move to"
|
|
|
|
|
|
|
|
|
|
* NEXT index view
|
|
|
|
|
|
|
|
|
|
* NEXT search field
|
|
|
|
|
|
|
|
|
|
just a quick name -> thing search, maybe description, too
|
|
|
|
|
|
|
|
|
|
* NEXT dynamic autocomplete for object names, inline move-to, insert-into.
|
|
|
|
|
|
|
|
|
|
* 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.
|