<commit_before>from logging import getLogger

from aiohttp.web_exceptions import HTTPBadRequest, HTTPConflict, HTTPNoContent

from virtool.api.response import NotFound, json_response
from virtool.data.errors import (
    ResourceConflictError,
    ResourceNotFoundError,
)
from virtool.data.utils import get_data_from_req
from virtool.http.routes import Routes
from virtool.http.schema import schema
from virtool.users.utils import Permission

logger = getLogger(__name__)

routes = Routes()


@routes.get("/jobs")
async def find(req):
    """
    Return a list of job documents.

    """
    return json_response(await get_data_from_req(req).jobs.find(req.query))


@routes.get("/jobs/{job_id}")
@routes.jobs_api.get("/jobs/{job_id}")
async def get(req):
    """
    Return the complete document for a given job.

    """
    try:
        document = await get_data_from_req(req).jobs.get(req.match_info["job_id"])
    except ResourceNotFoundError:
        raise NotFound()

    return json_response(document)


@routes.jobs_api.patch("/jobs/{job_id}")
@schema({"acquired": {"type": "boolean", "allowed": [True], "required": True}})
async def acquire(req):
    """
    Sets the acquired field on the job document.

    This is used to let the server know that a job process has accepted the ID and needs
    to have the secure token returned to it.

    Pushes a status record indicating the job is in the 'Preparing' state. This sets an
    arbitrary progress value of 3 to give visual feedback in the UI that the job has
    started.

    """
    try:
        document = await get_data_from_req(req).jobs.acquire(req.match_info["job_id"])
    except ResourceNotFoundError:
        raise NotFound()
    except ResourceConflictError:
        raise HTTPBadRequest(text="Job already acquired")

    return json_response(document)


@routes.patch("/jobs/{job_id}/archive")
@routes.jobs_api.patch("/jobs/{job_id}/archive")
async def archive(req):
    """
    Sets the archived field on the job document.

    """
    try:
        document = await get_data_from_req(req).jobs.archive(req.match_info["job_id"])
    except ResourceNotFoundError:
        raise NotFound()
    except ResourceConflictError:
        raise HTTPBadRequest(text="Job already archived")

    return json_response(document)


@routes.put("/jobs/{job_id}/cancel", permission=Permission.cancel_job.value)
async def cancel(req):
    """Cancel a job."""
    try:
        document = await get_data_from_req(req).jobs.cancel(req.match_info["job_id"])
    except ResourceNotFoundError:
        raise NotFound
    except ResourceConflictError:
        raise HTTPConflict(text="Job cannot be cancelled in its current state")

    return json_response(document)


@routes.post("/jobs/{job_id}/status")
@routes.jobs_api.post("/jobs/{job_id}/status")
@schema(
    {
        "error": {
            "type": "dict",
            "default": None,
            "nullable": True,
            "schema": {
                "type": {"type": "string", "required": True},
                "traceback": {
                    "type": "list",
                    "schema": {"type": "string"},
                    "required": True,
                },
                "details": {
                    "type": "list",
                    "schema": {"type": "string"},
                    "required": True,
                },
            },
        },
        "progress": {"type": "integer", "required": True, "min": 0, "max": 100},
        "stage": {
            "type": "string",
            "required": True,
        },
        "step_name": {"type": "string", "default": None, "nullable": True},
        "step_description": {"type": "string", "default": None, "nullable": True},
        "state": {
            "type": "string",
            "allowed": [
                "waiting",
                "running",
                "complete",
                "cancelled",
                "error",
                "terminated",
            ],
            "required": True,
        },
    }
)
async def push_status(req):
    """Push a status update to a job."""
    data = req["data"]

    if data["state"] == "error" and not data["error"]:
        raise HTTPBadRequest(text="Missing error information")

    try:
        document = await get_data_from_req(req).jobs.push_status(
            req.match_info["job_id"],
            data["state"],
            data["stage"],
            data["step_name"],
            data["step_description"],
            data["error"],
            data["progress"],
        )
    except ResourceNotFoundError:
        raise NotFound
    except ResourceConflictError:
        raise HTTPConflict(text="Job is finished")

    return json_response(document["status"][-1], status=201)


