aboutsummaryrefslogtreecommitdiff
"""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/<uuid:job_id>")
@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/<uuid:job_id>")
@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)