pulp-python 3.23.1__tar.gz → 3.24.1__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 (94) hide show
  1. {pulp_python-3.23.1 → pulp_python-3.24.1}/CHANGES.md +26 -2
  2. {pulp_python-3.23.1 → pulp_python-3.24.1}/PKG-INFO +1 -1
  3. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/__init__.py +1 -1
  4. pulp_python-3.24.1/pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py +24 -0
  5. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/utils.py +9 -6
  6. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_pypi_simple_api.py +39 -32
  7. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/constants.py +26 -0
  8. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/utils.py +51 -17
  9. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python.egg-info/PKG-INFO +1 -1
  10. {pulp_python-3.23.1 → pulp_python-3.24.1}/pyproject.toml +2 -2
  11. pulp_python-3.23.1/pulp_python/app/migrations/0019_create_missing_metadata_artifacts.py +0 -204
  12. {pulp_python-3.23.1 → pulp_python-3.24.1}/COMMITMENT +0 -0
  13. {pulp_python-3.23.1 → pulp_python-3.24.1}/COPYRIGHT +0 -0
  14. {pulp_python-3.23.1 → pulp_python-3.24.1}/LICENSE +0 -0
  15. {pulp_python-3.23.1 → pulp_python-3.24.1}/MANIFEST.in +0 -0
  16. {pulp_python-3.23.1 → pulp_python-3.24.1}/README.md +0 -0
  17. {pulp_python-3.23.1 → pulp_python-3.24.1}/functest_requirements.txt +0 -0
  18. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/__init__.py +0 -0
  19. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/global_access_conditions.py +0 -0
  20. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/management/__init__.py +0 -0
  21. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/management/commands/__init__.py +0 -0
  22. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
  23. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0001_initial.py +0 -0
  24. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
  25. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
  26. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
  27. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
  28. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
  29. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
  30. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
  31. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
  32. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
  33. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
  34. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
  35. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0012_add_domain.py +0 -0
  36. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
  37. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
  38. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
  39. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
  40. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +0 -0
  41. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/0018_packageprovenance.py +0 -0
  42. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/migrations/__init__.py +0 -0
  43. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/modelresource.py +0 -0
  44. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/models.py +0 -0
  45. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/provenance.py +0 -0
  46. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/pypi/__init__.py +0 -0
  47. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/pypi/serializers.py +0 -0
  48. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/pypi/views.py +0 -0
  49. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/replica.py +0 -0
  50. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/serializers.py +0 -0
  51. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/settings.py +0 -0
  52. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/tasks/__init__.py +0 -0
  53. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/tasks/publish.py +0 -0
  54. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/tasks/repair.py +0 -0
  55. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/tasks/sync.py +0 -0
  56. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/tasks/upload.py +0 -0
  57. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/tasks/vulnerability_report.py +0 -0
  58. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/urls.py +0 -0
  59. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/viewsets.py +0 -0
  60. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/webserver_snippets/__init__.py +0 -0
  61. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/webserver_snippets/apache.conf +0 -0
  62. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
  63. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/pytest_plugin.py +0 -0
  64. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/__init__.py +0 -0
  65. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/__init__.py +0 -0
  66. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/__init__.py +0 -0
  67. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_attestations.py +0 -0
  68. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
  69. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
  70. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_crud_content_unit.py +0 -0
  71. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
  72. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
  73. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_domains.py +0 -0
  74. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_download_content.py +0 -0
  75. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_export_import.py +0 -0
  76. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
  77. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
  78. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_rbac.py +0 -0
  79. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_repair.py +0 -0
  80. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_sync.py +0 -0
  81. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_upload.py +0 -0
  82. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/api/test_vulnerability_report.py +0 -0
  83. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation +0 -0
  84. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation +0 -0
  85. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/unit/__init__.py +0 -0
  86. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python/tests/unit/test_models.py +0 -0
  87. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python.egg-info/SOURCES.txt +0 -0
  88. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python.egg-info/dependency_links.txt +0 -0
  89. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python.egg-info/entry_points.txt +0 -0
  90. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python.egg-info/requires.txt +0 -0
  91. {pulp_python-3.23.1 → pulp_python-3.24.1}/pulp_python.egg-info/top_level.txt +0 -0
  92. {pulp_python-3.23.1 → pulp_python-3.24.1}/setup.cfg +0 -0
  93. {pulp_python-3.23.1 → pulp_python-3.24.1}/test_requirements.txt +0 -0
  94. {pulp_python-3.23.1 → pulp_python-3.24.1}/unittest_requirements.txt +0 -0