@routes.delete("/jobs", permission=Permission.remove_job.value)
async def clear(req):
    job_filter = req.query.get("filter")

    # Remove jobs that completed successfully.
    complete = job_filter in [None, "finished", "complete"]

    # Remove jobs that errored or were cancelled.
    failed = job_filter in [None, "failed", "finished" "terminated"]

    removed_job_ids = await get_data_from_req(req).jobs.clear(
        complete=complete, failed=failed
    )

    return json_response({"removed": removed_job_ids})


@routes.delete("/jobs/{job_id}", permission=Permission.remove_job.value)
async def remove(req):
    """
    Remove a job.

    """
    try:
        await get_data_from_req(req).jobs.delete(req.match_info["job_id"])
    except ResourceConflictError:
        raise HTTPConflict(text="Job is running or waiting and cannot be removed")
    except ResourceNotFoundError:
        raise NotFound()

    raise HTTPNoContent<commit_msg>chore: add auto-doc for jobs API endpoints<commit_after>from logging import getLogger
from typing import Union, List

from aiohttp.web_exceptions import HTTPBadRequest, HTTPConflict, HTTPNoContent
from aiohttp_pydantic import PydanticView

from virtool.api.response import NotFound, json_response
from virtool.data.errors import (
    ResourceConflictError,
    ResourceNotFoundError,
)
from virtool.data.utils import get_data_from_req
from aiohttp_pydantic.oas.typing import r200, r204, r400, r403, r404, r409
from virtool.http.routes import Routes
from virtool.http.schema import schema
from virtool.users.utils import Permission
from virtool.http.privileges import permissions
from virtool_core.models.job import Job, JobMinimal
logger = getLogger(__name__)

routes = Routes()


@routes.view("/jobs")
class JobsView(PydanticView):

    async def get(self) -> Union[List[JobMinimal], r200, r400]:
        """
        List all job documents.

        Status Codes:
            200: Successful operation
            400: Invalid query
        """
        return json_response(await get_data_from_req(self.request).jobs.find(self.request.query))

    @permissions(Permission.remove_job)
    async def delete(self) -> r200:
        """
        Clear completed, failed or all finished jobs.

        Status Codes:
            200: Successful Operation
        """
        job_filter = self.request.query.get("filter")

        # Remove jobs that completed successfully.
        complete = job_filter in [None, "finished", "complete"]

        # Remove jobs that errored or were cancelled.
        failed = job_filter in [None, "failed", "finished" "terminated"]

        removed_job_ids = await get_data_from_req(self.request).jobs.clear(
            complete=complete, failed=failed
        )

        return json_response({"removed": removed_job_ids})


@routes.view("/jobs/{job_id}")
class JobView(PydanticView):

    async def get(self) -> Union[List[JobMinimal], r200, r404]:
        """
        Return the complete document for a given job.

        Status Codes:
            200: Successful operation
            404: Not found
        """
        try:
            document = await get_data_from_req(self.request).jobs.get(self.request.match_info["job_id"])
        except ResourceNotFoundError:
            raise NotFound()

        return json_response(document)

    @permissions(Permission.remove_job)
    async def delete(self) -> Union[r204, r403, r404, r409]:
        """
        Remove a job.

        Status Codes:
            204: Successful operation
            403: Not permitted
            404: Not found
            409: Job is running or waiting and cannot be removed
        """
        try:
            await get_data_from_req(self.request).jobs.delete(self.request.match_info["job_id"])
        except ResourceConflictError:
            raise HTTPConflict(text="Job is running or waiting and cannot be removed")
        except ResourceNotFoundError:
            raise NotFound()

        raise HTTPNoContent


