Skip to content

Commit

Permalink
Catalog end-points (#1460)
Browse files Browse the repository at this point in the history
* List end-point for catalog

* Added retrieve end-point

* Improved error handling in the querysets

* Public group to settings

* Set public group name in the helm

* Create swagger documentation for new end-points

* Fix linter

* Changed author by user following feedback

* Fix None error reference

* Added comments for reverse

* Added url to the provider
  • Loading branch information
Tansito authored Aug 21, 2024
1 parent 4db6035 commit 4b088ae
Show file tree
Hide file tree
Showing 13 changed files with 551 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ spec:
value: "{{ .Values.tasks.providersConfiguration }}"
- name: FUNCTIONS_PERMISSIONS
value: "{{ .Values.tasks.functionsPermissions }}"
- name: PUBLIC_GROUP_NAME
value: {{ .Values.application.publicGroupName }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
Expand Down
1 change: 1 addition & 0 deletions charts/qiskit-serverless/charts/gateway/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ application:
url: "https://auth.quantum-computing.ibm.com/api"
iqpQcon:
url: "https://api-qcon.quantum.ibm.com/api"
publicGroupName: "ibm-q/open/main"

cos:
claimName: gateway-claim
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Generated by Django 5.1 on 2024-08-20 18:36

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0028_remove_provider_admin_group_provider_admin_groups"),
]

operations = [
migrations.AddField(
model_name="program",
name="additional_info",
field=models.TextField(blank=True, default="{}", null=True),
),
migrations.AddField(
model_name="program",
name="documentation_url",
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name="program",
name="type",
field=models.CharField(
choices=[
("GENERIC", "Generic"),
("APPLICATION", "Application"),
("CIRCUIT", "Circuit"),
],
default="GENERIC",
max_length=20,
),
),
migrations.AddField(
model_name="provider",
name="icon_url",
field=models.TextField(blank=True, default=None, null=True),
),
migrations.AddField(
model_name="provider",
name="url",
field=models.TextField(blank=True, default=None, null=True),
),
]
20 changes: 19 additions & 1 deletion gateway/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class Provider(models.Model):
updated = models.DateTimeField(auto_now=True, null=True)

name = models.CharField(max_length=255, db_index=True, unique=True)
url = models.TextField(null=True, blank=True, default=None)
icon_url = models.TextField(null=True, blank=True, default=None)
registry = models.CharField(max_length=255, null=True, blank=True, default=None)
admin_groups = models.ManyToManyField(Group)

Expand All @@ -61,11 +63,28 @@ def __str__(self):
class Program(ExportModelOperationsMixin("program"), models.Model):
"""Program model."""

GENERIC = "GENERIC"
APPLICATION = "APPLICATION"
CIRCUIT = "CIRCUIT"
PROGRAM_TYPES = [
(GENERIC, "Generic"),
(APPLICATION, "Application"),
(CIRCUIT, "Circuit"),
]

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
created = models.DateTimeField(auto_now_add=True, editable=False)

title = models.CharField(max_length=255, db_index=True)
type = models.CharField(
max_length=20,
choices=PROGRAM_TYPES,
default=GENERIC,
)
description = models.TextField(null=True, blank=True)
documentation_url = models.TextField(null=True, blank=True, default=None)
additional_info = models.TextField(null=True, blank=True, default="{}")

entrypoint = models.CharField(max_length=255, default=DEFAULT_PROGRAM_ENTRYPOINT)
artifact = models.FileField(
upload_to=get_upload_path,
Expand All @@ -74,7 +93,6 @@ class Program(ExportModelOperationsMixin("program"), models.Model):
validators=[FileExtensionValidator(allowed_extensions=["tar"])],
)
image = models.CharField(max_length=511, null=True, blank=True)

env_vars = models.TextField(null=False, blank=True, default="{}")
dependencies = models.TextField(null=False, blank=True, default="[]")

Expand Down
105 changes: 105 additions & 0 deletions gateway/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import logging
from typing import Tuple, Union
from django.conf import settings
from django.contrib.auth.models import Group, Permission
from django.db.models import Q
from rest_framework import serializers

from api.utils import build_env_variables, encrypt_env_vars
Expand All @@ -20,6 +22,7 @@
JobConfig,
RuntimeJob,
DEFAULT_PROGRAM_ENTRYPOINT,
RUN_PROGRAM_PERMISSION,
)

logger = logging.getLogger("gateway.serializers")
Expand Down Expand Up @@ -242,3 +245,105 @@ class RuntimeJobSerializer(serializers.ModelSerializer):

class Meta:
model = RuntimeJob


class CatalogProviderSerializer(serializers.ModelSerializer):
"""
Serializer for the Provider model in the Catalog View.
"""

class Meta:
model = Provider


class ListCatalogSerializer(serializers.ModelSerializer):
"""
List Serializer for the Catalog View.
"""

provider = CatalogProviderSerializer()
available = serializers.SerializerMethodField()

class Meta:
model = Program

def get_available(self, obj):
"""
This method populates available field.
If the user has RUN PERMISSION in any of its groups
available field will be True. If not, will be False.
"""
user = self.context.get("user", None)

if user is None:
logger.debug(
"User not authenticated in ListCatalogSerializer return available to False"
)
return False

# This will be refactorize it when we implement repository architecture
# pylint: disable=duplicate-code
run_program_permission = Permission.objects.get(codename=RUN_PROGRAM_PERMISSION)

user_criteria = Q(user=user)
run_permission_criteria = Q(permissions=run_program_permission)
user_groups_with_run_permissions = Group.objects.filter(
user_criteria & run_permission_criteria
)

return obj.instances.filter(
id__in=[group.id for group in user_groups_with_run_permissions]
).exists()


class RetrieveCatalogSerializer(serializers.ModelSerializer):
"""
Retrieve Serializer for the Catalog View.
"""

provider = CatalogProviderSerializer()
available = serializers.SerializerMethodField()

class Meta:
model = Program

def to_representation(self, instance):
representation = super().to_representation(instance)

json_additional_info = {}
if instance.additional_info is not None:
try:
json_additional_info = json.loads(instance.additional_info)
except json.decoder.JSONDecodeError:
logger.error("JSONDecodeError loading instance.additional_info")

representation["additional_info"] = json_additional_info
return representation

def get_available(self, obj):
"""
This method populates available field.
If the user has RUN PERMISSION in any of its groups
available field will be True. If not, will be False.
"""
user = self.context.get("user", None)

if user is None:
logger.debug(
"User not authenticated in ListCatalogSerializer return available to False"
)
return False

# This will be refactorize it when we implement repository architecture
# pylint: disable=duplicate-code
run_program_permission = Permission.objects.get(codename=RUN_PROGRAM_PERMISSION)

user_criteria = Q(user=user)
run_permission_criteria = Q(permissions=run_program_permission)
user_groups_with_run_permissions = Group.objects.filter(
user_criteria & run_permission_criteria
)

return obj.instances.filter(
id__in=[group.id for group in user_groups_with_run_permissions]
).exists()
41 changes: 41 additions & 0 deletions gateway/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Meta(serializers.ProgramSerializer.Meta):
"dependencies",
"provider",
"description",
"documentation_url",
]


Expand Down Expand Up @@ -172,3 +173,43 @@ class RuntimeJobSerializer(serializers.RuntimeJobSerializer):

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", "url", "icon_url"]


class ListCatalogSerializer(serializers.ListCatalogSerializer):
"""
List Serializer for the Catalog View.
"""

provider = CatalogProviderSerializer()

class Meta(serializers.ListCatalogSerializer.Meta):
fields = ["id", "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",
"type",
"description",
"documentation_url",
"provider",
"available",
"additional_info",
]
5 changes: 5 additions & 0 deletions gateway/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@
router.register(
r"files", v1_views.FilesViewSet, basename=v1_views.FilesViewSet.BASE_NAME
)
router.register(
r"catalog",
v1_views.CatalogViewSet,
basename=v1_views.CatalogViewSet.BASE_NAME,
)

urlpatterns = router.urls
30 changes: 30 additions & 0 deletions gateway/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,33 @@ class FilesViewSet(views.FilesViewSet):
"""

permission_classes = [permissions.IsAuthenticated, IsOwner]


class CatalogViewSet(views.CatalogViewSet):
"""
Quantum function view set first version. Use ProgramSerializer V1.
"""

@staticmethod
def get_serializer_retrieve_catalog(*args, **kwargs):
return v1_serializers.RetrieveCatalogSerializer(*args, **kwargs)

serializer_class = v1_serializers.ListCatalogSerializer
pagination_class = None
permission_classes = [permissions.IsAuthenticatedOrReadOnly]

@swagger_auto_schema(
operation_description="List public functions for catalog",
responses={status.HTTP_200_OK: v1_serializers.ListCatalogSerializer(many=True)},
)
def list(self, request):
return super().list(request)

@swagger_auto_schema(
operation_description="Get a specific public function for catalog",
responses={
status.HTTP_200_OK: v1_serializers.RetrieveCatalogSerializer(many=False)
},
)
def retrieve(self, request, pk=None):
return super().retrieve(request, pk)
Loading

0 comments on commit 4b088ae

Please sign in to comment.