From 095ab9746f2405c78c65eb786dac4464f794ed20 Mon Sep 17 00:00:00 2001
From: Reid D McKenzie <me@arrdem.com>
Date: Thu, 6 Feb 2025 02:15:45 -0700
Subject: [PATCH] Add support for running jobs with specified fillament

---
 projects/tentacles/src/tentacles/__main__.py  | 20 +++++----
 .../tentacles/src/tentacles/blueprints/api.py | 17 ++++++--
 .../src/tentacles/blueprints/file_ui.py       |  5 ++-
 .../src/tentacles/blueprints/job_ui.py        | 12 +++++-
 projects/tentacles/src/tentacles/db.py        | 41 +++++++++++++++++--
 .../tentacles/src/tentacles/sql/color.sql     |  2 +-
 projects/tentacles/src/tentacles/sql/jobs.sql | 14 ++++++-
 .../tentacles/src/tentacles/sql/printers.sql  | 12 ++++--
 .../tentacles/src/tentacles/sql/user_keys.sql |  4 +-
 .../tentacles/src/tentacles/sql/users.sql     | 14 ++++++-
 .../tentacles/templates/edit_printer.html.j2  |  2 +-
 .../tentacles/templates/files_list.html.j2    |  2 +-
 .../src/tentacles/templates/macros.html.j2    |  9 +++-
 projects/tentacles/src/tentacles/workers.py   | 15 ++++---
 14 files changed, 134 insertions(+), 35 deletions(-)

diff --git a/projects/tentacles/src/tentacles/__main__.py b/projects/tentacles/src/tentacles/__main__.py
index 2834899..53d1186 100644
--- a/projects/tentacles/src/tentacles/__main__.py
+++ b/projects/tentacles/src/tentacles/__main__.py
@@ -21,15 +21,21 @@ from tentacles.db import Db
 from tentacles.globals import _ctx, Ctx, ctx
 from tentacles import workers
 from contextlib import closing
+from tentacles.blueprints.util import salt
+
+
+log = logging.getLogger(__name__)
 
 
 def db_factory(app):