@@ -8,9 +8,25 @@
8
8
 
9
9
  [//]: # (towncrier release notes start)
10
10
 
11
- ## 3.23.1 (2026-01-20) {: #3.23.1 }
11
+ ## 3.24.1 (2026-01-22) {: #3.24.1 }
12
12
 
13
- #### Bugfixes {: #3.23.1-bugfix }
13
+ #### Bugfixes {: #3.24.1-bugfix }
14
+
15
+ - Changed migration 19 to reset package's metadata_sha256 to null. This field will be fixed in a later release.
16
+ [#1071](https://github.com/pulp/pulp_python/issues/1071)
17
+
18
+ ---
19
+
20
+ ## 3.24.0 (2026-01-20) {: #3.24.0 }
21
+
22
+ #### Features {: #3.24.0-feature }
23
+
24
+ - Added core metadata to Simple API (PEP 714)
25
+ [#997](https://github.com/pulp/pulp_python/issues/997)
26
+ - Added data-requires-python to Simple HTML API.
27
+ [#1054](https://github.com/pulp/pulp_python/issues/1054)
28
+
29
+ #### Bugfixes {: #3.24.0-bugfix }
14
30
 
15
31
  - Fixed migration error in 0019_create_missing_metadata_artifacts.
16
32
  [#1067](https://github.com/pulp/pulp_python/issues/1067)
@@ -33,6 +49,14 @@
33
49
 
34
50
  ---
35
51
 
52
+ ## 3.22.2 (2026-01-06) {: #3.22.2 }
53
+
54
+ #### Bugfixes {: #3.22.2-bugfix }
55
+
56
+ - Added missing Provenance content `package` and `sha256` filters.
57
+
58
+ ---
59
+
36
60
  ## 3.22.1 (2025-12-10) {: #3.22.1 }
37
61
 
38
62
  #### Bugfixes {: #3.22.1-bugfix }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.23.1
3
+ Version: 3.24.1
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.23.1"
13
+ version = "3.24.1"
14
14
  python_package_name = "pulp-python"
15
15
  domain_compatible = True
16
16
 
@@ -0,0 +1,24 @@
1
+ # Generated manually on 2025-12-15 14:00 for creating missing metadata artifacts
2
+ from django.db import migrations
3
+
4
+
5
+ def set_metadata_sha256_null(apps, schema_editor):
6
+ # We can't easily create the metadata artifacts in this migration, so just set the metadata_sha256
7
+ # to null and we will introduce a new command later to create them.
8
+ PythonPackageContent = apps.get_model("python", "PythonPackageContent")
9
+ PythonPackageContent.objects.filter(metadata_sha256__isnull=False).update(metadata_sha256=None)
10
+
11
+
12
+ class Migration(migrations.Migration):
13
+
14
+ dependencies = [
15
+ ("python", "0018_packageprovenance"),
16
+ ]
17
+
18
+ operations = [
19
+ migrations.RunPython(
20
+ set_metadata_sha256_null,
21
+ reverse_code=migrations.RunPython.noop,
22
+ elidable=True,
23
+ ),
24
+ ]
@@ -47,7 +47,7 @@ simple_index_template = """<!DOCTYPE html>
47
47
  </html>
48
48
  """
49
49
 
50
- # TODO in the future: data-requires-python (PEP 503)
50
+ # TODO in the future: data-yanked (not implemented yet because it is mutable)
51
51
  simple_detail_template = """<!DOCTYPE html>
52
52
  <html>
53
53
  <head>
@@ -58,13 +58,14 @@ simple_detail_template = """<!DOCTYPE html>
58
58
  <h1>Links for {{ project_name }}</h1>
59
59
  {%- for pkg in project_packages %}
60
60
  <a href="{{ pkg.url }}#sha256={{ pkg.sha256 }}" rel="internal"
61
- {%- if pkg.metadata_sha256 %} data-dist-info-metadata="sha256={{ pkg.metadata_sha256 }}"
61
+ {%- if pkg.requires_python %} data-requires-python="{{ pkg.requires_python }}" {%- endif %}
62
+ {%- if pkg.metadata_sha256 %} data-dist-info-metadata="sha256={{ pkg.metadata_sha256 }}" data-core-metadata="sha256={{ pkg.metadata_sha256 }}"
62
63
  {%- endif %} {% if pkg.provenance -%}
63
64
  data-provenance="{{ pkg.provenance }}"{% endif %}>{{ pkg.filename }}</a><br/>
64
65
  {%- endfor %}
65
66
  </body>
66
67
  </html>
67
- """
68
+ """ # noqa: E501
68
69
 
69
70
  DIST_EXTENSIONS = {
70
71
  ".whl": "bdist_wheel",
@@ -501,7 +502,7 @@ def write_simple_index(project_names, streamed=False):
501
502
 
502
503
  def write_simple_detail(project_name, project_packages, streamed=False):
503
504
  """Writes the simple detail page of a package."""
504
- detail = Template(simple_detail_template)
505
+ detail = Template(simple_detail_template, autoescape=True)
505
506
  context = {
506
507
  "SIMPLE_API_VERSION": SIMPLE_API_VERSION,
507
508
  "project_name": project_name,
@@ -536,12 +537,14 @@ def write_simple_detail_json(project_name, project_packages):
536
537
  "data-dist-info-metadata": (
537
538
  {"sha256": package["metadata_sha256"]} if package["metadata_sha256"] else False
538
539
  ),
540
+ # PEP 714
541
+ "core-metadata": (
542
+ {"sha256": package["metadata_sha256"]} if package["metadata_sha256"] else False
543
+ ),
539
544
  # yanked and yanked_reason are not implemented because they are mutable
540
545
  # (v1.1, PEP 700)
541
546
  "size": package["size"],
542
547
  "upload-time": format_upload_time(package["upload_time"]),
543
- # TODO in the future:
544
- # core-metadata (PEP 7.14)
545
548
  # (v1.3, PEP 740)
546
549
  "provenance": package.get("provenance", None),
547
550
  }
@@ -5,17 +5,23 @@ import requests
5
5
 
6
6
  from pulp_python.tests.functional.constants import (
7
7
  PYPI_SERIAL_CONSTANT,
8
- PYTHON_EGG_FILENAME,
9
- PYTHON_EGG_SHA256,
10
- PYTHON_EGG_URL,
11
8
  PYTHON_SM_FIXTURE_CHECKSUMS,
12
9
  PYTHON_SM_FIXTURE_RELEASES,
13
10
  PYTHON_SM_PROJECT_SPECIFIER,
14
- PYTHON_WHEEL_FILENAME,
15
- PYTHON_WHEEL_METADATA_SHA256,
16
- PYTHON_WHEEL_SHA256,
17
- PYTHON_WHEEL_URL,
18
- PYTHON_XS_FIXTURE_CHECKSUMS,
11
+ TWINE_EGG_FILENAME,
12
+ TWINE_EGG_REQUIRES_PYTHON,
13
+ TWINE_EGG_SHA256,
14
+ TWINE_EGG_SIZE,
15
+ TWINE_EGG_URL,
16
+ TWINE_FIXTURE_CHECKSUMS,
17
+ TWINE_FIXTURE_METADATA_SHA256,
18
+ TWINE_FIXTURE_REQUIRES_PYTHON,
19
+ TWINE_WHEEL_FILENAME,
20
+ TWINE_WHEEL_METADATA_SHA256,
21
+ TWINE_WHEEL_REQUIRES_PYTHON,
22
+ TWINE_WHEEL_SHA256,
23
+ TWINE_WHEEL_SIZE,
24
+ TWINE_WHEEL_URL,
19
25
  )
20
26
  from pulp_python.tests.functional.utils import ensure_simple
21
27
 
@@ -55,30 +61,27 @@ def test_simple_html_detail_api(
55
61
  python_distribution_factory,
56
62
  python_repo_factory,
57
63
  ):
58
- content_1 = python_content_factory(PYTHON_WHEEL_FILENAME, url=PYTHON_WHEEL_URL)
59
- content_2 = python_content_factory(PYTHON_EGG_FILENAME, url=PYTHON_EGG_URL)
64
+ content_1 = python_content_factory(TWINE_WHEEL_FILENAME, url=TWINE_WHEEL_URL)
65
+ content_2 = python_content_factory(TWINE_EGG_FILENAME, url=TWINE_EGG_URL)
60
66
  body = {"add_content_units": [content_1.pulp_href, content_2.pulp_href]}
61
67
 
62
68
  repo = python_repo_factory()
63
69
  monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
64
70
  distro = python_distribution_factory(repository=repo)
65
71
 
66
- url = f'{urljoin(distro.base_url, "simple/")}shelf-reader'
72
+ url = f'{urljoin(distro.base_url, "simple/")}twine'
67
73
  headers = {"Accept": PYPI_SIMPLE_V1_HTML}
68
74
 
69
75
  response = requests.get(url, headers=headers)
70
76
  assert response.headers["Content-Type"] == PYPI_SIMPLE_V1_HTML
71
77
  assert response.headers["X-PyPI-Last-Serial"] == str(PYPI_SERIAL_CONSTANT)
72
78
 
73
- metadata_sha_digests = {
74
- PYTHON_WHEEL_FILENAME: PYTHON_WHEEL_METADATA_SHA256,
75
- PYTHON_EGG_FILENAME: None, # egg files should not have metadata
76
- }
77
79
  proper, msgs = ensure_simple(
78
80
  urljoin(distro.base_url, "simple/"),
79
- {"shelf-reader": [PYTHON_WHEEL_FILENAME, PYTHON_EGG_FILENAME]},
80
- sha_digests=PYTHON_XS_FIXTURE_CHECKSUMS,
81
- metadata_sha_digests=metadata_sha_digests,
81
+ {"twine": [TWINE_WHEEL_FILENAME, TWINE_EGG_FILENAME]},
82
+ sha_digests=TWINE_FIXTURE_CHECKSUMS,
83
+ metadata_sha_digests=TWINE_FIXTURE_METADATA_SHA256,
84
+ requires_python=TWINE_FIXTURE_REQUIRES_PYTHON,
82
85
  )
83
86
  assert proper, f"Simple API validation failed: {msgs}"
84
87
 
@@ -114,15 +117,15 @@ def test_simple_json_detail_api(
114
117
  python_distribution_factory,
115
118
  python_repo_factory,
116
119
  ):
117
- content_1 = python_content_factory(PYTHON_WHEEL_FILENAME, url=PYTHON_WHEEL_URL)
118
- content_2 = python_content_factory(PYTHON_EGG_FILENAME, url=PYTHON_EGG_URL)
120
+ content_1 = python_content_factory(TWINE_WHEEL_FILENAME, url=TWINE_WHEEL_URL)
121
+ content_2 = python_content_factory(TWINE_EGG_FILENAME, url=TWINE_EGG_URL)
119
122
  body = {"add_content_units": [content_1.pulp_href, content_2.pulp_href]}
120
123
 
121
124
  repo = python_repo_factory()
122
125
  monitor_task(python_bindings.RepositoriesPythonApi.modify(repo.pulp_href, body).task)
123
126
  distro = python_distribution_factory(repository=repo)
124
127
 
125
- url = f'{urljoin(distro.base_url, "simple/")}shelf-reader'
128
+ url = f'{urljoin(distro.base_url, "simple/")}twine'
126
129
  headers = {"Accept": PYPI_SIMPLE_V1_JSON}
127
130
 
128
131
  response = requests.get(url, headers=headers)
@@ -131,27 +134,31 @@ def test_simple_json_detail_api(
131
134
 
132
135
  data = response.json()
133
136
  assert data["meta"] == {"api-version": API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT}
134
- assert data["name"] == "shelf-reader"
137
+ assert data["name"] == "twine"
135
138
  assert data["files"]
136
- assert data["versions"] == ["0.1"]
139
+ assert data["versions"] == ["5.1.0"]
137
140
 
138
141
  # Check data of a wheel
139
- file_whl = next((i for i in data["files"] if i["filename"] == PYTHON_WHEEL_FILENAME), None)
142
+ file_whl = next((i for i in data["files"] if i["filename"] == TWINE_WHEEL_FILENAME), None)
140
143
  assert file_whl is not None, "wheel file not found"
141
144
  assert file_whl["url"]
142
- assert file_whl["hashes"] == {"sha256": PYTHON_WHEEL_SHA256}
143
- assert file_whl["requires-python"] is None
144
- assert file_whl["data-dist-info-metadata"] == {"sha256": PYTHON_WHEEL_METADATA_SHA256}
145
- assert file_whl["size"] == 22455
145
+ assert file_whl["hashes"] == {"sha256": TWINE_WHEEL_SHA256}
146
+ assert file_whl["requires-python"] == TWINE_WHEEL_REQUIRES_PYTHON
147
+ assert file_whl["data-dist-info-metadata"] == {"sha256": TWINE_WHEEL_METADATA_SHA256}
148
+ assert file_whl["core-metadata"] == {"sha256": TWINE_WHEEL_METADATA_SHA256}
149
+ assert file_whl["size"] == TWINE_WHEEL_SIZE
146
150
  assert file_whl["upload-time"] is not None
151
+ assert file_whl["provenance"] is None
152
+
147
153
  # Check data of a tarball
148
- file_tar = next((i for i in data["files"] if i["filename"] == PYTHON_EGG_FILENAME), None)
154
+ file_tar = next((i for i in data["files"] if i["filename"] == TWINE_EGG_FILENAME), None)
149
155
  assert file_tar is not None, "tar file not found"
150
156
  assert file_tar["url"]
151
- assert file_tar["hashes"] == {"sha256": PYTHON_EGG_SHA256}
152
- assert file_tar["requires-python"] is None
157
+ assert file_tar["hashes"] == {"sha256": TWINE_EGG_SHA256}
158
+ assert file_tar["requires-python"] == TWINE_EGG_REQUIRES_PYTHON
153
159
  assert file_tar["data-dist-info-metadata"] is False
154
- assert file_tar["size"] == 19097
160
+ assert file_tar["core-metadata"] is False
161
+ assert file_tar["size"] == TWINE_EGG_SIZE
155
162
  assert file_tar["upload-time"] is not None
156
163
  assert file_tar["provenance"] is None
157
164
 
@@ -226,6 +226,32 @@ PYTHON_INFO_DATA = {
226
226
  # maybe add description, license is long for this one
227
227
  }
228
228
 
229
+ # twine pkg data for PyPI Simple API
230
+ TWINE_EGG_FILENAME = "twine-5.1.0.tar.gz"
231
+ TWINE_EGG_URL = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), TWINE_EGG_FILENAME)
232
+ TWINE_EGG_SHA256 = "4d74770c88c4fcaf8134d2a6a9d863e40f08255ff7d8e2acb3cbbd57d25f6e9d"
233
+ TWINE_EGG_SIZE = 224997
234
+ TWINE_EGG_REQUIRES_PYTHON = ">=3.8"
235
+
236
+ TWINE_WHEEL_FILENAME = "twine-5.1.0-py3-none-any.whl"
237
+ TWINE_WHEEL_URL = urljoin(urljoin(PYTHON_FIXTURES_URL, "packages/"), TWINE_WHEEL_FILENAME)
238
+ TWINE_WHEEL_SHA256 = "fe1d814395bfe50cfbe27783cb74efe93abeac3f66deaeb6c8390e4e92bacb43"
239
+ TWINE_WHEEL_SIZE = 38563
240
+ TWINE_WHEEL_REQUIRES_PYTHON = ">=3.8"
241
+ TWINE_WHEEL_METADATA_SHA256 = "0ac5cf457bd47512b3477949ff6274cc2258414f3e1f136e049585aac92e4ddb"
242
+
243
+ TWINE_FIXTURE_CHECKSUMS = {
244
+ TWINE_EGG_FILENAME: TWINE_EGG_SHA256,
245
+ TWINE_WHEEL_FILENAME: TWINE_WHEEL_SHA256,
246
+ }
247
+ TWINE_FIXTURE_METADATA_SHA256 = {
248
+ TWINE_WHEEL_FILENAME: TWINE_WHEEL_METADATA_SHA256,
249
+ TWINE_EGG_FILENAME: None, # egg files should not have metadata
250
+ }
251
+ TWINE_FIXTURE_REQUIRES_PYTHON = {
252
+ TWINE_WHEEL_FILENAME: TWINE_EGG_REQUIRES_PYTHON,
253
+ TWINE_EGG_FILENAME: TWINE_WHEEL_REQUIRES_PYTHON,
254
+ }
229
255
 
230
256
  # Current tests use PYTHON_FIXTURES_URL with an 'S', remove after adding api tests
231
257
  PYTHON_FIXTURE_URL = urljoin(PULP_FIXTURES_BASE_URL, "python-pypi/")
@@ -6,27 +6,55 @@ from lxml import html
6
6
 
7
7
  def _validate_metadata_sha_digest(link, filename, metadata_sha_digests):
8
8
  """
9
- Validate data-dist-info-metadata attribute for a release link.
9
+ Validate data-dist-info-metadata and data-core-metadata attributes for a release link.
10
10
  """
11
- data_dist_info_metadata = link.get("data-dist-info-metadata")
11
+ expected_metadata_sha = metadata_sha_digests.get(filename)
12
+ expected_attr = f"sha256={expected_metadata_sha}" if expected_metadata_sha else None
12
13
 
13
- if expected_metadata_sha := metadata_sha_digests.get(filename):
14
- expected_attr = f"sha256={expected_metadata_sha}"
15
- if data_dist_info_metadata != expected_attr:
16
- return (
17
- f"\nFile {filename} has incorrect data-dist-info-metadata: "
18
- f"expected '{expected_attr}', got '{data_dist_info_metadata}'"
19
- )
20
- else:
21
- if data_dist_info_metadata:
22
- return (
23
- f"\nFile {filename} should not have data-dist-info-metadata "
24
- f"but has '{data_dist_info_metadata}'"
14
+ msgs = ""
15
+ for attr_name in ["data-dist-info-metadata", "data-core-metadata"]:
16
+ attr_value = link.get(attr_name)
17
+ if attr_value != expected_attr:
18
+ if expected_attr:
19
+ msgs += (
20
+ f"\nFile {filename} has incorrect {attr_name}: "
21
+ f"expected '{expected_attr}', got '{attr_value}'"
22
+ )
23
+ else:
24
+ msgs += f"\nFile {filename} should not have {attr_name} but has '{attr_value}'"
25
+ return msgs
26
+
27
+
28
+ def _validate_requires_python(link, filename, requires_python, page_content):
29
+ """
30
+ Validate data-requires-python attribute for a release link.
31
+ """
32
+ expected_requires_python = requires_python.get(filename) if requires_python else None
33
+ attr_value = link.get("data-requires-python")
34
+
35
+ msgs = ""
36
+ if attr_value != expected_requires_python:
37
+ if expected_requires_python:
38
+ msgs += (
39
+ f"\nFile {filename} has incorrect data-requires-python: "
40
+ f"expected '{expected_requires_python}', got '{attr_value}'"
25
41
  )
26
- return ""
42
+ else:
43
+ msgs += f"\nFile {filename} should not have data-requires-python but has '{attr_value}'"
44
+
45
+ # Check HTML escaping
46
+ if expected_requires_python and any(char in expected_requires_python for char in [">", "<"]):
47
+ escaped_value = expected_requires_python.replace(">", "&gt;").replace("<", "&lt;")
48
+ escaped_attr = f'data-requires-python="{escaped_value}"'
49
+ if escaped_attr not in page_content:
50
+ msgs += f"\nFile {filename} has unescaped < or > in data-requires-python attribute"
51
+
52
+ return msgs
27
53
 
28
54
 
29
- def ensure_simple(simple_url, packages, sha_digests=None, metadata_sha_digests=None):
55
+ def ensure_simple(
56
+ simple_url, packages, sha_digests=None, metadata_sha_digests=None, requires_python=None
57
+ ):
30
58
  """
31
59
  Tests that the simple api at `url` matches the packages supplied.
32
60
  `packages`: dictionary of form {package_name: [release_filenames]}
@@ -41,7 +69,8 @@ def ensure_simple(simple_url, packages, sha_digests=None, metadata_sha_digests=N
41
69
 
42
70
  def explore_links(page_url, page_name, links_found, msgs):
43
71
  legit_found_links = []
44
- page = html.fromstring(requests.get(page_url).text)
72
+ page_content = requests.get(page_url).text
73
+ page = html.fromstring(page_content)
45
74
  page_links = page.xpath("/html/body/a")
46
75
  for link in page_links:
47
76
  if link.text in links_found:
@@ -53,6 +82,11 @@ def ensure_simple(simple_url, packages, sha_digests=None, metadata_sha_digests=N
53
82
  # Check metadata SHA digest if provided
54
83
  if metadata_sha_digests and page_name == "release":
55
84
  msgs += _validate_metadata_sha_digest(link, link.text, metadata_sha_digests)
85
+ # Check requires-python if provided
86
+ if requires_python and page_name == "release":
87
+ msgs += _validate_requires_python(
88
+ link, link.text, requires_python, page_content
89
+ )
56
90
  else:
57
91
  msgs += f"\nFound {page_name} link without href {link.text}"
58
92
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.23.1
3
+ Version: 3.24.1
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.23.1"
10
+ version = "3.24.1"
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.23.1"
80
+ current_version = "3.24.1"
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,204 +0,0 @@
1
- # Generated manually on 2025-12-15 14:00 for creating missing metadata artifacts
2
-
3
- from django.db import migrations
4
-
5
- BATCH_SIZE = 1000
6
-
7
-
8
- def pulp_hashlib_new(name, *args, **kwargs):
9
- """
10
- Copied and updated (to comply with migrations) from pulpcore.
11
- """
12
- import hashlib as the_real_hashlib
13
- from django.conf import settings
14
-
15
- if name not in settings.ALLOWED_CONTENT_CHECKSUMS:
16
- return None
17
-
18
- return the_real_hashlib.new(name, *args, **kwargs)
19
-
20
-
21
- def init_and_validate(file, artifact_model, expected_digests):
22
- """
23
- Copied and updated (to comply with migrations) from pulpcore.
24
- """
25
- from django.conf import settings
26
-
27
- digest_fields = []
28
- for alg in ("sha512", "sha384", "sha256", "sha224", "sha1", "md5"):
29
- if alg in settings.ALLOWED_CONTENT_CHECKSUMS:
30
- digest_fields.append(alg)
31
-
32
- if isinstance(file, str):
33
- with open(file, "rb") as f:
34
- hashers = {
35
- n: hasher for n in digest_fields if (hasher := pulp_hashlib_new(n)) is not None
36
- }
37
- if not hashers:
38
- return None
39
-
40
- size = 0
41
- while True:
42
- chunk = f.read(1048576) # 1 megabyte
43
- if not chunk:
44
- break
45
- for algorithm in hashers.values():
46
- algorithm.update(chunk)
47
- size = size + len(chunk)
48
- else:
49
- size = file.size
50
- hashers = file.hashers
51
-
52
- mismatched_sha256 = None
53
- for algorithm, expected_digest in expected_digests.items():
54
- if algorithm not in hashers:
55
- return None
56
- actual_digest = hashers[algorithm].hexdigest()
57
- if expected_digest != actual_digest:
58
- # Store the actual value for later fixing if it differs from the package value
59
- mismatched_sha256 = actual_digest
60
-
61
- attributes = {"size": size, "file": file}
62
- for algorithm in digest_fields:
63
- attributes[algorithm] = hashers[algorithm].hexdigest()
64
-
65
- return artifact_model(**attributes), mismatched_sha256
66
-
67
-
68
- def extract_wheel_metadata(filename):
69
- """
70
- Extract the metadata file content from a wheel file.
71
- Return the raw metadata content as bytes or None if metadata cannot be extracted.
72
- """
73
- import zipfile
74
-
75
- try:
76
- with zipfile.ZipFile(filename, "r") as f:
77
- for file_path in f.namelist():
78
- if file_path.endswith(".dist-info/METADATA"):
79
- return f.read(file_path)
80
- except (zipfile.BadZipFile, KeyError, OSError):
81
- pass
82
- return None
83
-
84
-
85
- def artifact_to_metadata_artifact(filename, artifact, md_digests, tmp_dir, artifact_model):
86
- """
87
- Create artifact for metadata from the provided wheel artifact.
88
- Return (artifact, mismatched_sha256) on success, None on any failure.
89
- """
90
- import shutil
91
- import tempfile
92
-
93
- with tempfile.NamedTemporaryFile("wb", dir=tmp_dir, suffix=filename, delete=False) as temp_file:
94
- temp_wheel_path = temp_file.name
95
- artifact.file.seek(0)
96
- shutil.copyfileobj(artifact.file, temp_file)
97
- temp_file.flush()
98
-
99
- metadata_content = extract_wheel_metadata(temp_wheel_path)
100
- if not metadata_content:
101
- return None
102
-
103
- with tempfile.NamedTemporaryFile(
104
- "wb", dir=tmp_dir, suffix=".metadata", delete=False
105
- ) as temp_md:
106
- temp_metadata_path = temp_md.name
107
- temp_md.write(metadata_content)
108
- temp_md.flush()
109
-
110
- return init_and_validate(temp_metadata_path, artifact_model, md_digests)
111
-
112
-
113
- def create_missing_metadata_artifacts(apps, schema_editor):
114
- """
115
- Create metadata artifacts for PythonPackageContent instances that have metadata_sha256
116
- but are missing the corresponding metadata artifact.
117
- """
118
- import tempfile
119
- from django.conf import settings
120
- from django.db import models
121
-
122
- PythonPackageContent = apps.get_model("python", "PythonPackageContent")
123
- ContentArtifact = apps.get_model("core", "ContentArtifact")
124
- Artifact = apps.get_model("core", "Artifact")
125
-
126
- packages = (
127
- PythonPackageContent.objects.filter(
128
- metadata_sha256__isnull=False,
129
- filename__endswith=".whl",
130
- contentartifact__artifact__isnull=False,
131
- contentartifact__relative_path=models.F("filename"),
132
- )
133
- .exclude(metadata_sha256="")
134
- .prefetch_related("_artifacts")
135
- .only("filename", "metadata_sha256")
136
- )
137
- artifact_batch = []
138
- contentartifact_batch = []
139
- packages_batch = []
140
-
141
- with tempfile.TemporaryDirectory(dir=settings.WORKING_DIRECTORY) as temp_dir:
142
- for package in packages:
143
- # Get the main artifact for package
144
- main_artifact = package._artifacts.get()
145
-
146
- filename = package.filename
147
- metadata_digests = {"sha256": package.metadata_sha256}
148
- result = artifact_to_metadata_artifact(
149
- filename, main_artifact, metadata_digests, temp_dir, Artifact
150
- )
151
- if result is None:
152
- # Unset metadata_sha256 when extraction or validation fails
153
- package.metadata_sha256 = None
154
- packages_batch.append(package)
155
- continue
156
- metadata_artifact, mismatched_sha256 = result
157
- if mismatched_sha256:
158
- # Fix the package if its metadata_sha256 differs from the actual value
159
- package.metadata_sha256 = mismatched_sha256
160
- packages_batch.append(package)
161
-
162
- # Set the domain on the metadata artifact to match the package's domain
163
- metadata_artifact.pulp_domain = package._pulp_domain
164
-
165
- contentartifact = ContentArtifact(
166
- artifact=metadata_artifact,
167
- content=package,
168
- relative_path=f"{filename}.metadata",
169
- )
170
- artifact_batch.append(metadata_artifact)
171
- contentartifact_batch.append(contentartifact)
172
-
173
- if len(artifact_batch) == BATCH_SIZE:
174
- Artifact.objects.bulk_create(artifact_batch, batch_size=BATCH_SIZE)
175
- ContentArtifact.objects.bulk_create(contentartifact_batch, batch_size=BATCH_SIZE)
176
- artifact_batch.clear()
177
- contentartifact_batch.clear()
178
- if len(packages_batch) == BATCH_SIZE:
179
- PythonPackageContent.objects.bulk_update(
180
- packages_batch, ["metadata_sha256"], batch_size=BATCH_SIZE
181
- )
182
- packages_batch.clear()
183
-
184
- if artifact_batch:
185
- Artifact.objects.bulk_create(artifact_batch, batch_size=BATCH_SIZE)
186
- ContentArtifact.objects.bulk_create(contentartifact_batch, batch_size=BATCH_SIZE)
187
- if packages_batch:
188
- PythonPackageContent.objects.bulk_update(
189
- packages_batch, ["metadata_sha256"], batch_size=BATCH_SIZE
190
- )
191
-
192
-
193
- class Migration(migrations.Migration):
194
-
195
- dependencies = [
196
- ("python", "0018_packageprovenance"),
197
- ]
198
-
199
- operations = [
200
- migrations.RunPython(
201
- create_missing_metadata_artifacts,
202
- reverse_code=migrations.RunPython.noop,
203
- ),
204
- ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes