276 lines
8.1 KiB
Python
276 lines
8.1 KiB
Python
"""
|
|
Serializers api for V1.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any
|
|
from packaging.requirements import Requirement, InvalidRequirement
|
|
from rest_framework.serializers import ValidationError
|
|
from api import serializers
|
|
from api.models import Provider
|
|
from api.utils import check_whitelisted
|
|
|
|
logger = logging.getLogger("gateway.serializers")
|
|
|
|
|
|
class ProgramSerializer(serializers.ProgramSerializer):
|
|
"""
|
|
Program serializer first version. Include basic fields from the initial model.
|
|
"""
|
|
|
|
class Meta(serializers.ProgramSerializer.Meta):
|
|
fields = [
|
|
"id",
|
|
"title",
|
|
"entrypoint",
|
|
"artifact",
|
|
"dependencies",
|
|
"provider",
|
|
"description",
|
|
"documentation_url",
|
|
"type",
|
|
]
|
|
|
|
|
|
class UploadProgramSerializer(serializers.UploadProgramSerializer):
|
|
"""
|
|
UploadProgramSerializer is used by the /upload end-point
|
|
"""
|
|
|
|
def validate_image(self, value):
|
|
"""Validaes image."""
|
|
# place to add image validation
|
|
return value
|
|
|
|
def _parse_dependency(self, dep: Any):
|
|
if not isinstance(dep, dict) and not isinstance(dep, str):
|
|
raise ValidationError(
|
|
"'dependencies' should be an array with strings or dict."
|
|
)
|
|
|
|
if isinstance(dep, str):
|
|
dep_string = dep
|
|
else:
|
|
dep_name = list(dep.keys())
|
|
if len(dep_name) > 1 or len(dep_name) == 0:
|
|
raise ValidationError(
|
|
"'dependencies' should be an array with dict containing one depencency only."
|
|
)
|
|
dep_name = str(dep_name[0])
|
|
dep_version = str(list(dep.values())[0])
|
|
|
|
# if starts with a number then prefix ==
|
|
try:
|
|
if int(dep_version[0]) >= 0:
|
|
dep_version = f"=={dep_version}"
|
|
except ValueError:
|
|
pass
|
|
|
|
dep_string = dep_name + dep_version
|
|
|
|
requirement = Requirement(dep_string)
|
|
req_specifier_list = list(requirement.specifier)
|
|
req_specifier_first = next(iter(req_specifier_list), None)
|
|
|
|
if len(req_specifier_list) > 1 or (
|
|
req_specifier_first and req_specifier_first.operator != "=="
|
|
):
|
|
raise ValidationError(
|
|
"'dependencies' needs one fixed version using the '==' operator."
|
|
)
|
|
|
|
return requirement
|
|
|
|
def _validate_deps(self, deps):
|
|
if not isinstance(deps, list):
|
|
raise ValidationError("'dependencies' should be an array.")
|
|
|
|
if len(deps) == 0:
|
|
return
|
|
|
|
try:
|
|
required_deps = [self._parse_dependency(dep) for dep in deps]
|
|
except InvalidRequirement as invalid_requirement:
|
|
raise ValidationError(
|
|
"Error while parsing dependencies."
|
|
) from invalid_requirement
|
|
|
|
try:
|
|
check_whitelisted(required_deps)
|
|
except ValueError as value_error:
|
|
raise ValidationError(value_error.args[0]) from value_error
|
|
|
|
def validate(self, attrs): # pylint: disable=too-many-branches
|
|
"""Validates serializer data."""
|
|
entrypoint = attrs.get("entrypoint", None)
|
|
image = attrs.get("image", None)
|
|
if entrypoint is None and image is None:
|
|
raise ValidationError(
|
|
"At least one of attributes (entrypoint, image) is required."
|
|
)
|
|
try:
|
|
# validate dependencies
|
|
deps = json.loads(attrs.get("dependencies", "[]"))
|
|
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
raise ValidationError(
|
|
"'dependencies' should be an array of strings or objects: "
|
|
"`['pendulum==3.0.0', '...'] or [{'pendulum':'3.0.0'}, {...}]`"
|
|
) from exc
|
|
|
|
self._validate_deps(deps)
|
|
|
|
title = attrs.get("title")
|
|
provider = attrs.get("provider", None)
|
|
if provider and "/" in title:
|
|
raise ValidationError("Provider defined in title and in provider fields.")
|
|
|
|
title_split = title.split("/")
|
|
if len(title_split) > 2:
|
|
raise ValidationError(
|
|
"Qiskit Function title is malformed. It can only contain one slash."
|
|
)
|
|
|
|
if image is not None:
|
|
if provider is None and len(title_split) != 2:
|
|
raise ValidationError(
|
|
"Custom images are only available if you are a provider."
|
|
)
|
|
if not provider:
|
|
provider = title_split[0]
|
|
provider_instance = Provider.objects.filter(name=provider).first()
|
|
if provider_instance is None:
|
|
raise ValidationError(f"{provider} is not valid provider.")
|
|
if provider_instance.registry and not image.startswith(
|
|
provider_instance.registry
|
|
):
|
|
raise ValidationError(
|
|
f"Custom images must be in {provider_instance.registry}."
|
|
)
|
|
|
|
return super().validate(attrs)
|
|
|
|
class Meta(serializers.UploadProgramSerializer.Meta):
|
|
fields = [
|
|
"title",
|
|
"entrypoint",
|
|
"artifact",
|
|
"dependencies",
|
|
"env_vars",
|
|
"image",
|
|
"provider",
|
|
"description",
|
|
"type",
|
|
]
|
|
|
|
|
|
class RunProgramSerializer(serializers.RunProgramSerializer):
|
|
"""
|
|
RunExistingProgramSerializer is used by the /run end-point
|
|
"""
|
|
|
|
|
|
class JobConfigSerializer(serializers.JobConfigSerializer):
|
|
"""
|
|
JobConfig serializer first version. Include basic fields from the initial model.
|
|
"""
|
|
|
|
class Meta(serializers.JobConfigSerializer.Meta):
|
|
fields = [
|
|
"workers",
|
|
"min_workers",
|
|
"max_workers",
|
|
"auto_scaling",
|
|
]
|
|
|
|
|
|
class RunJobSerializer(serializers.RunJobSerializer):
|
|
"""
|
|
RunJobSerializer is used by the /run end-point
|
|
"""
|
|
|
|
class Meta(serializers.RunJobSerializer.Meta):
|
|
fields = ["id", "result", "status", "program", "created", "arguments"]
|
|
|
|
|
|
class JobSerializer(serializers.JobSerializer):
|
|
"""
|
|
Job serializer first version. Include basic fields from the initial model.
|
|
"""
|
|
|
|
program = ProgramSerializer(many=False)
|
|
|
|
class Meta(serializers.JobSerializer.Meta):
|
|
fields = ["id", "result", "status", "program", "created", "sub_status"]
|
|
|
|
|
|
class JobSerializerWithoutResult(serializers.JobSerializer):
|
|
"""
|
|
Job serializer first version. Include basic fields from the initial model.
|
|
"""
|
|
|
|
program = ProgramSerializer(many=False)
|
|
|
|
class Meta(serializers.JobSerializer.Meta):
|
|
fields = ["id", "status", "program", "created", "sub_status"]
|
|
|
|
|
|
class RuntimeJobSerializer(serializers.RuntimeJobSerializer):
|
|
"""
|
|
Runtime job serializer first version. Serializer for the runtime job model.
|
|
"""
|
|
|
|
job = JobSerializer(many=False)
|
|
|
|
class Meta(serializers.RuntimeJobSerializer.Meta):
|
|
fields = ["job", "runtime_job"]
|
|
|
|
|
|
class CatalogProviderSerializer(serializers.CatalogProviderSerializer):
|
|
"""
|
|
Serializer for the Provider model in the Catalog View.
|
|
"""
|
|
|
|
class Meta(serializers.CatalogProviderSerializer.Meta):
|
|
fields = ["name", "readable_name", "url", "icon_url"]
|
|
|
|
|
|
class ListCatalogSerializer(serializers.ListCatalogSerializer):
|
|
"""
|
|
List Serializer for the Catalog View.
|
|
"""
|
|
|
|
provider = CatalogProviderSerializer()
|
|
|
|
class Meta(serializers.ListCatalogSerializer.Meta):
|
|
fields = [
|
|
"id",
|
|
"title",
|
|
"readable_title",
|
|
"type",
|
|
"description",
|
|
"provider",
|
|
"available",
|
|
]
|
|
|
|
|
|
class RetrieveCatalogSerializer(serializers.RetrieveCatalogSerializer):
|
|
"""
|
|
Retrieve Serializer for the Catalog View.
|
|
"""
|
|
|
|
provider = CatalogProviderSerializer()
|
|
|
|
class Meta(serializers.RetrieveCatalogSerializer.Meta):
|
|
fields = [
|
|
"id",
|
|
"title",
|
|
"readable_title",
|
|
"type",
|
|
"description",
|
|
"documentation_url",
|
|
"provider",
|
|
"available",
|
|
"additional_info",
|
|
]
|