Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGES/+debver.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add filters to content units to be diltered and ordered by their versions.
165 changes: 165 additions & 0 deletions pulp_deb/app/migrations/0032_debver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Generated by Django 4.2.24 on 2025-09-10 07:38

from django.db import migrations
import pulp_deb.fields


class Migration(migrations.Migration):

dependencies = [
("deb", "0031_add_domains"),
]

operations = [
migrations.RunSQL(
sql="""
CREATE COLLATION debver (provider='ICU', deterministic=false, locale='und', rules=$$
[reorder digit latn symbol punct others][numericOrdering on]
&[first variable]<'~'<'\\u0000'
$$);

CREATE TYPE debver AS
(
sort_key TEXT COLLATE debver,
value TEXT
);

CREATE FUNCTION debver(value bpchar) RETURNS debver
AS $$
DECLARE
pos integer;
rest bpchar;
epoch bpchar;
version bpchar;
revision bpchar;
BEGIN
pos := position(':' IN value);
IF pos > 0
THEN
epoch := left(value, pos - 1);
rest := right(value, -pos);
ELSE
epoch := '0';
rest := value;
END IF;
pos := position('-' IN reverse(rest));
IF pos > 0
THEN
version := left(rest, -pos);
revision := right(rest, pos -1);
ELSE
version := rest;
revision := '';
END IF;
return (epoch || '\\ufffd' || version || '\\u0000\\ufffd' || revision || '\\u0000', value)::debver;
END;
$$
LANGUAGE plpgsql
IMMUTABLE
RETURNS NULL ON NULL INPUT;

CREATE FUNCTION text(version debver) RETURNS text
AS $$
BEGIN
RETURN version.value;
END;
$$
LANGUAGE plpgsql
IMMUTABLE
RETURNS NULL ON NULL INPUT;

CREATE CAST (text AS debver) WITH FUNCTION debver(bpchar) AS IMPLICIT;
CREATE CAST (varchar AS debver) WITH FUNCTION debver(bpchar) AS IMPLICIT;
CREATE CAST (bpchar AS debver) WITH FUNCTION debver(bpchar) AS IMPLICIT;
CREATE CAST (debver AS text) WITH FUNCTION text(debver) AS IMPLICIT;
CREATE CAST (debver AS varchar) WITH FUNCTION text(debver) AS IMPLICIT;
CREATE CAST (debver AS bpchar) WITH FUNCTION text(debver) AS IMPLICIT;

CREATE FUNCTION debver_eq(a debver, b debver) RETURNS boolean
LANGUAGE sql
IMMUTABLE
RETURNS NULL ON NULL INPUT
RETURN a.sort_key = b.sort_key;

CREATE FUNCTION debver_neq(a debver, b debver) RETURNS boolean
LANGUAGE sql
IMMUTABLE
RETURNS NULL ON NULL INPUT
RETURN a.sort_key <> b.sort_key;

CREATE FUNCTION debver_lt(a debver, b debver) RETURNS boolean
LANGUAGE sql
IMMUTABLE
RETURNS NULL ON NULL INPUT
RETURN a.sort_key < b.sort_key;

CREATE FUNCTION debver_lte(a debver, b debver) RETURNS boolean
LANGUAGE sql
IMMUTABLE
RETURNS NULL ON NULL INPUT
RETURN a.sort_key <= b.sort_key;

CREATE FUNCTION debver_gte(a debver, b debver) RETURNS boolean
LANGUAGE sql
IMMUTABLE
RETURNS NULL ON NULL INPUT
RETURN a.sort_key >= b.sort_key;

CREATE FUNCTION debver_gt(a debver, b debver) RETURNS boolean
LANGUAGE sql
IMMUTABLE
RETURNS NULL ON NULL INPUT
RETURN a.sort_key > b.sort_key;

CREATE OPERATOR = (
LEFTARG = debver,
RIGHTARG = debver,
FUNCTION = debver_eq
);

CREATE OPERATOR <> (
LEFTARG = debver,
RIGHTARG = debver,
FUNCTION = debver_neq
);

CREATE OPERATOR < (
LEFTARG = debver,
RIGHTARG = debver,
FUNCTION = debver_lt
);

CREATE OPERATOR <= (
LEFTARG = debver,
RIGHTARG = debver,
FUNCTION = debver_lte
);

CREATE OPERATOR >= (
LEFTARG = debver,
RIGHTARG = debver,
FUNCTION = debver_gte
);

CREATE OPERATOR > (
LEFTARG = debver,
RIGHTARG = debver,
FUNCTION = debver_gt
);
""",
reverse_sql="""
DROP TYPE IF EXISTS debver CASCADE;
DROP COLLATION IF EXISTS debver;
""",
),
migrations.AlterField(
model_name="installerpackage",
name="version",
field=pulp_deb.fields.DebVersionField(),
),
migrations.AlterField(
model_name="package",
name="version",
field=pulp_deb.fields.DebVersionField(),
),
]
4 changes: 3 additions & 1 deletion pulp_deb/app/models/content/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from pulpcore.plugin.models import Content
from pulpcore.plugin.util import get_domain_pk