-    store = Db(
-        Path(
-            app.config["ROOT_FOLDER"],
-            app.config.get("db", {}).get("uri"),
-        )
+    p = Path(
+        app.config["ROOT_FOLDER"],
+        app.config.get("db", {}).get("uri"),
     )
+
+    # log.info("Using db from %r", p)
+
+    store = Db(p)
     store.connect()
     return store
 
@@ -117,6 +123,8 @@ def serve(hostname: str, port: int, config: Path, trace: bool):
     if trace:
         logging.getLogger("tentacles.db").setLevel(logging.TRACE)
 
+    logging.getLogger("aiosql.adapters.sqlite3").setLevel(logging.TRACE)
+
     app = make_app()
 
     if config:
@@ -124,8 +132,6 @@ def serve(hostname: str, port: int, config: Path, trace: bool):
             app.config.update(tomllib.load(fp))
         app.config["ROOT_FOLDER"] = str(Path(config).absolute().parent)
 
-    print(app.config)
-
     # Run migrations once at startup rather than when connecting
     with closing(db_factory(app)) as db:
         db.migrate()
diff --git a/projects/tentacles/src/tentacles/blueprints/api.py b/projects/tentacles/src/tentacles/blueprints/api.py
index b728b9b..1f9b9a5 100644
--- a/projects/tentacles/src/tentacles/blueprints/api.py
+++ b/projects/tentacles/src/tentacles/blueprints/api.py
@@ -99,10 +99,14 @@ def create_file(location: Optional[str] = None):
         if os.path.exists(sanitized_path):
             return {"error": "file exists already"}, 409
 
-        # FIXME: Explicitly interpolating the path here kinda rots
-        file.save(
-            sanitized_path.replace("$ROOT_FOLDER", current_app.config["ROOT_FOLDER"])
+        real_path = sanitized_path.replace(
+            "$ROOT_FOLDER", current_app.config["ROOT_FOLDER"]
         )
+
+        print(file.filename, real_path)
+
+        # FIXME: Explicitly interpolating the path here kinda rots
+        file.save(real_path)
         row = ctx.db.create_file(
             uid=ctx.uid,
             filename=file.filename,
@@ -110,7 +114,12 @@ def create_file(location: Optional[str] = None):
         )
 
         if request.form.get("print", "").lower() == "true":
-            ctx.db.create_job(uid=ctx.uid, fid=row.id)
+            ctx.db.create_job(
+                uid=ctx.uid,
+                fid=row.id,
+                cid=None,
+                pid=None,
+            )
 
         return {"status": "ok"}, 202
 
diff --git a/projects/tentacles/src/tentacles/blueprints/file_ui.py b/projects/tentacles/src/tentacles/blueprints/file_ui.py
index 377673b..b5f05da 100644
--- a/projects/tentacles/src/tentacles/blueprints/file_ui.py
+++ b/projects/tentacles/src/tentacles/blueprints/file_ui.py
@@ -43,7 +43,10 @@ def manipulate_files():
                 return render_template("files.html.j2"), code
 
         case "download":
-            file = ctx.db.fetch_file(uid=ctx.uid, fid=int(request.form.get("file_id")))
+            file = ctx.db.fetch_file(
+                uid=ctx.uid,
+                fid=int(request.form.get("file_id")),
+            )
             if file:
                 return send_file(
                     file.path,
diff --git a/projects/tentacles/src/tentacles/blueprints/job_ui.py b/projects/tentacles/src/tentacles/blueprints/job_ui.py
index 7d3c5fc..05a5b07 100644
--- a/projects/tentacles/src/tentacles/blueprints/job_ui.py
+++ b/projects/tentacles/src/tentacles/blueprints/job_ui.py
@@ -18,6 +18,11 @@ log = logging.getLogger(__name__)
 BLUEPRINT = Blueprint("jobs", __name__)
 
 
+def maybe(f, x):
+    if x is not None:
+        return f(x)
+
+
 @BLUEPRINT.route("/jobs", methods=["GET"])
 @requires_auth
 def list_jobs():
@@ -29,7 +34,12 @@ def list_jobs():
 def manipulate_jobs():
     match request.form.get("action"):
         case "enqueue":
-            ctx.db.create_job(uid=ctx.uid, fid=int(request.form.get("file_id")))
+            job = ctx.db.create_job(
+                uid=ctx.uid,
+                fid=int(request.form.get("file_id")),
+                cid=maybe(int, request.form.get("color_id")),
+                pid=maybe(int, request.form.get("printer_id")),
+            )
             flash("Job created!", category="info")
 
         case "duplicate":
diff --git a/projects/tentacles/src/tentacles/db.py b/projects/tentacles/src/tentacles/db.py
index a374296..dafb25b 100644
--- a/projects/tentacles/src/tentacles/db.py
+++ b/projects/tentacles/src/tentacles/db.py
@@ -165,7 +165,13 @@ class Db(Queries):
     ################################################################################
     # Wrappers for doing Python type mapping
 
-    def create_key(self, *, uid: int, name: str, ttl: Optional[timedelta]):
+    def create_key(
+        self,
+        *,
+        uid: int,
+        name: str,
+        ttl: Optional[timedelta],
+    ):
         return super().create_key(
             uid=uid,
             name=name,
@@ -173,7 +179,11 @@ class Db(Queries):
         )
 
     def try_login(
-        self, *, username: str, password: str, ttl: timedelta
+        self,
+        *,
+        username: str,
+        password: str,
+        ttl: timedelta,
     ) -> Optional[str]:
         """Given a username and an (unsecured) password, attempt to authenticate the named user.
 
@@ -190,7 +200,13 @@ class Db(Queries):
         return self.create_key(uid=res.id, name="web session", ttl=ttl)
 
     def try_create_user(
-        self, *, username: str, email: str, password: str, gid: int = 1, sid: int = -3
+        self,
+        *,
+        username: str,
+        email: str,
+        password: str,
+        gid: int = 1,
+        sid: int = -3,
     ):
         digest = sha3_256()
         digest.update(password.encode("utf-8"))
@@ -202,6 +218,25 @@ class Db(Queries):
             sid=sid,
         )
 
+    def force_create_user(
+        self,
+        *,
+        username: str,
+        email: str,
+        password: str,
+        gid: int = 1,
+        sid: int = -3,
+    ):
+        digest = sha3_256()
+        digest.update(password.encode("utf-8"))
+        return super().force_create_user(
+            name=username,
+            email=email,
+            hash=digest.hexdigest(),
+            gid=gid,
+            sid=sid,
+        )
+
     def refresh_key(self, *, kid: str, ttl: timedelta):
         """Automagically renew an API key which is still in use.
 
diff --git a/projects/tentacles/src/tentacles/sql/color.sql b/projects/tentacles/src/tentacles/sql/color.sql
index bd0a403..9609cf2 100644
--- a/projects/tentacles/src/tentacles/sql/color.sql
+++ b/projects/tentacles/src/tentacles/sql/color.sql
@@ -54,7 +54,7 @@ WHERE
   id = :cid
 ;
 
--- name: list-color
+-- name: list-colors
 SELECT *
 FROM filament_color
 ;
diff --git a/projects/tentacles/src/tentacles/sql/jobs.sql b/projects/tentacles/src/tentacles/sql/jobs.sql
index 81bcfc5..ea737f1 100644
--- a/projects/tentacles/src/tentacles/sql/jobs.sql
+++ b/projects/tentacles/src/tentacles/sql/jobs.sql
@@ -29,17 +29,27 @@ CREATE TABLE IF NOT EXISTS jobs (
 -- name: migration-0001-jobs-add-print-time#
 ALTER TABLE jobs ADD COLUMN time_left INTEGER DEFAULT (0);
 
+-- name: migration-0002-jobs-add-print-color#
+ALTER TABLE jobs ADD COLUMN color_id INTEGER DEFAULT (0);
+
+-- name: migration-0003-jobs-add-queued-at#
+ALTER TABLE jobs ADD COLUMN queued_at TEXT DEFAULT (datetime('now'));
+
 -- name: create-job^
 INSERT INTO jobs (
    user_id
  , file_id
+ , color_id
+ , printer_id
 )
 VALUES (
    :uid
  , :fid
+ , :cid
+ , :pid
 )
 RETURNING
-   id
+   *
 ;
 -- name: fetch-job^
 SELECT
@@ -73,6 +83,7 @@ WHERE
 SELECT
    j.id as id
  , j.file_id
+ , coalesce(j.color_id, fa.color_id) as color_id
  , fa.id as analysis_id
  , fa.max_x
  , fa.max_y
@@ -81,7 +92,6 @@ SELECT
  , fa.max_end
  , fa.nozzle_diameter
  , fa.filament_id
- , fa.color_id
  , (SELECT name FROM filament WHERE id = fa.filament_id) AS filament_name
  , (SELECT name AS name FROM filament_color WHERE id = fa.color_id) AS color_name
  , (SELECT name FROM job_statuses WHERE id = j.status_id) AS status
diff --git a/projects/tentacles/src/tentacles/sql/printers.sql b/projects/tentacles/src/tentacles/sql/printers.sql
index a7233d6..a2bed9b 100644
--- a/projects/tentacles/src/tentacles/sql/printers.sql
+++ b/projects/tentacles/src/tentacles/sql/printers.sql
@@ -71,7 +71,6 @@ INSERT OR IGNORE INTO filament (name) VALUES ('PETG');
 
 ALTER TABLE printers ADD filament_id INTEGER REFERENCES filament(id) DEFAULT 1;
 
-
 -- name: migration-0004-create-printer-enabled#
 ALTER TABLE printers ADD enabled BOOLEAN DEFAULT TRUE;
 
@@ -81,17 +80,24 @@ ALTER TABLE printers ADD nozzle_diameter FLOAT DEFAULT 0.4;
 -- name: migration-0006-create-printer-level-date#
 ALTER TABLE printers ADD last_level_date TEXT DEFAULT NULL;
 
--- name: migration-0006-create-printer-filament-color#
+-- name: migration-0007-create-printer-filament-color#
 ALTER TABLE printers ADD color_id INTEGER REFERENCES filament_color(id) DEFAULT 1;
 
 -- name: try-create-printer^
 INSERT INTO printers (
    name
  , url
+ , stream_url
  , api_key
  , status_id
 )
-VALUES (:name, :url, :api_key, :sid)
+VALUES (
+  :name
+ , :url
+ , :stream_url
+ , :api_key
+ , :sid
+)
 RETURNING
    id
 ;
diff --git a/projects/tentacles/src/tentacles/sql/user_keys.sql b/projects/tentacles/src/tentacles/sql/user_keys.sql
index 9d841ef..d418b63 100644
--- a/projects/tentacles/src/tentacles/sql/user_keys.sql
+++ b/projects/tentacles/src/tentacles/sql/user_keys.sql
@@ -81,7 +81,7 @@ WHERE
         OR u.group_id = 0)       -- or the user is a root
 ;
 
--- name: refresh-key
+-- name: refresh-key!
 UPDATE user_keys
 SET
    expiration = :expiration
@@ -89,7 +89,7 @@ WHERE
    id = :kid
 ;
 
--- name: delete-key
+-- name: delete-key!
 DELETE FROM user_keys
 WHERE
    user_id = :uid
diff --git a/projects/tentacles/src/tentacles/sql/users.sql b/projects/tentacles/src/tentacles/sql/users.sql
index dab1464..ff431c6 100644
--- a/projects/tentacles/src/tentacles/sql/users.sql
+++ b/projects/tentacles/src/tentacles/sql/users.sql
@@ -43,8 +43,20 @@ CREATE TABLE IF NOT EXISTS users (
  , UNIQUE(email)
 );
 
+-- name: force-create-user^
+INSERT OR REPLACE INTO users (
+   name
+ , email
+ , hash
+ , group_id
+ , status_id
+)
+VALUES (:name, :email, :hash, :gid, :sid)
+RETURNING *
+;
+
 -- name: try-create-user^
-INSERT INTO users (
+INSERT OR IGNORE INTO users (
    name
  , email
  , hash
diff --git a/projects/tentacles/src/tentacles/templates/edit_printer.html.j2 b/projects/tentacles/src/tentacles/templates/edit_printer.html.j2
index cecad76..0f13eb6 100644
--- a/projects/tentacles/src/tentacles/templates/edit_printer.html.j2
+++ b/projects/tentacles/src/tentacles/templates/edit_printer.html.j2
@@ -47,7 +47,7 @@
       <div class="three columns">
         <label for="filament">Filament color</label>
         <select name="color_id">
-          {% for c in ctx.db.list_color() %}
+          {% for c in ctx.db.list_colors() %}
           <option value="{{ c.id }}" {% if printer.color_id == c.id %}selected{%endif%}>{{ c.name or c.code }}</option>
           {% endfor %}
         </select>
diff --git a/projects/tentacles/src/tentacles/templates/files_list.html.j2 b/projects/tentacles/src/tentacles/templates/files_list.html.j2
index 283bce6..8872f02 100644
--- a/projects/tentacles/src/tentacles/templates/files_list.html.j2
+++ b/projects/tentacles/src/tentacles/templates/files_list.html.j2
@@ -26,7 +26,7 @@
   </div>
   <div class="controls u-flex u-ml-auto u-mv-auto">
     {{ macros.download_file(file.id) }}
-    {{ macros.start_job(file.id) }}
+    {{ macros.start_job(file) }}
     {{ macros.delete_file(file.id) }}
   </div>
 </div>
diff --git a/projects/tentacles/src/tentacles/templates/macros.html.j2 b/projects/tentacles/src/tentacles/templates/macros.html.j2
index 62c3667..a003ba0 100644
--- a/projects/tentacles/src/tentacles/templates/macros.html.j2
+++ b/projects/tentacles/src/tentacles/templates/macros.html.j2
@@ -1,9 +1,14 @@
 {# #################################################################################################### #}
 {# Job CRUD #}
-{% macro start_job(id) %}
+{% macro start_job(file) %}
 <form class="inline" method="post" action="/jobs">
   <input type="hidden" name="action" value="enqueue" />
-  <input type="hidden" name="file_id" value="{{ id }}" />
+  <input type="hidden" name="file_id" value="{{ file.id }}" />
+  <select name="color_id">
+    {% for c in ctx.db.list_colors() %}
+      <option value="{{c.id}}" {% if file.color_id == c.id %}selected{%endif%}>{{c.name}}</option>
+    {% endfor %}
+  </select>
   <input id="submit" type="image" src="/static/print.svg" height="24" width="24" />
 </form>
 {% endmacro %}
diff --git a/projects/tentacles/src/tentacles/workers.py b/projects/tentacles/src/tentacles/workers.py
index f47743b..f3659a7 100644
--- a/projects/tentacles/src/tentacles/workers.py
+++ b/projects/tentacles/src/tentacles/workers.py
@@ -186,14 +186,15 @@ def push_jobs(app: App, db: Db) -> None:
                 continue
 
             try:
+                # FIXME: Add a helper for this replacement?
+                p = file.path.replace("$ROOT_FOLDER", app.config["ROOT_FOLDER"])
+
                 # Nuke the file if it exists just in case
-                if client.files_info("local", Path(file.path).name):
-                    client.delete(f"local/{Path(file.path).name}")
+                if client.files_info("local", Path(p).name):
+                    client.delete(f"local/{Path(p).name}")
 
                 # FIXME: Explicitly interpolating the path here kinda rots
-                client.upload(
-                    file.path.replace("$ROOT_FOLDER", app.config["ROOT_FOLDER"])
-                )
+                client.upload(p)
 
             except HTTPError as e:
                 if e.response.status_code == 409:
@@ -349,7 +350,9 @@ def send_emails(app, db: Db):
 
 def analyze_files(app: App, db: Db):
     for file in db.list_unanalyzed_files():
-        p = Path(file.path)
+        # FIXME: Add a helper for this replacement?
+        p = Path(file.path.replace("$ROOT_FOLDER", app.config["ROOT_FOLDER"]))
+
         if not p.is_file():
             log.error(f"Deleting missing file {file.id}!")
             db.delete_file(uid=file.user_id, fid=file.id)