Compare commits

...

3 Commits

Author SHA1 Message Date
Ryan Rix e1f02e2772 fix [localapi] wrapper script 2024-03-20 14:54:26 -07:00
Ryan Rix 708480f4ed add [localapi] file by-tag endpoints with keywords etc 2024-03-20 14:54:16 -07:00
Ryan Rix 5c21254a3a make the [localapi] options a table that can be viewed online 2024-03-20 12:40:00 -07:00
4 changed files with 164 additions and 102 deletions

View File

@ -176,7 +176,7 @@ The NixOS module which defines =services.arcology-ng= is in [[id:20240213T124300
* Deploy manifests for [[id:cce/home-manager][a Dynamic Home Manager Configuration]]
This deploys [[id:20240313T153901.656967][A Localhost API for the Arcology]] to any Home Manager user using =systemd= User Units:
This deploys [[id:20240313T153901.656967][A Localhost API for the Arcology]] to any Home Manager user using =systemd= User Units. See [[id:20240320T123253.558216][LocalAPI Deployment Options]] for the list of options that can be changed and how to use it:
#+begin_src nix :tangle ~/arroyo-nix/hm/arcology-localapi.nix
{ pkgs, ... }:
@ -186,7 +186,6 @@ let
arcology = pkgs.callPackage /home/rrix/org/arcology-django/default.nix { inherit arroyo_rs; };
in {
imports = [ ./arcology-localapi-mod.nix ];
home.packages = [ arcology ];
services.arcology2 = {
enable = true;
@ -197,6 +196,7 @@ in {
}
#+end_src
** NEXT address the =callPackages= here...
* Service Configuration
:PROPERTIES:

View File

@ -31,6 +31,7 @@ The API will be simple, with a bearer token provided by a local state file:
#+begin_src emacs-lisp :tangle ~/org/cce/arcology-localapi-commands.el
(use-package plz)
(defvar arcology-localapi-bearer-token nil)
(defun arcology-fetch-localapi-bearer-token ()
(interactive)
(let ((tok (or arcology-localapi-bearer-token
@ -67,6 +68,7 @@ urlpatterns = [
path("", views.index),
path("generate/<slug:module>/<slug:role>", views.generate, name="keyword_by_key"),
path("generate/<slug:module>", views.generate, name="keyword_by_key"),
path("file/by-tag/<slug:key>", views.files_by_tag, name="files_by_tag"),
path("keywords/<slug:key>", views.keyword_by_key, name="keyword_by_key"),
re_path("keywords/(?P<key>[0-9a-zA-Z_-]+)/(?P<value>[0-9a-zA-Z/_\-]+)", views.keyword_by_key_value, name="keyword_by_key"),
re_path("page/(?P<route_key>[0-9a-zA-Z/_\-]+)", views.page_metadata, name="page_metadata"),
@ -133,6 +135,7 @@ def authenticated(func):
return wrapper
#+end_src
* Keyword metadata
There is a simple set of HTTP GET APIs to query the file/key/value store:
@ -188,10 +191,16 @@ import arcology.models
def _json_page(page):
return dict(
title=page.title,
url=page.to_url(),
site=page.site.title,
)
title=page.title,
url=page.to_url(),
site=page.site.title,
keywords={
kw.keyword: kw.value
for kw in page.collect_keywords().all()
},
tags=list(set([tag.tag for tag in page.collect_tags()])),
references=[reference.ref for reference in page.collect_references()],
)
#+end_src
=GET http://127.0.0.1:8000/api/v1/page/arcology/localapi= or this Emacs Lisp command:
@ -229,14 +238,19 @@ def page_metadata(request, route_key):
=GET http://127.0.0.1:8000/api/v1/file/arcology-django/localapi.org= and the Emacs Lisp command which will do the same:
#+begin_src emacs-lisp :tangle ~/org/cce/arcology-localapi-commands.el
(defun arcology-file-metadata (file-path)
(thread-last
(file-relative-name file-path org-roam-directory)
(format "/file/%s")
(arcology-localapi-call 'get)
(alist-get 'file)))
(defun arcology-file-to-url (file-path &optional heading-id)
(interactive "fKey: \nsHeading ID: ")
(let ((url (concat
(thread-last
(file-relative-name file-path org-roam-directory)
(format "/file/%s")
(arcology-localapi-call 'get)
(alist-get 'file)
file-path
(arcology-file-metadata)
(alist-get 'page)
(alist-get 'url))
(when heading-id (format "#%s" heading-id)))))
@ -264,6 +278,36 @@ def file_metadata(request, file_path):
))
#+end_src
* Tag search
** Get all files by tag
#+begin_src emacs-lisp :tangle ~/org/cce/arcology-localapi-commands.el
(defun arcology-files-by-tag (tag)
(thread-last
tag
(format "/file/by-tag/%s")
(arcology-localapi-call 'get)
(alist-get 'files)
(mapcar #'identity)))
#+end_src
#+begin_src python :tangle localapi/views.py
@authenticated
def files_by_tag(request, key):
# final_path = settings.ARCOLOGY_BASE_DIR.joinpath(file_path)
the_tags = roam.models.Tag.objects.filter(tag=key).all()
return JsonResponse(dict(
state="ok :)",
tag=key,
files=list(set([
tag.heading.path.path
for tag in the_tags
]))
))
#+end_src
* Some more ELisp helpers
:PROPERTIES:
:ID: 20240313T212950.461285
@ -355,20 +399,71 @@ def generate(request, module, role=None):
this is all full of jank. having to pass to a temporary file because the generate commands are locked away in a django management command is silly, i should refactor all of this. the branch is silly, too. But it's very easy now to get my literate programming helpers to quickly generate the code imports and whatnot as soon as the local server is running.
* INPROGRESS [[id:cce/home-manager][home-manager]] deployment
* DONE Deploying the Arcology as a Local API via [[id:cce/home-manager][home-manager]]
:LOGBOOK:
- State "DONE" from "INPROGRESS" [2024-03-20 Wed 12:37]
- State "INPROGRESS" from "NEXT" [2024-03-18 Mon 16:08]
:END:
This thing needs to run as a =systemd= User Unit for the local tangles and whatnot to work. Bootstrapping this will be Fun, we'll just end up needing a bootstrap script to put the =systemd= user units in place for future deployments...
This is just the configurable module; the configuration itself is in the [[id:arcology/django/config][Arcology Project Configuration]] page like the Wobserver module.
#+ARROYO_HOME_MODULE: hm/arcology-localapi.nix
#+ARROYO_SYSTEM_ROLE: endpoint
: #+ARROYO_SYSTEM_ROLE: droid # need to make sure there is a wrapper script to launch this
#+ARROYO_SYSTEM_ROLE: droid
#+begin_src nix :tangle ~/arroyo-nix/hm/arcology-localapi-mod.nix
This thing needs to run as a =systemd= User Unit for the local tangles and whatnot to work. Bootstrapping this will be Fun, we'll just end up needing a bootstrap script to put the =systemd= user units in place for future deployments...
This is just the configurable module; the configuration itself is in the [[id:arcology/django/config][Arcology Project Configuration]] page like the Wobserver module. Here are the options:
** LocalAPI Deployment Options
:PROPERTIES:
:ID: 20240320T123253.558216
:END:
#+NAME: hm-options
| name | type | default | mdDoc |
|-------------------+--------------------------------------+-----------------------+----------------------------------------------------------------------------------|
| packages.arcology | package | | Arcology package to use |
| address | str | "localhost" | Web interface listening address |
| port | port | 29543 | Web interface listening port |
| workerCount | number | 4 | gunicorn worker count; they recommend 2-4 workers per core. |
| environmentFile | str | "${cfg.dataDir}/env" | A file containing =ARCOLOGY_SYNCTHING_KEY= and =ARCOLOGY_LOCALAPI_BEARER_TOKEN=. |
| logLevel | enum ["ERROR" "WARN" "INFO" "DEBUG"] | "INFO" | Django root logger level |
| orgDir | str | "~/org" | Directory containing the org-mode documents. Provide r/o access. |
| folderId | str | | Syncthing folder ID containing the org files |
| cacheDir | str | "~/.cache/arcology2" | Location to cache HTML files and the like. |
| dataDir | str | "~/.config/arcology2" | Location to store the arcology metadata store. |
That table does some really gross code generation:
#+NAME: generate-hm-options
#+begin_src emacs-lisp :var tbl=hm-options :results drawer
(thread-last
tbl
(-map (pcase-lambda (`(,name ,type ,default ,doc))
(let ((default-str
(cond ((numberp default)
(prin1-to-string default))
((and (stringp default)
(string-empty-p default))
nil)
(t (prin1-to-string default)))))
(concat
name " = mkOption {\n"
" type = with types; " type ";\n"
(if default-str
(concat " default = " default-str ";\n")
"")
(if doc
(concat " description = mdDoc ''" doc "'';\n")
"\n")
"};\n"))))
(s-join "\n"))
#+end_src
** Home Manager Service Definition
This does some code injection using the table above. It's configured from [[id:arcology/django/config][the Project Configuration]], but basically it sets up a few wrapper scripts that configure the process environment and put the webserver and the =watchsync= commands as SystemD User units as well as providing an =arcologyctl= command in =PATH=. Eventually it would be nice to pull the Syncthing key from the local =config.xml= but maybe that's another management command...
#+begin_src nix :tangle ~/arroyo-nix/hm/arcology-localapi-mod.nix :noweb yes
{ pkgs, config, lib, ... }:
let
@ -383,103 +478,40 @@ let
"ARCOLOGY_CACHE_PATH=${cfg.cacheDir}"
];
pyenv = pkgs.python3.withPackages(pp: [arcology]);
preStartScript = pkgs.writeScriptBin "arcology-watchsync-prestart" ''
mkdir -p ${cfg.dataDir}
${arcology}/bin/arcology migrate
${arcology}/bin/arcology seed || true
'';
watchsyncScript = pkgs.writeScriptBin "arcology-watchsync" ''
${arcology}/bin/arcology watchsync -f ${cfg.folderId}
exec ${arcology}/bin/arcology watchsync -f ${cfg.folderId}
'';
localapiScript = pkgs.writeScriptBin "arcology-localapi" ''
${arcology}/bin/arcology runserver ${toString cfg.address}:${toString cfg.port}
exec ${arcology}/bin/arcology runserver --noreload ${toString cfg.address}:${toString cfg.port}
'';
wrapperScript = pkgs.writeScriptBin "arcology" ''
set -eEuo pipefail
${lib.strings.concatMapStrings (e: "export ${e}\n") env}
source ${cfg.environmentFile}
export ARCOLOGY_SYNCTHING_KEY
export ARCOLOGY_LOCALAPI_BEARER_TOKEN
exec ${arcology}/bin/arcology "$@"
'';
in {
options.services.arcology2 = with lib; {
enable = mkEnableOption (mdDoc "Arcology Local API");
packages.arcology = mkOption {
type = types.package;
description = mdDoc ''
'';
};
address = mkOption {
type = types.str;
default = "localhost";
description = lib.mdDoc "Web interface address.";
};
port = mkOption {
type = types.port;
default = 29543;
description = lib.mdDoc "Web interface port.";
};
workerCount = mkOption {
type = types.number;
default = 4;
description = lib.mdDoc "gunicorn worker count; they recommend 2-4 workers per core.";
};
logLevel = mkOption {
type = types.enum ["ERROR" "WARN" "INFO" "DEBUG"];
default = "INFO";
description = mdDoc ''
Set the Django root logging level
'';
};
orgDir = mkOption {
type = types.str;
default = "~/org";
description = mdDoc ''
Directory containing the org-mode documents.
Arcology needs read-only access to this directory.
'';
};
environmentFile = mkOption {
type = types.path;
default = "${cfg.dataDir}/env";
description = mdDoc ''
A file containing environment variables you may not want to put in the nix store.
For example, you could put a syncthing key
and a bearer token for the Local API in there:
ARCOLOGY_SYNCTHING_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA;
ARCOLOGY_LOCALAPI_BEARER_TOKEN=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA;
'';
};
folderId = mkOption {
type = types.str;
description = mdDoc ''
Syncthing folder ID containing the org files.
'';
};
cacheDir = mkOption {
type = types.str;
default = "~/.cache/arcology2/";
description = mdDoc ''
Location to cache HTML files and the like.
'';
};
dataDir = mkOption {
type = types.str;
default = "~/.config/arcology2/";
description = mdDoc ''
Location to store the arcology metadata store.
'';
};
<<generate-hm-options()>>
};
config = {
home.packages = [
wrapperScript
];
systemd.user.services = {
arcology-watchsync = {
Unit.Description = "Dynamically update the Arcology database.";
@ -488,6 +520,9 @@ in {
ExecStart = "${pkgs.stdenv.shell} ${watchsyncScript}/bin/arcology-watchsync";
Restart = "on-failure";
RestartSec=5;
RestartSteps=10;
RestartMaxDelaySec="1min";
Environment = env;
EnvironmentFile = cfg.environmentFile;
};
@ -500,6 +535,9 @@ in {
ExecStart = "${pkgs.stdenv.shell} ${localapiScript}/bin/arcology-localapi";
Restart = "on-failure";
RestartSec=5;
RestartSteps=10;
RestartMaxDelaySec="1min";
Environment = env;
EnvironmentFile = cfg.environmentFile;
};
@ -514,3 +552,5 @@ in {
** NEXT is the arcology wobserver deployment better as a systemd user unit?
still need nixos module for the vhosts etc, but... hm.
running as my main user *would* make a lot of the environment stuff easier to deal with, i guess

View File

@ -8,6 +8,7 @@ urlpatterns = [
path("", views.index),
path("generate/<slug:module>/<slug:role>", views.generate, name="keyword_by_key"),
path("generate/<slug:module>", views.generate, name="keyword_by_key"),
path("file/by-tag/<slug:key>", views.files_by_tag, name="files_by_tag"),
path("keywords/<slug:key>", views.keyword_by_key, name="keyword_by_key"),
re_path("keywords/(?P<key>[0-9a-zA-Z_-]+)/(?P<value>[0-9a-zA-Z/_\-]+)", views.keyword_by_key_value, name="keyword_by_key"),
re_path("page/(?P<route_key>[0-9a-zA-Z/_\-]+)", views.page_metadata, name="page_metadata"),

View File

@ -56,10 +56,16 @@ import arcology.models
def _json_page(page):
return dict(
title=page.title,
url=page.to_url(),
site=page.site.title,
)
title=page.title,
url=page.to_url(),
site=page.site.title,
keywords={
kw.keyword: kw.value
for kw in page.collect_keywords().all()
},
tags=list(set([tag.tag for tag in page.collect_tags()])),
references=[reference.ref for reference in page.collect_references()],
)
# Page and File metadata:1 ends here
# [[file:../localapi.org::*Page and File metadata][Page and File metadata:3]]
@ -94,6 +100,21 @@ def file_metadata(request, file_path):
))
# Page and File metadata:5 ends here
# [[file:../localapi.org::*Get all files by tag][Get all files by tag:2]]
@authenticated
def files_by_tag(request, key):
# final_path = settings.ARCOLOGY_BASE_DIR.joinpath(file_path)
the_tags = roam.models.Tag.objects.filter(tag=key).all()
return JsonResponse(dict(
state="ok :)",
tag=key,
files=list(set([
tag.heading.path.path
for tag in the_tags
]))
))
# Get all files by tag:2 ends here
# [[file:../localapi.org::*\[\[id:20231217T154938.132553\]\[Arcology generate Command\]\]][[[id:20231217T154938.132553][Arcology generate Command]]:3]]
import tempfile
from django.core.management import call_command