pulp-python 3.13.4__tar.gz → 3.14.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {pulp_python-3.13.4 → pulp_python-3.14.0}/CHANGES.md +36 -3
  2. {pulp_python-3.13.4 → pulp_python-3.14.0}/PKG-INFO +1 -1
  3. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/__init__.py +1 -1
  4. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/models.py +1 -0
  5. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/pypi/views.py +36 -24
  6. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/utils.py +79 -1
  7. pulp_python-3.14.0/pulp_python/tests/functional/api/test_full_mirror.py +140 -0
  8. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python.egg-info/PKG-INFO +1 -1
  9. {pulp_python-3.13.4 → pulp_python-3.14.0}/pyproject.toml +2 -2
  10. pulp_python-3.13.4/pulp_python/tests/functional/api/test_full_mirror.py +0 -72
  11. {pulp_python-3.13.4 → pulp_python-3.14.0}/COMMITMENT +0 -0
  12. {pulp_python-3.13.4 → pulp_python-3.14.0}/COPYRIGHT +0 -0
  13. {pulp_python-3.13.4 → pulp_python-3.14.0}/LICENSE +0 -0
  14. {pulp_python-3.13.4 → pulp_python-3.14.0}/MANIFEST.in +0 -0
  15. {pulp_python-3.13.4 → pulp_python-3.14.0}/README.md +0 -0
  16. {pulp_python-3.13.4 → pulp_python-3.14.0}/functest_requirements.txt +0 -0
  17. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/__init__.py +0 -0
  18. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/global_access_conditions.py +0 -0
  19. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/management/__init__.py +0 -0
  20. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/management/commands/__init__.py +0 -0
  21. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
  22. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0001_initial.py +0 -0
  23. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
  24. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
  25. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
  26. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
  27. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
  28. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
  29. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
  30. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
  31. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
  32. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
  33. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
  34. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
  35. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/migrations/__init__.py +0 -0
  36. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/modelresource.py +0 -0
  37. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/pypi/__init__.py +0 -0
  38. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/pypi/serializers.py +0 -0
  39. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/replica.py +0 -0
  40. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/serializers.py +0 -0
  41. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/settings.py +0 -0
  42. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/tasks/__init__.py +0 -0
  43. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/tasks/publish.py +0 -0
  44. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/tasks/sync.py +0 -0
  45. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/tasks/upload.py +0 -0
  46. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/urls.py +0 -0
  47. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/viewsets.py +0 -0
  48. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
  49. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
  50. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
  51. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/pytest_plugin.py +0 -0
  52. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/__init__.py +0 -0
  53. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/__init__.py +0 -0
  54. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/__init__.py +0 -0
  55. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
  56. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
  57. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +0 -0
  58. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
  59. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
  60. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_domains.py +0 -0
  61. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
  62. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
  63. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
  64. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
  65. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_repair.py +0 -0
  66. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/api/test_sync.py +0 -0
  67. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/constants.py +0 -0
  68. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/functional/utils.py +0 -0
  69. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/unit/__init__.py +0 -0
  70. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python/tests/unit/test_models.py +0 -0
  71. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python.egg-info/SOURCES.txt +0 -0
  72. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python.egg-info/dependency_links.txt +0 -0
  73. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python.egg-info/entry_points.txt +0 -0
  74. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python.egg-info/requires.txt +0 -0
  75. {pulp_python-3.13.4 → pulp_python-3.14.0}/pulp_python.egg-info/top_level.txt +0 -0
  76. {pulp_python-3.13.4 → pulp_python-3.14.0}/setup.cfg +0 -0
  77. {pulp_python-3.13.4 → pulp_python-3.14.0}/test_requirements.txt +0 -0
  78. {pulp_python-3.13.4 → pulp_python-3.14.0}/unittest_requirements.txt +0 -0
@@ -8,9 +8,21 @@
8
8
 
9
9
  [//]: # (towncrier release notes start)
10
10
 
11
- ## 3.13.4 (2025-04-10) {: #3.13.4 }
11
+ ## 3.14.0 (2025-04-10) {: #3.14.0 }
12
12
 
13
- #### Misc {: #3.13.4-misc }
13
+ #### Features {: #3.14.0-feature }
14
+
15
+ - Pull-through caching now respects the include/exclude filters on the upstream remote.
16
+ [#706](https://github.com/pulp/pulp_python/issues/706)
17
+ - Added support for automatically saving pull-through content to a repository.
18
+ Requires pulpcore 3.74 or later.
19
+ [#815](https://github.com/pulp/pulp_python/issues/815)
20
+
21
+ #### Bugfixes {: #3.14.0-bugfix }
22
+
23
+ - Fixed a proxy sync regression introduced in 3.13.0.
24
+
25
+ #### Misc {: #3.14.0-misc }
14
26
 
15
27
  - [#809](https://github.com/pulp/pulp_python/issues/809)
16
28
 
@@ -68,6 +80,14 @@ No significant changes.
68
80
 
69
81
  ---
70
82
 
83
+ ## 3.12.6 (2025-02-26) {: #3.12.6 }
84
+
85
+ #### Misc {: #3.12.6-misc }
86
+
87
+ -
88
+
89
+ ---
90
+
71
91
  ## 3.12.5 (2024-10-25) {: #3.12.5 }
72
92
 
73
93
  #### Bugfixes {: #3.12.5-bugfix }
@@ -142,7 +162,20 @@ No significant changes.
142
162
 
143
163
  ---
144
164
 
145
- ## 3.11.3 (2024-08-21) {: #3.11.3 }
165
+ ## 3.11.4 (2025-02-20) {: #3.11.4 }
166
+
167
+ #### Bugfixes {: #3.11.4-bugfix }
168
+
169
+ - Fixed the JSONField specification so it doesn't break ruby bindings.
170
+ See context [here](https://github.com/pulp/pulp_rpm/issues/3639).
171
+
172
+ #### Misc {: #3.11.4-misc }
173
+
174
+ -
175
+
176
+ ---
177
+
178
+ # ## 3.11.3 (2024-08-21) {: #3.11.3 }
146
179
 
147
180
  #### Bugfixes {: #3.11.3-bugfix }
148
181
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.13.4
3
+ Version: 3.14.0
4
4
  Summary: pulp-python plugin for the Pulp Project
5
5
  Author-email: Pulp Team <pulp-list@redhat.com>
6
6
  Project-URL: Homepage, https://pulpproject.org
@@ -10,7 +10,7 @@ class PulpPythonPluginAppConfig(PulpPluginAppConfig):
10
10
 
11
11
  name = "pulp_python.app"
12
12
  label = "python"
13
- version = "3.13.4"
13
+ version = "3.14.0"
14
14
  python_package_name = "pulp-python"
15
15
  domain_compatible = True
16
16
 
@@ -275,6 +275,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
275
275
  TYPE = "python"
276
276
  CONTENT_TYPES = [PythonPackageContent]
277
277
  REMOTE_TYPES = [PythonRemote]
278
+ PULL_THROUGH_SUPPORTED = True
278
279
 
279
280
  autopublish = models.BooleanField(default=False)
280
281
 
@@ -1,6 +1,7 @@
1
+ import json
1
2
  import logging
2
- import requests
3
3
 
4
+ from aiohttp.client_exceptions import ClientError
4
5
  from rest_framework.viewsets import ViewSet
5
6
  from rest_framework.response import Response
6
7
  from django.core.exceptions import ObjectDoesNotExist
@@ -15,7 +16,8 @@ from django.http.response import (
15
16
  Http404,
16
17
  HttpResponseForbidden,
17
18
  HttpResponseBadRequest,
18
- StreamingHttpResponse
19
+ StreamingHttpResponse,
20
+ HttpResponse,
19
21
  )
20
22
  from drf_spectacular.utils import extend_schema
21
23
  from dynaconf import settings
@@ -23,11 +25,12 @@ from itertools import chain
23
25
  from packaging.utils import canonicalize_name
24
26
  from urllib.parse import urljoin, urlparse, urlunsplit
25
27
  from pathlib import PurePath
26
- from pypi_simple import parse_links_stream_response
28
+ from pypi_simple import ACCEPT_JSON_PREFERRED, ProjectPage
27
29
 
28
30
  from pulpcore.plugin.viewsets import OperationPostponedResponse
29
31
  from pulpcore.plugin.tasking import dispatch
30
32
  from pulpcore.plugin.util import get_domain
33
+ from pulpcore.plugin.exceptions import TimeoutException
31
34
  from pulp_python.app.models import (
32
35
  PythonDistribution,
33
36
  PythonPackageContent,
@@ -37,7 +40,7 @@ from pulp_python.app.pypi.serializers import (
37
40
  SummarySerializer,
38
41
  PackageMetadataSerializer,
39
42
  PackageUploadSerializer,
40
- PackageUploadTaskSerializer
43
+ PackageUploadTaskSerializer,
41
44
  )
42
45
  from pulp_python.app.utils import (
43
46
  write_simple_index,
@@ -45,6 +48,7 @@ from pulp_python.app.utils import (
45
48
  python_content_to_json,
46
49
  PYPI_LAST_SERIAL,
47
50
  PYPI_SERIAL_CONSTANT,
51
+ get_remote_package_filter,
48
52
  )
49
53
 
50
54
  from pulp_python.app import tasks
@@ -233,27 +237,36 @@ class SimpleView(PackageUploadMixin, ViewSet):
233
237
 
234
238
  def pull_through_package_simple(self, package, path, remote):
235
239
  """Gets the package's simple page from remote."""
236
- def parse_url(link):
237
- parsed = urlparse(link.url)
238
- digest, _, value = parsed.fragment.partition('=')
240
+ def parse_package(release_package):
241
+ parsed = urlparse(release_package.url)
239
242
  stripped_url = urlunsplit(chain(parsed[:3], ("", "")))
240
- redirect = f'{path}/{link.text}?redirect={stripped_url}'
241
- d_url = urljoin(self.base_content_url, redirect)
242
- return link.text, d_url, value if digest == 'sha256' else ''
243
+ redirect_path = f'{path}/{release_package.filename}?redirect={stripped_url}'
244
+ d_url = urljoin(self.base_content_url, redirect_path)
245
+ return release_package.filename, d_url, release_package.digests.get("sha256", "")
246
+
247
+ rfilter = get_remote_package_filter(remote)
248
+ if not rfilter.filter_project(package):
249
+ raise Http404(f"{package} does not exist.")
243
250
 
244
251
  url = remote.get_remote_artifact_url(f'simple/{package}/')
245
- kwargs = {}
246
- if proxy_url := remote.proxy_url:
247
- if remote.proxy_username or remote.proxy_password:
248
- parsed_proxy = urlparse(proxy_url)
249
- netloc = f"{remote.proxy_username}:{remote.proxy_password}@{parsed_proxy.netloc}"
250
- proxy_url = urlunsplit((parsed_proxy.scheme, netloc, "", "", ""))
251
- kwargs["proxies"] = {"http": proxy_url, "https": proxy_url}
252
-
253
- response = requests.get(url, stream=True, **kwargs)
254
- links = parse_links_stream_response(response)
255
- packages = (parse_url(link) for link in links)
256
- return StreamingHttpResponse(write_simple_detail(package, packages, streamed=True))
252
+ remote.headers = remote.headers or []
253
+ remote.headers.append({"Accept": ACCEPT_JSON_PREFERRED})
254
+ downloader = remote.get_downloader(url=url, max_retries=1)
255
+ try:
256
+ d = downloader.fetch()
257
+ except ClientError:
258
+ return HttpResponse(f"Failed to fetch {package} from {remote.url}.", status=502)
259
+ except TimeoutException:
260
+ return HttpResponse(f"{remote.url} timed out while fetching {package}.", status=504)
261
+
262
+ if d.headers["content-type"] == "application/vnd.pypi.simple.v1+json":
263
+ page = ProjectPage.from_json_data(json.load(open(d.path, "rb")), base_url=remote.url)
264
+ else:
265
+ page = ProjectPage.from_html(package, open(d.path, "rb").read(), base_url=remote.url)
266
+ packages = [
267
+ parse_package(p) for p in page.packages if rfilter.filter_release(package, p.version)
268
+ ]
269
+ return HttpResponse(write_simple_detail(package, packages))
257
270
 
258
271
  @extend_schema(operation_id="pypi_simple_package_read", summary="Get package simple page")
259
272
  def retrieve(self, request, path, package):
@@ -262,8 +275,7 @@ class SimpleView(PackageUploadMixin, ViewSet):
262
275
  # Should I redirect if the normalized name is different?
263
276
  normalized = canonicalize_name(package)
264
277
  if self.distribution.remote:
265
- if not repo_ver or not content.filter(name__normalize=normalized).exists():
266
- return self.pull_through_package_simple(normalized, path, self.distribution.remote)
278
+ return self.pull_through_package_simple(normalized, path, self.distribution.remote)
267
279
  if self.should_redirect(repo_version=repo_ver):
268
280
  return redirect(urljoin(self.base_content_url, f'{path}/simple/{normalized}/'))
269
281
  packages = (
@@ -7,7 +7,8 @@ from collections import defaultdict
7
7
  from django.conf import settings
8
8
  from jinja2 import Template
9
9
  from packaging.utils import canonicalize_name
10
- from packaging.version import parse
10
+ from packaging.requirements import Requirement
11
+ from packaging.version import parse, InvalidVersion
11
12
 
12
13
 
13
14
  PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL"
@@ -356,3 +357,80 @@ def write_simple_detail(project_name, project_packages, streamed=False):
356
357
  detail = Template(simple_detail_template)
357
358
  context = {"project_name": project_name, "project_packages": project_packages}
358
359
  return detail.stream(**context) if streamed else detail.render(**context)
360
+
361
+
362
+ class PackageIncludeFilter:
363
+ """A special class to help filter Package's based on a remote's include/exclude"""
364
+
365
+ def __init__(self, remote):
366
+ self.remote = remote.cast()
367
+ self._filter_includes = self._parse_packages(self.remote.includes)
368
+ self._filter_excludes = self._parse_packages(self.remote.excludes)
369
+
370
+ def _parse_packages(self, packages):
371
+ config = defaultdict(lambda: defaultdict(list))
372
+ for value in packages:
373
+ requirement = Requirement(value)
374
+ requirement.name = canonicalize_name(requirement.name)
375
+ if requirement.specifier:
376
+ requirement.specifier.prereleases = True
377
+ config["range"][requirement.name].append(requirement)
378
+ else:
379
+ config["full"][requirement.name].append(requirement)
380
+ return config
381
+
382
+ def filter_project(self, project_name):
383
+ """Return true/false if project_name would be allowed through remote's filters."""
384
+ project_name = canonicalize_name(project_name)
385
+ include_full = self._filter_includes.get("full", {})
386
+ include_range = self._filter_includes.get("range", {})
387
+ include = set(include_range.keys()).union(include_full.keys())
388
+ if include and project_name not in include:
389
+ return False
390
+
391
+ exclude_full = self._filter_excludes.get("full", {})
392
+ if project_name in exclude_full:
393
+ return False
394
+
395
+ return True
396
+
397
+ def filter_release(self, project_name, version):
398
+ """Returns true/false if release would be allowed through remote's filters."""
399
+ project_name = canonicalize_name(project_name)
400
+ if not self.filter_project(project_name):
401
+ return False
402
+
403
+ try:
404
+ version = parse(version)
405
+ except InvalidVersion:
406
+ return False
407
+
408
+ include_range = self._filter_includes.get("range", {})
409
+ if project_name in include_range:
410
+ for req in include_range[project_name]:
411
+ if version in req.specifier:
412
+ break
413
+ else:
414
+ return False
415
+
416
+ exclude_range = self._filter_excludes.get("range", {})
417
+ if project_name in exclude_range:
418
+ for req in exclude_range[project_name]:
419
+ if version in req.specifier:
420
+ return False
421
+
422
+ return True
423
+
424
+
425
+ _remote_filters = {}
426
+
427
+
428
+ def get_remote_package_filter(remote):
429
+ if date_filter_tuple := _remote_filters.get(remote.pulp_id):
430
+ last_update, rfilter = date_filter_tuple
431
+ if last_update == remote.pulp_last_updated:
432
+ return rfilter
433
+
434
+ rfilter = PackageIncludeFilter(remote)
435
+ _remote_filters[remote.pulp_id] = (remote.pulp_last_updated, rfilter)
436
+ return rfilter
@@ -0,0 +1,140 @@
1
+ import pytest
2
+ import requests
3
+ import subprocess
4
+
5
+ from pulp_python.tests.functional.constants import (
6
+ PYPI_URL,
7
+ PYTHON_XS_FIXTURE_CHECKSUMS,
8
+ PYTHON_SM_PROJECT_SPECIFIER,
9
+ PYTHON_SM_FIXTURE_RELEASES,
10
+ )
11
+
12
+ from pypi_simple import ProjectPage
13
+ from packaging.version import parse
14
+ from urllib.parse import urljoin, urlsplit
15
+ from random import sample
16
+
17
+
18
+ def test_pull_through_install(
19
+ python_bindings, python_remote_factory, python_distribution_factory, delete_orphans_pre
20
+ ):
21
+ """Tests that a pull-through distro can be installed from."""
22
+ remote = python_remote_factory(url=PYPI_URL, includes=[])
23
+ distro = python_distribution_factory(remote=remote.pulp_href)
24
+ PACKAGE = "pulpcore-releases"
25
+
26
+ # Check if already installed
27
+ stdout = subprocess.run(("pip", "list"), capture_output=True).stdout.decode("utf-8")
28
+ if stdout.find(PACKAGE) != -1:
29
+ subprocess.run(("pip", "uninstall", PACKAGE, "-y"))
30
+
31
+ # Perform pull-through install
32
+ host = urlsplit(distro.base_url).hostname
33
+ url = f"{distro.base_url}simple/"
34
+ cmd = ("pip", "install", "--trusted-host", host, "-i", url, PACKAGE)
35
+ subprocess.run(cmd, check=True)
36
+
37
+ stdout = subprocess.run(("pip", "list"), capture_output=True).stdout.decode("utf-8")
38
+ assert stdout.find(PACKAGE) != -1
39
+ subprocess.run(("pip", "uninstall", PACKAGE, "-y"))
40
+ content = python_bindings.ContentPackagesApi.list(name=PACKAGE)
41
+ assert content.count == 1
42
+
43
+
44
+ @pytest.mark.parallel
45
+ def test_pull_through_simple(python_remote_factory, python_distribution_factory, pulp_content_url):
46
+ """Tests that the simple page is properly modified when requesting a pull-through."""
47
+ remote = python_remote_factory(url=PYPI_URL, includes=["shelf-reader"])
48
+ distro = python_distribution_factory(remote=remote.pulp_href)
49
+
50
+ url = f"{distro.base_url}simple/shelf-reader/"
51
+ project_page = ProjectPage.from_response(requests.get(url), "shelf-reader")
52
+
53
+ assert len(project_page.packages) == 2
54
+ for package in project_page.packages:
55
+ assert package.filename in PYTHON_XS_FIXTURE_CHECKSUMS
56
+ relative_path = f"{distro.base_path}/{package.filename}?redirect="
57
+ assert urljoin(pulp_content_url, relative_path) in package.url
58
+ assert PYTHON_XS_FIXTURE_CHECKSUMS[package.filename] == package.digests["sha256"]
59
+
60
+
61
+ @pytest.mark.parallel
62
+ def test_pull_through_filter(python_remote_factory, python_distribution_factory):
63
+ """Tests that pull-through respects the includes/excludes filter on the remote."""
64
+ remote = python_remote_factory(url=PYPI_URL, includes=["shelf-reader"])
65
+ distro = python_distribution_factory(remote=remote.pulp_href)
66
+
67
+ r = requests.get(f"{distro.base_url}simple/pulpcore/")
68
+ assert r.status_code == 404
69
+ assert r.json() == {"detail": "pulpcore does not exist."}
70
+
71
+ r = requests.get(f"{distro.base_url}simple/shelf-reader/")
72
+ assert r.status_code == 200
73
+
74
+ # Test complex include specifiers
75
+ remote = python_remote_factory(includes=PYTHON_SM_PROJECT_SPECIFIER)
76
+ distro = python_distribution_factory(remote=remote.pulp_href)
77
+ for package, releases in PYTHON_SM_FIXTURE_RELEASES.items():
78
+ url = f"{distro.base_url}simple/{package}/"
79
+ project_page = ProjectPage.from_response(requests.get(url), package)
80
+ packages = {p.filename for p in project_page.packages if not parse(p.version).is_prerelease}
81
+ assert packages == set(releases)
82
+
83
+ # Test exclude logic
84
+ remote = python_remote_factory(includes=[], excludes=["django"])
85
+ distro = python_distribution_factory(remote=remote.pulp_href)
86
+
87
+ r = requests.get(f"{distro.base_url}simple/django/")
88
+ assert r.status_code == 404
89
+ assert r.json() == {"detail": "django does not exist."}
90
+
91
+ r = requests.get(f"{distro.base_url}simple/pulpcore/")
92
+ assert r.status_code == 502
93
+ assert r.text == f"Failed to fetch pulpcore from {remote.url}."
94
+
95
+ r = requests.get(f"{distro.base_url}simple/shelf-reader/")
96
+ assert r.status_code == 200
97
+
98
+
99
+ @pytest.mark.parallel
100
+ def test_pull_through_with_repo(
101
+ python_repo_factory,
102
+ python_remote_factory,
103
+ python_distribution_factory,
104
+ python_bindings,
105
+ pulpcore_bindings,
106
+ monitor_task,
107
+ has_pulp_plugin,
108
+ ):
109
+ """Tests that content is saved to repository."""
110
+ remote = python_remote_factory(includes=[])
111
+ repo = python_repo_factory()
112
+ distro = python_distribution_factory(repository=repo.pulp_href, remote=remote.pulp_href)
113
+
114
+ # Perform a download of aiohttp to ensure it's saved to the repo
115
+ url = urljoin(distro.base_url, "simple/aiohttp/")
116
+ project_page = ProjectPage.from_response(requests.get(url), "aiohttp")
117
+ for package in sample(project_page.packages, 3):
118
+ assert "?redirect=" in package.url
119
+ r = requests.get(package.url)
120
+ assert r.status_code == 200
121
+
122
+ if has_pulp_plugin("core", max="3.73"):
123
+ pytest.skip("Pull-through repository save added in 3.74")
124
+
125
+ tasks = pulpcore_bindings.TasksApi.list(reserved_resources=repo.prn)
126
+ assert tasks.count == 3
127
+
128
+ for task in tasks.results:
129
+ monitor_task(task.pulp_href)
130
+
131
+ repo = python_bindings.RepositoriesPythonApi.read(repo.pulp_href)
132
+ assert repo.latest_version_href[-2] == "3"
133
+ content = python_bindings.ContentPackagesApi.list(repository_version=repo.latest_version_href)
134
+ assert content.count == 3
135
+
136
+ # Check that getting content that is already present doesn't trigger a task
137
+ r = requests.get(package.url)
138
+ assert r.status_code == 200
139
+ tasks = pulpcore_bindings.TasksApi.list(reserved_resources=repo.prn)
140
+ assert tasks.count == 3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.13.4
3
+ Version: 3.14.0
4
4
  Summary: pulp-python plugin for the Pulp Project
5
5
  Author-email: Pulp Team <pulp-list@redhat.com>
6
6
  Project-URL: Homepage, https://pulpproject.org
@@ -7,7 +7,7 @@ build-backend = 'setuptools.build_meta'
7
7
 
8
8
  [project]
9
9
  name = "pulp-python"
10
- version = "3.13.4"
10
+ version = "3.14.0"
11
11
  description = "pulp-python plugin for the Pulp Project"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -77,7 +77,7 @@ ignore = [
77
77
  [tool.bumpversion]
78
78
  # This section is managed by the plugin template. Do not edit manually.
79
79
 
80
- current_version = "3.13.4"
80
+ current_version = "3.14.0"
81
81
  commit = false
82
82
  tag = false
83
83
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<alpha>0a)?(?P<patch>\\d+)(\\.(?P<release>[a-z]+))?"
@@ -1,72 +0,0 @@
1
- import pytest
2
- import requests
3
- import subprocess
4
-
5
- from pulp_python.tests.functional.constants import (
6
- PYPI_URL,
7
- PYTHON_XS_FIXTURE_CHECKSUMS,
8
- )
9
-
10
- from pypi_simple import ProjectPage
11
- from urllib.parse import urljoin, urlsplit
12
-
13
-
14
- def test_pull_through_install(
15
- python_bindings, python_remote_factory, python_distribution_factory, delete_orphans_pre
16
- ):
17
- """Tests that a pull-through distro can be installed from."""
18
- remote = python_remote_factory(url=PYPI_URL)
19
- distro = python_distribution_factory(remote=remote.pulp_href)
20
- PACKAGE = "pulpcore-releases"
21
-
22
- # Check if already installed
23
- stdout = subprocess.run(("pip", "list"), capture_output=True).stdout.decode("utf-8")
24
- if stdout.find(PACKAGE) != -1:
25
- subprocess.run(("pip", "uninstall", PACKAGE, "-y"))
26
-
27
- # Perform pull-through install
28
- host = urlsplit(distro.base_url).hostname
29
- url = f"{distro.base_url}simple/"
30
- cmd = ("pip", "install", "--trusted-host", host, "-i", url, PACKAGE)
31
- subprocess.run(cmd, check=True)
32
-
33
- stdout = subprocess.run(("pip", "list"), capture_output=True).stdout.decode("utf-8")
34
- assert stdout.find(PACKAGE) != -1
35
- subprocess.run(("pip", "uninstall", PACKAGE, "-y"))
36
- content = python_bindings.ContentPackagesApi.list(name=PACKAGE)
37
- assert content.count == 1
38
-
39
-
40
- @pytest.mark.parallel
41
- def test_pull_through_simple(python_remote_factory, python_distribution_factory, pulp_content_url):
42
- """Tests that the simple page is properly modified when requesting a pull-through."""
43
- remote = python_remote_factory(url=PYPI_URL)
44
- distro = python_distribution_factory(remote=remote.pulp_href)
45
-
46
- url = f"{distro.base_url}simple/shelf-reader/"
47
- project_page = ProjectPage.from_response(requests.get(url), "shelf-reader")
48
-
49
- assert len(project_page.packages) == 2
50
- for package in project_page.packages:
51
- assert package.filename in PYTHON_XS_FIXTURE_CHECKSUMS
52
- relative_path = f"{distro.base_path}/{package.filename}?redirect="
53
- assert urljoin(pulp_content_url, relative_path) in package.url
54
- assert PYTHON_XS_FIXTURE_CHECKSUMS[package.filename] == package.digests["sha256"]
55
-
56
-
57
- @pytest.mark.parallel
58
- def test_pull_through_with_repo(
59
- python_repo_with_sync, python_remote_factory, python_distribution_factory
60
- ):
61
- """Tests that if content is already in repository, pull-through isn't used."""
62
- remote = python_remote_factory()
63
- repo = python_repo_with_sync(remote)
64
- distro = python_distribution_factory(repository=repo.pulp_href, remote=remote.pulp_href)
65
-
66
- url = urljoin(distro.base_url, "simple/shelf-reader/")
67
- project_page = ProjectPage.from_response(requests.get(url), "shelf-reader")
68
-
69
- assert len(project_page.packages) == 2
70
- for package in project_page.packages:
71
- assert package.filename in PYTHON_XS_FIXTURE_CHECKSUMS
72
- assert "?redirect=" not in package.url
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes