diff --git a/dev-requirements.txt b/dev-requirements.txt index f1b5f1c..718bfe8 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile --constraint=requirements.txt --extra=dev --output-file=dev-requirements.txt pyproject.toml @@ -60,14 +60,9 @@ click-repl==0.3.0 colorama==0.4.6 # via # -c requirements.txt - # build - # click # omotes-rest (pyproject.toml) - # pytest coverage[toml]==7.4.4 # via pytest-cov -exceptiongroup==1.2.0 - # via pytest flake8==6.0.0 # via # flake8-docstrings @@ -87,6 +82,7 @@ flask==2.3.3 # flask-dotenv # flask-smorest # omotes-rest (pyproject.toml) + # types-flask-cors flask-cors==4.0.0 # via # -c requirements.txt @@ -164,10 +160,8 @@ multidict==6.0.5 # yarl mypy==1.5.1 # via - # -c requirements.txt # omotes-rest (pyproject.toml) # sqlalchemy - # sqlalchemy-stubs mypy-extensions==1.0.0 # via # -c requirements.txt @@ -252,10 +246,6 @@ sqlalchemy[mypy]==2.0.28 # via # -c requirements.txt # omotes-rest (pyproject.toml) -sqlalchemy-stubs==0.4 - # via - # -c requirements.txt - # omotes-rest (pyproject.toml) streamcapture==1.2.2 # via # -c requirements.txt @@ -264,27 +254,17 @@ structlog==23.1.0 # via # -c requirements.txt # omotes-rest (pyproject.toml) -toml==0.10.2 - # via setuptools-git-versioning tomli==2.0.1 - # via - # -c requirements.txt - # black - # build - # coverage - # flake8-pyproject - # mypy - # pyproject-hooks - # pytest + # via black +types-flask-cors==4.0.0.20240405 + # via omotes-rest (pyproject.toml) types-protobuf==4.24.0.20240311 # via omotes-rest (pyproject.toml) typing-extensions==4.7.1 # via # -c requirements.txt - # marshmallow-dataclass # mypy # sqlalchemy - # sqlalchemy-stubs # typing-inspect typing-inspect==0.9.0 # via diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index a6bf1f0..0000000 --- a/mypy.ini +++ /dev/null @@ -1,11 +0,0 @@ -[mypy] -python_version = 3.10 -warn_return_any = True -warn_unused_configs = True -show_error_codes = True -namespace_packages = True -mypy_path = './backend' -files = 'tno' - -[mypy-setuptools.*] -ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 3bd8e25..08994c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,6 @@ dependencies = [ "python-dotenv ~= 1.0.0", "structlog ~= 23.1.0", "SQLAlchemy == 2.0.28", - "sqlalchemy-stubs == 0.4", "omotes-sdk-python ~= 0.0.12", ] @@ -55,7 +54,8 @@ dev = [ "build ~= 1.0.3", "setuptools-git-versioning < 2", "sqlalchemy[mypy]", - "types-protobuf ~= 4.24.0" + "types-protobuf ~= 4.24.0", + "types-Flask-Cors" ] [project.urls] @@ -116,7 +116,7 @@ exclude = [ 'testmodel', 'tryouts.py', ] -plugins = ['sqlalchemy.ext.mypy.plugin'] +plugins = ['sqlalchemy.ext.mypy.plugin', "marshmallow_dataclass.mypy"] # mypy per-module options: [[tool.mypy.overrides]] @@ -125,5 +125,5 @@ check_untyped_defs = true ignore_missing_imports = true [[tool.mypy.overrides]] -module = "celery.*" +module = ["flask_smorest.*", "flask_dotenv.*", "gunicorn.*"] ignore_missing_imports = true diff --git a/requirements.old b/requirements.old deleted file mode 100644 index bcfc421..0000000 --- a/requirements.old +++ /dev/null @@ -1,231 +0,0 @@ -# -# This file is autogenerated by pip-compile with python 3.10 -# To update, run: -# -# pip-compile -# -alembic==1.7.5 - # via flask-migrate -apispec[marshmallow]==5.1.1 - # via flask-smorest -astroid==2.9.3 - # via - # pylint - # pylint-flask -black==21.12b0 - # via -r requirements.in -certifi==2021.10.8 - # via requests -charset-normalizer==2.0.10 - # via requests -click==8.0.3 - # via - # black - # flask - # pip-tools -colorama==0.4.4 - # via -r requirements.in -decorator==5.1.1 - # via validators -dnspython==2.1.0 - # via email-validator -email-validator==1.1.3 - # via wtforms-components -et-xmlfile==1.1.0 - # via openpyxl -flake8==4.0.1 - # via -r requirements.in -flask==2.0.2 - # via - # -r requirements.in - # flask-cors - # flask-dotenv - # flask-migrate - # flask-reverse-proxy-fix - # flask-smorest - # flask-sqlalchemy -flask-cors==3.0.10 - # via -r requirements.in -flask-dotenv==0.1.2 - # via -r requirements.in -flask-migrate==3.1.0 - # via -r requirements.in -flask-reverse-proxy-fix==0.2.1 - # via -r requirements.in -flask-smorest==0.35.0 - # via -r requirements.in -flask-sqlalchemy==2.5.1 - # via - # -r requirements.in - # flask-migrate - # pylint-flask-sqlalchemy -geoalchemy2==0.10.2 - # via -r requirements.in -gunicorn==20.1.0 - # via -r requirements.in -idna==3.3 - # via - # email-validator - # requests -infinity==1.5 - # via intervals -intervals==0.9.2 - # via wtforms-components -isort==5.10.1 - # via pylint -itsdangerous==2.0.1 - # via flask -jinja2==3.0.3 - # via flask -lazy-object-proxy==1.7.1 - # via astroid -mako==1.1.6 - # via alembic -markupsafe==2.0.1 - # via - # jinja2 - # mako - # wtforms - # wtforms-components -marshmallow==3.14.1 - # via - # -r requirements.in - # apispec - # flask-smorest - # marshmallow-dataclass - # marshmallow-enum - # marshmallow-sqlalchemy - # webargs -marshmallow-dataclass[enum]==8.5.3 - # via -r requirements.in -marshmallow-enum==1.5.1 - # via - # -r requirements.in - # marshmallow-dataclass -marshmallow-sqlalchemy==0.27.0 - # via -r requirements.in -mccabe==0.6.1 - # via - # flake8 - # pylint -mypy==0.931 - # via -r requirements.in -mypy-extensions==0.4.3 - # via - # black - # mypy - # typing-inspect -numpy==1.22.0 - # via - # -r requirements.in - # pandas -openpyxl==3.0.9 - # via -r requirements.in -packaging==21.3 - # via - # geoalchemy2 - # webargs -pandas==1.3.5 - # via -r requirements.in -pathspec==0.9.0 - # via black -pep517==0.12.0 - # via pip-tools -pip-tools==6.4.0 - # via -r requirements.in -platformdirs==2.4.1 - # via - # black - # pylint -psycopg2-binary==2.9.3 - # via -r requirements.in -pycodestyle==2.8.0 - # via flake8 -pyflakes==2.4.0 - # via flake8 -pylint==2.12.2 - # via - # pylint-flask - # pylint-flask-sqlalchemy - # pylint-plugin-utils -pylint-flask==0.6 - # via -r requirements.in -pylint-flask-sqlalchemy==0.2.0 - # via -r requirements.in -pylint-plugin-utils==0.7 - # via pylint-flask -pyparsing==3.0.6 - # via packaging -python-dateutil==2.8.2 - # via pandas -python-dotenv==0.19.2 - # via -r requirements.in -pytz==2021.3 - # via pandas -requests==2.27.1 - # via -r requirements.in -six==1.16.0 - # via - # flask-cors - # python-dateutil - # sqlalchemy-utils - # validators - # wtforms-components -sqlalchemy==1.3.23 - # via - # -r requirements.in - # alembic - # flask-sqlalchemy - # geoalchemy2 - # marshmallow-sqlalchemy - # sqlalchemy-serializer - # sqlalchemy-utils - # wtforms-alchemy -sqlalchemy-serializer==1.4.1 - # via -r requirements.in -sqlalchemy-utils==0.38.2 - # via - # -r requirements.in - # wtforms-alchemy -structlog==21.5.0 - # via -r requirements.in -toml==0.10.2 - # via pylint -tomli==1.2.3 - # via - # black - # mypy - # pep517 -typing-extensions==4.0.1 - # via - # black - # mypy - # typing-inspect -typing-inspect==0.7.1 - # via marshmallow-dataclass -urllib3==1.26.8 - # via requests -validators==0.18.2 - # via wtforms-components -webargs==8.1.0 - # via flask-smorest -werkzeug==2.0.2 - # via - # flask - # flask-smorest -wheel==0.37.1 - # via pip-tools -wrapt==1.13.3 - # via astroid -wtforms==3.0.1 - # via - # wtforms-alchemy - # wtforms-components -wtforms-alchemy==0.18.0 - # via -r requirements.in -wtforms-components==0.10.5 - # via wtforms-alchemy - -# The following packages are considered to be unsafe in a requirements file: -# pip -# setuptools diff --git a/requirements.txt b/requirements.txt index 91ae0dd..ffea1ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # -# This file is autogenerated by pip-compile with Python 3.10 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --output-file=requirements.txt ./pyproject.toml +# pip-compile --output-file=requirements.txt pyproject.toml # aio-pika==9.3.1 # via omotes-sdk-python @@ -32,9 +32,7 @@ click-plugins==1.1.1 click-repl==0.3.0 # via celery colorama==0.4.6 - # via - # click - # omotes-rest (pyproject.toml) + # via omotes-rest (pyproject.toml) flask==2.3.3 # via # flask-cors @@ -82,12 +80,8 @@ marshmallow-enum==1.5.1 # via omotes-rest (pyproject.toml) multidict==6.0.5 # via yarl -mypy==1.5.1 - # via sqlalchemy-stubs mypy-extensions==1.0.0 - # via - # mypy - # typing-inspect + # via typing-inspect omotes-sdk-protocol==0.0.8 # via omotes-sdk-python omotes-sdk-python==0.0.12 @@ -116,20 +110,13 @@ six==1.16.0 # via python-dateutil sqlalchemy==2.0.28 # via omotes-rest (pyproject.toml) -sqlalchemy-stubs==0.4 - # via omotes-rest (pyproject.toml) streamcapture==1.2.2 # via omotes-sdk-python structlog==23.1.0 # via omotes-rest (pyproject.toml) -tomli==2.0.1 - # via mypy typing-extensions==4.7.1 # via - # marshmallow-dataclass - # mypy # sqlalchemy - # sqlalchemy-stubs # typing-inspect typing-inspect==0.9.0 # via marshmallow-dataclass diff --git a/src/omotes_rest/__init__.py b/src/omotes_rest/__init__.py index f0c590d..d3602dd 100644 --- a/src/omotes_rest/__init__.py +++ b/src/omotes_rest/__init__.py @@ -22,22 +22,22 @@ env = DotEnv() -def create_app(object_name): +def create_app(object_name: str) -> Flask: """Create Flask app. - An flask application factory, as explained here: + A flask application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/ - Arguments: - object_name: the python path of the config object, - e.g. influxdbgraphs.api.settings.ProdConfig + :param object_name: the python path of the config object, e.g. + influxdbgraphs.api.settings.ProdConfig + :return: The initalised Flask app. """ logger = logging.getLogger("omotes_rest") logger.info("Setting up app.") app = Flask(__name__) app.config.from_object(object_name) - app.wsgi_app = ProxyFix(app.wsgi_app) + app.wsgi_app = ProxyFix(app.wsgi_app) # type: ignore[method-assign] env.init_app(app) api.init_app(app) diff --git a/src/omotes_rest/apis/api_dataclasses.py b/src/omotes_rest/apis/api_dataclasses.py index 185804a..20e13cb 100644 --- a/src/omotes_rest/apis/api_dataclasses.py +++ b/src/omotes_rest/apis/api_dataclasses.py @@ -2,10 +2,10 @@ from dataclasses import field from datetime import datetime from enum import Enum -from typing import Any +from typing import Any, ClassVar, Type -from marshmallow_dataclass import dataclass -from marshmallow_dataclass import add_schema +from marshmallow import Schema +from marshmallow_dataclass import add_schema, dataclass class JobRestStatus(Enum): @@ -32,13 +32,15 @@ class JobRestStatus(Enum): class JobInput: """Input needed to start a new job.""" + Schema: ClassVar[Type[Schema]] = Schema + job_name: str = "job name" workflow_type: str = "grow_optimizer" user_name: str = "user name" input_esdl: str = "input ESDL base64string" project_name: str = "project name" - input_params_dict: dict[str, Any] | None = field(default_factory=dict) - timeout_after_s: int | None = 3600 + input_params_dict: dict[str, Any] = field(default_factory=dict) + timeout_after_s: int = 3600 @add_schema @@ -46,6 +48,8 @@ class JobInput: class JobStatusResponse: """Response with job status.""" + Schema: ClassVar[Type[Schema]] = Schema + job_id: uuid.UUID status: JobRestStatus @@ -55,6 +59,8 @@ class JobStatusResponse: class JobResultResponse: """Response with job result.""" + Schema: ClassVar[Type[Schema]] = Schema + job_id: uuid.UUID output_esdl: str | None @@ -64,6 +70,8 @@ class JobResultResponse: class JobDeleteResponse: """Response for job deletion.""" + Schema: ClassVar[Type[Schema]] = Schema + job_id: uuid.UUID deleted: bool @@ -73,6 +81,8 @@ class JobDeleteResponse: class JobCancelResponse: """Response for job cancellation.""" + Schema: ClassVar[Type[Schema]] = Schema + job_id: uuid.UUID cancelled: bool @@ -82,6 +92,8 @@ class JobCancelResponse: class JobLogsResponse: """Response with job logs.""" + Schema: ClassVar[Type[Schema]] = Schema + job_id: uuid.UUID logs: str | None @@ -91,6 +103,8 @@ class JobLogsResponse: class JobResponse: """Response with all job data.""" + Schema: ClassVar[Type[Schema]] = Schema + job_id: uuid.UUID job_name: str workflow_type: str @@ -115,6 +129,8 @@ class JobResponse: class JobSummary: """Response with job summary used in job lists.""" + Schema: ClassVar[Type[Schema]] = Schema + job_id: uuid.UUID job_name: str workflow_type: str diff --git a/src/omotes_rest/apis/job.py b/src/omotes_rest/apis/job.py index 4d43da0..f9f8900 100644 --- a/src/omotes_rest/apis/job.py +++ b/src/omotes_rest/apis/job.py @@ -1,9 +1,12 @@ import base64 +import uuid +from typing import cast -from flask import current_app +from flask import current_app as flask_app, Flask, Response from flask_smorest import Blueprint from flask.views import MethodView +from omotes_rest import RestInterface from omotes_rest.apis.api_dataclasses import ( JobInput, JobResponse, @@ -16,6 +19,8 @@ ) import logging +from omotes_rest.db_models.job_rest import JobRest + logger = logging.getLogger("omotes_rest") api = Blueprint( @@ -26,18 +31,27 @@ ) +class OmotesRestApp(Flask): + """Type-complete description with extensions of the Flask app.""" + + rest_if: RestInterface + + +current_app = cast(OmotesRestApp, flask_app) + + @api.route("/") class JobAPI(MethodView): """Requests.""" @api.arguments(JobInput.Schema()) @api.response(200, JobStatusResponse.Schema()) - def post(self, job_input: JobInput): + def post(self, job_input: JobInput) -> JobStatusResponse: """Start new job: 'input_params_dict' can have lists and (nested) dicts as values.""" return current_app.rest_if.submit_job(job_input) @api.response(200, JobSummary.Schema(many=True)) - def get(self): + def get(self) -> list[JobRest]: """Return a summary of all jobs.""" return current_app.rest_if.get_jobs() @@ -47,14 +61,15 @@ class JobFromIdAPI(MethodView): """Requests.""" @api.response(200, JobResponse.Schema()) - def get(self, job_id: str): + def get(self, job_id: str) -> JobRest | None: """Return job details.""" - return current_app.rest_if.get_job(job_id) + return current_app.rest_if.get_job(uuid.UUID(job_id)) @api.response(200, JobDeleteResponse.Schema()) - def delete(self, job_id: str): + def delete(self, job_id: str) -> JobDeleteResponse: """Delete job, and cancel if queued or running.""" - return JobDeleteResponse(job_id=job_id, deleted=current_app.rest_if.delete_job(job_id)) + job_uuid = uuid.UUID(job_id) + return JobDeleteResponse(job_id=job_uuid, deleted=current_app.rest_if.delete_job(job_uuid)) @api.route("//cancel") @@ -62,9 +77,11 @@ class JobCancelAPI(MethodView): """Requests.""" @api.response(200, JobCancelResponse.Schema()) - def get(self, job_id: str): + def get(self, job_id: str) -> JobCancelResponse: """Cancel job if queued or running.""" - return JobCancelResponse(job_id=job_id, cancelled=current_app.rest_if.cancel_job(job_id)) + job_uuid = uuid.UUID(job_id) + return JobCancelResponse(job_id=job_uuid, + cancelled=current_app.rest_if.cancel_job(job_uuid)) @api.route("//status") @@ -72,9 +89,17 @@ class JobStatusAPI(MethodView): """Requests.""" @api.response(200, JobStatusResponse.Schema()) - def get(self, job_id: str): + def get(self, job_id: str) -> JobStatusResponse | Response: """Return job status.""" - return JobStatusResponse(job_id=job_id, status=current_app.rest_if.get_job_status(job_id)) + job_uuid = uuid.UUID(job_id) + status = current_app.rest_if.get_job_status(job_uuid) + + result: JobStatusResponse | Response + if status: + result = JobStatusResponse(job_id=job_uuid, status=status) + else: + result = Response(status=404, response=f'Unknown job {job_id}.') + return result @api.route("//result") @@ -82,12 +107,13 @@ class JobResultAPI(MethodView): """Requests.""" @api.response(200, JobResultResponse.Schema()) - def get(self, job_id: int): + def get(self, job_id: str) -> JobResultResponse: """Return job result with output ESDL (can be None).""" - output_esdl = current_app.rest_if.get_job_output_esdl(job_id) + job_uuid = uuid.UUID(job_id) + output_esdl = current_app.rest_if.get_job_output_esdl(job_uuid) if output_esdl: output_esdl = base64.b64encode(bytes(output_esdl, "utf-8")).decode("utf-8") - return JobResultResponse(job_id=job_id, output_esdl=output_esdl) + return JobResultResponse(job_id=job_uuid, output_esdl=output_esdl) @api.route("//logs") @@ -95,12 +121,13 @@ class JobLogsAPI(MethodView): """Requests.""" @api.response(200, JobLogsResponse.Schema()) - def get(self, job_id: int): + def get(self, job_id: str) -> JobLogsResponse: """Return job logs.""" - logs = current_app.rest_if.get_job_logs(job_id) + job_uuid = uuid.UUID(job_id) + logs = current_app.rest_if.get_job_logs(job_uuid) if not logs: logs = "No logs received for this job." - return JobLogsResponse(job_id=job_id, logs=logs) + return JobLogsResponse(job_id=job_uuid, logs=logs) @api.route("/user/") @@ -108,7 +135,7 @@ class JobsByUserAPI(MethodView): """Requests.""" @api.response(200, JobSummary.Schema(many=True)) - def get(self, user_name: str): + def get(self, user_name: str) -> list[JobRest]: """Return all jobs from user.""" return current_app.rest_if.get_jobs_from_user(user_name) @@ -118,6 +145,6 @@ class JobByProjectAPI(MethodView): """Requests.""" @api.response(200, JobSummary.Schema(many=True)) - def get(self, project_name: str): + def get(self, project_name: str) -> list[JobRest]: """Return all jobs from project.""" return current_app.rest_if.get_jobs_from_project(project_name) diff --git a/src/omotes_rest/db_models/job_rest.py b/src/omotes_rest/db_models/job_rest.py index 8ac9e98..e76212e 100644 --- a/src/omotes_rest/db_models/job_rest.py +++ b/src/omotes_rest/db_models/job_rest.py @@ -3,6 +3,7 @@ from datetime import datetime import sqlalchemy as db +from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.dialects.postgresql import UUID @@ -25,7 +26,7 @@ class JobRest(Base): """Name of the workflow this job runs.""" status: JobRestStatus = db.Column(db.Enum(JobRestStatus), nullable=False) """Last received status of the job.""" - progress_fraction: float = db.Column(db.Float, nullable=False) + progress_fraction: Mapped[float] = mapped_column(db.Float, nullable=False) """Last received progress (fraction) of the job.""" progress_message: str = db.Column(db.String, nullable=False) """Last received progress (fraction) of the job.""" diff --git a/src/omotes_rest/main.py b/src/omotes_rest/main.py index 4847b9b..97fe732 100644 --- a/src/omotes_rest/main.py +++ b/src/omotes_rest/main.py @@ -1,10 +1,16 @@ import json +from os import PathLike from time import strftime +from typing import cast -from flask import request, send_from_directory, current_app +from flask import request, send_from_directory, current_app as flask_app, Response as FlaskResponse from werkzeug.exceptions import HTTPException +from werkzeug.wrappers.response import Response as WerkzeugResponse +from gunicorn.arbiter import Arbiter +from gunicorn.workers.sync import SyncWorker from omotes_rest import create_app +from omotes_rest.apis.job import OmotesRestApp from omotes_rest.rest_interface import RestInterface from omotes_rest.settings import EnvSettings import logging @@ -16,20 +22,23 @@ """Flask application.""" app = create_app("omotes_rest.settings.%sConfig" % EnvSettings.env().capitalize()) +current_app = cast(OmotesRestApp, flask_app) + @app.before_request -def before_request(): +def before_request() -> None: """Log before request.""" timestamp = strftime("[%Y-%b-%d %H:%M]") + logger.debug( f"Request, timestamp '{timestamp}', remote_addr '{request.remote_addr}'," f" method '{request.method}', scheme '{request.scheme}', full_path '{request.full_path}," - f" 'payload '{request.get_data()}', 'headers '{request.headers}'") + f" 'payload '{request.get_data()!r}', 'headers '{request.headers}'") # return response @app.after_request -def after_request(response): +def after_request(response: FlaskResponse) -> FlaskResponse: """Log after request.""" timestamp = strftime("[%Y-%b-%d %H:%M]") logger.debug( @@ -40,16 +49,16 @@ def after_request(response): @app.route("/") -def serve_static(path): +def serve_static(path: PathLike | str) -> FlaskResponse: """Serve static.""" return send_from_directory("static", path) @app.errorhandler(HTTPException) -def handle_exception(e): +def handle_exception(e: HTTPException) -> WerkzeugResponse: """Return JSON instead of HTML for HTTP errors.""" response = e.get_response() - response.data = json.dumps( + data = json.dumps( { "code": e.code, "name": e.name, @@ -57,14 +66,19 @@ def handle_exception(e): } ) response.content_type = "application/json" - return response + return WerkzeugResponse(response=data, + status=response.status_code, + headers=response.headers, mimetype=response.mimetype, + content_type=response.content_type) @app.errorhandler(Exception) -def handle_500(e): +def handle_500(e: Exception) -> tuple[str, int]: """Handle exceptions.""" logger.exception(f"Unhandled exception occurred {str(e)}") - return json.dumps({"message": "Internal Server Error"}), 500 + return json.dumps({ + "message": "Internal Server Error" + }), 500 # TODO to be retrieved via de omotes_sdk in the future @@ -95,7 +109,7 @@ def handle_500(e): ) -def post_fork(_, __): +def post_fork(_: Arbiter, __: SyncWorker) -> None: """Called just after a worker has been forked.""" with app.app_context(): """current_app is only within the app context""" diff --git a/src/omotes_rest/postgres_interface.py b/src/omotes_rest/postgres_interface.py index 7649ac8..f79d6d4 100644 --- a/src/omotes_rest/postgres_interface.py +++ b/src/omotes_rest/postgres_interface.py @@ -1,4 +1,4 @@ -from uuid import uuid4 +import uuid from contextlib import contextmanager from datetime import datetime from typing import Generator @@ -129,7 +129,7 @@ def stop(self) -> None: def put_new_job( self, - job_id: uuid4, + job_id: uuid.UUID, job_input: JobInput, esdl_input: str, ) -> None: @@ -159,7 +159,7 @@ def put_new_job( session.add(new_job) logger.debug("Job %s is submitted as new job in database", job_id) - def set_job_registered(self, job_id: uuid4) -> None: + def set_job_registered(self, job_id: uuid.UUID) -> None: """Set the status of the job to 'REGISTERED'. :param job_id: Job id. @@ -176,7 +176,7 @@ def set_job_registered(self, job_id: uuid4) -> None: ) session.execute(stmnt) - def set_job_enqueued(self, job_id: uuid4) -> None: + def set_job_enqueued(self, job_id: uuid.UUID) -> None: """Set the status of the job to 'ENQUEUED'. :param job_id: Job id. @@ -193,7 +193,7 @@ def set_job_enqueued(self, job_id: uuid4) -> None: ) session.execute(stmnt) - def set_job_running(self, job_id: uuid4) -> None: + def set_job_running(self, job_id: uuid.UUID) -> None: """Set the status of the job to 'RUNNING'. :param job_id: Job id. @@ -210,8 +210,8 @@ def set_job_running(self, job_id: uuid4) -> None: ) session.execute(stmnt) - def set_job_stopped(self, job_id: uuid4, new_status: JobRestStatus, logs: str = None, - output_esdl: str = None) -> None: + def set_job_stopped(self, job_id: uuid.UUID, new_status: JobRestStatus, logs: str | None = None, + output_esdl: str | None = None) -> None: """Set the job to stopped with supplied status. :param job_id: Job id. @@ -231,7 +231,7 @@ def set_job_stopped(self, job_id: uuid4, new_status: JobRestStatus, logs: str = ) session.execute(stmnt) - def set_job_progress(self, job_id: uuid4, progress_fraction: float, + def set_job_progress(self, job_id: uuid.UUID, progress_fraction: float, progress_message: str) -> None: """Set the status of the job to RUNNING. @@ -251,7 +251,7 @@ def set_job_progress(self, job_id: uuid4, progress_fraction: float, ) session.execute(stmnt) - def get_job_status(self, job_id: uuid4) -> JobRestStatus | None: + def get_job_status(self, job_id: uuid.UUID) -> JobRestStatus | None: """Retrieve the current job status. :param job_id: Job id. @@ -263,7 +263,7 @@ def get_job_status(self, job_id: uuid4) -> JobRestStatus | None: job_status = session.scalar(stmnt) return job_status - def get_job(self, job_id: uuid4) -> JobRest | None: + def get_job(self, job_id: uuid.UUID) -> JobRest | None: """Retrieve the job info from the database. :param job_id: Job id. @@ -275,7 +275,7 @@ def get_job(self, job_id: uuid4) -> JobRest | None: job = session.scalar(stmnt) return job - def delete_job(self, job_id: uuid4) -> bool: + def delete_job(self, job_id: uuid.UUID) -> bool: """Remove the job from the database. :param job_id: Job id. @@ -294,7 +294,7 @@ def delete_job(self, job_id: uuid4) -> bool: return job_deleted - def get_jobs(self, job_ids: list[uuid4] | None = None) -> list[JobRest]: + def get_jobs(self, job_ids: list[uuid.UUID] | None = None) -> list[JobRest]: """Retrieve a list of the jobs. :param job_ids: Optional list of uuid's to select specific jobs, default is all jobs. @@ -311,10 +311,10 @@ def get_jobs(self, job_ids: list[uuid4] | None = None) -> list[JobRest]: else: logger.debug("Retrieving job data for all jobs") - jobs = session.scalars(stmnt).all() + jobs = list(session.scalars(stmnt).all()) return jobs - def get_job_output_esdl(self, job_id: uuid4) -> str | None: + def get_job_output_esdl(self, job_id: uuid.UUID) -> str | None: """Retrieve the output ESDL of a job. :param job_id: Job id. @@ -323,10 +323,10 @@ def get_job_output_esdl(self, job_id: uuid4) -> str | None: logger.debug("Retrieving job output esdl for job with id '%s'", job_id) with session_scope() as session: stmnt = select(JobRest.output_esdl).where(JobRest.job_id == job_id) - job_output_esdl: JobRest = session.scalar(stmnt) + job_output_esdl: str | None = session.scalar(stmnt) return job_output_esdl - def get_job_logs(self, job_id: uuid4) -> str: + def get_job_logs(self, job_id: uuid.UUID) -> str | None: """Retrieve the logs of a job. :param job_id: Job id. @@ -335,7 +335,7 @@ def get_job_logs(self, job_id: uuid4) -> str: logger.debug("Retrieving job log for job with id '%s'", job_id) with session_scope() as session: stmnt = select(JobRest.logs).where(JobRest.job_id == job_id) - job_logs: JobRest = session.scalar(stmnt) + job_logs: str | None = session.scalar(stmnt) return job_logs def get_jobs_from_user(self, user_name: str) -> list[JobRest]: @@ -347,7 +347,7 @@ def get_jobs_from_user(self, user_name: str) -> list[JobRest]: logger.debug(f"Retrieving job data for jobs from user '{user_name}'") with session_scope(do_expunge=True) as session: stmnt = SELECT_JOB_SUMMARY_STMT.where(JobRest.user_name == user_name) - jobs = session.scalars(stmnt).all() + jobs = list(session.scalars(stmnt).all()) return jobs def get_jobs_from_project(self, project_name: str) -> list[JobRest]: @@ -359,5 +359,5 @@ def get_jobs_from_project(self, project_name: str) -> list[JobRest]: logger.debug(f"Retrieving job data for jobs from project '{project_name}'") with session_scope(do_expunge=True) as session: stmnt = SELECT_JOB_SUMMARY_STMT.where(JobRest.project_name == project_name) - jobs = session.scalars(stmnt).all() + jobs = list(session.scalars(stmnt).all()) return jobs diff --git a/src/omotes_rest/rest_interface.py b/src/omotes_rest/rest_interface.py index 71c5f86..f7d4f50 100644 --- a/src/omotes_rest/rest_interface.py +++ b/src/omotes_rest/rest_interface.py @@ -1,6 +1,6 @@ import base64 +import uuid from datetime import timedelta -from uuid import uuid4 from omotes_sdk.omotes_interface import OmotesInterface from omotes_sdk.internal.common.config import EnvRabbitMQConfig @@ -137,7 +137,7 @@ def submit_job(self, job_input: JobInput) -> JobStatusResponse: ) return JobStatusResponse(job_id=job.id, status=JobRestStatus.REGISTERED) - def get_job(self, job_id: uuid4) -> JobRest | None: + def get_job(self, job_id: uuid.UUID) -> JobRest | None: """Get job by id. :param job_id: Job id. @@ -152,23 +152,29 @@ def get_jobs(self) -> list[JobRest]: """ return self.postgres_if.get_jobs() - def cancel_job(self, job_id: uuid4) -> bool: + def cancel_job(self, job_id: uuid.UUID) -> bool: """Cancel job by id. :param job_id: Job id. :return: True if job found and cancelled. """ - job = Job( - id=job_id, - workflow_type=WorkflowType( - workflow_type_name=self.get_job(job_id).workflow_type, - workflow_type_description_name="some descr" + job_in_db = self.get_job(job_id) + + if job_in_db: + job = Job( + id=job_id, + workflow_type=WorkflowType( + workflow_type_name=job_in_db.workflow_type, + workflow_type_description_name="some descr" + ) ) - ) - self.omotes_if.cancel_job(job) - return True + self.omotes_if.cancel_job(job) + result = True + else: + result = False + return result - def delete_job(self, job_id: uuid4) -> bool: + def delete_job(self, job_id: uuid.UUID) -> bool: """Delete job by id. :param job_id: Job id. @@ -177,7 +183,7 @@ def delete_job(self, job_id: uuid4) -> bool: self.cancel_job(job_id) return self.postgres_if.delete_job(job_id) - def get_job_status(self, job_id: uuid4) -> JobRestStatus | None: + def get_job_status(self, job_id: uuid.UUID) -> JobRestStatus | None: """Get job status by id. :param job_id: Job id. @@ -185,7 +191,7 @@ def get_job_status(self, job_id: uuid4) -> JobRestStatus | None: """ return self.postgres_if.get_job_status(job_id) - def get_job_output_esdl(self, job_id: uuid4) -> str | None: + def get_job_output_esdl(self, job_id: uuid.UUID) -> str | None: """Get job output ESDL by id. :param job_id: Job id. @@ -193,7 +199,7 @@ def get_job_output_esdl(self, job_id: uuid4) -> str | None: """ return self.postgres_if.get_job_output_esdl(job_id) - def get_job_logs(self, job_id: uuid4) -> str | None: + def get_job_logs(self, job_id: uuid.UUID) -> str | None: """Get job logs by id. :param job_id: Job id. diff --git a/src/omotes_rest/settings.py b/src/omotes_rest/settings.py index ca3cc38..2ab4f87 100644 --- a/src/omotes_rest/settings.py +++ b/src/omotes_rest/settings.py @@ -25,7 +25,7 @@ def flask_server_port() -> int: return 9200 @staticmethod - def is_production(): + def is_production() -> bool: """Check if production.""" return EnvSettings.env() == "prod"