diff --git a/projects/tentacles/BUILD b/projects/tentacles/BUILD index 88703b5..83b47df 100644 --- a/projects/tentacles/BUILD +++ b/projects/tentacles/BUILD @@ -7,6 +7,7 @@ py_project( py_requirement("click"), py_requirement("flask"), py_requirement("jinja2"), + py_requirement("octorest"), ], main_data = [ "//projects/tentacles/src/python/tentacles/static/css", diff --git a/projects/tentacles/src/python/tentacles/__main__.py b/projects/tentacles/src/python/tentacles/__main__.py index 7999a88..f334e63 100644 --- a/projects/tentacles/src/python/tentacles/__main__.py +++ b/projects/tentacles/src/python/tentacles/__main__.py @@ -1,18 +1,17 @@ #!/usr/bin/env python3 -""" - -""" +"""The core app entrypoint.""" from pathlib import Path import click -from flask import Flask, request, current_app +from flask import Flask, request import tomllib from datetime import datetime from tentacles.blueprints import user_ui, printer_ui, api from tentacles.store import Store from tentacles.globals import _ctx, Ctx, ctx +from tentacles.workers import create_workers @click.group() @@ -20,10 +19,15 @@ def cli(): pass +def store_factory(app): + store = Store(app.config.get("db", {}).get("uri")) + store.connect() + return store + + def custom_ctx(app, wsgi_app): def helper(environ, start_response): - store = Store(app.config.get("db", {}).get("uri")) - store.connect() + store = store_factory(app) token = _ctx.set(Ctx(store)) try: return wsgi_app(environ, start_response) @@ -34,10 +38,10 @@ def custom_ctx(app, wsgi_app): return helper -def create_j2_request_global(): - current_app.jinja_env.globals["ctx"] = ctx - current_app.jinja_env.globals["request"] = request - current_app.jinja_env.globals["datetime"] = datetime +def create_j2_request_global(app): + app.jinja_env.globals["ctx"] = ctx + app.jinja_env.globals["request"] = request + app.jinja_env.globals["datetime"] = datetime def user_session(): @@ -65,8 +69,8 @@ def serve(hostname: str, port: int, config: Path): print(app.config) # Before first request - with app.app_context(): - create_j2_request_global() + create_j2_request_global(app) + shutdown_event = create_workers(lambda: store_factory(app)) # Before request app.before_request(user_session) @@ -80,7 +84,10 @@ def serve(hostname: str, port: int, config: Path): app.wsgi_app = custom_ctx(app, app.wsgi_app) # And run the blame thing - app.run(host=hostname, port=port) + try: + app.run(host=hostname, port=port) + finally: + shutdown_event.set() if __name__ == "__main__": diff --git a/projects/tentacles/src/python/tentacles/schema.sql b/projects/tentacles/src/python/tentacles/schema.sql index a808341..b32e9a7 100644 --- a/projects/tentacles/src/python/tentacles/schema.sql +++ b/projects/tentacles/src/python/tentacles/schema.sql @@ -14,9 +14,9 @@ CREATE TABLE IF NOT EXISTS user_statuses ( , UNIQUE(name) ); -INSERT OR IGNORE INTO user_statuses (id, name) values (-1, 'disabled'); -INSERT OR IGNORE INTO user_statuses (id, name) values (-2, 'unverified'); -INSERT OR IGNORE INTO user_statuses (id, name) values (1, 'enabled'); +INSERT OR IGNORE INTO user_statuses (id, name) VALUES (-1, 'disabled'); +INSERT OR IGNORE INTO user_statuses (id, name) VALUES (-2, 'unverified'); +INSERT OR IGNORE INTO user_statuses (id, name) VALUES (1, 'enabled'); CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT diff --git a/projects/tentacles/src/python/tentacles/static/css/_tirefire.scss b/projects/tentacles/src/python/tentacles/static/css/_tirefire.scss index 6b69451..b596b4e 100644 --- a/projects/tentacles/src/python/tentacles/static/css/_tirefire.scss +++ b/projects/tentacles/src/python/tentacles/static/css/_tirefire.scss @@ -135,13 +135,14 @@ $secondary_dark_grey: #9A9A9A; //////////////////////////////////////////////////////////////////////////////////////////////////// // A timer animation + .timer { background: -webkit-linear-gradient(left, skyBlue 50%, #eee 50%); border-radius: 100%; height: calc(var(--size) * 1px); width: calc(var(--size) * 1px); position: relative; - -webkit-animation: time calc(var(--duration) * 1s) steps(1000, start) infinite; + -webkit-animation: time calc(var(--duration) * 1s) steps(1000, start); -webkit-mask: radial-gradient(transparent 50%,#000 50%); mask: radial-gradient(transparent 50%,#000 50%); } @@ -152,7 +153,7 @@ $secondary_dark_grey: #9A9A9A; position: absolute; top: 0; width: 50%; - -webkit-animation: mask calc(var(--duration) * 1s) steps(500, start) infinite; + -webkit-animation: mask calc(var(--duration) * 1s) steps(500, start); -webkit-transform-origin: 100% 50%; } @-webkit-keyframes time { @@ -178,3 +179,10 @@ $secondary_dark_grey: #9A9A9A; -webkit-transform: rotate(-180deg); } } + +.alert .timer { + --size: 10; + --duration: 5; + padding: 6px; + margin: 6px; +} diff --git a/projects/tentacles/src/python/tentacles/store.py b/projects/tentacles/src/python/tentacles/store.py index 5f75b54..305b54c 100644 --- a/projects/tentacles/src/python/tentacles/store.py +++ b/projects/tentacles/src/python/tentacles/store.py @@ -242,15 +242,53 @@ class Store(object): [name, url, api_key], ).fetchone() + @requires_conn + def fetch_printer(self, printer_id: int): + return self._conn.execute( + """ + SELECT + p.id + , p.name + , p.url + , p.api_key + , p.last_poll_date + , s.name as status + FROM printers p + INNER JOIN printer_statuses s + ON p.status_id = s.id + WHERE p.id = ?1 + """, + [printer_id], + ).fetchone() + @requires_conn def list_printers(self): return self._conn.execute( - "SELECT p.id, p.name, p.url, p.last_poll_date, s.name as status FROM printers p INNER JOIN printer_statuses s ON p.status_id = s.id" + "SELECT p.id, p.name, p.url, p.api_key, p.last_poll_date, s.name as status FROM printers p INNER JOIN printer_statuses s ON p.status_id = s.id" + ).fetchall() + + @fmap(one) + @requires_conn + def list_idle_printers(self): + return self._conn.execute( + """ + SELECT p.id + FROM printers p + LEFT JOIN jobs j ON p.id = j.printer_id + WHERE j.id IS NULL + """ ).fetchall() @requires_conn - def update_printer_status(self, printer_id, status_id): - pass + def update_printer_status(self, printer_id, state: str): + (status_id,) = self._conn.execute( + "SELECT id FROM printer_statuses WHERE name = ?1", [state] + ).fetchone() + + self._conn.execute( + "UPDATE printers SET status_id = ?2, last_poll_date = datetime('now') WHERE id = ?1", + [printer_id, status_id], + ) ################################################################################ # Files @@ -274,6 +312,10 @@ class Store(object): def delete_file(self, uid: int, fid: int): self._conn.execute("DELETE FROM files WHERE user_id = ? AND id = ?", [uid, fid]) + @requires_conn + def fetch_file(self, fid: int): + return self._conn.execute("SELECT * FROM files WHERE id = ?", [fid]).fetchone() + ################################################################################ # Job # @@ -301,10 +343,40 @@ class Store(object): """Enumerate jobs in priority order.""" cond = f"AND user_id = {uid}" if uid else "" return self._conn.execute( - f"SELECT * FROM jobs WHERE started_at IS NULL {cond} ORDER BY priority DESC", + f""" + SELECT * FROM jobs + WHERE started_at IS NULL AND printer_id IS NULL {cond} + ORDER BY priority DESC + """, [], ).fetchall() + @requires_conn + def list_mapped_jobs(self): + """Scheduler detail. List mapped but not started jobs.""" + + return self._conn.execute( + """ + SELECT * FROM jobs + WHERE started_at IS NULL AND printer_id IS NOT NULL + ORDER BY priority DESC + """, + [], + ).fetchall() + + @requires_conn + def poll_job_queue(self): + return self._conn.execute( + """ + SELECT id + FROM jobs + WHERE started_at IS NULL + AND printer_id IS NULL + ORDER BY priority DESC + LIMIT 1 + """ + ).fetchone() + @requires_conn def fetch_job(self, uid: int, jid: int) -> Optional[tuple]: return self._conn.execute( @@ -312,21 +384,15 @@ class Store(object): ).fetchone() @requires_conn - def alter_job(self, job: tuple): - fields = [ - "id", - "user_id", - "file_id", - "priority", - "started_at", - "finished_at", - "state", - "printer_id", - ] - assert len(job) == len(fields) + def assign_job(self, job_id: int, printer_id: int): return self._conn.execute( - f"INSERT OR REPLACE INTO jobs ({', '.join(fields)}) VALUES ({', '.join(['?'] * len(fields))})", - job, + "UPDATE jobs SET printer_id = ?2 WHERE id = ?1", [job_id, printer_id] + ) + + @requires_conn + def start_job(self, job_id: int): + return self._conn.execute( + "UPDATE jobs SET started_at = datetime('now') WHERE id = ?1", [job_id] ) @requires_conn diff --git a/projects/tentacles/src/python/tentacles/templates/base.html.j2 b/projects/tentacles/src/python/tentacles/templates/base.html.j2 index 70962ee..197d8ee 100644 --- a/projects/tentacles/src/python/tentacles/templates/base.html.j2 +++ b/projects/tentacles/src/python/tentacles/templates/base.html.j2 @@ -44,9 +44,9 @@