Skip to content

Commit

Permalink
Merge pull request pulp#647 from sdherr/component-filter
Browse files Browse the repository at this point in the history
pulp#646: Add filters to select by related pulp_deb content types
  • Loading branch information
quba42 authored Jun 1, 2023
2 parents 4f1e2f2 + a047393 commit a0816d5
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGES/646.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added API filters to limit results by related pulp_deb content types.
167 changes: 166 additions & 1 deletion pulp_deb/app/viewsets/content.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from gettext import gettext as _ # noqa

from django_filters import Filter
from pulpcore.plugin.models import Repository, RepositoryVersion
from pulpcore.plugin.serializers.content import ValidationError
from pulpcore.plugin.viewsets import (
ContentViewSet,
ContentFilter,
ContentViewSet,
NamedModelViewSet,
SingleArtifactContentUploadViewSet,
)

Expand Down Expand Up @@ -36,11 +40,145 @@ class GenericContentViewSet(SingleArtifactContentUploadViewSet):
filterset_class = GenericContentFilter


class ContentRelationshipFilter(Filter):
"""
Base class for filters that allow you to ask meaningful questions about the relationships of
deb-specific content types. Subclasses must provide a HELP message and implement _filter.
The value for all these filters is a string that is a comma-separated 2-tuple, where the second
value is the HREF of the RepositoryVersion you care about. This is logically necessary if you
want to ask any question beyond "list Package|ReleaseComponent|whatever that were ever at any
point in this Repository|Release|whatever". I will try to explain by example.
Question: "What Packages are in the most recent RepositoryVersion of a Release?"
Imagine we have a very simple repo with two packages and two releases, and this state is stored
in RepositoryVersion1:
Repository -> Release1 -> ReleaseComponent1 -> PackageReleaseComponent1 -> Package1
-> PackageReleaseComponent2 -> Package2
-> Release2 -> ReleaseComponent2 -> PackageReleaseComponent3 -> Package2
We then update the repo to remove Package2 from ReleaseComponent1 and this state gets stored
in RepositoryVersion2:
Repository -> Release1 -> ReleaseComponent1 -> PackageReleaseComponent1 -> Package1
-> Release2 -> ReleaseComponent2 -> PackageReleaseComponent3 -> Package2
We could try answer the question using the existing ContentFilter.repository_version filter in
conjunction with a new filter that naively follows the foreign key references in the database:
packages.filter(deb_packagereleasecomponent__release_component__release=release_uuid)
What Django does if you call two separate filters is use the first to filter the QuerySet,
then use the second to filter the QuerySet further. This is *different* than calling
filter once with both conditions.
https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships
Example: packages.filter("in RepositoryVersion2").filter("in Release1")
This will return both Package1 and Package2, which is not what we wanted. In the first filter it
looks and says "yep, both Package1 and Package2 are in RepositoryVersion2", and then the second
filter is applied and it says "yep, both Package1 and Package2 were in Release1 at some point".
This is because the linkage via PackageReleaseComponent2 still *exists*, it's just not in
RepositoryVersion2.
What we really _actually_ want is to apply _both_ conditions to the PackageReleaseComponent
mapping as an intermediate step, so both release_uuid and repository_version_href must be
passed to our new filter:
packages.filter(package.PRC in PRC.filter("in RepositoryVersion2", "in Release1"))
This guarantees that we are only considering Packages with both requirements, and returns only
Package1.
"""

HELP = "Override with your value-specific help message"
ARG = "Override with the type of your arg, for example package_href"
ARG_CLASS = models.Package # Override with the correct model in subclass
GENERIC_HELP = """
Must be a comma-separated string: "{arg},repository_or_repository_version_href"
{arg}: {help}
repository_or_repository_version_href: The RepositoryVersion href to filter by, or Repository
href (assume latest version)
"""

def __init__(self, *args, **kwargs):
kwargs.setdefault(
"help_text", _(self.GENERIC_HELP).format(arg=_(self.ARG), help=_(self.HELP))
)
super().__init__(*args, **kwargs)

def filter(self, qs, value):
"""
Args:
qs (django.db.models.query.QuerySet): The Content Queryset
value (string, "value,repository_version_href"): The values to filter by
"""
if value is None:
# user didn't supply a value
return qs

