Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 90 additions & 51 deletions gateway/api/repositories/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,39 @@
Repository implementation for Job model
"""
import logging
from typing import List, Optional
from django.db.models import Q
from dataclasses import dataclass
from datetime import datetime
from typing import List, Optional, Tuple, Any
from django.db.models import Q, QuerySet
from api.models import Job
from api.models import Program as Function
from api.views.enums.type_filter import TypeFilter

logger = logging.getLogger("gateway")


@dataclass(slots=True)
class JobFilters:
"""
Filters for Job queries.

Attributes:
user: Author of the job
type: Type of job to filter
status: Current job status
created_after: Filter jobs created after this date
provider: Provider who owns the function
function: Function title
"""

user: Optional[Any] = None
type: Optional[TypeFilter] = None
status: Optional[str] = None
created_after: Optional[datetime] = None
provider: Optional[str] = None
function: Optional[str] = None


class JobsRepository:
"""
The main objective of this class is to manage the access to the Job model
Expand All @@ -29,7 +54,7 @@ def get_job_by_id(self, job_id: str) -> Job:
result_queryset = Job.objects.filter(id=job_id).first()

if result_queryset is None:
logger.info("Job [%s] was not found", id)
logger.info("Job [%s] was not found", job_id)

return result_queryset

Expand All @@ -47,54 +72,6 @@ def get_program_jobs(self, function: Function, ordering="-created") -> List[Job]
function_criteria = Q(program=function)
return Job.objects.filter(function_criteria).order_by(ordering)

def get_user_jobs(self, user, ordering="-created") -> List[Job]:
"""
Retrieves jobs created by a specific user.

Args:
user (User): The user whose jobs are to be retrieved.
ordering (str, optional): The field to order the results by. Defaults to "-created".

Returns:
List[Jobs]: a list of Jobs
"""
user_criteria = Q(author=user)
return Job.objects.filter(user_criteria).order_by(ordering)

def get_user_jobs_with_provider(self, user, ordering="-created") -> List[Job]:
"""
Retrieves jobs created by a specific user that have an associated provider.

Args:
user (User): The user whose jobs are to be retrieved.
ordering (str, optional): The field to order the results by. Defaults to "-created".

Returns:
List[Jobs]: a list of Jobs
"""
user_criteria = Q(author=user)
provider_exists_criteria = ~Q(program__provider=None)
return Job.objects.filter(user_criteria & provider_exists_criteria).order_by(
ordering
)

def get_user_jobs_without_provider(self, user, ordering="-created") -> List[Job]:
"""
Retrieves jobs created by a specific user that do not have an associated provider.

Args:
user (User): The user whose jobs are to be retrieved.
ordering (str, optional): The field to order the results by. Defaults to "-created".

Returns:
List[Job]: A queryset of Job objects without a provider.
"""
user_criteria = Q(author=user)
provider_not_exists_criteria = Q(program__provider=None)
return Job.objects.filter(
user_criteria & provider_not_exists_criteria
).order_by(ordering)

def update_job_sub_status(self, job: Job, sub_status: Optional[str]) -> bool:
"""
Updates the sub-status of a running job.
Expand Down Expand Up @@ -128,3 +105,65 @@ def update_job_sub_status(self, job: Job, sub_status: Optional[str]) -> bool:
)

return updated == 1

def get_user_jobs(
self,
filters: Optional[JobFilters] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
ordering: str = "-created",
) -> Tuple[QuerySet[Job], int]:
"""
Get user jobs with optional filters and pagination.

Args:
filters: Optional filters to apply
limit: Max number of results
offset: Number of results to skip
ordering: Field to order by. Default: -created (newest first)

Returns:
(queryset, total_count): Filtered results and total count
"""
queryset = Job.objects.order_by(ordering)

if filters:
queryset = self._apply_filters(queryset, filters)

return self._paginate_queryset(queryset, limit, offset)

def _apply_filters(self, queryset: QuerySet, filters: JobFilters) -> QuerySet:
"""Apply filters to job queryset."""
if filters.user:
queryset = queryset.filter(author=filters.user)

match filters.type:
case TypeFilter.CATALOG:
queryset = queryset.exclude(program__provider=None)
case TypeFilter.SERVERLESS:
queryset = queryset.filter(program__provider=None)

if filters.status:
queryset = queryset.filter(status=filters.status)

if filters.created_after:
queryset = queryset.filter(created__gte=filters.created_after)

if filters.function:
queryset = queryset.filter(program__title=filters.function)

return queryset

def _paginate_queryset(
self, queryset: QuerySet, limit: int | None, offset: int | None
) -> tuple[QuerySet, int]:
"""Apply pagination to job queryset."""
total_count = queryset.count()

start = offset or 0
end = start + limit if limit else None

if start >= total_count > 0:
return queryset.none(), total_count

return queryset[start:end], total_count
54 changes: 54 additions & 0 deletions gateway/api/use_cases/get_jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""This module contains the usecase get_jos"""
import logging
from typing import List, Optional
from datetime import datetime
from api.models import Job
from api.repositories.jobs import JobsRepository, JobFilters
from api.views.enums.type_filter import TypeFilter