@routes.jobs_api.get("/jobs/{job_id}")
async def get(req):
    """
    Return the complete document for a given job.
    """
    try:
        document = await get_data_from_req(req).jobs.get(req.match_info["job_id"])
    except ResourceNotFoundError:
        raise NotFound()

    return json_response(document)


@routes.jobs_api.patch("/jobs/{job_id}")
@schema({"acquired": {"type": "boolean", "allowed": [True], "required": True}})
async def acquire(req):
    """
    Sets the acquired field on the job document.
    This is used to let the server know that a job process has accepted the ID and needs
    to have the secure token returned to it.
    Pushes a status record indicating the job is in the 'Preparing' state. This sets an
    arbitrary progress value of 3 to give visual feedback in the UI that the job has
    started.
    """
    try:
        document = await get_data_from_req(req).jobs.acquire(req.match_info["job_id"])
    except ResourceNotFoundError:
        raise NotFound()
    except ResourceConflictError:
        raise HTTPBadRequest(text="Job already acquired")

    return json_response(document)


@routes.patch("/jobs/{job_id}/archive")
@routes.jobs_api.patch("/jobs/{job_id}/archive")
async def archive(req):
    """
    Sets the archived field on the job document.
    """
    try:
        document = await get_data_from_req(req).jobs.archive(req.match_info["job_id"])
    except ResourceNotFoundError:
        raise NotFound()
    except ResourceConflictError:
        raise HTTPBadRequest(text="Job already archived")

    return json_response(document)


@routes.view("/jobs/{job_id}/cancel")
class CancelJobView(PydanticView):

    @permissions(Permission.cancel_job)
    async def put(self) -> Union[Job, r200, r403, r404, r409]:
        """
        Cancel a job.

        Status Codes:
            200: Successful operation
            403: Not permitted
            404: Not found
            409: Not cancellable
        """
        try:
            document = await get_data_from_req(self.request).jobs.cancel(self.request.match_info["job_id"])
        except ResourceNotFoundError:
            raise NotFound
        except ResourceConflictError:
            raise HTTPConflict(text="Job cannot be cancelled in its current state")

        return json_response(document)


@routes.post("/jobs/{job_id}/status")
@routes.jobs_api.post("/jobs/{job_id}/status")
@schema(
    {
        "error": {
            "type": "dict",
            "default": None,
            "nullable": True,
            "schema": {
                "type": {"type": "string", "required": True},
                "traceback": {
                    "type": "list",
                    "schema": {"type": "string"},
                    "required": True,
                },
                "details": {
                    "type": "list",
                    "schema": {"type": "string"},
                    "required": True,
                },
            },
        },
        "progress": {"type": "integer", "required": True, "min": 0, "max": 100},
        "stage": {
            "type": "string",
            "required": True,
        },
        "step_name": {"type": "string", "default": None, "nullable": True},
        "step_description": {"type": "string", "default": None, "nullable": True},
        "state": {
            "type": "string",
            "allowed": [
                "waiting",
                "running",
                "complete",
                "cancelled",
                "error",
                "terminated",
            ],
            "required": True,
        },
    }
)
async def push_status(req):
    """Push a status update to a job."""
    data = req["data"]

    if data["state"] == "error" and not data["error"]:
        raise HTTPBadRequest(text="Missing error information")

    try:
        document = await get_data_from_req(req).jobs.push_status(
            req.match_info["job_id"],
            data["state"],
            data["stage"],
            data["step_name"],
            data["step_description"],
            data["error"],
            data["progress"],
        )
    except ResourceNotFoundError:
        raise NotFound
    except ResourceConflictError:
        raise HTTPConflict(text="Job is finished")

    return json_response(document["status"][-1], status=201)
{'fraction_non_alphanumeric': 0.11899839191362278, 'fraction_numerical': 0.008729611762003216, 'mean_word_length': 2.445910290237467, 'pattern_counts': {'":': 86, '<': 3, '<?xml version=': 0, '>': 8, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4291355', 'n_tokens_mistral': 3787, 'n_tokens_neox': 3589, 'n_words': 967}