poka_ijo/poka-ijo.org

8.3 KiB
Raw Permalink Blame History

Poka Ijo

Poka Ijo is a catalog of my nearby things. Poka Ijo is 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

CLOCK: [2021-09-18 Sat 16:33][2021-09-18 Sat 16:56] => 0:23

.gitignore file

__pycache__
env
poka-ijo.db

requirements.txt and mach-nix setup

fastapi
uvicorn[standard]
sqlmodel
jinja2
{ ... }:
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)
}

in /rrix/poka_ijo/src/branch/main/(websockets) reference, patchPhase in websockets is broken…

sqlmodel is uninstallable in mach-nix right now, just use venv…

let
  pkgs = import <nixpkgs> {};
in pkgs.mkShell {
  packages = [
    pkgs.python3
  ];
}

FastAPI HTTP Server

  • Set up FastAPI; uvicorn server:app will use that FastAPI app object.
  • Set up Jinja2: templates is a FastAPI helper which can easily render things tangled in to ./templates.
  • Create the DB using metadata attached to the SQLModel class, make sure an engine is lying around for querying.
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>>

Some really basic app template:

<!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>

INPROGRESS index view

  • State "INPROGRESS" from "NEXT" [2021-09-18 Sat 18:15]

This page will list all the items with some sort of pagination strategy.

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})
{% 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 %}

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

CLOCK: [2021-09-18 Sat 16:56][2021-09-18 Sat 16:58] => 0:02

from typing import Optional, List
from sqlmodel import Field, Relationship, SQLModel, create_engine

<<ThingTagLink>>
<<Thing>>
<<Tag>>
<<Attachment>>
<<create_db_and_tables>>

Thing is a discrete object or collection in my Inventory

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 Syncthing share.
  • Things have Many-to-Many with Tags which are simple strings for queryin's sake.
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)

Tag is a simple string which can be joined against for a Good Time

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)

ThingTagLink is a Many-to-Many link between Things and Tags

SQLModel docs on many-to-many:

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
    )

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 Syncthingability.

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")

Model table creation

SQLModel gives us this "for free". Eventually this will go in a configuration file.

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()

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.