"""Generic views and utilities to handle background jobs.""" import uuid import importlib from typing import Callable from functools import partial from flask import ( url_for, redirect, Response, Blueprint, render_template, current_app as app) from gn_libs import jobs from gn_libs import sqlite3 from gn_libs.jobs.jobs import JobNotFound from uploader.authorisation import require_login background_jobs_bp = Blueprint("background-jobs", __name__) HandlerType = Callable[[dict], Response] def __default_error_handler__(job: dict) -> Response: return redirect(url_for("background-jobs.job_error", job_id=job["job_id"])) def register_handlers( job_type: str, success_handler: HandlerType, # pylint: disable=[redefined-outer-name] error_handler: HandlerType = __default_error_handler__ # pylint: disable=[redefined-outer-name] ) -> str: """Register success and error handlers for each job type.""" if not bool(app.config.get("background-jobs")): app.config["background-jobs"] = {} if not bool(app.config["background-jobs"].get(job_type)): app.config["background-jobs"][job_type] = { "success": success_handler, "error": error_handler } return job_type def register_job_handlers(job: str): """Related to register handlers above.""" def __load_handler__(absolute_function_path): _parts = absolute_function_path.split(".") app.logger.debug("THE PARTS ARE: %s", _parts) assert len(_parts) > 1, f"Invalid path: {absolute_function_path}" module = importlib.import_module(f".{_parts[-2]}", package=".".join(_parts[0:-2])) return getattr(module, _parts[-1]) metadata = job["metadata"] if metadata["success_handler"]: _success_handler = __load_handler__(metadata["success_handler"]) try: _error_handler = __load_handler__(metadata["error_handler"]) except Exception as _exc:# pylint: disable=[broad-exception-caught] _error_handler = __default_error_handler__ register_handlers( metadata["job-type"], _success_handler, _error_handler) def handler(job: dict, handler_type: str) -> HandlerType: """Fetch a handler for the job.""" _job_type = job["metadata"]["job-type"] _handler = app.config.get( "background-jobs", {} ).get( _job_type, {} ).get(handler_type) if bool(_handler): return _handler(job) raise Exception(# pylint: disable=[broad-exception-raised] f"No '{handler_type}' handler registered for job type: {_job_type}") error_handler = partial(handler, handler_type="error") success_handler = partial(handler, handler_type="success") @background_jobs_bp.route("/status/") @require_login def job_status(job_id: uuid.UUID): """View the job status.""" with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: try: job = jobs.job(conn, job_id, fulldetails=True) status = job["metadata"]["status"] register_job_handlers(job) if status == "error": return error_handler(job) if status == "completed": return success_handler(job) return render_template("jobs/job-status.html", job=job) except JobNotFound as _jnf: return render_template( "jobs/job-not-found.html", job_id=job_id) @background_jobs_bp.route("/error/") @require_login def job_error(job_id: uuid.UUID): """Handle job errors in a generic manner.""" with sqlite3.connection(app.config["ASYNCHRONOUS_JOBS_SQLITE_DB"]) as conn: try: job = jobs.job(conn, job_id, fulldetails=True) return render_template("jobs/job-error.html", job=job) except JobNotFound as _jnf: return render_template("jobs/job-not-found.html", job_id=job_id)