Skip to content

Commit 9338213

Browse files
committed
Address review: FilterSet catalog params, collapse in FilterSet, tighter docs.
Assisted-By: Cursor
1 parent ccae248 commit 9338213

7 files changed

Lines changed: 127 additions & 153 deletions

File tree

‎CHANGES/1358.feature‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Added repository package catalog and metrics endpoints, plus ``collapse_builds`` and ``base_version`` on the Python package content API. The catalog includes ``last_updated``, ``ordering``, newest-first PEP 440 ``versions``/``latest_releases``, and ``name_normalized`` prefix/substring search (at least 3 characters). A trailing rebuild suffix is ``\.[a-zA-Z]+-[^.]+$`` (for example ``5.3.17.rhlw-00001-n0001`` groups with ``5.3.17``). ``latest_releases[].release`` is that suffix on the newest unit in the group, or empty when the stored version has none. Existing installs pick up access policy for the new actions on migrate unless the policy was customized.
1+
Added repository package catalog and metrics endpoints, plus `collapse_builds` and `base_version` on the Python package content API.

‎CLAUDE.md‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,3 @@ When patchback fails to cherry-pick a PR into an older branch, you need to manua
5858
## Contributing
5959

6060
When preparing to commit and create a PR you **must** follow our [PR checklist](https://pulpproject.org/pulpcore/docs/dev/guides/pull-request-walkthrough/) Important to note is the AI attribution requirement in our commit messages. Also, note that our changelog entries are markdown.
61-
62-
## Catalog `strip_build_suffix` and CI unit tests
63-
64-
CI runs unit tests with ``pytest -p no:pulpcore``. Collection must not import Django-backed modules (``pulp_python.app.utils``, ``catalog``, models, viewsets). Keep ``strip_build_suffix``, ``BUILD_SUFFIX_PATTERN``, ``version_sort_key``, ``normalize_package_index_ordering``, and ``normalize_name_normalized_search`` in ``pulp_python/app/versions.py``. The rebuild suffix is the last dot-segment matching POSIX ``\.[a-zA-Z]+-[^.]+$`` (letters, dash, rest of that segment; not hard-coded to ``rhlw``). Python ``re`` and SQL ``REGEXP_REPLACE`` share ``BUILD_SUFFIX_PATTERN``; ``catalog.py`` may import it. Catalog ``latest_releases`` keeps the newest ``pulp_created`` unit per logical version; ``release`` is ``rebuild_release`` of that stored ``version`` (empty when there is no suffix). Catalog ``name_normalized`` prefix/substring filters lowercase the input, use ``LIKE`` (not ``ILIKE``) against the trigram GIN index, and reject values shorter than 3 characters. Simple-index ``DISTINCT ON (name_normalized)`` must ``ORDER BY name_normalized, name`` so the displayed project name is deterministic when metadata names differ (``msg-parser`` vs ``msg_parser``). Without the secondary sort, ``ensure_simple`` can miss the ``msg-parser`` link even though both files were published.

‎docs/user/guides/catalog.md‎

Lines changed: 33 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
# Browse the package catalog
22

3-
Pulp CLI commands for these endpoints are generated from the OpenAPI spec in a separate package; until that is updated, use HTTP.
3+
The content API lists **one row per file** (wheel, sdist, and so on). Use the repository
4+
package catalog when you want **one row per package name**, for example in a UI that
5+
shows Django once with its versions underneath.
46

5-
The content list (`/pulp/api/v3/content/python/packages/`) returns **one row per distribution file** (wheel, sdist, …). For catalog UIs and automation that need **one row per package name**, plus repository metrics, use the repository package index.
6-
7-
These endpoints default to the **latest complete repository version**. `{pulp_id}` is the repository UUID. Pass `repository_version` (HREF or PRN) to read a specific version of that repository.
7+
Both catalog endpoints default to the repository's latest complete version. Pass
8+
`repository_version` (HREF or PRN) to read an older snapshot. `{pulp_id}` is the
9+
repository UUID.
810

911
## List packages
1012

1113
```bash
1214
http GET "${BASE_ADDR}/pulp/api/v3/repositories/python/python/${REPO_PK}/packages/?limit=10"
1315
```
1416

15-
Pagination `count` is the number of **distinct packages** (`name_normalized`), not files.
16-
17-
Each row includes both a simple version list and per-version metadata:
17+
`count` is the number of distinct packages, not files. Each row looks like:
1818

1919
```json
2020
{
@@ -32,27 +32,24 @@ Each row includes both a simple version list and per-version metadata:
3232
}
3333
```
3434

35-
`set(versions)` is always the same as `set(latest_releases[].version)`. Both lists are newest-first using PEP 440 version order (`1.10` before `1.9` before `1.2`). There is one `latest_releases` entry per **logical version** (after stripping a trailing rebuild suffix `\.[a-zA-Z]+-[^.]+$`), not per wheel or sdist. A rebuild is the last dot-segment that is letters, a dash, then the rest of that segment (for example `5.3.17.rhlw-00001-n0001` → `5.3.17`). Public and predisclosure files of the same `name_normalized` and logical version collapse to that one row.
36-
37-
`version` is that base. `release` is the stripped suffix without the leading dot (`rhlw-00001` or `rhlw-00001-n0001`) of the newest unit (`pulp_created`) in that group, otherwise empty.
38-
39-
`created_at` is when that logical version entered the repository: `RepositoryContent.pulp_created` of the selected newest rebuild, falling back to the content unit's `pulp_created`.
40-
41-
`last_updated` is when the **package** was last updated in this repository version: the latest `RepositoryContent.pulp_created` among **all** Python package units for that `name_normalized` (any rebuild), falling back to the content unit's `pulp_created`. A rebuild of an older version uploaded yesterday updates `last_updated` even if a newer version number already exists.
35+
- `versions` is the list of version numbers, newest first (PEP 440, so `1.10` before `1.9`).
36+
- `latest_releases` is the same versions with extra metadata. `release` is filled when
37+
that version has a rebuild (for example `5.3.17.rhlw-00001` is shown as version
38+
`5.3.17` with `release` `rhlw-00001`); otherwise it is empty.
39+
- `created_at` is when that version was added to the repository.
40+
- `last_updated` is when **any** file for the package last changed in this repository
41+
version, including a rebuild of an older version.
4242

4343
### Ordering
4444

45-
Default order is `name`. Pass `ordering` to change it:
45+
Default order is `name`. Allowed fields: `name`, `name_normalized`, `last_updated`.
46+
Prefix with `-` for descending.
4647

4748
```bash
48-
http GET "${BASE_ADDR}/pulp/api/v3/repositories/python/python/${REPO_PK}/packages/" \
49-
ordering==name
5049
http GET "${BASE_ADDR}/pulp/api/v3/repositories/python/python/${REPO_PK}/packages/" \
5150
ordering==-last_updated
5251
```
5352

54-
Allowed fields: `name`, `name_normalized`, `last_updated`. Prefix with `-` for descending. `last_updated` uses `name` then `name_normalized` as a stable pagination tiebreaker. Unknown fields return 400.
55-
5653
### Name search
5754

5855
```bash
@@ -62,7 +59,9 @@ http GET "${BASE_ADDR}/pulp/api/v3/repositories/python/python/${REPO_PK}/package
6259
name_normalized__icontains==http
6360
```
6461

65-
`name_normalized__istartswith` and `name_normalized__icontains` are case-insensitive: the value is lowercased and matched with `LIKE` against already-canonical `name_normalized`. Each requires **at least 3 characters** (shorter values return 400). `name__istartswith` is still `ILIKE` on the original package name and has no minimum length. Name search belongs on this index, not on the flat content list.
62+
`name_normalized__istartswith` and `name_normalized__icontains` match the PEP 503
63+
normalized name and require at least 3 characters. `name__istartswith` matches the
64+
original project name and has no minimum length.
6665

6766
## Repository metrics
6867

@@ -78,21 +77,19 @@ http GET "${BASE_ADDR}/pulp/api/v3/repositories/python/python/${REPO_PK}/metrics
7877
}
7978
```
8079

81-
Counts use Python package content units in that repository version (not filtered by `packagetype`):
82-
83-
| Field | Identity |
84-
|-------|----------|
85-
| `package_count` | distinct `name_normalized` |
86-
| `version_count` | distinct `(name_normalized, base_version)` after rebuild-suffix strip |
87-
| `build_count` | distinct `(name_normalized, full version)` |
80+
| Field | Meaning |
81+
|-------|---------|
82+
| `package_count` | Distinct packages |
83+
| `version_count` | Distinct packages × versions (rebuilds of the same version count as one) |
84+
| `build_count` | Distinct packages × stored version strings (each rebuild counted) |
8885

89-
Until rebuild suffixes exist, `version_count` equals `build_count`.
86+
Until a repository contains rebuilds, `version_count` equals `build_count`.
9087

91-
## List versions of a package
88+
## List files for a package
9289

93-
Use the existing content API. Pass `packagetype=sdist` for one representative file per PEP version (retry with `packagetype=bdist_wheel` if a release is wheel-only).
94-
95-
`collapse_builds=true` keeps one unit per logical version (`name_normalized` + `base_version`), the one with the latest `pulp_created`. Do not nest rebuilds on this list. Clients can drain Pulp `next` if the page is full.
90+
Use the content API. `packagetype=sdist` returns one sdist per version (retry with
91+
`packagetype=bdist_wheel` if a release is wheel-only). `collapse_builds=true` keeps
92+
the newest rebuild per version so you do not have to page through every rebuild.
9693

9794
```bash
9895
http GET "${BASE_ADDR}/pulp/api/v3/content/python/packages/" \
@@ -102,11 +99,11 @@ http GET "${BASE_ADDR}/pulp/api/v3/content/python/packages/" \
10299
repository_version=="${LATEST_VERSION_HREF}"
103100
```
104101

105-
Every content row includes `base_version` (stripped version; equal to `version` when there is no suffix).
106-
107-
## Get one version
102+
Each content row includes `base_version`: the version without a rebuild suffix
103+
(equal to `version` when there is none).
108104

109-
Omit `collapse_builds`. Filter with `name`, `version`, and `packagetype=sdist`:
105+
To fetch a single version, omit `collapse_builds` and filter by `name`, `version`,
106+
and `packagetype`:
110107

111108
```bash
112109
http GET "${BASE_ADDR}/pulp/api/v3/content/python/packages/" \

