From 96c64f697099b7291b0224375d382156e2ef8abf Mon Sep 17 00:00:00 2001 From: Jitka Halova Date: Tue, 10 Mar 2026 18:31:44 +0100 Subject: [PATCH 1/2] Serve Python content form a repository version closes #1324 Assisted by: Claude Sonnet 4 --- CHANGES/1324.feature | 1 + pulp-glue/src/pulp_glue/python/context.py | 5 + src/pulpcore/cli/python/distribution.py | 93 ++++++++++++++++++- .../scripts/pulp_python/test_distribution.sh | 23 +++++ 4 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 CHANGES/1324.feature diff --git a/CHANGES/1324.feature b/CHANGES/1324.feature new file mode 100644 index 000000000..c805a8a05 --- /dev/null +++ b/CHANGES/1324.feature @@ -0,0 +1 @@ +Added support for serving Python content form a repository version. diff --git a/pulp-glue/src/pulp_glue/python/context.py b/pulp-glue/src/pulp_glue/python/context.py index e05839e12..486ebf98a 100644 --- a/pulp-glue/src/pulp_glue/python/context.py +++ b/pulp-glue/src/pulp_glue/python/context.py @@ -72,6 +72,11 @@ def preprocess_entity(self, body: EntityDefinition, partial: bool = False) -> En body["publication"] = None if "repository" not in body and "publication" in body: body["repository"] = None + + version = body.pop("version", None) + if version is not None: + repository_href = body.pop("repository") + body["repository_version"] = f"{repository_href}versions/{version}/" return body diff --git a/src/pulpcore/cli/python/distribution.py b/src/pulpcore/cli/python/distribution.py index 39f67fb31..bd33b37b9 100644 --- a/src/pulpcore/cli/python/distribution.py +++ b/src/pulpcore/cli/python/distribution.py @@ -1,6 +1,13 @@ +import typing as t + import click -from pulp_glue.common.context import PluginRequirement +from pulp_glue.common.context import ( + EntityDefinition, + EntityFieldDefinition, + PluginRequirement, + PulpEntityContext, +) from pulp_glue.common.i18n import get_translation from pulp_glue.python.context import ( PulpPythonDistributionContext, @@ -20,13 +27,13 @@ label_command, list_command, name_option, + pass_entity_context, pass_pulp_context, pulp_group, pulp_labels_option, pulp_option, resource_option, show_command, - update_command, ) translation = get_translation(__package__) @@ -40,6 +47,7 @@ context_table={"python:python": PulpPythonRepositoryContext}, help=_( "Repository to be used for auto-distributing." + " When used with --version, this will create repository_version instead." " When set, this will unset the 'publication'." " Specified as '[[:]:]' or as href." ), @@ -75,6 +83,12 @@ def distribution(ctx: click.Context, pulp_ctx: PulpCLIContext, /, distribution_t ), ), repository_option, + pulp_option( + "--version", + type=int, + help=_("A repository version number, leave blank for latest."), + needs_plugins=[PluginRequirement("python", specifier=">=3.21.0")], + ), content_guard_option, pulp_option( "--allow-uploads/--block-uploads", @@ -96,8 +110,77 @@ def distribution(ctx: click.Context, pulp_ctx: PulpCLIContext, /, distribution_t distribution.add_command(list_command(decorators=distribution_filter_options)) distribution.add_command(show_command(decorators=lookup_options)) distribution.add_command(create_command(decorators=create_options)) -distribution.add_command( - update_command(decorators=lookup_options + update_options + [click.option("--base-path")]) -) distribution.add_command(destroy_command(decorators=lookup_options)) distribution.add_command(label_command(decorators=nested_lookup_options)) + + +def apply_decorators(decorators_list: list[t.Callable[..., t.Any]]) -> t.Callable[..., t.Any]: + def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + for d in decorators_list: + func = d(func) + return func + + return decorator + + +@distribution.command() +@apply_decorators(lookup_options + update_options + [click.option("--base-path")]) +@pass_entity_context +def update( + distribution_ctx: PulpEntityContext, + /, + publication: str | None, + repository: EntityFieldDefinition, + version: int | None, + content_guard: EntityFieldDefinition, + allow_uploads: bool | None, + remote: EntityFieldDefinition, + pulp_labels: dict[str, str] | None, + base_path: str | None, +) -> None: + """ + Update a Python distribution. + """ + assert isinstance(distribution_ctx, PulpPythonDistributionContext) + + dist_body: EntityDefinition = distribution_ctx.entity + body: EntityDefinition = dict() + + if publication is not None: + body["publication"] = publication + if content_guard is not None: + body["content_guard"] = content_guard + if allow_uploads is not None: + body["allow_uploads"] = allow_uploads + if remote is not None: + body["remote"] = remote + if pulp_labels is not None: + body["pulp_labels"] = pulp_labels + if base_path is not None: + body["base_path"] = base_path + + if repository is not None and isinstance(repository, PulpPythonRepositoryContext): + repo = repository.entity + if version is not None: + if dist_body.get("repository"): + distribution_ctx.update(body={"repository": ""}, non_blocking=True) + body["repository_version"] = f"{repo['versions_href']}{version}/" + else: + if dist_body.get("repository_version"): + distribution_ctx.update(body={"repository_version": ""}, non_blocking=True) + body["repository"] = repo["pulp_href"] + elif version is not None: + if dist_body.get("repository"): + distribution_ctx.update(body={"repository": ""}, non_blocking=True) + body["repository_version"] = f"{dist_body['repository']}versions/{version}/" + elif dist_body.get("repository_version"): + repository_href = dist_body["repository_version"].partition("versions")[0] + body["repository_version"] = f"{repository_href}versions/{version}/" + else: + raise click.ClickException( + _( + "Distribution doesn't have a repository set, " + "please specify the repository to use with --repository" + ) + ) + distribution_ctx.update(body=body) diff --git a/tests/scripts/pulp_python/test_distribution.sh b/tests/scripts/pulp_python/test_distribution.sh index 69695d080..d81b15161 100755 --- a/tests/scripts/pulp_python/test_distribution.sh +++ b/tests/scripts/pulp_python/test_distribution.sh @@ -10,6 +10,7 @@ cleanup() { pulp python repository destroy --name "cli_test_python_distribution_repository" || true pulp python remote destroy --name "cli_test_python_distribution_remote" || true pulp python distribution destroy --name "cli_test_python_distro" || true + pulp python distribution destroy --name "cli_test_python_distro_repo_version" || true } trap cleanup EXIT @@ -42,3 +43,25 @@ expect_succ pulp python distribution update \ --remote "cli_test_python_distribution_remote" expect_succ pulp python distribution destroy --distribution "cli_test_python_distro" + +# Test repository_version functionality +pulp debug has-plugin --name "python" --specifier ">=3.21.0" || exit 0 + +expect_succ pulp python distribution create \ + --name "cli_test_python_distro_repo_version" \ + --base-path "cli_test_python_distro_repo_version" \ + --repository "cli_test_python_distribution_repository" \ + --version 0 +expect_succ pulp python distribution show --distribution "cli_test_python_distro_repo_version" +echo "$OUTPUT" | jq -e '.repository_version | contains("/versions/0/")' +echo "$OUTPUT" | jq -e '.repository == null' + +expect_succ pulp python distribution update \ + --distribution "cli_test_python_distro_repo_version" \ + --repository "cli_test_python_distribution_repository" \ + --version 1 +expect_succ pulp python distribution show --distribution "cli_test_python_distro_repo_version" +echo "$OUTPUT" | jq -e '.repository_version | contains("/versions/1/")' +echo "$OUTPUT" | jq -e '.repository == null' + +expect_succ pulp python distribution destroy --distribution "cli_test_python_distro_repo_version" From ca8d8ea39ca1b0b568cb7b0f179e1922b93165c2 Mon Sep 17 00:00:00 2001 From: Jitka Halova Date: Thu, 12 Mar 2026 20:48:19 +0100 Subject: [PATCH 2/2] WIP --- pulp-glue/src/pulp_glue/python/context.py | 21 ++++- src/pulpcore/cli/python/distribution.py | 86 ++----------------- .../scripts/pulp_python/test_distribution.sh | 14 +++ 3 files changed, 36 insertions(+), 85 deletions(-) diff --git a/pulp-glue/src/pulp_glue/python/context.py b/pulp-glue/src/pulp_glue/python/context.py index 486ebf98a..fd55894b3 100644 --- a/pulp-glue/src/pulp_glue/python/context.py +++ b/pulp-glue/src/pulp_glue/python/context.py @@ -73,10 +73,23 @@ def preprocess_entity(self, body: EntityDefinition, partial: bool = False) -> En if "repository" not in body and "publication" in body: body["repository"] = None - version = body.pop("version", None) - if version is not None: - repository_href = body.pop("repository") - body["repository_version"] = f"{repository_href}versions/{version}/" + if self.pulp_ctx.has_plugin(PluginRequirement("python", specifier=">=3.21.0")): + version = body.pop("version", None) + if version is not None: + if repository_href := body.pop("repository", None): + body["repository_version"] = f"{repository_href}versions/{version}/" + body["repository"] = None + else: + current_entity = getattr(self, "entity", {}) + if repository_href := current_entity.get("repository"): + body["repository_version"] = f"{repository_href}versions/{version}/" + body["repository"] = None + elif repository_version_href := current_entity.get("repository_version"): + repository_href = repository_version_href.partition("versions")[0] + body["repository_version"] = f"{repository_href}versions/{version}/" + elif "repository" in body: + body["repository_version"] = None + return body diff --git a/src/pulpcore/cli/python/distribution.py b/src/pulpcore/cli/python/distribution.py index bd33b37b9..c94e0f7cd 100644 --- a/src/pulpcore/cli/python/distribution.py +++ b/src/pulpcore/cli/python/distribution.py @@ -1,13 +1,6 @@ -import typing as t - import click -from pulp_glue.common.context import ( - EntityDefinition, - EntityFieldDefinition, - PluginRequirement, - PulpEntityContext, -) +from pulp_glue.common.context import PluginRequirement from pulp_glue.common.i18n import get_translation from pulp_glue.python.context import ( PulpPythonDistributionContext, @@ -27,13 +20,13 @@ label_command, list_command, name_option, - pass_entity_context, pass_pulp_context, pulp_group, pulp_labels_option, pulp_option, resource_option, show_command, + update_command, ) translation = get_translation(__package__) @@ -110,77 +103,8 @@ def distribution(ctx: click.Context, pulp_ctx: PulpCLIContext, /, distribution_t distribution.add_command(list_command(decorators=distribution_filter_options)) distribution.add_command(show_command(decorators=lookup_options)) distribution.add_command(create_command(decorators=create_options)) +distribution.add_command( + update_command(decorators=lookup_options + update_options + [click.option("--base-path")]) +) distribution.add_command(destroy_command(decorators=lookup_options)) distribution.add_command(label_command(decorators=nested_lookup_options)) - - -def apply_decorators(decorators_list: list[t.Callable[..., t.Any]]) -> t.Callable[..., t.Any]: - def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: - for d in decorators_list: - func = d(func) - return func - - return decorator - - -@distribution.command() -@apply_decorators(lookup_options + update_options + [click.option("--base-path")]) -@pass_entity_context -def update( - distribution_ctx: PulpEntityContext, - /, - publication: str | None, - repository: EntityFieldDefinition, - version: int | None, - content_guard: EntityFieldDefinition, - allow_uploads: bool | None, - remote: EntityFieldDefinition, - pulp_labels: dict[str, str] | None, - base_path: str | None, -) -> None: - """ - Update a Python distribution. - """ - assert isinstance(distribution_ctx, PulpPythonDistributionContext) - - dist_body: EntityDefinition = distribution_ctx.entity - body: EntityDefinition = dict() - - if publication is not None: - body["publication"] = publication - if content_guard is not None: - body["content_guard"] = content_guard - if allow_uploads is not None: - body["allow_uploads"] = allow_uploads - if remote is not None: - body["remote"] = remote - if pulp_labels is not None: - body["pulp_labels"] = pulp_labels - if base_path is not None: - body["base_path"] = base_path - - if repository is not None and isinstance(repository, PulpPythonRepositoryContext): - repo = repository.entity - if version is not None: - if dist_body.get("repository"): - distribution_ctx.update(body={"repository": ""}, non_blocking=True) - body["repository_version"] = f"{repo['versions_href']}{version}/" - else: - if dist_body.get("repository_version"): - distribution_ctx.update(body={"repository_version": ""}, non_blocking=True) - body["repository"] = repo["pulp_href"] - elif version is not None: - if dist_body.get("repository"): - distribution_ctx.update(body={"repository": ""}, non_blocking=True) - body["repository_version"] = f"{dist_body['repository']}versions/{version}/" - elif dist_body.get("repository_version"): - repository_href = dist_body["repository_version"].partition("versions")[0] - body["repository_version"] = f"{repository_href}versions/{version}/" - else: - raise click.ClickException( - _( - "Distribution doesn't have a repository set, " - "please specify the repository to use with --repository" - ) - ) - distribution_ctx.update(body=body) diff --git a/tests/scripts/pulp_python/test_distribution.sh b/tests/scripts/pulp_python/test_distribution.sh index d81b15161..3a7b94c62 100755 --- a/tests/scripts/pulp_python/test_distribution.sh +++ b/tests/scripts/pulp_python/test_distribution.sh @@ -64,4 +64,18 @@ expect_succ pulp python distribution show --distribution "cli_test_python_distro echo "$OUTPUT" | jq -e '.repository_version | contains("/versions/1/")' echo "$OUTPUT" | jq -e '.repository == null' +expect_succ pulp python distribution update \ + --distribution "cli_test_python_distro_repo_version" \ + --version 0 +expect_succ pulp python distribution show --distribution "cli_test_python_distro_repo_version" +echo "$OUTPUT" | jq -e '.repository_version | contains("/versions/0/")' +echo "$OUTPUT" | jq -e '.repository == null' + +expect_succ pulp python distribution update \ + --distribution "cli_test_python_distro_repo_version" \ + --repository "cli_test_python_distribution_repository" +expect_succ pulp python distribution show --distribution "cli_test_python_distro_repo_version" +echo "$OUTPUT" | jq -e '.repository_version == null' +echo "$OUTPUT" | jq -e '.repository != null' + expect_succ pulp python distribution destroy --distribution "cli_test_python_distro_repo_version"