from pulp_deb.fields import DebVersionField

BOOL_CHOICES = [(True, "yes"), (False, "no")]


Expand All @@ -33,7 +35,7 @@ class BasePackage(Content):

package = models.TextField() # package name
source = models.TextField(null=True) # source package name
version = models.TextField()
version = DebVersionField()
architecture = models.TextField() # all, i386, ...
section = models.TextField(null=True) # admin, comm, database, ...
priority = models.TextField(null=True) # required, standard, optional, extra
Expand Down
2 changes: 1 addition & 1 deletion pulp_deb/app/viewsets/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ class Meta:
fields = {
"package": NAME_FILTER_OPTIONS,
"source": ["exact"],
"version": ["exact"],
"version": ["exact", "ne", "lt", "lte", "gt", "gte"],
"architecture": ["exact"],
"section": ["exact"],
"priority": ["exact"],
Expand Down
24 changes: 24 additions & 0 deletions pulp_deb/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.db import models


class _DebVer(models.Value):
def __init__(self, value):
self.value = value

def as_sql(self, compiler, connection):
return "debver(%s)", [self.value]


class DebVersionField(models.CharField):
description = "Debian Version"

def db_type(self, connection):
return "debver"

def get_prep_value(self, value):
if value is not None:
return _DebVer(value)
return value

def select_format(self, compiler, sql, params):
return f"({sql}).value", params
25 changes: 21 additions & 4 deletions pulp_deb/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest
from pathlib import Path

from pulpcore.tests.functional.utils import BindingsNamespace

from pulp_deb.tests.functional.utils import gen_deb_remote, gen_distribution, gen_repo
from pulp_deb.tests.functional.constants import DEB_FIXTURE_STANDARD_REPOSITORY_NAME

Expand All @@ -16,6 +18,21 @@
)


@pytest.fixture(scope="session")
def deb_bindings(_api_client_set, bindings_cfg):
"""
A namespace providing preconfigured pulpcore api clients.

e.g. `pulpcore_bindings.WorkersApi.list()`.
"""
from pulpcore.client import pulp_deb as bindings_module

api_client = bindings_module.ApiClient(bindings_cfg)
_api_client_set.add(api_client)
yield BindingsNamespace(bindings_module, api_client)
_api_client_set.remove(api_client)


@pytest.fixture(scope="session")
def apt_client(_api_client_set, bindings_cfg):
"""Fixture for APT client."""
Expand Down Expand Up @@ -124,7 +141,7 @@ def _deb_remote_factory(url, **kwargs):
return _deb_remote_factory


@pytest.fixture
@pytest.fixture(scope="class")
def deb_sync_repository(apt_repository_api, monitor_task):
"""Fixture that synchronizes a given repository with a given remote
and returns the monitored task.
Expand All @@ -148,15 +165,15 @@ def _deb_sync_repository(remote, repo, mirror=False, optimize=True):
return _deb_sync_repository


@pytest.fixture
@pytest.fixture(scope="class")
def deb_fixture_server(gen_fixture_server):
"""A fixture that spins up a local web server to serve test data."""
p = Path(__file__).parent.absolute()
fixture_path = p.joinpath("functional/data/")
yield gen_fixture_server(fixture_path, None)


@pytest.fixture
@pytest.fixture(scope="class")
def deb_get_fixture_server_url(deb_fixture_server):
"""A fixture that provides the url of the local web server."""

Expand All @@ -171,7 +188,7 @@ def _deb_get_fixture_server_url(repo_name=DEB_FIXTURE_STANDARD_REPOSITORY_NAME):
return _deb_get_fixture_server_url


@pytest.fixture
@pytest.fixture(scope="class")
def deb_init_and_sync(
apt_repository_api,
deb_get_fixture_server_url,
Expand Down
36 changes: 36 additions & 0 deletions pulp_deb/tests/functional/api/test_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pytest


@pytest.mark.parallel
class TestPackageVersionFilter:
@pytest.fixture(scope="class")
def repository(self, deb_init_and_sync):
_repository, _ = deb_init_and_sync()
return _repository

@pytest.mark.parametrize(
"filter,count",
[
pytest.param({"version": "1.0"}, 4, id="exact"),
pytest.param({"version__ne": "1.0"}, 0, id="ne"),
pytest.param({"version__gt": "1.0~"}, 4, id="gt with tilde"),
pytest.param({"version__gt": "1.0"}, 0, id="gt"),
pytest.param({"version__gt": "1.0+"}, 0, id="gt with plus"),
pytest.param({"version__gte": "1.0~"}, 4, id="gte with tilde"),
pytest.param({"version__gte": "1.0"}, 4, id="gte"),
pytest.param({"version__gte": "1.0+"}, 0, id="gte with plus"),
pytest.param({"version__lt": "1.0~"}, 0, id="lt with tilde"),
pytest.param({"version__lt": "1.0"}, 0, id="lt"),
pytest.param({"version__lt": "1.0+"}, 4, id="lt with plus"),
pytest.param({"version__lte": "1.0~"}, 0, id="lte with tilde"),
pytest.param({"version__lte": "1.0"}, 4, id="lte"),
pytest.param({"version__lte": "1.0+"}, 4, id="lte with plus"),
],
)
def test_returns_a_certain_count_of_entries(self, deb_bindings, repository, filter, count):
"""Verify that Packages can be filtered by versions."""
# Query content units with filters
result = deb_bindings.ContentPackagesApi.list(
repository_version=repository.latest_version_href, **filter
)
assert result.count == count
34 changes: 34 additions & 0 deletions pulp_deb/tests/unit/test_debversion_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from pulp_deb.app.models import Package


VERSIONS = [
"1",
"1.0~asdf",
"1.0",
"1.0-1~0",
"1.0-1~1",
"1.0-1",
"1.0-1+1",
"1.0-1+1.2",
"1.0-1+2",
"1.0-1+12",
"1.0-1+a",
"1.0-1+b~",
"1.0-1+b",
"2",
"2:1.0",
"11:1.0",
]


def test_sort_debver(db):
for version in reversed(VERSIONS):
Package.objects.create(relative_path=f"test_sort_debver-{version}", version=version)

sorted_versions = (
Package.objects.filter(relative_path__startswith="test_sort_debver-")
.order_by("version")
.values_list("version", flat=True)
)

assert list(sorted_versions) == VERSIONS
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ requires-python = ">=3.11"
dependencies = [
# All things django and asyncio are deliberately left to pulpcore
# Example transitive requirements: asgiref, asyncio, aiohttp
"pulpcore>=3.75.0,<3.100",
"pulpcore>=3.85.0,<3.100",
"python-debian>=0.1.44,<0.2.0",
"python-gnupg>=0.5,<0.6",
"jsonschema>=4.6,<5.0",
Expand Down