repo_version: RepositoryVersion = None
arg_href, r_or_rv_href = value.split(",", 1)
if not arg_href or not r_or_rv_href or "," in r_or_rv_href:
raise ValidationError(detail=_("Unparsable argument supplied for content filter"))

repo_version = NamedModelViewSet.get_resource(r_or_rv_href)
if isinstance(repo_version, Repository):
repo_version = repo_version.latest_version()

if not isinstance(repo_version, RepositoryVersion):
raise ValidationError(
detail=_("Could not resolve a RepositoryVersion from content filter argument")
)

arg_instance = NamedModelViewSet.get_resource(arg_href, self.ARG_CLASS)
if not repo_version.content.filter(pk=arg_instance.pk).exists():
raise ValidationError(
detail=_("Specified filter argument is not in specified RepositoryVersion")
)

return self._filter(qs, arg_instance, repo_version.content)

def _filter(self, qs, arg, rv_content):
"""
Args:
qs (django.db.models.query.QuerySet): The Content Queryset
arg (ARG_CLASS): The specific self.ARG that we're filtering by
rv_content (django.db.models.query.QuerySet): QuerySet of Content in
requested RepositoryVersion
"""
raise NotImplementedError


class PackageToReleaseComponentFilter(ContentRelationshipFilter):
HELP = "Filter results where Package in ReleaseComponent"
ARG = "release_component_href"
ARG_CLASS = models.ReleaseComponent

def _filter(self, qs, arg, rv_content):
prc_qs = models.PackageReleaseComponent.objects.filter(
pk__in=rv_content, release_component=arg.pk
)
return qs.filter(deb_packagereleasecomponent__in=prc_qs)


class PackageToReleaseFilter(ContentRelationshipFilter):
HELP = "Filter results where Package in Release"
ARG = "release_href"
ARG_CLASS = models.Release

def _filter(self, qs, arg, rv_content):
prc_qs = models.PackageReleaseComponent.objects.filter(
pk__in=rv_content, release_component__distribution=arg.distribution
)
return qs.filter(deb_packagereleasecomponent__in=prc_qs)


class PackageFilter(ContentFilter):
"""
FilterSet for Package.
"""

release_component = PackageToReleaseComponentFilter()
release = PackageToReleaseFilter()

class Meta:
model = models.Package
fields = [
Expand Down Expand Up @@ -215,11 +353,26 @@ class InstallerFileIndexViewSet(ContentViewSet):
filterset_class = InstallerFileIndexFilter


class ReleaseToPackageFilter(ContentRelationshipFilter):
HELP = "Filter results where Release contains Package"
ARG = "package_href"
ARG_CLASS = models.Package

def _filter(self, qs, arg, rv_content):
prc_qs = models.PackageReleaseComponent.objects.filter(pk__in=rv_content, package=arg)
rc_qs = models.ReleaseComponent.objects.filter(
pk__in=rv_content, deb_packagereleasecomponent__in=prc_qs
)
return qs.filter(pk__in=rv_content, distribution__in=rc_qs.values("distribution"))


class ReleaseFilter(ContentFilter):
"""
FilterSet for Release.
"""

package = ReleaseToPackageFilter()

class Meta:
model = models.Release
fields = ["codename", "suite", "distribution"]
Expand Down Expand Up @@ -274,11 +427,23 @@ class ReleaseArchitectureViewSet(ContentViewSet):
filterset_class = ReleaseArchitectureFilter


class ReleaseComponentToPackageFilter(ContentRelationshipFilter):
HELP = "Filter results where ReleaseComponent contains Package"
ARG = "package_href"
ARG_CLASS = models.Package

def _filter(self, qs, arg, rv_content):
prc_qs = models.PackageReleaseComponent.objects.filter(pk__in=rv_content, package=arg)
return qs.filter(deb_packagereleasecomponent__in=prc_qs)


class ReleaseComponentFilter(ContentFilter):
"""
FilterSet for ReleaseComponent.
"""

package = ReleaseComponentToPackageFilter()

class Meta:
model = models.ReleaseComponent
fields = ["component", "distribution", "codename", "suite"]
Expand Down

0 comments on commit a0816d5

Please sign in to comment.