From f6eebf3e4e2eff26b34d1e1a291a94d335b088d8 Mon Sep 17 00:00:00 2001 From: Rohan McGovern Date: Tue, 24 Aug 2021 07:47:32 +1000 Subject: [PATCH] Introduce Criteria.with_unit_type It was already possible for callers to search for units of a specific type by doing, for example: Criteria.with_field("content_type_id", Matcher.in_(["rpm", "srpm"])) The problem with this is that it requires the caller to know which values of content_type_id map to each model class such as RpmUnit. The library already knows this implementation detail internally and it's best to avoid duplicating it also in the user of the library. This commit adds a helper which accepts Unit classes as input, as in: Criteria.with_unit_type(RpmUnit) This allows the mapping between unit subclasses and content types to remain fully encapsulated within this library. --- pubtools/pulplib/_impl/client/search.py | 2 +- pubtools/pulplib/_impl/criteria.py | 37 +++++++++++- pubtools/pulplib/_impl/model/unit/__init__.py | 2 +- pubtools/pulplib/_impl/model/unit/base.py | 12 ++++ setup.py | 2 +- tests/client/test_client.py | 57 +++++++++++++++++++ tests/criteria/test_criteria.py | 7 +++ tests/fake/test_fake_search_content.py | 15 +++++ 8 files changed, 130 insertions(+), 4 deletions(-) diff --git a/pubtools/pulplib/_impl/client/search.py b/pubtools/pulplib/_impl/client/search.py index 4a9b6cd6..219e72a6 100644 --- a/pubtools/pulplib/_impl/client/search.py +++ b/pubtools/pulplib/_impl/client/search.py @@ -92,7 +92,7 @@ def accumulate_from_match(self, match_expr): raise ValueError( ( "Can't serialize criteria for Pulp query; too complicated. " - "Try simplifying the query with respect to content_type_id." + "Try simplifying the query with respect to unit_type/content_type_id." ) ) diff --git a/pubtools/pulplib/_impl/criteria.py b/pubtools/pulplib/_impl/criteria.py index bf7b0e07..8bea17c1 100644 --- a/pubtools/pulplib/_impl/criteria.py +++ b/pubtools/pulplib/_impl/criteria.py @@ -16,6 +16,8 @@ from pubtools.pulplib._impl import compat_attr as attr +from .model.unit import type_ids_for_class + class Criteria(object): """Represents a Pulp search criteria. @@ -24,7 +26,7 @@ class Criteria(object): or used directly. Instances of this class should be obtained and composed by calls to the documented class methods. - Example: + Example - searching a repository: .. code-block:: python # With Pulp 2.x / mongo, this is roughly equivalent @@ -40,6 +42,18 @@ class Criteria(object): # criteria may now be used with client to execute a search repos = client.search_repository(crit) + + Example - searching across all repos for a specific content type: + .. code-block:: python + + crit = Criteria.and_( + Criteria.with_unit_type(RpmUnit), + Criteria.with_field("sha256sum", Matcher.in_([ + "49ae93732fcf8d63fe1cce759664982dbd5b23161f007dba8561862adc96d063", + "6b30e91df993d96df0bef0f9d232d1068fa2f7055f13650208d77b43cd7c99f6"]))) + + # Will find RpmUnit instances with above sums + units = client.search_content(crit) """ exists = object() @@ -87,6 +101,27 @@ def with_field(cls, field_name, field_value): """ return FieldMatchCriteria(field_name, field_value) + @classmethod + def with_unit_type(cls, unit_type): + """Args: + unit_type (class) + A subclass of :class:`~pubtools.pulplib.Unit`. + + Returns: + Criteria + criteria for finding units of type ``unit_type``. + + .. versionadded:: 2.14.0 + """ + + # This is just a thin wrapper for searching on content_type_id which allows + # the caller to avoid having to handle the (unit class <=> type id) mapping. + type_ids = type_ids_for_class(unit_type) + if not type_ids: + raise TypeError("Expected a Unit type, got: %s" % repr(unit_type)) + + return FieldMatchCriteria("content_type_id", Matcher.in_(type_ids)) + @classmethod def with_field_in(cls, field_name, field_value): warnings.warn( diff --git a/pubtools/pulplib/_impl/model/unit/__init__.py b/pubtools/pulplib/_impl/model/unit/__init__.py index adbf66b4..c371021f 100644 --- a/pubtools/pulplib/_impl/model/unit/__init__.py +++ b/pubtools/pulplib/_impl/model/unit/__init__.py @@ -1,4 +1,4 @@ -from .base import Unit +from .base import Unit, type_ids_for_class from .file import FileUnit from .rpm import RpmUnit from .modulemd import ModulemdUnit diff --git a/pubtools/pulplib/_impl/model/unit/base.py b/pubtools/pulplib/_impl/model/unit/base.py index 70623a21..75d7d5e5 100644 --- a/pubtools/pulplib/_impl/model/unit/base.py +++ b/pubtools/pulplib/_impl/model/unit/base.py @@ -19,6 +19,18 @@ def decorate(klass): return decorate +def type_ids_for_class(unit_class): + # Given a concrete Unit subclass, returns those Pulp type id(s) + # which may be used to find/load an object of that class. + out = [] + + for pulp_type, klass in UNIT_CLASSES.items(): + if klass is unit_class: + out.append(pulp_type) + + return sorted(out) + + @attr.s(kw_only=True, frozen=True) class Unit(PulpObject): """Represents a Pulp unit (a single piece of content). diff --git a/setup.py b/setup.py index 8dc6ae4a..0fa6f036 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_requirements(): setup( name="pubtools-pulplib", - version="2.13.0", + version="2.14.0", packages=find_packages(exclude=["tests"]), package_data={"pubtools.pulplib._impl.schema": ["*.yaml"]}, url="https://github.com/release-engineering/pubtools-pulplib", diff --git a/tests/client/test_client.py b/tests/client/test_client.py index f35d639a..683d8d8d 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -620,3 +620,60 @@ def test_can_search_content_pagination(client, requests_mocker): assert [h.url for h in requests_mocker.request_history].count( "https://pulp.example.com/pulp/api/v2/content/units/srpm/search/" ) == 2 + + +def test_can_search_content_by_type(client, requests_mocker): + """search_content can search for a specified unit type.""" + requests_mocker.get( + "https://pulp.example.com/pulp/api/v2/plugins/types/", + json=[{"id": "rpm"}, {"id": "srpm"}, {"id": "iso"}], + ) + # Note although "iso" is supported, we don't mock the search URL for + # that content type, thus proving that we don't query it if we search + # for a specific unit_type + requests_mocker.post( + "https://pulp.example.com/pulp/api/v2/content/units/rpm/search/", + json=RPM_TEST_UNITS, + ) + requests_mocker.post( + "https://pulp.example.com/pulp/api/v2/content/units/srpm/search/", + json=SRPM_TEST_UNITS, + ) + + units = client.search_content(Criteria.with_unit_type(RpmUnit)) + + # It should have returned the expected units + assert sorted(units) == [ + RpmUnit( + content_type_id="srpm", + sha256sum="4f5a3a0da6f404f6d9988987cd75f13982bd655a0a4f692406611afbbc597679", + arch="src", + epoch="0", + name="glibc", + release="2.57.el4.1", + repository_memberships=["fake-repository-id-3"], + sourcerpm=None, + version="2.3.4", + ), + RpmUnit( + sha256sum="4f5a3a0da6f404f6d9988987cd75f13982bd655a0a4f692406611afbbc597679", + arch="ia64", + epoch="0", + name="glibc-headers", + release="2.57.el4.1", + repository_memberships=["fake-repository-id-3"], + sourcerpm="glibc-2.3.4-2.57.el4.1.src.rpm", + version="2.3.4", + ), + RpmUnit( + sha1sum="ca995eb1a635c97393466f67aaec8e9e753b8ed5", + sha256sum="1c4baac658fd56e6ec9cca37f440a4bd8c9c0b02a21f41b30b8ea17b402a1907", + arch="i386", + epoch="0", + name="gnu-efi-debuginfo", + release="1.1", + repository_memberships=["fake-repository-id-1", "fake-repository-id-2"], + sourcerpm="gnu-efi-3.0c-1.1.src.rpm", + version="3.0c", + ), + ] diff --git a/tests/criteria/test_criteria.py b/tests/criteria/test_criteria.py index e1088530..cd16457a 100644 --- a/tests/criteria/test_criteria.py +++ b/tests/criteria/test_criteria.py @@ -13,3 +13,10 @@ def test_field_in_str_invalid(): with pytest.raises(ValueError) as exc_info: Criteria.with_field_in("x", "someval") assert "Must be an iterable: 'someval'" in str(exc_info.value) + + +def test_unit_type_invalid(): + """Criteria.with_unit_type raises if provided value isn't a unit subclass.""" + with pytest.raises(TypeError) as exc_info: + Criteria.with_unit_type([1, 2, 3]) + assert "Expected a Unit type, got: [1, 2, 3]" in str(exc_info.value) diff --git a/tests/fake/test_fake_search_content.py b/tests/fake/test_fake_search_content.py index d41003e2..b85f3cb7 100644 --- a/tests/fake/test_fake_search_content.py +++ b/tests/fake/test_fake_search_content.py @@ -120,6 +120,21 @@ def test_search_content_by_unit_field(populated_repo): ] +def test_search_content_by_unit_type(populated_repo): + """search_content on unit_type returns only units of that type""" + + crit = Criteria.with_unit_type(ModulemdUnit) + units = list(populated_repo.search_content(crit)) + assert sorted(units) == [ + ModulemdUnit( + name="module1", stream="s1", version=1234, context="a1b2", arch="x86_64" + ), + ModulemdUnit( + name="module2", stream="s2", version=1234, context="a1b2", arch="x86_64" + ), + ] + + def test_search_content_mixed_fields(populated_repo): """search_content crossing multiple fields and types returns matching units"""