logger = logging.getLogger("gateway.use_cases.get_jobs")


class GetJobsUseCase:
"""Use case for retrieving user jobs with optional filtering and pagination."""

jobs_repository: JobsRepository = JobsRepository()

def __init__( # pylint: disable=too-many-positional-arguments
self,
user,
limit: Optional[int],
offset: int = 0,
type_filter: Optional[TypeFilter] = None,
status: Optional[str] = None,
created_after: Optional[datetime] = None,
function_name: Optional[str] = None,
):
self.user = user
self.limit = limit
self.offset = offset
self.type_filter = type_filter
self.status = status
self.created_after = created_after
self.function_name = function_name

def execute(self) -> tuple[List[Job], int]:
"""
Retrieve user jobs with optional filters and pagination.

Returns:
tuple[list[Job], int]: (jobs, total_count)
"""
filters = JobFilters(
user=self.user,
type=self.type_filter,
status=self.status,
created_after=self.created_after,
function=self.function_name,
)

queryset, total = self.jobs_repository.get_user_jobs(
filters=filters, limit=self.limit, offset=self.offset
)

return list(queryset), total
79 changes: 79 additions & 0 deletions gateway/api/use_cases/get_provider_jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""This module contains the usecase get_jobs"""
import logging
from datetime import datetime
from typing import List, Tuple, Optional
from api.models import Job, VIEW_PROGRAM_PERMISSION
from api.access_policies.providers import ProviderAccessPolicy
from api.repositories.jobs import JobsRepository, JobFilters
from api.repositories.providers import ProviderRepository
from api.repositories.functions import FunctionRepository

logger = logging.getLogger("gateway.use_cases.get_jobs")


class ProviderNotFoundException(Exception):
"""Provider not found or access denied."""


class FunctionNotFoundException(Exception):
"""Function not found or access denied."""


class GetProviderJobsUseCase:
"""Use case for retrieving provider jobs with optional filtering and pagination."""

provider_repo: ProviderRepository = ProviderRepository()
function_repo: FunctionRepository = FunctionRepository()
jobs_repo: JobsRepository = JobsRepository()

def __init__( # pylint: disable=too-many-positional-arguments
self,
user,
provider: str,
function_name: str,
limit: Optional[int],
offset: Optional[int],
status: Optional[str] = None,
created_after: Optional[datetime] = None,
):
self.user = user
self.provider = provider
self.function_name = function_name
self.limit = limit
self.offset = offset
self.status = status
self.created_after = created_after

def execute(self) -> Tuple[List[Job], int]:
"""
Retrieve provider jobs with access validation.

Returns:
tuple[list[Job], int]: (jobs, total_count)

Raises:
ProviderNotFoundException: If provider doesn't exist or access denied.
FunctionNotFoundException: If function doesn't exist or access denied.
"""
provider = self.provider_repo.get_provider_by_name(self.provider)
if not provider or not ProviderAccessPolicy.can_access(self.user, provider):
raise ProviderNotFoundException()

function = self.function_repo.get_function_by_permission(
user=self.user,
permission_name=VIEW_PROGRAM_PERMISSION,
function_title=self.function_name,
provider_name=self.provider,
)
if not function:
raise FunctionNotFoundException()

filters = JobFilters(
status=self.status,
created_after=self.created_after,
function=self.function_name,
)
queryset, total = self.jobs_repo.get_user_jobs(
filters=filters, limit=self.limit, offset=self.offset
)
return list(queryset), total
11 changes: 10 additions & 1 deletion gateway/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ class Meta(serializers.ProgramSerializer.Meta):
]


class ProgramSummarySerializer(serializers.ProgramSerializer):
"""
Program serializer with summary fields for job listings.
"""

class Meta(serializers.ProgramSerializer.Meta):
fields = ["id", "title", "provider"]


class UploadProgramSerializer(serializers.UploadProgramSerializer):
"""
UploadProgramSerializer is used by the /upload end-point
Expand Down Expand Up @@ -209,7 +218,7 @@ class JobSerializerWithoutResult(serializers.JobSerializer):
Job serializer first version. Include basic fields from the initial model.
"""

program = ProgramSerializer(many=False)
program = ProgramSummarySerializer(many=False)

class Meta(serializers.JobSerializer.Meta):
fields = ["id", "status", "program", "created", "sub_status"]
Expand Down
2 changes: 1 addition & 1 deletion gateway/api/v1/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@
basename=v1_views.CatalogViewSet.BASE_NAME,
)

urlpatterns = router.urls + RouteRegistry.get()
urlpatterns = RouteRegistry.get() + router.urls
2 changes: 1 addition & 1 deletion gateway/api/v1/views/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def provider_list(self, request):
description="Qiskit Function title",
type=openapi.TYPE_STRING,
required=True,
),
), # pylint: disable=duplicate-code
openapi.Parameter(
"provider",
openapi.IN_QUERY,
Expand Down
Loading
Loading