Source code for pyfarm.master.api.jobtypes

# No shebang line, this module is meant to be imported
#
# Copyright 2013 Oliver Palmer
# Copyright 2014 Ambient Entertainment GmbH & Co. KG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Jobtypes
--------

This module defines an API for managing and querying jobtypes
"""

try:
    from httplib import (
        OK, CREATED, CONFLICT, NOT_FOUND, BAD_REQUEST, NO_CONTENT,
        METHOD_NOT_ALLOWED)
except ImportError:  # pragma: no cover
    from http.client import (
        OK, CREATED, CONFLICT, NOT_FOUND, BAD_REQUEST, NO_CONTENT,
        METHOD_NOT_ALLOWED)

from flask import g, Response
from flask.views import MethodView

from sqlalchemy import or_, func, sql

from pyfarm.core.logger import getLogger
from pyfarm.core.enums import STRING_TYPES, PY3
from pyfarm.models.software import (
    Software, SoftwareVersion, JobTypeSoftwareRequirement)
from pyfarm.models.jobtype import JobType, JobTypeVersion
from pyfarm.master.application import db
from pyfarm.master.utility import jsonify

logger = getLogger("api.jobtypes")


[docs]class ObjectNotFound(Exception): pass
[docs]def parse_requirements(requirements): """ Takes a list dicts specifying a software and optional min- and max-versions and returns a list of :class:`JobRequirement` objects. Raises TypeError if the input was not as expected or ObjectNotFound if a referenced software of or version was not found. :param list requirements: A list of of dicts specifying a software and optionally min_version and/or max_version. :raises TypeError: Raised if ``requirements`` is not a list or if an entry in ``requirements`` is not a dictionary. :raises ValueError: Raised if there's a problem with the content of at least one of the requirement dictionaries. :raises ObjectNotFound: Raised if the referenced software or version was not found """ if not isinstance(requirements, list): raise TypeError("software_requirements must be a list") out = [] for entry in requirements: if not isinstance(entry, dict): raise TypeError("Every software_requirement must be a dict") requirement = JobTypeSoftwareRequirement() software_name = entry.pop("software", None) if software_name is None: raise ValueError("Software requirement does not specify a software.") software = Software.query.filter_by(software=software_name).first() if not software: raise ObjectNotFound("Software %s not found" % software_name) requirement.software = software min_version_str = entry.pop("min_version", None) if min_version_str is not None: min_version = SoftwareVersion.query.filter( SoftwareVersion.software == software, SoftwareVersion.version == min_version_str).first() if not min_version: raise ObjectNotFound("Version %s of software %s not found" % (software_name, min_version_str)) requirement.min_version = min_version max_version_str = entry.pop("max_version", None) if max_version_str is not None: max_version = SoftwareVersion.query.filter( SoftwareVersion.software == software, SoftwareVersion.version == max_version_str).first() if not max_version: raise ObjectNotFound("Version %s of software %s not found" % (software_name, max_version_str)) requirement.max_version = max_version if entry: raise ValueError("Unexpected keys in software requirement: %r" % entry.keys()) out.append(requirement) return out
[docs]def schema(): """ Returns the basic schema of :class:`.JobType` .. http:get:: /api/v1/jobtypes/schema HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/jobtypes/schema HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "batch_contiguous": "BOOLEAN", "classname": "VARCHAR(64)", "code": "TEXT", "description": "TEXT", "id": "INTEGER", "version": "INTEGER", "max_batch": "INTEGER", "no_automatic_start_time": "INTEGER", "name": "VARCHAR(64)" } :statuscode 200: no error """ schema_dict = JobTypeVersion.to_schema() schema_dict.update(JobType.to_schema()) return jsonify(schema_dict), OK
[docs]class JobTypeIndexAPI(MethodView):
[docs] def post(self): """ A ``POST`` to this endpoint will create a new jobtype. .. http:post:: /api/v1/jobtypes/ HTTP/1.1 **Request** .. sourcecode:: http POST /api/v1/jobtypes/ HTTP/1.1 Accept: application/json { "name": "TestJobType", "classname": "TestJobType", "description": "Jobtype for testing inserts and queries", "code": "\\nfrom pyfarm.jobtypes.core.jobtype import " "JobType\\n\\nclass TestJobType(JobType):\\n" " def get_command(self):\\n" " return \"/usr/bin/touch\"\\n\\n" " def get_arguments(self):\\n" " return [os.path.join(" "self.assignment_data[\"job\"][\"data\"][\"path\"], " "\"%04d\" % self.assignment_data[\"tasks\"]" "[0][\"frame\"])]\\n" } **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "id": 1, "batch_contiguous": true, "software_requirements": [], "version": 1, "max_batch": 1, "name": "TestJobType", "classname": "TestJobType", "description": "Jobtype for testing inserts and queries", "code": "\\nfrom pyfarm.jobtypes.core.jobtype import " "JobType\\n\\nclass TestJobType(JobType):\\n" " def get_command(self):\\n" " return \"/usr/bin/touch\"\\n\\n" " def get_arguments(self):\\n" " return [os.path.join(" "self.assignment_data[\"job\"][\"data\"][\"path\"], " "\"%04d\" % self.assignment_data[\"tasks\"]" "[0][\"frame\"])]\\n" } :statuscode 201: a new jobtype item was created :statuscode 400: there was something wrong with the request (such as invalid columns being included) :statuscode 409: a conflicting jobtype already exists """ if "name" not in g.json: return jsonify(error="Jobtype does not specify a name"), BAD_REQUEST jobtype = JobType.query.filter_by(name=g.json["name"]).first() if jobtype: return (jsonify(error="Jobtype %s already exixts" % g.json["name"]), CONFLICT) try: jobtype = JobType() jobtype.name = g.json.pop("name") jobtype.description = g.json.pop("description", None) jobtype_version = JobTypeVersion() jobtype_version.jobtype = jobtype jobtype_version.version = 1 jobtype_version.code = g.json.pop("code") jobtype_version.classname = g.json.pop("classname", None) jobtype_version.batch_contiguous = g.json.pop("batch_contiguous", None) jobtype_version.no_automatic_start_time = g.json.pop( "no_automatic_start_time", None) if "max_batch" in g.json and g.json["max_batch"] is None: g.json.pop("max_batch") jobtype_version.max_batch = sql.null() else: jobtype_version.max_batch = g.json.pop("max_batch", None) except KeyError as e: return (jsonify(error="Missing key in input: %r" % e.args), BAD_REQUEST) if "software_requirements" in g.json: try: for r in parse_requirements(g.json["software_requirements"]): r.jobtype_version = jobtype_version db.session.add(r) except (TypeError, ValueError) as e: return jsonify(error=e.args), BAD_REQUEST except ObjectNotFound as e: return jsonify(error=e.args), NOT_FOUND del g.json["software_requirements"] if g.json: return (jsonify(error="Unexpected keys in input: %r" % g.json.keys()), BAD_REQUEST) db.session.add_all([jobtype, jobtype_version]) db.session.commit() jobtype_data = jobtype_version.to_dict(unpack_relationships=False) jobtype_data.update(jobtype.to_dict( unpack_relationships=["software_requirements"])) del jobtype_data["jobtype_id"] logger.info("created jobtype %s: %r", jobtype.name, jobtype_data) return jsonify(jobtype_data), CREATED
[docs] def get(self): """ A ``GET`` to this endpoint will return a list of registered jobtypes. .. http:get:: /api/v1/jobtypes/ HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/jobtypes/ HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json [ { "id": 1, "name": "TestJobType" } ] :statuscode 200: no error """ out = [] q = db.session.query(JobType.id, JobType.name) for id, name in q: out.append({"id": id, "name": name}) return jsonify(out), OK
[docs]class SingleJobTypeAPI(MethodView):
[docs] def get(self, jobtype_name): """ A ``GET`` to this endpoint will return the most recent version of the referenced jobtype, by name or id. .. http:get:: /api/v1/jobtypes/<str:tagname> HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/jobtypes/TestJobType HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "batch_contiguous": true, "classname": null, "code": "\\nfrom pyfarm.jobtypes.core.jobtype import " "JobType\\n\\nclass TestJobType(JobType):\\n" " def get_command(self):\\n" " return \"/usr/bin/touch\"\\n\\n" " def get_arguments(self):\\n" " return [os.path.join(" "self.assignment_data[\"job\"][\"data\"][\"path\"], " "\"%04d\" % self.assignment_data[\"tasks\"]" "[0][\"frame\"])]\\n", "id": 1, "version": 1, "max_batch": 1, "name": "TestJobType", "software_requirements": [ { "max_version": null, "max_version_id": null, "min_version": "8.21", "min_version_id": 1, "software": "/bin/touch", "software_id": 1 } ] } :statuscode 200: no error :statuscode 404: jobtype or version not found """ if isinstance(jobtype_name, STRING_TYPES): jobtype = JobType.query.filter(JobType.name == jobtype_name).first() else: jobtype = JobType.query.filter_by(id=jobtype_name).first() jobtype_version = JobTypeVersion.query.filter_by( jobtype=jobtype).order_by("version desc").first() if not jobtype or not jobtype_version: return (jsonify(error="JobType %s not found" % jobtype_name), NOT_FOUND) # For some reason, sqlalchemy sometimes returns this column as bytes # instead of string. jsonify cannot decode that. if PY3 and isinstance(jobtype_version.code, bytes): # pragma: no cover jobtype_version.code = jobtype_version.code.decode() jobtype_data = jobtype_version.to_dict( unpack_relationships=["software_requirements"]) jobtype_data.update(jobtype.to_dict(unpack_relationships=False)) del jobtype_data["jobtype_id"] return jsonify(jobtype_data), OK
[docs] def put(self, jobtype_name): """ A ``PUT`` to this endpoint will create a new jobtype under the given URI. If a jobtype already exists under that URI, a new version will be created with the given data. You should only call this by id for updating an existing jobtype or if you have a reserved jobtype id. There is currently no way to reserve a jobtype id. .. http:put:: /api/v1/jobtypes/[<str:name>|<int:id>] HTTP/1.1 **Request** .. sourcecode:: http PUT /api/v1/jobtypes/TestJobType HTTP/1.1 Accept: application/json { "name": "TestJobType", "description": "Jobtype for testing inserts and queries", "code": "\\nfrom pyfarm.jobtypes.core.jobtype import " "JobType\\n\\nclass TestJobType(JobType):\\n" " def get_command(self):\\n" " return \"/usr/bin/touch\"\\n\\n" " def get_arguments(self):\\n" " return [os.path.join(" "self.assignment_data[\"job\"][\"data\"][\"path\"], " "\"%04d\" % self.assignment_data[\"tasks\"]" "[0][\"frame\"])]\\n" } **Response** .. sourcecode:: http HTTP/1.1 201 CREATED Content-Type: application/json { "batch_contiguous": true, "classname": null, "code": "\\nfrom pyfarm.jobtypes.core.jobtype import " "JobType\\n\\nclass TestJobType(JobType):\\n" " def get_command(self):\\n" " return \"/usr/bin/touch\"\\n\\n" " def get_arguments(self):\\n" " return [os.path.join(" "self.assignment_data[\"job\"][\"data\"][\"path\"], " "\"%04d\" % self.assignment_data[\"tasks\"]" "[0][\"frame\"])]\\n", "id": 1, "max_batch": 1, "name": "TestJobType", "description": "Jobtype for testing inserts and queries", "software_requirements": [] } :statuscode 201: a new jobtype was created :statuscode 400: there was something wrong with the request (such as invalid columns being included) """ if isinstance(jobtype_name, STRING_TYPES): jobtype = JobType.query.filter(JobType.name == jobtype_name).first() else: jobtype = JobType.query.filter_by(id=jobtype_name).first() max_version = None new = False if jobtype else True if jobtype: logger.debug( "jobtype %s will get a new version with data %r on commit", jobtype.name, g.json) max_version, = db.session.query(func.max( JobTypeVersion.version)).filter_by(jobtype=jobtype).first() else: jobtype = JobType() if max_version is not None: version = max_version + 1 else: version = 1 try: jobtype.name = g.json.pop("name") jobtype.description = g.json.pop("description", None) jobtype_version = JobTypeVersion() jobtype_version.jobtype = jobtype jobtype_version.version = version jobtype_version.code = g.json.pop("code") jobtype_version.classname = g.json.pop("classname", None) jobtype_version.batch_contiguous = g.json.pop("batch_contiguous", None) jobtype_version.no_automatic_start_time =\ g.json.pop("no_automatic_start_time", None) if "max_batch" in g.json and g.json["max_batch"] is None: g.json.pop("max_batch") jobtype_version.max_batch = sql.null() else: jobtype_version.max_batch = g.json.pop("max_batch", None) except KeyError as e: return (jsonify(error="Missing key in input: %r" % e.args), BAD_REQUEST) if "software_requirements" in g.json: try: for r in parse_requirements(g.json["software_requirements"]): r.jobtype_version = jobtype_version db.session.add(r) except (TypeError, ValueError) as e: return jsonify(error=e.args), BAD_REQUEST except ObjectNotFound as e: return jsonify(error=e.args), NOT_FOUND del g.json["software_requirements"] elif not new: # If the user did not specify a list of software requirements and # this jobtype is not new, retain the requirements from the previous # version previous_version = JobTypeVersion.query.filter( JobTypeVersion.jobtype == jobtype, JobTypeVersion.version != version).order_by( "version desc").first() if previous_version: for old_req in previous_version.software_requirements: new_req = JobTypeSoftwareRequirement() new_req.jobtype_version = jobtype_version new_req.software_id = old_req.software_id new_req.min_version_id = old_req.min_version_id new_req.max_version_id = old_req.max_version_id db.session.add(new_req) if g.json: return (jsonify(error="Unexpected keys in input: %s" % g.json.keys()), BAD_REQUEST) db.session.add_all([jobtype, jobtype_version]) db.session.commit() jobtype_data = jobtype_version.to_dict( unpack_relationships=["software_requirements"]) jobtype_data.update(jobtype.to_dict(unpack_relationships=False)) del jobtype_data["jobtype_id"] logger.info("%s jobtype %s in put: %r", "created" if new else "updated", jobtype.name, jobtype_data) return jsonify(jobtype_data), CREATED
[docs] def delete(self, jobtype_name): """ A ``DELETE`` to this endpoint will delete the requested jobtype .. http:delete:: /api/v1/jobtypes/[<str:name>|<int:id>] HTTP/1.1 **Request** .. sourcecode:: http DELETE /api/v1/jobtypes/TestJobType HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 204 NO CONTENT :statuscode 204: the jobtype was deleted or didn't exist """ if isinstance(jobtype_name, STRING_TYPES): jobtype = JobType.query.filter(JobType.name == jobtype_name).first() else: jobtype = JobType.query.filter(JobType.id == jobtype_name).first() if jobtype: logger.debug("jobtype %s will be deleted",jobtype.name) db.session.delete(jobtype) db.session.commit() logger.info("jobtype %s has been deleted",jobtype.name) return jsonify(None), NO_CONTENT
[docs]class JobTypeVersionsIndexAPI(MethodView):
[docs] def get(self, jobtype_name): """ A ``GET`` to this endpoint will return a sorted list of of all known versions of the specified jobtype. .. http:get:: /api/v1/jobtypes/[<str:name>|<int:id>]/versions/ HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/jobtypes/TestJobType/versions/ HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json [1, 2] :statuscode 200: no error :statuscode 404: jobtype not found """ if isinstance(jobtype_name, STRING_TYPES): jobtype = JobType.query.filter(JobType.name == jobtype_name).first() else: jobtype = JobType.query.filter(JobType.id == jobtype_name).first() if not jobtype: return jsonify(error="jobtype not found"), NOT_FOUND out = [x.version for x in jobtype.versions] return jsonify(sorted(out)), OK
[docs]class VersionedJobTypeAPI(MethodView):
[docs] def get(self, jobtype_name, version): """ A ``GET`` to this endpoint will return the specified version of the referenced jobtype, by name or id. .. http:get:: /api/v1/jobtypes/[<str:name>|<int:id>]/versions/<int:version> HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/jobtypes/TestJobType/versions/1 HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "batch_contiguous": true, "classname": null, "name": "TestJobType", "code": "\\nfrom pyfarm.jobtypes.core.jobtype import " "JobType\\n\\nclass TestJobType(JobType):\\n" " def get_command(self):\\n" " return \"/usr/bin/touch\"\\n\\n" " def get_arguments(self):\\n" " return [os.path.join(" "self.assignment_data[\"job\"][\"data\"][\"path\"], " "\"%04d\" % self.assignment_data[\"tasks\"]" "[0][\"frame\"])]\\n", "id": 1, "version": 1, "max_batch": 1, "software_requirements": [ { "max_version": null, "max_version_id": null, "min_version": "8.21", "min_version_id": 1, "software": "/bin/touch", "software_id": 1 } ] } :statuscode 200: no error :statuscode 404: jobtype or version not found """ if isinstance(jobtype_name, STRING_TYPES): jobtype = JobType.query.filter(JobType.name == jobtype_name).first() else: jobtype = JobType.query.filter(JobType.id == jobtype_name).first() jobtype_version = JobTypeVersion.query.filter_by( jobtype=jobtype, version=version).first() if not jobtype or not jobtype_version: return (jsonify(error="JobType %s version %s not found" % (jobtype_name, version)), NOT_FOUND) # For some reason, sqlalchemy sometimes returns this column as bytes # instead of string. jsonify cannot decode that. if PY3 and isinstance(jobtype_version.code, bytes): # pragma: no cover jobtype_version.code = jobtype_version.code.decode() jobtype_data = jobtype_version.to_dict( unpack_relationships=["software_requirements"]) jobtype_data.update(jobtype.to_dict(unpack_relationships=False)) del jobtype_data["jobtype_id"] return jsonify(jobtype_data), OK
[docs] def delete(self, jobtype_name, version): """ A ``DELETE`` to this endpoint will delete the requested version of the specified jobtype. .. http:delete:: /api/v1/jobtypes/[<str:name>|<int:id>]/versions/<int:version> HTTP/1.1 **Request** .. sourcecode:: http DELETE /api/v1/jobtypes/TestJobType/versions/1 HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 204 NO CONTENT :statuscode 204: the version was deleted or didn't exist """ if isinstance(jobtype_name, STRING_TYPES): jobtype = JobType.query.filter(JobType.name == jobtype_name).first() else: jobtype = JobType.query.filter(JobType.id == jobtype_name).first() jobtype_version = JobTypeVersion.query.filter_by( jobtype=jobtype, version=version).first() if jobtype_version: logger.debug("version %s of jobtype %s will be deleted", version, jobtype.name) db.session.delete(jobtype_version) db.session.commit() logger.info("version %s of jobtype %s has been deleted", version, jobtype.name) return jsonify(None), NO_CONTENT
[docs]class JobTypeCodeAPI(MethodView):
[docs] def get(self, jobtype_name, version): """ A ``GET`` to this endpoint will return just the python code for this version of the specified jobtype. .. http:get:: /api/v1/jobtypes/[<str:name>|<int:id>]/versions/<int:version>/code HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/jobtypes/TestJobType/versions/1/code HTTP/1.1 Accept: text/x-python **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: text/x-python from pyfarm.jobtypes.core.jobtype import JobType class TestJobType(JobType): def get_command(self): return "/usr/bin/touch" def get_arguments(self): return [os.path.join( self.assignment_data["job"]["data"]["path"], "%04d" % self.assignment_data["tasks"][0]["frame"])] :statuscode 200: no error :statuscode 404: jobtype or version not found """ if isinstance(jobtype_name, STRING_TYPES): jt_tuple = db.session.query( JobType, JobTypeVersion).filter( JobType.id == JobTypeVersion.jobtype_id, JobType.name == jobtype_name, JobTypeVersion.version == version).first() else: jt_tuple = db.session.query( JobType, JobTypeVersion).filter( JobType.id == JobTypeVersion.jobtype_id, JobType.id == jobtype_name, JobTypeVersion.version == version).first() if not jt_tuple: return (jsonify(error="JobType %s, version %s not found" % (jobtype_name, version)), NOT_FOUND) jobtype, jobtype_version = jt_tuple return Response(jobtype_version.code, OK, mimetype="text/x-python")
[docs]class JobTypeSoftwareRequirementsIndexAPI(MethodView):
[docs] def get(self, jobtype_name, version=None): """ A ``GET`` to this endpoint will return a list of all the software requirements of the specified jobtype .. http:get:: /api/v1/jobtypes/[<str:name>|<int:id>]/software_requirements/ HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/jobtypes/TestJobType/software_requirements/ HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json [ { "software": { "software": "/bin/touch", "id": 1 }, "max_version": null, "min_version": { "version": "8.21", "id": 1 }, "jobtype_version": { "version": 7, "jobtype": "TestJobType" } } ] :statuscode 200: no error :statuscode 404: jobtype or version not found """ if isinstance(jobtype_name, STRING_TYPES): jobtype = JobType.query.filter_by(name=jobtype_name).first() else: jobtype = JobType.query.filter_by(id=jobtype_name).first() if not jobtype: return (jsonify(error="JobType %s not found" % jobtype_name), NOT_FOUND) if version: jobtype_version = JobTypeVersion.query.filter( JobTypeVersion.jobtype == jobtype, JobTypeVersion.version == version).first() else: jobtype_version = JobTypeVersion.query.filter_by( jobtype=jobtype).order_by("version desc").first() if not jobtype_version: return jsonify(error="JobType version not found"), NOT_FOUND out = [] for requirement in jobtype_version.software_requirements: rq_data = requirement.to_dict() del rq_data["jobtype_version_id"] del rq_data["software_id"] del rq_data["min_version_id"] del rq_data["max_version_id"] out.append(rq_data) return jsonify(out), OK
[docs] def post(self, jobtype_name, version=None): """ A ``POST`` to this endpoint will create a new software_requirement for the specified jobtype. This will transparently create a new jobtype version .. http:post:: /api/v1/jobtypes/[<str:name>|<int:id>]/software_requirements/ HTTP/1.1 **Request** .. sourcecode:: http POST /api/v1/jobtypes/TestJobType/software_requirements/ HTTP/1.1 Accept: application/json { "software": "blender", "min_version": "2.69" } **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "jobtype_version": { "id": 8, "jobtype": "TestJobType", "version": 7 }, "max_version": null, "min_version": { "id": 2, "version": "1.69" }, "software": { "id": 2, "software": "blender" } } :statuscode 201: a new software requirement was created :statuscode 400: there was something wrong with the request (such as invalid columns being included) :statuscode 405: you tried calling this method on a specific version :statuscode 409: a conflicting software requirement already exists """ if version is not None: return (jsonify( error="POST not allowed for specific jobtype versions"), METHOD_NOT_ALLOWED) if isinstance(jobtype_name, STRING_TYPES): jobtype = JobType.query.filter_by(name=jobtype_name).first() else: jobtype = JobType.query.filter_by(id=jobtype_name).first() if not jobtype: return (jsonify(error="JobType %s not found" % jobtype_name), NOT_FOUND) jobtype_version = JobTypeVersion.query.filter_by( jobtype=jobtype).order_by("version desc").first() if not jobtype_version: return jsonify(error="JobType has no versions"), NOT_FOUND if ("software" not in g.json or not isinstance(g.json["software"], STRING_TYPES)): return (jsonify(error="Software not specified or not a string"), BAD_REQUEST) software = Software.query.filter_by(software=g.json["software"]).first() if not software: return jsonify(error="Software not found"), NOT_FOUND existing_requirement = JobTypeSoftwareRequirement.query.filter( JobTypeSoftwareRequirement.jobtype_version == jobtype_version, JobTypeSoftwareRequirement.software == software).first() if existing_requirement: return jsonify(error="A software requirement for this jobtype " "version and this software exists"), CONFLICT new_version = JobTypeVersion() for name in JobTypeVersion.types().columns: if name not in JobTypeVersion.types().primary_keys: setattr(new_version, name, getattr(jobtype_version, name)) new_version.version += 1 db.session.add(new_version) for old_req in jobtype_version.software_requirements: new_req = JobTypeSoftwareRequirement() for name in JobTypeSoftwareRequirement.types().columns: setattr(new_req, name, getattr(old_req, name)) new_req.jobtype_version = new_version db.session.add(new_req) min_version = None if "min_version" in g.json: if not isinstance(g.json["min_version"], STRING_TYPES): return jsonify(error="min_version not a string"), BAD_REQUEST min_version = SoftwareVersion.query.filter_by( version=g.json["min_version"]).first() if not min_version: return jsonify(error="min_version not found"), NOT_FOUND max_version = None if "max_version" in g.json: if not isinstance(g.json["max_version"], STRING_TYPES): return jsonify(error="max_version not a string"), BAD_REQUEST max_version = SoftwareVersion.query.filter_by( version=g.json["max_version"]).first() if not max_version: return jsonify(error="max_version not found"), NOT_FOUND requirement = JobTypeSoftwareRequirement() requirement.jobtype_version = new_version requirement.software = software requirement.min_version = min_version requirement.max_version = max_version db.session.add(requirement) db.session.commit() requirement_data = requirement.to_dict() del requirement_data["jobtype_version_id"] del requirement_data["software_id"] del requirement_data["min_version_id"] del requirement_data["max_version_id"] logger.info("Created new software requirement for jobtype %s: %r", jobtype.id, requirement_data) return jsonify(requirement_data), CREATED
[docs]class JobTypeSoftwareRequirementAPI(MethodView):
[docs] def get(self, jobtype_name, software): """ A ``GET`` to this endpoint will return the specified software requirement from the newest version of the requested jobtype. .. http:get:: /api/v1/jobtypes/[<str:name>|<int:id>]/software_requirements/<int:id> HTTP/1.1 **Request** .. sourcecode:: http GET /api/v1/jobtypes/TestJobType/software_requirements/1 HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 200 OK Content-Type: application/json { "software": { "software": "/bin/touch", "id": 1 }, "max_version": null, "min_version": { "version": "8.21", "id": 1 }, "jobtype_version": { "version": 7, "jobtype": "TestJobType" } } :statuscode 200: no error :statuscode 404: jobtype or software requirement not found """ if isinstance(jobtype_name, STRING_TYPES): jobtype = JobType.query.filter_by(name=jobtype_name).first() else: jobtype = JobType.query.filter_by(id=jobtype_name).first() if not jobtype: return (jsonify(error="JobType %s not found" % jobtype_name), NOT_FOUND) current_version = JobTypeVersion.query.filter_by( jobtype=jobtype).order_by("version desc").first() if not current_version: return jsonify(error="JobType has no versions"), NOT_FOUND requirement = JobTypeSoftwareRequirement.query.filter( JobTypeSoftwareRequirement.jobtype_version == current_version, JobTypeSoftwareRequirement.software.has( Software.software == software)).first() if not requirement: return (jsonify(error="JobType software requirement %s for jobtype " "%s not found" % (software, jobtype_name)), NOT_FOUND) requirement_data = requirement.to_dict() del requirement_data["jobtype_version_id"] del requirement_data["software_id"] del requirement_data["min_version_id"] del requirement_data["max_version_id"] return jsonify(requirement_data), OK
[docs] def delete(self, jobtype_name, software): """ A ``DELETE`` to this endpoint will delete the requested software requirement from the specified jobtype, creating a new version of the jobtype in the process .. http:delete:: /api/v1/jobtypes/[<str:name>|<int:id>]/software_requirements/<int:id> HTTP/1.1 **Request** .. sourcecode:: http DELETE /api/v1/jobtypes/TestJobType/software_requirements/1 HTTP/1.1 Accept: application/json **Response** .. sourcecode:: http HTTP/1.1 204 NO CONTENT :statuscode 204: the software requirement was deleted or didn't exist """ if isinstance(jobtype_name, STRING_TYPES): jobtype = JobType.query.filter_by(name=jobtype_name).first() else: jobtype = JobType.query.filter_by(id=jobtype_name).first() if not jobtype: return (jsonify(error="JobType %s not found" % jobtype_name), NOT_FOUND) jobtype_version = JobTypeVersion.query.filter_by( jobtype=jobtype).order_by("version desc").first() if not jobtype_version: return jsonify(error="JobType has no versions"), NOT_FOUND new_version = JobTypeVersion() for name in JobTypeVersion.types().columns: if name not in JobTypeVersion.types().primary_keys: setattr(new_version, name, getattr(jobtype_version, name)) new_version.version += 1 for old_req in jobtype_version.software_requirements: if old_req.software.software != software: new_req = JobTypeSoftwareRequirement() for name in JobTypeSoftwareRequirement.types().columns: setattr(new_req, name, getattr(old_req, name)) new_req.jobtype_version = new_version db.session.add(new_req) db.session.add(new_version) db.session.commit() logger.info("Deleted software requirement %s for jobtype %s, creating " "new version %s", software, jobtype.id, new_version.version) return jsonify(None), NO_CONTENT