Skip to content

Commit 0f36f68

Browse files
committed
Add PEP 440 version_specifier filter
closes #1277 Assisted By: Claude Opus 4.6
1 parent 0b49215 commit 0f36f68

3 files changed

Lines changed: 59 additions & 0 deletions

File tree

‎CHANGES/1277.feature‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added `version_specifier` filter to package content API for PEP 440 version matching.

‎pulp_python/app/viewsets.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from django_filters import CharFilter
66
from django_filters.rest_framework import filters as drf_filters
77
from drf_spectacular.utils import extend_schema, extend_schema_view
8+
from packaging.specifiers import InvalidSpecifier, SpecifierSet
89
from packaging.utils import canonicalize_name
10+
from packaging.version import InvalidVersion, Version
911
from rest_framework import status
1012
from rest_framework.decorators import action
1113
from rest_framework.mixins import (
@@ -466,6 +468,28 @@ class NormalizedNameInFilter(drf_filters.BaseInFilter, NormalizedNameFilter):
466468
"""In-filter that normalizes each input value and queries name_normalized."""
467469

468470

471+
class VersionSpecifierFilter(CharFilter):
472+
"""Filter that matches versions against a PEP 440 specifier string."""
473+
474+
def filter(self, qs, value):
475+
if not value:
476+
return qs
477+
try:
478+
spec = SpecifierSet(value, prereleases=True)
479+
except InvalidSpecifier:
480+
raise ValidationError(
481+
{"version_specifier": f"Invalid PEP 440 version specifier: {value}"}
482+
)
483+
matching_pks = []
484+
for pk, version in qs.values_list("pk", self.field_name):
485+
try:
486+
if Version(version) in spec:
487+
matching_pks.append(pk)
488+
except InvalidVersion:
489+
pass
490+
return qs.filter(pk__in=matching_pks)
491+
492+
469493
class PythonPackageContentFilter(core_viewsets.ContentFilter):
470494
"""
471495
FilterSet for PythonPackageContent.
@@ -474,6 +498,10 @@ class PythonPackageContentFilter(core_viewsets.ContentFilter):
474498
name = NormalizedNameFilter(field_name="name_normalized", lookup_expr="exact")
475499
name__in = NormalizedNameInFilter(field_name="name_normalized", lookup_expr="in")
476500
name__contains = CharFilter(field_name="name", lookup_expr="contains")
501+
version_specifier = VersionSpecifierFilter(
502+
field_name="version",
503+
help_text="Filter by PEP 440 version specifier (e.g., >=2.4,<3.0 or ~=1.26)",
504+
)
477505

478506
class Meta:
479507
model = python_models.PythonPackageContent
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import pytest
2+
3+
from pulp_python.tests.functional.constants import PYTHON_SM_PROJECT_SPECIFIER
4+
5+
6+
@pytest.mark.parallel
7+
def test_version_specifier_filter(
8+
python_bindings, python_repo_with_sync, python_remote_factory
9+
):
10+
"""Test filtering content by PEP 440 version specifier."""
11+
remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER)
12+
repo = python_repo_with_sync(remote=remote)
13+
14+
result = python_bindings.ContentPackagesApi.list(
15+
repository_version=repo.latest_version_href,
16+
name="Django",
17+
version_specifier=">=1.10.2,<1.10.4",
18+
)
19+
versions = {c.version for c in result.results}
20+
assert "1.10.2" in versions
21+
assert "1.10.3" in versions
22+
assert "1.10.4" not in versions
23+
24+
25+
@pytest.mark.parallel
26+
def test_version_specifier_filter_invalid(python_bindings):
27+
"""Test that an invalid specifier returns a 400 error."""
28+
with pytest.raises(python_bindings.ApiException) as exc:
29+
python_bindings.ContentPackagesApi.list(version_specifier=">=invalid!version")
30+
assert exc.value.status == 400

0 commit comments

Comments
 (0)