org-roam database API is in place.
main
Ryan Rix 2021-09-21 21:30:35 -07:00
commit c27fe661f8
8 changed files with 475 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use nix

321
arcology-fastapi.org Normal file
View File

@ -0,0 +1,321 @@
:PROPERTIES:
:ID: arcology/fastapi
:ROAM_ALIASES: "Arcology FastAPI"
:END:
#+TITLE: Arcology Python Prototype
#+AUTO_TANGLE: t
#+filetags: :Project:
I learned a lot in building [[id:1d917282-ecf4-4d4c-ba49-628cbb4bb8cc][The Arcology Project]] the first time around, and now that I have [[id:26762cec-7934-4275-8f0b-731ee0e22e07][Migrated to org-roam v2]] I need to evaluate the project, fix it, and get it running again.
Over the last few months, I have been playing with a [[id:cce/python][Python]] package called [[https://fastapi.tiangolo.com/][FastAPI]] and loving the "batteries included" approach with modern Python 3 and an Flask- or Express-like router model rather than a full MVC framework which I was working with on [[id:cce/elixir][Elixir]]...
* Frontmatter
** =requirements.txt= and mach-nix setup
#+begin_src txt :tangle requirements.txt :comments none
fastapi
uvicorn[standard]
sqlmodel
sexpdata
#+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
- [[id:45fc2a02-fcd0-40c6-a29e-897c0ee7b1c7][direnv]] will get a lot of the way, but without mach we still need:
- [[elisp:(pyvenv-activate "~/org/arcology-fastapi/env")]] and turn on =pyvenv-mode=...
- then we can click for deps [[shell:pip install -r requirements.txt &]]
* Model the [[id:cce/org-roam][org-roam]] schema in SQLModel
Getting these to all fit together is gonna hopefully not be too fiddly. I am happy that I don't have to coerce all these primary keys and shit, I just need to be able to read from the database and walk among ORM objects to extract an Arcology Page out of it
** Arcology "just" reads the [[id:cce/org-roam][org-roam]] database
#+begin_src python :tangle arcology/models.py
from sqlmodel import create_engine
sqlite_file_name = "/home/rrix/.emacs.d/org-roam.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
def make_engine():
return create_engine(sqlite_url, echo=True)
engine = make_engine()
from typing import Optional, List
from sqlmodel import Field, Relationship, SQLModel
from arcology.parse import parse_sexp
#+end_src
*** NEXT make path configurable
bring in a configuration framework in this?
** Nodes have-many Links and Nodes have-many-to-many Nodes through Links
:LOGBOOK:
- State "INPROGRESS" from "NEXT" [2021-09-21 Tue 19:35]
:END:
Setting up the links here is a bit fussy; the class key =source_ref= is the database key =source= and =dest_ref= similarly maps to =dest=; this requires some extra bits to be defined to properly de-reference these, especially when using it as the LinkModel: the link models need to know to query against =source_ref= and =dest_ref!
#+begin_src sql
CREATE TABLE links (pos NOT NULL, source NOT NULL, dest NOT NULL, type NOT NULL, properties NOT NULL,
FOREIGN KEY (source) REFERENCES nodes (id) ON DELETE CASCADE);
#+end_src
#+begin_src python :tangle arcology/models.py
class Link(SQLModel, table=True):
__tablename__ = 'links'
pos: str
source_ref: Optional[str] = Field(primary_key=True, foreign_key="nodes.id",
sa_column_kwargs=dict(name='source'))
dest_ref: Optional[str] = Field(primary_key=True, foreign_key="nodes.id",
sa_column_kwargs=dict(name='dest'))
type: str
properties: str
source: Optional["Node"] = Relationship(# back_populates="outlinks",
sa_relationship_kwargs=dict(
foreign_keys="[Link.source_ref]",
))
dest: Optional["Node"] = Relationship(back_populates="backlinks",
sa_relationship_kwargs=dict(
foreign_keys="[Link.dest_ref]",
))
#+end_src
** Node is the "root" object
:LOGBOOK:
- State "INPROGRESS" from [2021-09-21 Tue 18:31]
:END:
[[id:cce/org-roam][org-roam]] v2 is node-oriented, but Arcology is file-oriented.
#+begin_src sql
CREATE TABLE nodes (id NOT NULL PRIMARY KEY, file NOT NULL, level NOT NULL, pos NOT NULL,
todo , priority , scheduled text, deadline text, title , properties , olp ,
FOREIGN KEY (file) REFERENCES files (file) ON DELETE CASCADE);
#+end_src
=file_ref= is defined strangely so that the relationship can be the reasonable one to access. Similarly, there are =source= and =dest= hackery-pokery here, as well.
#+begin_src python :tangle arcology/models.py :mkdirp yes
class Node(SQLModel, table=True):
__tablename__ = 'nodes'
id: Optional[int] = Field(default=None, primary_key=True)
file_ref: Optional[str] = Field(foreign_key="files.file",
sa_column_kwargs={'name': 'file'})
level: int
pos: int
title: str
todo: str
priority: str
scheduled: str
deadline: str
properties: str
olp: str
file: List["File"] = Relationship(back_populates="nodes")
aliases: List["Alias"] = Relationship(back_populates="node")
tags: List["Tag"] = Relationship(back_populates="node")
references: List["Reference"] = Relationship(back_populates="node")
backlinks: List["Link"] = Relationship(back_populates="source",
sa_relationship_kwargs=dict(
foreign_keys="[Link.source_ref]",
primaryjoin="Node.id==Link.source_ref",
))
outlinks: List["Link"] = Relationship(back_populates="dest",
sa_relationship_kwargs=dict(
foreign_keys="[Link.dest_ref]",
primaryjoin="Node.id==Link.dest_ref",
))
# https://github.com/tiangolo/sqlmodel/issues/89
backlink_files: List["Node"] = Relationship(
back_populates="outlink_files",
link_model=Link,
sa_relationship_kwargs=dict(
primaryjoin="Node.id==Link.source_ref",
secondaryjoin="Node.id==Link.dest_ref",
)
)
outlink_files: List["Node"] = Relationship(
back_populates="backlink_files",
link_model=Link,
sa_relationship_kwargs=dict(
primaryjoin="Node.id==Link.dest_ref",
secondaryjoin="Node.id==Link.source_ref",
)
)
#+end_src
** Nodes have-one file
:LOGBOOK:
- State "INPROGRESS" from [2021-09-21 Tue 18:31]
:END:
The node at level-0 is the File Properties node.
#+begin_src sql
CREATE TABLE files (file UNIQUE PRIMARY KEY, hash NOT NULL,
atime NOT NULL, mtime NOT NULL);
#+end_src
#+begin_src python :tangle arcology/models.py :mkdirp yes
class File(SQLModel, table=True):
__tablename__ = 'files'
file: Optional[str] = Field(default=None, primary_key=True)
hash: str
atime: str
mtime: str
nodes: List["Node"] = Relationship(back_populates="file")
#+end_src
** Nodes have-many Aliases
:LOGBOOK:
- State "INPROGRESS" from "NEXT" [2021-09-21 Tue 19:30]
:END:
#+begin_src sql
CREATE TABLE aliases (node_id NOT NULL, alias ,
FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE);
#+end_src
#+begin_src python :tangle arcology/models.py :mkdirp yes
class Alias(SQLModel, table=True):
__tablename__ = 'aliases'
node_id: str = Field(primary_key=True, foreign_key='nodes.id')
alias: str
node: Optional["Node"] = Relationship(back_populates="aliases")
def __str__(self):
return parse_sexp(self.alias)
#+end_src
*** DONE un-wrap Alias.__str__
:LOGBOOK:
- State "DONE" from "NEXT" [2021-09-21 Tue 21:22]
:END:
** Nodes have-many Tags
:LOGBOOK:
- State "INPROGRESS" from "NEXT" [2021-09-21 Tue 19:31]
:END:
#+begin_src sql
CREATE TABLE tags (node_id NOT NULL, tag ,
FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE);
#+end_src
#+begin_src python :tangle arcology/models.py :mkdirp yes
class Tag(SQLModel, table=True):
__tablename__ = 'tags'
node_id: str = Field(primary_key=True, foreign_key='nodes.id')
tag: str
node: Optional["Node"] = Relationship(back_populates="tags")
def __str__(self):
return parse_sexp(self.tag)
#+end_src
*** DONE un-wrap Tag.__str__
:LOGBOOK:
- State "DONE" from "NEXT" [2021-09-21 Tue 21:22]
:END:
** Nodes have-many References
:LOGBOOK:
- State "INPROGRESS" from "NEXT" [2021-09-21 Tue 19:32]
:END:
#+begin_src sql
CREATE TABLE refs (node_id NOT NULL, ref NOT NULL, type NOT NULL,
FOREIGN KEY (node_id) REFERENCES nodes (id) ON DELETE CASCADE);
#+end_src
#+begin_src python :tangle arcology/models.py :mkdirp yes
class Reference(SQLModel, table=True):
__tablename__ = 'refs'
node_id: str = Field(primary_key=True, foreign_key='nodes.id')
ref: str
node: Optional["Node"] = Relationship(back_populates="references")
def __str__(self):
return parse_sexp(self.ref)
#+end_src
*** DONE Unwrap Reference.__str__
:LOGBOOK:
- State "DONE" from "NEXT" [2021-09-21 Tue 21:17]
:END:
** Translate in/out of s-expression forms
Use [[https://sexpdata.readthedocs.io/en/latest/][sexpdata]] to decode some of the keys. At some point I could do some hackery-pokery to monkeypatch this in to some points. For now it'll be great in =__str__= and some property access methods.
#+begin_src python :tangle arcology/parse.py
import sexpdata as sexp
def parse_sexp(in_sexp: str):
return sexp.loads(in_sexp)
#+end_src
** Attach [[id:arroyo/system-cache][arroyo-db]] for Keywords
** higher-level =arcology.Page= class
The =arcology.page.Page= (aliased as =arcology.Page= elsewhere=
#+begin_src python :tangle arcology/page.py
#+end_src
* inotify-watcher
** [[id:arroyo/system-cache][arroyo-db]] batch update functions and script
** pandoc or whatever
* fastapi
** "local" routing
** URL re-writing
** pandoc memoization
**

124
arcology/models.py Normal file
View File

@ -0,0 +1,124 @@
from sqlmodel import create_engine
sqlite_file_name = "/home/rrix/.emacs.d/org-roam.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"
def make_engine():
return create_engine(sqlite_url, echo=True)
engine = make_engine()
from typing import Optional, List
from sqlmodel import Field, Relationship, SQLModel
from arcology.parse import parse_sexp
class Link(SQLModel, table=True):
__tablename__ = 'links'
pos: str
source_ref: Optional[str] = Field(primary_key=True, foreign_key="nodes.id",
sa_column_kwargs=dict(name='source'))
dest_ref: Optional[str] = Field(primary_key=True, foreign_key="nodes.id",
sa_column_kwargs=dict(name='dest'))
type: str
properties: str
source: Optional["Node"] = Relationship(# back_populates="outlinks",
sa_relationship_kwargs=dict(
foreign_keys="[Link.source_ref]",
))
dest: Optional["Node"] = Relationship(back_populates="backlinks",
sa_relationship_kwargs=dict(
foreign_keys="[Link.dest_ref]",
))
class Node(SQLModel, table=True):
__tablename__ = 'nodes'
id: Optional[int] = Field(default=None, primary_key=True)
file_ref: Optional[str] = Field(foreign_key="files.file",
sa_column_kwargs={'name': 'file'})
level: int
pos: int
title: str
todo: str
priority: str
scheduled: str
deadline: str
properties: str
olp: str
file: List["File"] = Relationship(back_populates="nodes")
aliases: List["Alias"] = Relationship(back_populates="node")
tags: List["Tag"] = Relationship(back_populates="node")
references: List["Reference"] = Relationship(back_populates="node")
backlinks: List["Link"] = Relationship(back_populates="source",
sa_relationship_kwargs=dict(
foreign_keys="[Link.source_ref]",
primaryjoin="Node.id==Link.source_ref",
))
outlinks: List["Link"] = Relationship(back_populates="dest",
sa_relationship_kwargs=dict(
foreign_keys="[Link.dest_ref]",
primaryjoin="Node.id==Link.dest_ref",
))
# https://github.com/tiangolo/sqlmodel/issues/89
backlink_files: List["Node"] = Relationship(
back_populates="outlink_files",
link_model=Link,
sa_relationship_kwargs=dict(
primaryjoin="Node.id==Link.source_ref",
secondaryjoin="Node.id==Link.dest_ref",
)
)
outlink_files: List["Node"] = Relationship(
back_populates="backlink_files",
link_model=Link,
sa_relationship_kwargs=dict(
primaryjoin="Node.id==Link.dest_ref",
secondaryjoin="Node.id==Link.source_ref",
)
)
class File(SQLModel, table=True):
__tablename__ = 'files'
file: Optional[str] = Field(default=None, primary_key=True)
hash: str
atime: str
mtime: str
nodes: List["Node"] = Relationship(back_populates="file")
class Alias(SQLModel, table=True):
__tablename__ = 'aliases'
node_id: str = Field(primary_key=True, foreign_key='nodes.id')
alias: str
node: Optional["Node"] = Relationship(back_populates="aliases")
def __str__(self):
return parse_sexp(self.alias)
class Tag(SQLModel, table=True):
__tablename__ = 'tags'
node_id: str = Field(primary_key=True, foreign_key='nodes.id')
tag: str
node: Optional["Node"] = Relationship(back_populates="tags")
def __str__(self):
return parse_sexp(self.tag)
class Reference(SQLModel, table=True):
__tablename__ = 'refs'
node_id: str = Field(primary_key=True, foreign_key='nodes.id')
ref: str
node: Optional["Node"] = Relationship(back_populates="references")
def __str__(self):
return parse_sexp(self.ref)

1
arcology/page.py Normal file
View File

@ -0,0 +1 @@

4
arcology/parse.py Normal file
View File

@ -0,0 +1,4 @@
import sexpdata as sexp
def parse_sexp(in_sexp: str):
return sexp.loads(in_sexp)

13
mach.nix Normal file
View File

@ -0,0 +1,13 @@
{ ... }:
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 = ""; #
}

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
fastapi
uvicorn[standard]
sqlmodel
sexpdata

7
shell.nix Normal file
View File

@ -0,0 +1,7 @@
let
pkgs = import <nixpkgs> {};
in pkgs.mkShell {
packages = [
pkgs.python3
];
}