Add support for running jobs with specified fillament

This commit is contained in:
Reid D McKenzie 2025-02-06 02:15:45 -07:00
parent f995ccd113
commit 095ab9746f
14 changed files with 134 additions and 35 deletions

View file

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

View file

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

View file

@ -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,

View file

@ -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":

View file

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

View file

@ -54,7 +54,7 @@ WHERE
id = :cid
;
-- name: list-color
-- name: list-colors
SELECT *
FROM filament_color
;

View file

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

View file

@ -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
;

View file

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

View file

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

View file

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

View file

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

View file

@ -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 %}

View file

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