diff --git a/client/quantum_serverless/core/job.py b/client/quantum_serverless/core/job.py index 9592a66bd..5b6509376 100644 --- a/client/quantum_serverless/core/job.py +++ b/client/quantum_serverless/core/job.py @@ -426,12 +426,12 @@ def run( # pylint: disable=too-many-locals "entrypoint": program.entrypoint, "arguments": json.dumps(arguments or {}, cls=QiskitObjectsEncoder), "dependencies": json.dumps(program.dependencies or []), - "env_var": json.dumps(program.env_vars or {}), - } # type: Dict[str, Any] + "env_vars": json.dumps(program.env_vars or {}), + } if config: - data["config"] = asdict(config) + data["config"] = json.dumps(asdict(config)) else: - data["config"] = {} + data["config"] = "{}" response_data = safe_json_request( request=lambda: requests.post( diff --git a/gateway/api/exceptions.py b/gateway/api/exceptions.py deleted file mode 100644 index 706d1c8c6..000000000 --- a/gateway/api/exceptions.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Custom exceptions for the gateway application -""" - -from rest_framework import status - - -class GatewayException(Exception): - """ - Generic custom exception for our application - """ - - def __init__(self, message): - super().__init__(message) - - -class GatewayHttpException(GatewayException): - """ - Generic http custom exception for our application - """ - - def __init__(self, message, http_code): - super().__init__(message) - self.http_code = http_code - - -class InternalServerErrorException(GatewayHttpException): - """ - A wrapper for when we want to raise an internal server error - """ - - def __init__(self, message, http_code=status.HTTP_500_INTERNAL_SERVER_ERROR): - super().__init__(message, http_code) - - -class ResourceNotFoundException(GatewayHttpException): - """ - A wrapper for when we want to raise a 404 error - """ - - def __init__(self, message, http_code=status.HTTP_404_NOT_FOUND): - super().__init__(message, http_code) diff --git a/gateway/api/serializers.py b/gateway/api/serializers.py index 6a7756fd9..083d1e66f 100644 --- a/gateway/api/serializers.py +++ b/gateway/api/serializers.py @@ -50,7 +50,7 @@ def update(self, instance, validated_data): ) instance.entrypoint = validated_data.get("entrypoint") instance.dependencies = validated_data.get("dependencies", "[]") - instance.env_vars = validated_data.get("env_vars", "{}") + instance.env_vars = validated_data.get("env_vars", {}) instance.artifact = validated_data.get("artifact") instance.author = validated_data.get("author") instance.save() @@ -138,9 +138,9 @@ def create(self, validated_data): pass -class RunExistingJobSerializer(serializers.ModelSerializer): +class RunJobSerializer(serializers.ModelSerializer): """ - Job serializer for the /run_existing end-point + Job serializer for the /run and /run_existing end-point """ class Meta: @@ -187,3 +187,72 @@ class RuntimeJobSerializer(serializers.ModelSerializer): class Meta: model = RuntimeJob + + +class RunProgramSerializer(serializers.Serializer): + """ + Program serializer for the /run end-point + """ + + title = serializers.CharField(max_length=255, allow_blank=False) + entrypoint = serializers.CharField(max_length=255) + artifact = serializers.FileField(allow_empty_file=False, use_url=False) + dependencies = serializers.CharField(allow_blank=False) + arguments = serializers.CharField(allow_blank=False) + env_vars = serializers.CharField(allow_blank=False) + config = serializers.CharField(allow_blank=False) + + def to_representation(self, instance): + """ + Transforms string `config` to JSON + """ + representation = super().to_representation(instance) + representation["config"] = json.loads(representation["config"]) + return representation + + def retrieve_one_by_title(self, title, author): + """ + This method returns a Program entry if it finds an entry searching by the title, if not None + """ + return ( + Program.objects.filter(title=title, author=author) + .order_by("-created") + .first() + ) + + def create(self, validated_data): + pass + + def update(self, instance, validated_data): + pass + + +class RunProgramModelSerializer(serializers.ModelSerializer): + """ + Program model serializer for the /run end-point + """ + + arguments = serializers.CharField(read_only=True) + config = serializers.CharField(read_only=True) + + class Meta: + model = Program + + def create(self, validated_data): + title = validated_data.get("title") + logger.info("Creating program [%s] with RunProgramSerializer", title) + env_vars = validated_data.get("env_vars") + if env_vars: + encrypted_env_vars = encrypt_env_vars(json.loads(env_vars)) + validated_data["env_vars"] = json.dumps(encrypted_env_vars) + return Program.objects.create(**validated_data) + + def update(self, instance, validated_data): + logger.info("Updating program [%s] with RunProgramSerializer", instance.title) + instance.entrypoint = validated_data.get("entrypoint") + instance.dependencies = validated_data.get("dependencies", "[]") + instance.env_vars = validated_data.get("env_vars", {}) + instance.artifact = validated_data.get("artifact") + instance.author = validated_data.get("author") + instance.save() + return instance diff --git a/gateway/api/services.py b/gateway/api/services.py deleted file mode 100644 index 989f66c51..000000000 --- a/gateway/api/services.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -Services for api application: - - Program Service - - Job Service - -Version services inherit from the different services. -""" - -# pylint: disable=too-few-public-methods -# pylint: disable=duplicate-code -# Disable duplicate code due to refactorization. This file will be delited. - -import logging -import json - -from .models import Program, JobConfig, Job -from .exceptions import InternalServerErrorException, ResourceNotFoundException -from .utils import encrypt_env_vars, build_env_variables - -logger = logging.getLogger("gateway.services") - - -class ProgramService: - """ - Program service allocate the logic related with programs - """ - - @staticmethod - def save(serializer, author, artifact) -> Program: - """ - Save method gets a program serializer and creates or updates a program - - Args: - serializer: django program model serializer - author: user tipically got it from request - artifact: file that is going to be run - - Returns: - program: new Program instance - """ - - title = serializer.data.get("title") - existing_program = ( - Program.objects.filter(title=title, author=author) - .order_by("-created") - .first() - ) - - if existing_program is not None: - program = existing_program - program.arguments = serializer.data.get("arguments") - program.entrypoint = serializer.data.get("entrypoint") - program.dependencies = serializer.data.get("dependencies", "[]") - program.env_vars = serializer.data.get("env_vars", "{}") - logger.debug("Program [%s] will be updated by [%s]", title, author) - else: - program = Program(**serializer.data) - logger.debug("Program [%s] will be created by [%s]", title, author) - program.artifact = artifact - program.author = author - - # It would be nice if we could unify all the saves logic in one unique entry-point - try: - program.save() - except (Exception) as save_program_exception: - logger.error( - "Exception was caught saving the program [%s] by [%s] \n" - "Error trace: %s", - title, - author, - save_program_exception, - ) - raise InternalServerErrorException( - "Unexpected error saving the program" - ) from save_program_exception - - logger.debug("Program [%s] saved", title) - - return program - - @staticmethod - def find_one_by_title(title, author) -> Program: - """ - It returns the last created Program by title from an author - - Args: - title: program title - author: user tipically got it from request - - Returns: - program: Program instance found - """ - - logger.debug("Filtering Program by title[%s] and author [%s]", title, author) - program = ( - Program.objects.filter(title=title, author=author) - .order_by("-created") - .first() - ) - - if program is None: - logger.error("Program [%s] by author [%s] not found", title, author) - raise ResourceNotFoundException("Program [{title}] was not found") - - return program - - -class JobConfigService: - """ - JobConfig service allocate the logic related with job configuration - """ - - @staticmethod - def save_with_serializer(serializer) -> JobConfig: - """ - It returns a new JobConfig from its serializer - - Args: - serializer: JobConfig serializer from the model - - Returns: - JobConfig: new JobConfig instance - """ - - # It would be nice if we could unify all the saves logic in one unique entry-point - try: - jobconfig = serializer.save() - except (Exception) as save_job_config_exception: - logger.error( - "Exception was caught saving a JobConfig. \n Error trace: %s", - save_job_config_exception, - ) - raise InternalServerErrorException( - "Unexpected error saving the configuration of the job" - ) from save_job_config_exception - - logger.debug("JobConfig [%s] saved", jobconfig.id) - - return jobconfig - - -class JobService: - """ - Job service allocate the logic related with a job - """ - - @staticmethod - def save( - program: Program, - arguments: str, - author, - jobconfig: JobConfig, - token: str, - carrier, - status=Job.QUEUED, - ) -> Job: - """ - Creates or updates a job - - Args: - program: instance of a Program - arguments: arguments from the serializer - author: author from the request - jobconfig: instance of a JobConfig - token: token from the request after being decoded - carrier: object injected from TraceContextTextMapPropagator - status: status of the job, QUEUED by default - - Returns: - Job instance - """ - - job = Job( - program=program, - arguments=arguments, - author=author, - status=status, - config=jobconfig, - ) - env = encrypt_env_vars(build_env_variables(token, job, arguments)) - try: - env["traceparent"] = carrier["traceparent"] - except KeyError: - pass - - try: - job.env_vars = json.dumps(env) - job.save() - except (Exception) as save_job_exception: - logger.error( - "Exception was caught saving the Job[%s]. \n Error trace: %s", - job.id, - save_job_exception, - ) - raise InternalServerErrorException( - "Unexpected error saving the environment variables of the job" - ) from save_job_exception - - return job diff --git a/gateway/api/v1/serializers.py b/gateway/api/v1/serializers.py index a8ee67d60..8c0761237 100644 --- a/gateway/api/v1/serializers.py +++ b/gateway/api/v1/serializers.py @@ -55,12 +55,12 @@ class Meta(serializers.JobConfigSerializer.Meta): ] -class RunExistingJobSerializer(serializers.RunExistingJobSerializer): +class RunJobSerializer(serializers.RunJobSerializer): """ - RunExistingJobSerializer is used by the /run_existing end-point + RunJobSerializer is used by the /run and /run_existing end-points """ - class Meta(serializers.RunExistingJobSerializer.Meta): + class Meta(serializers.RunJobSerializer.Meta): fields = ["id", "result", "status", "program", "created", "arguments"] @@ -84,3 +84,24 @@ class RuntimeJobSerializer(serializers.RuntimeJobSerializer): class Meta(serializers.RuntimeJobSerializer.Meta): fields = ["job", "runtime_job"] + + +class RunProgramSerializer(serializers.RunProgramSerializer): + """ + RunProgram serializer is used in /run end-point + """ + + +class RunProgramModelSerializer(serializers.RunProgramModelSerializer): + """ + RunProgram model serializer is used in /run end-point + """ + + class Meta(serializers.RunProgramModelSerializer.Meta): + fields = [ + "title", + "entrypoint", + "artifact", + "dependencies", + "env_vars", + ] diff --git a/gateway/api/v1/services.py b/gateway/api/v1/services.py deleted file mode 100644 index 4b357e17c..000000000 --- a/gateway/api/v1/services.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Services api for V1. -""" - -# pylint: disable=too-few-public-methods - -from api import services - - -class ProgramService(services.ProgramService): - """ - Program service first version. - """ - - -class JobConfigService(services.JobConfigService): - """ - JobConfig service first version. - """ - - -class JobService(services.JobService): - """ - Job service first version. - """ diff --git a/gateway/api/v1/views.py b/gateway/api/v1/views.py index 278c41a93..82c42cf86 100644 --- a/gateway/api/v1/views.py +++ b/gateway/api/v1/views.py @@ -11,7 +11,6 @@ from api.models import Program, Job, RuntimeJob from api.permissions import IsOwner from . import serializers as v1_serializers -from . import services as v1_services class ProgramViewSet(views.ProgramViewSet): # pylint: disable=too-many-ancestors @@ -23,22 +22,6 @@ class ProgramViewSet(views.ProgramViewSet): # pylint: disable=too-many-ancestor serializer_class = v1_serializers.ProgramSerializer permission_classes = [permissions.IsAuthenticated] - @staticmethod - def get_service_program_class(): - return v1_services.ProgramService - - @staticmethod - def get_service_job_config_class(): - return v1_services.JobConfigService - - @staticmethod - def get_service_job_class(): - return v1_services.JobService - - @staticmethod - def get_serializer_job(*args, **kwargs): - return v1_serializers.JobSerializer(*args, **kwargs) - @staticmethod def get_serializer_job_config(*args, **kwargs): return v1_serializers.JobConfigSerializer(*args, **kwargs) @@ -52,8 +35,16 @@ def get_serializer_run_existing_program(*args, **kwargs): return v1_serializers.RunExistingProgramSerializer(*args, **kwargs) @staticmethod - def get_serializer_run_existing_job(*args, **kwargs): - return v1_serializers.RunExistingJobSerializer(*args, **kwargs) + def get_serializer_run_job(*args, **kwargs): + return v1_serializers.RunJobSerializer(*args, **kwargs) + + @staticmethod + def get_serializer_run_program(*args, **kwargs): + return v1_serializers.RunProgramSerializer(*args, **kwargs) + + @staticmethod + def get_model_serializer_run_program(*args, **kwargs): + return v1_serializers.RunProgramModelSerializer(*args, **kwargs) def get_serializer_class(self): return v1_serializers.ProgramSerializer @@ -70,12 +61,21 @@ def upload(self, request): @swagger_auto_schema( operation_description="Run an existing Qiskit Pattern", request_body=v1_serializers.RunExistingProgramSerializer, - responses={status.HTTP_200_OK: v1_serializers.RunExistingJobSerializer}, + responses={status.HTTP_200_OK: v1_serializers.RunJobSerializer}, ) @action(methods=["POST"], detail=False) def run_existing(self, request): return super().run_existing(request) + @swagger_auto_schema( + operation_description="Run and upload a Qiskit Pattern", + request_body=v1_serializers.RunProgramSerializer, + responses={status.HTTP_200_OK: v1_serializers.RunJobSerializer}, + ) + @action(methods=["POST"], detail=False) + def run(self, request): + return super().run(request) + class JobViewSet(views.JobViewSet): # pylint: disable=too-many-ancestors """ diff --git a/gateway/api/views.py b/gateway/api/views.py index 8a455b87b..b6decb1ec 100644 --- a/gateway/api/views.py +++ b/gateway/api/views.py @@ -29,17 +29,16 @@ from rest_framework.response import Response from utils import sanitize_file_path -from .exceptions import InternalServerErrorException from .models import Program, Job, RuntimeJob from .ray import get_job_handler from .serializers import ( - JobSerializer, JobConfigSerializer, - RunExistingJobSerializer, + RunJobSerializer, RunExistingProgramSerializer, + RunProgramModelSerializer, + RunProgramSerializer, UploadProgramSerializer, ) -from .services import JobService, ProgramService, JobConfigService logger = logging.getLogger("gateway") resource = Resource(attributes={SERVICE_NAME: "QuantumServerless-Gateway"}) @@ -64,38 +63,6 @@ class ProgramViewSet(viewsets.ModelViewSet): # pylint: disable=too-many-ancesto BASE_NAME = "programs" - @staticmethod - def get_service_program_class(): - """ - This method returns Program service to be used in Program ViewSet. - """ - - return ProgramService - - @staticmethod - def get_service_job_config_class(): - """ - This method return JobConfig service to be used in Program ViewSet. - """ - - return JobConfigService - - @staticmethod - def get_service_job_class(): - """ - This method return Job service to be used in Program ViewSet. - """ - - return JobService - - @staticmethod - def get_serializer_job(*args, **kwargs): - """ - This method returns Job serializer to be used in Program ViewSet. - """ - - return JobSerializer(*args, **kwargs) - @staticmethod def get_serializer_job_config(*args, **kwargs): """ @@ -121,12 +88,28 @@ def get_serializer_run_existing_program(*args, **kwargs): return RunExistingProgramSerializer(*args, **kwargs) @staticmethod - def get_serializer_run_existing_job(*args, **kwargs): + def get_serializer_run_job(*args, **kwargs): """ This method returns the job serializer for the run_existing end-point """ - return RunExistingJobSerializer(*args, **kwargs) + return RunJobSerializer(*args, **kwargs) + + @staticmethod + def get_serializer_run_program(*args, **kwargs): + """ + This method returns the program serializer for the run end-point + """ + + return RunProgramSerializer(*args, **kwargs) + + @staticmethod + def get_model_serializer_run_program(*args, **kwargs): + """ + This method returns the program model serializer for the run end-point + """ + + return RunProgramModelSerializer(*args, **kwargs) def get_serializer_class(self): return self.serializer_class @@ -223,22 +206,18 @@ def run_existing(self, request): token = "" if request.auth: token = request.auth.token.decode() - job_serializer = self.get_serializer_run_existing_job(data={}) + job_data = {"arguments": arguments, "program": program.id} + job_serializer = self.get_serializer_run_job(data=job_data) if not job_serializer.is_valid(): logger.error( - "RunExistingJobSerializer validation failed:\n %s", + "RunJobSerializer validation failed:\n %s", serializer.errors, ) return Response( job_serializer.errors, status=status.HTTP_400_BAD_REQUEST ) job = job_serializer.save( - arguments=arguments, - author=author, - carrier=carrier, - token=token, - program=program, - config=jobconfig, + author=author, carrier=carrier, token=token, config=jobconfig ) logger.info("Returning Job [%s] created.", job.id) @@ -250,59 +229,77 @@ def run(self, request): tracer = trace.get_tracer("gateway.tracer") ctx = TraceContextTextMapPropagator().extract(carrier=request.headers) with tracer.start_as_current_span("gateway.program.run", context=ctx): - serializer = self.get_serializer(data=request.data) + serializer = self.get_serializer_run_program(data=request.data) if not serializer.is_valid(): + logger.error( + "RunProgramSerializer validation failed:\n %s", + serializer.errors, + ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + title = serializer.validated_data.get("title") author = request.user - program = None - program_service = self.get_service_program_class() - try: - program = program_service.save( - serializer=serializer, - author=author, - artifact=request.FILES.get("artifact"), + program = serializer.retrieve_one_by_title(title=title, author=author) + # We need to add request artifact to maintain the reference that the serializer lost + program_data = serializer.data + program_data["artifact"] = request.data.get("artifact") + if program is None: + logger.info("Program not found. [%s] is going to be created", title) + program_serializer = self.get_model_serializer_run_program( + data=program_data ) - except InternalServerErrorException as exception: - return Response(exception, exception.http_code) + else: + logger.info("Program found. [%s] is going to be updated", title) + program_serializer = self.get_model_serializer_run_program( + program, data=program_data + ) + if not program_serializer.is_valid(): + logger.error( + "RunProgramModelSerializer validation failed with program instance:\n %s", + program_serializer.errors, + ) + return Response( + program_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + program = program_serializer.save(author=author) jobconfig = None - config_data = request.data.get("config") - if config_data: - config_serializer = self.get_serializer_job_config( - data=json.loads(config_data) - ) - if not config_serializer.is_valid(): - return Response( - config_serializer.errors, status=status.HTTP_400_BAD_REQUEST + config_json = serializer.data.get("config") + if config_json: + logger.info("Configuration for [%s] was found.", title) + job_config_serializer = self.get_serializer_job_config(data=config_json) + if not job_config_serializer.is_valid(): + logger.error( + "JobConfigSerializer validation failed:\n %s", + serializer.errors, ) - try: - jobconfig = ( - self.get_service_job_config_class().save_with_serializer( - config_serializer - ) + return Response( + job_config_serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - except InternalServerErrorException as exception: - return Response(exception, exception.http_code) + jobconfig = job_config_serializer.save() + logger.info("JobConfig [%s] created.", jobconfig.id) - job = None carrier = {} TraceContextTextMapPropagator().inject(carrier) - arguments = serializer.data.get("arguments") or "{}" - token = request.auth.token.decode() - try: - job = self.get_service_job_class().save( - program=program, - arguments=arguments, - author=author, - jobconfig=jobconfig, - token=token, - carrier=carrier, + arguments = serializer.data.get("arguments") + token = "" + if request.auth: + token = request.auth.token.decode() + job_data = {"arguments": arguments, "program": program.id} + job_serializer = self.get_serializer_run_job(data=job_data) + if not job_serializer.is_valid(): + logger.error( + "RunJobSerializer validation failed:\n %s", + serializer.errors, ) - except InternalServerErrorException as exception: - return Response(exception, exception.http_code) + return Response( + job_serializer.errors, status=status.HTTP_400_BAD_REQUEST + ) + job = job_serializer.save( + author=author, carrier=carrier, token=token, config=jobconfig + ) + logger.info("Returning Job [%s] created.", job.id) - job_serializer = self.get_serializer_job(job) return Response(job_serializer.data) diff --git a/gateway/main/settings.py b/gateway/main/settings.py index dd18f8751..9c3b8a55c 100644 --- a/gateway/main/settings.py +++ b/gateway/main/settings.py @@ -126,11 +126,6 @@ "level": LOG_LEVEL, "propagate": False, }, - "gateway.services": { - "handlers": ["console"], - "level": LOG_LEVEL, - "propagate": False, - }, "gateway.serializers": { "handlers": ["console"], "level": LOG_LEVEL, diff --git a/gateway/tests/api/test_v1_program.py b/gateway/tests/api/test_v1_program.py index e8cf6f44a..0bd29f385 100644 --- a/gateway/tests/api/test_v1_program.py +++ b/gateway/tests/api/test_v1_program.py @@ -1,10 +1,15 @@ """Tests program APIs.""" + +import json +import os + +from django.contrib.auth import models +from django.core.files.base import ContentFile from django.urls import reverse from rest_framework import status from rest_framework.test import APITestCase -from api.models import Job, JobConfig -import json -from django.contrib.auth import models + +from api.models import Job class TestProgramApi(APITestCase): @@ -54,12 +59,14 @@ def test_run_existing(self): user = models.User.objects.get(username="test_user") self.client.force_authenticate(user=user) + + arguments = json.dumps({"MY_ARGUMENT_KEY": "MY_ARGUMENT_VALUE"}) programs_response = self.client.post( "/api/v1/programs/run_existing/", data={ "title": "Program", "entrypoint": "program.py", - "arguments": "{}", + "arguments": arguments, "dependencies": "[]", "config": { "workers": None, @@ -72,6 +79,50 @@ def test_run_existing(self): ) job_id = programs_response.data.get("id") job = Job.objects.get(id=job_id) + self.assertEqual(job.status, Job.QUEUED) + self.assertEqual(job.arguments, arguments) + self.assertEqual(job.program.dependencies, "[]") + self.assertEqual(job.config.min_workers, 1) + self.assertEqual(job.config.max_workers, 5) + self.assertEqual(job.config.workers, None) + self.assertEqual(job.config.auto_scaling, True) + + def test_run(self): + """Tests run authorized.""" + + fake_file = ContentFile(b"print('Hello World')") + fake_file.name = "test_run.tar" + + user = models.User.objects.get(username="test_user") + self.client.force_authenticate(user=user) + + arguments = json.dumps({"MY_ARGUMENT_KEY": "MY_ARGUMENT_VALUE"}) + env_vars = json.dumps({"MY_ENV_VAR_KEY": "MY_ENV_VAR_VALUE"}) + programs_response = self.client.post( + "/api/v1/programs/run/", + data={ + "title": "Program", + "entrypoint": "program.py", + "arguments": arguments, + "dependencies": "[]", + "env_vars": env_vars, + "config": json.dumps( + { + "workers": None, + "min_workers": 1, + "max_workers": 5, + "auto_scaling": True, + } + ), + "artifact": fake_file, + }, + ) + job_id = programs_response.data.get("id") + job = Job.objects.get(id=job_id) + self.assertEqual(job.status, Job.QUEUED) + self.assertEqual(job.arguments, arguments) + self.assertEqual(job.program.dependencies, "[]") + self.assertEqual(job.program.env_vars, env_vars) self.assertEqual(job.config.min_workers, 1) self.assertEqual(job.config.max_workers, 5) self.assertEqual(job.config.workers, None) diff --git a/gateway/tests/api/test_v1_serializers.py b/gateway/tests/api/test_v1_serializers.py index c34d200be..370e8244e 100644 --- a/gateway/tests/api/test_v1_serializers.py +++ b/gateway/tests/api/test_v1_serializers.py @@ -11,7 +11,9 @@ JobConfigSerializer, UploadProgramSerializer, RunExistingProgramSerializer, - RunExistingJobSerializer, + RunJobSerializer, + RunProgramSerializer, + RunProgramModelSerializer, ) from api.models import JobConfig, Program @@ -155,30 +157,186 @@ def test_run_existing_program_serializer_config_json(self): self.assertEqual(type(assert_json), type(config)) self.assertDictEqual(assert_json, config) - def test_run_existing_job_serializer_check_empty_data(self): - data = {} - - serializer = RunExistingJobSerializer(data=data) - self.assertTrue(serializer.is_valid()) - - def test_run_existing_job_serializer_creates_job(self): + def test_run_job_serializer_creates_job(self): user = models.User.objects.get(username="test_user") program_instance = Program.objects.get( id="1a7947f9-6ae8-4e3d-ac1e-e7d608deec82" ) arguments = "{}" - job_serializer = RunExistingJobSerializer(data={}) + config_data = { + "workers": None, + "min_workers": 1, + "max_workers": 5, + "auto_scaling": True, + } + config_serializer = JobConfigSerializer(data=config_data) + config_serializer.is_valid() + jobconfig = config_serializer.save() + + job_data = {"arguments": arguments, "program": program_instance.id} + job_serializer = RunJobSerializer(data=job_data) job_serializer.is_valid() job = job_serializer.save( - arguments=arguments, - author=user, - carrier={}, - token="my_token", - program=program_instance, - config=None, + author=user, carrier={}, token="my_token", config=jobconfig ) env_vars = json.loads(job.env_vars) + + self.assertIsNotNone(job) + self.assertIsNotNone(job.program) + self.assertIsNotNone(job.arguments) + self.assertIsNotNone(job.config) + self.assertIsNotNone(job.author) self.assertTrue(env_vars["PROGRAM_ENV1"] == "VALUE1") self.assertTrue(env_vars["PROGRAM_ENV2"] == "VALUE2") - self.assertIsNotNone(job) + + def test_run_program_serializer_check_emtpy_data(self): + data = {} + + serializer = RunProgramSerializer(data=data) + self.assertFalse(serializer.is_valid()) + errors = serializer.errors + self.assertListEqual( + [ + "title", + "entrypoint", + "artifact", + "dependencies", + "arguments", + "env_vars", + "config", + ], + list(errors.keys()), + ) + + def test_run_program_serializer_fails_at_validation(self): + path_to_resource_artifact = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "..", + "resources", + "artifact.tar", + ) + file_data = File(open(path_to_resource_artifact, "rb")) + upload_file = SimpleUploadedFile( + "artifact.tar", file_data.read(), content_type="multipart/form-data" + ) + + data = { + "title": "Program", + "entrypoint": "pattern.py", + "dependencies": [], + "arguments": {}, + "env_vars": {}, + "config": {}, + } + data["artifact"] = upload_file + + serializer = RunProgramSerializer(data=data) + self.assertFalse(serializer.is_valid()) + errors = serializer.errors + self.assertListEqual( + ["dependencies", "arguments", "env_vars", "config"], list(errors.keys()) + ) + + def test_run_program_serializer_config_json(self): + path_to_resource_artifact = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "..", + "resources", + "artifact.tar", + ) + file_data = File(open(path_to_resource_artifact, "rb")) + upload_file = SimpleUploadedFile( + "artifact.tar", file_data.read(), content_type="multipart/form-data" + ) + + assert_json = { + "workers": None, + "min_workers": 1, + "max_workers": 5, + "auto_scaling": True, + } + + data = { + "title": "Program", + "entrypoint": "pattern.py", + "dependencies": "[]", + "arguments": "{}", + "env_vars": "{}", + "config": json.dumps(assert_json), + } + data["artifact"] = upload_file + + serializer = RunProgramSerializer(data=data) + self.assertTrue(serializer.is_valid()) + + config = serializer.data.get("config") + self.assertEqual(type(assert_json), type(config)) + self.assertDictEqual(assert_json, config) + + def test_run_program_model_serializer_creates_program(self): + path_to_resource_artifact = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "..", + "resources", + "artifact.tar", + ) + file_data = File(open(path_to_resource_artifact, "rb")) + upload_file = SimpleUploadedFile( + "artifact.tar", file_data.read(), content_type="multipart/form-data" + ) + + user = models.User.objects.get(username="test_user") + + title = "Hello world" + entrypoint = "pattern.py" + dependencies = "[]" + + data = {} + data["title"] = title + data["entrypoint"] = entrypoint + data["dependencies"] = dependencies + data["artifact"] = upload_file + + serializer = RunProgramModelSerializer(data=data) + self.assertTrue(serializer.is_valid()) + + program: Program = serializer.save(author=user) + self.assertEqual(title, program.title) + self.assertEqual(entrypoint, program.entrypoint) + self.assertEqual(dependencies, program.dependencies) + + def test_run_program_model_serializer_check_empty_data(self): + data = {} + + serializer = RunProgramModelSerializer(data=data) + self.assertFalse(serializer.is_valid()) + errors = serializer.errors + self.assertListEqual(["title", "entrypoint", "artifact"], list(errors.keys())) + + def test_run_program_model_serializer_fails_at_validation(self): + path_to_resource_artifact = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "..", + "resources", + "artifact.tar", + ) + file_data = File(open(path_to_resource_artifact, "rb")) + upload_file = SimpleUploadedFile( + "artifact.tar", file_data.read(), content_type="multipart/form-data" + ) + + title = "Hello world" + entrypoint = "pattern.py" + dependencies = [] + + data = {} + data["title"] = title + data["entrypoint"] = entrypoint + data["artifact"] = upload_file + data["dependencies"] = dependencies + + serializer = RunProgramModelSerializer(data=data) + self.assertFalse(serializer.is_valid()) + errors = serializer.errors + self.assertListEqual(["dependencies"], list(errors.keys())) diff --git a/gateway/tests/api/test_v1_services.py b/gateway/tests/api/test_v1_services.py deleted file mode 100644 index bd8544a8c..000000000 --- a/gateway/tests/api/test_v1_services.py +++ /dev/null @@ -1,85 +0,0 @@ -import json -from unittest.mock import MagicMock -from rest_framework.test import APITestCase - -from api.exceptions import ResourceNotFoundException -from api.v1.services import ProgramService, JobConfigService, JobService -from api.v1.serializers import ProgramSerializer, JobConfigSerializer -from api.models import Job, Program, JobConfig -from django.contrib.auth.models import User - - -class ServicesTest(APITestCase): - """Tests for V1 services.""" - - fixtures = ["tests/fixtures/fixtures.json"] - - def test_save_program(self): - """Test to verify that the service creates correctly an entry with its serializer.""" - - user = User.objects.get(id=1) - data = '{"title": "My Qiskit Pattern", "entrypoint": "pattern.py"}' - program_serializer = ProgramSerializer(data=json.loads(data)) - program_serializer.is_valid() - - program = ProgramService.save(program_serializer, user, "path") - entry = Program.objects.get(id=program.id) - - self.assertIsNotNone(entry) - self.assertEqual(program.title, entry.title) - - def test_find_one_program_by_title(self): - """The test must return one Program filtered by specific title.""" - - user = User.objects.get(id=1) - title = "Program" - - program = ProgramService.find_one_by_title(title, user) - - self.assertIsNotNone(program) - self.assertEqual(program.title, title) - - def test_fail_to_find_program_by_title(self): - """The test must raise a 404 exception when we don't find a Program with a specific title.""" - - user = User.objects.get(id=1) - title = "This Program doesn't exist" - - with self.assertRaises(ResourceNotFoundException): - ProgramService.find_one_by_title(title, user) - - def test_create_job_config(self): - """The test will create a job config with a basic configuration.""" - - data = "{}" - job_config_serializer = JobConfigSerializer(data=json.loads(data)) - job_config_serializer.is_valid() - - job_config = JobConfigService.save_with_serializer(job_config_serializer) - entry = JobConfig.objects.get(id=job_config.id) - - self.assertIsNotNone(job_config) - self.assertEqual(entry.id, job_config.id) - - def test_create_job(self): - """Creating a job with basic consfiguration.""" - - user = User.objects.get(id=1) - program = Program.objects.get(pk="1a7947f9-6ae8-4e3d-ac1e-e7d608deec82") - arguments = "{}" - token = "42" - carrier = {} - jobconfig = None - - job = JobService.save( - program=program, - arguments=arguments, - author=user, - jobconfig=jobconfig, - token=token, - carrier=carrier, - ) - - self.assertIsNotNone(job) - self.assertEqual(Job.objects.count(), 4) - self.assertEqual(job.status, Job.QUEUED)