‎pulp_python/app/catalog.py‎

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@ def collapse_python_builds(queryset):
3939
row per logical version (not per wheel/sdist) should also filter
4040
``packagetype``.
4141
"""
42+
# DISTINCT ON cannot reuse pulpcore's list prefetches (cloned lookups /
43+
# JOINs). Drop them, collapse, then prefetch artifacts for the reduced set.
4244
return (
4345
queryset.prefetch_related(None)
4446
.annotate(_collapse_base_version=base_version_annotation())
4547
.order_by("name_normalized", "_collapse_base_version", "-pulp_created")
4648
.distinct("name_normalized", "_collapse_base_version")
49+
.prefetch_related("contentartifact_set")
4750
)
4851

4952

@@ -54,22 +57,6 @@ def python_packages_in_version(repository_version):
5457
return PythonPackageContent.objects.filter(pk__in=repository_version.content)
5558

5659

57-
def apply_package_prefix_filters(
58-
queryset,
59-
name_normalized_prefix=None,
60-
name_prefix=None,
61-
name_normalized_contains=None,
62-
):
63-
"""Apply case-insensitive name filters used by the package index."""
64-
if name_normalized_prefix:
65-
queryset = queryset.filter(name_normalized__startswith=name_normalized_prefix)
66-
if name_normalized_contains:
67-
queryset = queryset.filter(name_normalized__contains=name_normalized_contains)
68-
if name_prefix:
69-
queryset = queryset.filter(name__istartswith=name_prefix)
70-
return queryset
71-
72-
7360
def membership_in_version_q(repository, repository_version):
7461
"""Q-object matching RepositoryContent rows present in ``repository_version``."""
7562
return Q(
@@ -129,7 +116,7 @@ def assemble_package_index(content_qs, name_rows, repository, repository_version
129116

130117
newest_units = list(
131118
content_qs.filter(name_normalized__in=names)
132-
.prefetch_related(None)
119+
.prefetch_related(None) # DISTINCT ON; see collapse_python_builds
133120
.annotate(_base_version=base_version_annotation())
134121
.order_by("name_normalized", "_base_version", "-pulp_created")
135122
.distinct("name_normalized", "_base_version")

‎pulp_python/app/serializers.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -695,15 +695,13 @@ class PythonRepositoryPackageSerializer(serializers.Serializer):
695695
versions = serializers.ListField(
696696
child=serializers.CharField(),
697697
help_text=_(
698-
"Distinct logical version keys after rebuild-suffix strip, newest first "
699-
"(PEP 440). The set of values matches latest_releases[].version."
698+
"Distinct logical version keys after rebuild-suffix strip, newest first (PEP 440)."
700699
),
701700
)
702701
latest_releases = PythonPackageReleaseSerializer(
703702
many=True,
704703
help_text=_(
705-
"Newest rebuild per logical version (latest pulp_created), newest version first. "
706-
"set(versions) === set(latest_releases[].version)."
704+
"Newest rebuild per logical version (latest pulp_created), newest version first."
707705
),
708706
)
709707

‎pulp_python/app/versions.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from packaging.version import InvalidVersion, Version
1010

1111
# Last dot-segment is a rebuild if it is letters, dash, rest of that segment.
12-
# POSIX string shared with SQL REGEXP_REPLACE. Not hard-coded to "rhlw".
1312
BUILD_SUFFIX_PATTERN = r"\.[a-zA-Z]+-[^.]+$"
1413
BUILD_SUFFIX_RE = re.compile(BUILD_SUFFIX_PATTERN)
1514

0 commit comments

Comments
 (0)