pulp-python 3.15.1__tar.gz → 3.17.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 (82) hide show
  1. {pulp_python-3.15.1 → pulp_python-3.17.0}/CHANGES.md +23 -0
  2. {pulp_python-3.15.1 → pulp_python-3.17.0}/PKG-INFO +4 -3
  3. pulp_python-3.17.0/README.md +9 -0
  4. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/__init__.py +1 -1
  5. pulp_python-3.17.0/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +34 -0
  6. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/models.py +50 -33
  7. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/serializers.py +107 -74
  8. pulp_python-3.17.0/pulp_python/app/tasks/repair.py +198 -0
  9. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/utils.py +77 -29
  10. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +32 -2
  11. pulp_python-3.17.0/pulp_python/tests/functional/api/test_repair.py +226 -0
  12. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/constants.py +6 -5
  13. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/PKG-INFO +4 -3
  14. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/SOURCES.txt +1 -0
  15. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/requires.txt +1 -1
  16. {pulp_python-3.15.1 → pulp_python-3.17.0}/pyproject.toml +4 -4
  17. pulp_python-3.15.1/README.md +0 -8
  18. pulp_python-3.15.1/pulp_python/app/tasks/repair.py +0 -89
  19. pulp_python-3.15.1/pulp_python/tests/functional/api/test_repair.py +0 -126
  20. {pulp_python-3.15.1 → pulp_python-3.17.0}/COMMITMENT +0 -0
  21. {pulp_python-3.15.1 → pulp_python-3.17.0}/COPYRIGHT +0 -0
  22. {pulp_python-3.15.1 → pulp_python-3.17.0}/LICENSE +0 -0
  23. {pulp_python-3.15.1 → pulp_python-3.17.0}/MANIFEST.in +0 -0
  24. {pulp_python-3.15.1 → pulp_python-3.17.0}/functest_requirements.txt +0 -0
  25. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/__init__.py +0 -0
  26. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/global_access_conditions.py +0 -0
  27. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/management/__init__.py +0 -0
  28. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/management/commands/__init__.py +0 -0
  29. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
  30. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0001_initial.py +0 -0
  31. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
  32. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
  33. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
  34. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
  35. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
  36. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
  37. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
  38. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
  39. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
  40. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
  41. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
  42. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
  43. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/__init__.py +0 -0
  44. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/modelresource.py +0 -0
  45. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/pypi/__init__.py +0 -0
  46. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/pypi/serializers.py +0 -0
  47. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/pypi/views.py +0 -0
  48. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/replica.py +0 -0
  49. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/settings.py +0 -0
  50. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/tasks/__init__.py +0 -0
  51. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/tasks/publish.py +0 -0
  52. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/tasks/sync.py +0 -0
  53. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/tasks/upload.py +0 -0
  54. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/urls.py +0 -0
  55. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/viewsets.py +0 -0
  56. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
  57. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
  58. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
  59. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/pytest_plugin.py +0 -0
  60. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/__init__.py +0 -0
  61. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/__init__.py +0 -0
  62. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/__init__.py +0 -0
  63. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
  64. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
  65. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
  66. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
  67. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_domains.py +0 -0
  68. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
  69. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
  70. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
  71. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
  72. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
  73. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_sync.py +0 -0
  74. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/utils.py +0 -0
  75. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/unit/__init__.py +0 -0
  76. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/unit/test_models.py +0 -0
  77. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/dependency_links.txt +0 -0
  78. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/entry_points.txt +0 -0
  79. {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/top_level.txt +0 -0
  80. {pulp_python-3.15.1 → pulp_python-3.17.0}/setup.cfg +0 -0
  81. {pulp_python-3.15.1 → pulp_python-3.17.0}/test_requirements.txt +0 -0
  82. {pulp_python-3.15.1 → pulp_python-3.17.0}/unittest_requirements.txt +0 -0
@@ -8,6 +8,29 @@
8
8
 
9
9
  [//]: # (towncrier release notes start)
10
10
 
11
+ ## 3.17.0 (2025-07-23) {: #3.17.0 }
12
+
13
+ #### Features {: #3.17.0-feature }
14
+
15
+ - Added full support for the latest core metadata (up to 2.4).
16
+ [#689](https://github.com/pulp/pulp_python/issues/689)
17
+
18
+ ---
19
+
20
+ ## 3.16.0 (2025-06-10) {: #3.16.0 }
21
+
22
+ #### Features {: #3.16.0-feature }
23
+
24
+ - Added support for on-demand content to `repair_metadata` endpoint.
25
+ [#849](https://github.com/pulp/pulp_python/issues/849)
26
+
27
+ #### Bugfixes {: #3.16.0-bugfix }
28
+
29
+ - Fixed pull-through caching not working for indexes that use relative URLs.
30
+ [#842](https://github.com/pulp/pulp_python/issues/842)
31
+
32
+ ---
33
+
11
34
  ## 3.15.1 (2025-06-10) {: #3.15.1 }
12
35
 
13
36
  #### Bugfixes {: #3.15.1-bugfix }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-python
3
- Version: 3.15.1
3
+ Version: 3.17.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
@@ -22,14 +22,15 @@ Requires-Python: >=3.9
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: pulpcore<3.85,>=3.49.0
25
- Requires-Dist: pkginfo<1.13.0,>=1.10.0
25
+ Requires-Dist: pkginfo<1.13.0,>=1.12.0
26
26
  Requires-Dist: bandersnatch<6.4,>=6.3.0
27
27
  Requires-Dist: pypi-simple<2.0,>=1.5.0
28
28
  Dynamic: license-file
29
29
 
30
30
  # pulp_python
31
31
 
32
- ![Pulp Nightly CI/CD](https://github.com/pulp/pulp_python/actions/workflows/nightly.yml/badge.svg)
32
+ [![PyPI](https://img.shields.io/pypi/v/pulp_python.svg)](https://pypi.python.org/pypi/pulp_python)
33
+ [![Pulp Nightly CI/CD](https://github.com/pulp/pulp_python/actions/workflows/nightly.yml/badge.svg)](https://github.com/pulp/pulp_python/actions/workflows/nightly.yml)
33
34
 
34
35
  A Pulp plugin to support hosting your own pip compatible Python packages.
35
36
 
@@ -0,0 +1,9 @@
1
+ # pulp_python
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/pulp_python.svg)](https://pypi.python.org/pypi/pulp_python)
4
+ [![Pulp Nightly CI/CD](https://github.com/pulp/pulp_python/actions/workflows/nightly.yml/badge.svg)](https://github.com/pulp/pulp_python/actions/workflows/nightly.yml)
5
+
6
+ A Pulp plugin to support hosting your own pip compatible Python packages.
7
+
8
+ For more information, please see the [documentation](https://docs.pulpproject.org/pulp_python/) or the
9
+ [Pulp project page](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.15.1"
13
+ version = "3.17.0"
14
14
  python_package_name = "pulp-python"
15
15
  domain_compatible = True
16
16
 
@@ -0,0 +1,34 @@
1
+ # Generated by Django 4.2.19 on 2025-07-09 08:05
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('python', '0013_add_rbac_permissions'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='pythonpackagecontent',
15
+ name='dynamic',
16
+ field=models.JSONField(default=list),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='pythonpackagecontent',
20
+ name='license_expression',
21
+ field=models.TextField(default=''),
22
+ preserve_default=False,
23
+ ),
24
+ migrations.AddField(
25
+ model_name='pythonpackagecontent',
26
+ name='license_file',
27
+ field=models.JSONField(default=list),
28
+ ),
29
+ migrations.AddField(
30
+ model_name='pythonpackagecontent',
31
+ name='provides_extras',
32
+ field=models.JSONField(default=list),
33
+ ),
34
+ ]
@@ -140,49 +140,66 @@ class PythonPackageContent(Content):
140
140
  """
141
141
  A Content Type representing Python's Distribution Package.
142
142
 
143
- As defined in pep-0426 and pep-0345.
143
+ Core Metadata:
144
+ https://packaging.python.org/en/latest/specifications/core-metadata/
144
145
 
145
- https://www.python.org/dev/peps/pep-0491/
146
- https://www.python.org/dev/peps/pep-0345/
147
- """
148
-
149
- PROTECTED_FROM_RECLAIM = False
146
+ Release metadata (JSON API):
147
+ https://docs.pypi.org/api/json/
150
148
 
151
- TYPE = "python"
152
- repo_key_fields = ("filename",)
153
- # Required metadata
154
- filename = models.TextField(db_index=True)
155
- packagetype = models.TextField(choices=PACKAGE_TYPES)
156
- name = models.TextField()
157
- name.register_lookup(NormalizeName)
158
- version = models.TextField()
159
- sha256 = models.CharField(db_index=True, max_length=64)
160
- # Optional metadata
161
- python_version = models.TextField()
162
- metadata_version = models.TextField()
163
- summary = models.TextField()
164
- description = models.TextField()
165
- keywords = models.TextField()
166
- home_page = models.TextField()
167
- download_url = models.TextField()
149
+ File Formats:
150
+ https://packaging.python.org/en/latest/specifications/source-distribution-format/
151
+ https://packaging.python.org/en/latest/specifications/binary-distribution-format/
152
+ """
153
+ # Core metadata
154
+ # Version 1.0
168
155
  author = models.TextField()
169
156
  author_email = models.TextField()
157
+ description = models.TextField()
158
+ home_page = models.TextField() # Deprecated in favour of Project-URL
159
+ keywords = models.TextField()
160
+ license = models.TextField() # Deprecated in favour of License-Expression
161
+ metadata_version = models.TextField()
162
+ name = models.TextField()
163
+ platform = models.TextField()
164
+ summary = models.TextField()
165
+ version = models.TextField()
166
+ # Version 1.1
167
+ classifiers = models.JSONField(default=list)
168
+ download_url = models.TextField() # Deprecated in favour of Project-URL
169
+ supported_platform = models.TextField()
170
+ # Version 1.2
170
171
  maintainer = models.TextField()
171
172
  maintainer_email = models.TextField()
172
- license = models.TextField()
173
- requires_python = models.TextField()
173
+ obsoletes_dist = models.JSONField(default=list)
174
174
  project_url = models.TextField()
175
- platform = models.TextField()
176
- supported_platform = models.TextField()
177
- requires_dist = models.JSONField(default=list)
175
+ project_urls = models.JSONField(default=dict)
178
176
  provides_dist = models.JSONField(default=list)
179
- obsoletes_dist = models.JSONField(default=list)
180
177
  requires_external = models.JSONField(default=list)
181
- classifiers = models.JSONField(default=list)
182
- project_urls = models.JSONField(default=dict)
178
+ requires_dist = models.JSONField(default=list)
179
+ requires_python = models.TextField()
180
+ # Version 2.1
183
181
  description_content_type = models.TextField()
184
- # Pulp Domains
185
- _pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT)
182
+ provides_extras = models.JSONField(default=list)
183
+ # Version 2.2
184
+ dynamic = models.JSONField(default=list)
185
+ # Version 2.4
186
+ license_expression = models.TextField()
187
+ license_file = models.JSONField(default=list)
188
+
189
+ # Release metadata
190
+ filename = models.TextField(db_index=True)
191
+ packagetype = models.TextField(choices=PACKAGE_TYPES)
192
+ python_version = models.TextField()
193
+ sha256 = models.CharField(db_index=True, max_length=64)
194
+
195
+ # From pulpcore
196
+ PROTECTED_FROM_RECLAIM = False
197
+ TYPE = "python"
198
+ _pulp_domain = models.ForeignKey(
199
+ "core.Domain", default=get_domain_pk, on_delete=models.PROTECT
200
+ )
201
+ name.register_lookup(NormalizeName)
202
+ repo_key_fields = ("filename",)
186
203
 
187
204
  @staticmethod
188
205
  def init_from_artifact_and_relative_path(artifact, relative_path):
@@ -72,69 +72,69 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
72
72
  """
73
73
  A Serializer for PythonPackageContent.
74
74
  """
75
-
76
- filename = serializers.CharField(
77
- help_text=_('The name of the distribution package, usually of the format:'
78
- ' {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}'
79
- '-{platform tag}.{packagetype}'),
80
- read_only=True,
81
- )
82
- packagetype = serializers.CharField(
83
- help_text=_('The type of the distribution package '
84
- '(e.g. sdist, bdist_wheel, bdist_egg, etc)'),
85
- read_only=True,
86
- )
87
- name = serializers.CharField(
88
- help_text=_('The name of the python project.'),
89
- read_only=True,
90
- )
91
- version = serializers.CharField(
92
- help_text=_('The packages version number.'),
93
- read_only=True,
94
- )
95
- sha256 = serializers.CharField(
96
- default='',
97
- help_text=_('The SHA256 digest of this package.'),
98
- )
99
- metadata_version = serializers.CharField(
100
- help_text=_('Version of the file format'),
101
- read_only=True,
75
+ # Core metadata
76
+ # Version 1.0
77
+ author = serializers.CharField(
78
+ required=False, allow_blank=True,
79
+ help_text=_('Text containing the author\'s name. Contact information can also be added,'
80
+ ' separated with newlines.')
102
81
  )
103
- summary = serializers.CharField(
82
+ author_email = serializers.CharField(
104
83
  required=False, allow_blank=True,
105
- help_text=_('A one-line summary of what the package does.')
84
+ help_text=_('The author\'s e-mail address. ')
106
85
  )
107
86
  description = serializers.CharField(
108
87
  required=False, allow_blank=True,
109
88
  help_text=_('A longer description of the package that can run to several paragraphs.')
110
89
  )
111
- description_content_type = serializers.CharField(
90
+ home_page = serializers.CharField(
112
91
  required=False, allow_blank=True,
113
- help_text=_('A string stating the markup syntax (if any) used in the distribution’s'
114
- ' description, so that tools can intelligently render the description.')
92
+ help_text=_('The URL for the package\'s home page.')
115
93
  )
116
94
  keywords = serializers.CharField(
117
95
  required=False, allow_blank=True,
118
96
  help_text=_('Additional keywords to be used to assist searching for the '
119
97
  'package in a larger catalog.')
120
98
  )
121
- home_page = serializers.CharField(
99
+ license = serializers.CharField(
122
100
  required=False, allow_blank=True,
123
- help_text=_('The URL for the package\'s home page.')
101
+ help_text=_('Text indicating the license covering the distribution')
124
102
  )
125
- download_url = serializers.CharField(
103
+ metadata_version = serializers.CharField(
104
+ help_text=_('Version of the file format'),
105
+ read_only=True,
106
+ )
107
+ name = serializers.CharField(
108
+ help_text=_('The name of the python project.'),
109
+ read_only=True,
110
+ )
111
+ platform = serializers.CharField(
126
112
  required=False, allow_blank=True,
127
- help_text=_('Legacy field denoting the URL from which this package can be downloaded.')
113
+ help_text=_('A comma-separated list of platform specifications, '
114
+ 'summarizing the operating systems supported by the package.')
128
115
  )
129
- author = serializers.CharField(
116
+ summary = serializers.CharField(
130
117
  required=False, allow_blank=True,
131
- help_text=_('Text containing the author\'s name. Contact information can also be added,'
132
- ' separated with newlines.')
118
+ help_text=_('A one-line summary of what the package does.')
133
119
  )
134
- author_email = serializers.CharField(
120
+ version = serializers.CharField(
121
+ help_text=_('The packages version number.'),
122
+ read_only=True,
123
+ )
124
+ # Version 1.1
125
+ classifiers = serializers.JSONField(
126
+ required=False, default=list,
127
+ help_text=_('A JSON list containing classification values for a Python package.')
128
+ )
129
+ download_url = serializers.CharField(
135
130
  required=False, allow_blank=True,
136
- help_text=_('The author\'s e-mail address. ')
131
+ help_text=_('Legacy field denoting the URL from which this package can be downloaded.')
137
132
  )
133
+ supported_platform = serializers.CharField(
134
+ required=False, allow_blank=True,
135
+ help_text=_('Field to specify the OS and CPU for which the binary package was compiled. ')
136
+ )
137
+ # Version 1.2
138
138
  maintainer = serializers.CharField(
139
139
  required=False, allow_blank=True,
140
140
  help_text=_('The maintainer\'s name at a minimum; '
@@ -144,14 +144,11 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
144
144
  required=False, allow_blank=True,
145
145
  help_text=_('The maintainer\'s e-mail address.')
146
146
  )
147
- license = serializers.CharField(
148
- required=False, allow_blank=True,
149
- help_text=_('Text indicating the license covering the distribution')
150
- )
151
- requires_python = serializers.CharField(
152
- required=False, allow_blank=True,
153
- help_text=_('The Python version(s) that the distribution is guaranteed to be '
154
- 'compatible with.')
147
+ obsoletes_dist = serializers.JSONField(
148
+ required=False, default=list,
149
+ help_text=_('A JSON list containing names of a distutils project\'s distribution which '
150
+ 'this distribution renders obsolete, meaning that the two projects should not '
151
+ 'be installed at the same time.')
155
152
  )
156
153
  project_url = serializers.CharField(
157
154
  required=False, allow_blank=True,
@@ -161,39 +158,73 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
161
158
  required=False, default=dict,
162
159
  help_text=_('A dictionary of labels and URLs for the project.')
163
160
  )
164
- platform = serializers.CharField(
165
- required=False, allow_blank=True,
166
- help_text=_('A comma-separated list of platform specifications, '
167
- 'summarizing the operating systems supported by the package.')
161
+ provides_dist = serializers.JSONField(
162
+ required=False, default=list,
163
+ help_text=_('A JSON list containing names of a Distutils project which is contained'
164
+ ' within this distribution.')
168
165
  )
169
- supported_platform = serializers.CharField(
170
- required=False, allow_blank=True,
171
- help_text=_('Field to specify the OS and CPU for which the binary package was compiled. ')
166
+ requires_external = serializers.JSONField(
167
+ required=False, default=list,
168
+ help_text=_('A JSON list containing some dependency in the system that the distribution '
169
+ 'is to be used.')
172
170
  )
173
171
  requires_dist = serializers.JSONField(
174
172
  required=False, default=list,
175
173
  help_text=_('A JSON list containing names of some other distutils project '
176
174
  'required by this distribution.')
177
175
  )
178
- provides_dist = serializers.JSONField(
179
- required=False, default=list,
180
- help_text=_('A JSON list containing names of a Distutils project which is contained'
181
- ' within this distribution.')
176
+ requires_python = serializers.CharField(
177
+ required=False, allow_blank=True,
178
+ help_text=_('The Python version(s) that the distribution is guaranteed to be '
179
+ 'compatible with.')
182
180
  )
183
- obsoletes_dist = serializers.JSONField(
181
+ # Version 2.1
182
+ description_content_type = serializers.CharField(
183
+ required=False, allow_blank=True,
184
+ help_text=_('A string stating the markup syntax (if any) used in the distribution’s'
185
+ ' description, so that tools can intelligently render the description.')
186
+ )
187
+ provides_extras = serializers.JSONField(
184
188
  required=False, default=list,
185
- help_text=_('A JSON list containing names of a distutils project\'s distribution which '
186
- 'this distribution renders obsolete, meaning that the two projects should not '
187
- 'be installed at the same time.')
189
+ help_text=_('A JSON list containing names of optional features provided by the package.')
188
190
  )
189
- requires_external = serializers.JSONField(
191
+ # Version 2.2
192
+ dynamic = serializers.JSONField(
190
193
  required=False, default=list,
191
- help_text=_('A JSON list containing some dependency in the system that the distribution '
192
- 'is to be used.')
194
+ help_text=_('A JSON list containing names of other core metadata fields which are '
195
+ 'permitted to vary between sdist and bdist packages. Fields NOT marked '
196
+ 'dynamic MUST be the same between bdist and sdist.')
193
197
  )
194
- classifiers = serializers.JSONField(
198
+ # Version 2.4
199
+ license_expression = serializers.CharField(
200
+ required=False, allow_blank=True,
201
+ help_text=_('Text string that is a valid SPDX license expression.')
202
+ )
203
+ license_file = serializers.JSONField(
195
204
  required=False, default=list,
196
- help_text=_('A JSON list containing classification values for a Python package.')
205
+ help_text=_('A JSON list containing names of the paths to license-related files.')
206
+ )
207
+ # Release metadata
208
+ filename = serializers.CharField(
209
+ help_text=_('The name of the distribution package, usually of the format:'
210
+ ' {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}'
211
+ '-{platform tag}.{packagetype}'),
212
+ read_only=True,
213
+ )
214
+ packagetype = serializers.CharField(
215
+ help_text=_('The type of the distribution package '
216
+ '(e.g. sdist, bdist_wheel, bdist_egg, etc)'),
217
+ read_only=True,
218
+ )
219
+ python_version = serializers.CharField(
220
+ help_text=_(
221
+ 'The tag that indicates which Python implementation or version the package requires.'
222
+ ),
223
+ read_only=True,
224
+ )
225
+ sha256 = serializers.CharField(
226
+ default='',
227
+ help_text=_('The SHA256 digest of this package.'),
197
228
  )
198
229
 
199
230
  def deferred_validate(self, data):
@@ -242,11 +273,13 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
242
273
 
243
274
  class Meta:
244
275
  fields = core_serializers.SingleArtifactContentUploadSerializer.Meta.fields + (
245
- 'filename', 'packagetype', 'name', 'version', 'sha256', 'metadata_version', 'summary',
246
- 'description', 'description_content_type', 'keywords', 'home_page', 'download_url',
247
- 'author', 'author_email', 'maintainer', 'maintainer_email', 'license',
248
- 'requires_python', 'project_url', 'project_urls', 'platform', 'supported_platform',
249
- 'requires_dist', 'provides_dist', 'obsoletes_dist', 'requires_external', 'classifiers'
276
+ 'author', 'author_email', 'description', 'home_page', 'keywords', 'license',
277
+ 'metadata_version', 'name', 'platform', 'summary', 'version', 'classifiers',
278
+ 'download_url', 'supported_platform', 'maintainer', 'maintainer_email',
279
+ 'obsoletes_dist', 'project_url', 'project_urls', 'provides_dist', 'requires_external',
280
+ 'requires_dist', 'requires_python', 'description_content_type',
281
+ 'provides_extras', 'dynamic', 'license_expression', 'license_file',
282
+ 'filename', 'packagetype', 'python_version', 'sha256'
250
283
  )
251
284
  model = python_models.PythonPackageContent
252
285
 
@@ -0,0 +1,198 @@
1
+ import logging
2
+ from collections import defaultdict
3
+ from gettext import gettext as _
4
+ from itertools import groupby
5
+ from uuid import UUID
6
+
7
+ from django.db.models import Prefetch
8
+ from django.db.models.query import QuerySet
9
+ from pulp_python.app.models import PythonPackageContent, PythonRepository
10
+ from pulp_python.app.utils import (
11
+ artifact_to_python_content_data,
12
+ fetch_json_release_metadata,
13
+ parse_metadata,
14
+ )
15
+ from pulpcore.plugin.models import ContentArtifact, ProgressReport
16
+ from pulpcore.plugin.util import get_domain
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ BULK_SIZE = 1000
22
+
23
+
24
+ def repair(repository_pk: UUID) -> None:
25
+ """
26
+ Repairs metadata of all packages for the specified repository.
27
+
28
+ Args:
29
+ repository_pk (UUID): The primary key of the repository to repair.
30
+
31
+ Returns:
32
+ None
33
+ """
34
+ repository = PythonRepository.objects.get(pk=repository_pk)
35
+
36
+ log.info(
37
+ _(
38
+ "Repairing packages' metadata for the latest version of repository {}."
39
+ ).format(repository.name)
40
+ )
41
+ content_set = repository.latest_version().content.values_list("pk", flat=True)
42
+ content = PythonPackageContent.objects.filter(pk__in=content_set)
43
+
44
+ num_repaired, pkgs_not_repaired = repair_metadata(content)
45
+ log.info(
46
+ _(
47
+ "{} packages' metadata repaired. Not repaired packages due to either "
48
+ "inaccessible URL or mismatched sha256: {}."
49
+ ).format(num_repaired, pkgs_not_repaired)
50
+ )
51
+
52
+
53
+ def repair_metadata(content: QuerySet[PythonPackageContent]) -> tuple[int, set[str]]:
54
+ """
55
+ Repairs metadata for a queryset of PythonPackageContent objects
56
+ and updates the progress report.
57
+
58
+ Args:
59
+ content (QuerySet[PythonPackageContent]): The queryset of items to repair.
60
+
61
+ Returns:
62
+ tuple[int, set[str]]: A tuple containing:
63
+ - The number of packages that were repaired.
64
+ - A set of packages' PKs that were not repaired.
65
+ """
66
+ immediate_content = (
67
+ content.filter(contentartifact__artifact__isnull=False)
68
+ .distinct()
69
+ .prefetch_related("_artifacts")
70
+ )
71
+ on_demand_content = (
72
+ content.filter(contentartifact__artifact__isnull=True)
73
+ .distinct()
74
+ .prefetch_related(
75
+ Prefetch(
76
+ "contentartifact_set",
77
+ queryset=ContentArtifact.objects.prefetch_related("remoteartifact_set"),
78
+ )
79
+ )
80
+ .order_by("name", "version")
81
+ )
82
+ domain = get_domain()
83
+
84
+ batch = []
85
+ set_of_update_fields = set()
86
+ total_repaired = 0
87
+ # Keep track of on-demand packages that were not repaired
88
+ pkgs_not_repaired = set()
89
+
90
+ progress_report = ProgressReport(
91
+ message="Repairing packages' metadata",
92
+ code="repair.metadata",
93
+ total=content.count(),
94
+ )
95
+ progress_report.save()
96
+ with progress_report:
97
+ for package in progress_report.iter(
98
+ immediate_content.iterator(chunk_size=BULK_SIZE)
99
+ ):
100
+ new_data = artifact_to_python_content_data(
101
+ package.filename, package._artifacts.get(), domain
102
+ )
103
+ total_repaired += update_package_if_needed(
104
+ package, new_data, batch, set_of_update_fields
105
+ )
106
+
107
+ # For on-demand content, we expect that:
108
+ # 1. PythonPackageContent always has correct name and version
109
+ # 2. RemoteArtifact always has correct sha256
110
+ for (name, version), group in groupby(
111
+ on_demand_content.iterator(chunk_size=BULK_SIZE),
112
+ key=lambda x: (x.name, x.version),
113
+ ):
114
+ group_set = set(group)
115
+ grouped_by_url = defaultdict(list)
116
+
117
+ for package in group_set:
118
+ for ra in package.contentartifact_set.get().remoteartifact_set.all():
119
+ grouped_by_url[ra.remote.url].append((package, ra))
120
+
121
+ # Prioritize the URL that can serve the most packages
122
+ for url, pkg_ra_pairs in sorted(
123
+ grouped_by_url.items(), key=lambda x: len(x[1]), reverse=True
124
+ ):
125
+ if not group_set:
126
+ break # No packages left to repair, move onto the next group
127
+ remotes = set([pkg_ra[1].remote for pkg_ra in pkg_ra_pairs])
128
+ try:
129
+ json_data = fetch_json_release_metadata(name, version, remotes)
130
+ except Exception:
131
+ continue
132
+
133
+ for package, ra in pkg_ra_pairs:
134
+ if package not in group_set:
135
+ continue # Package was already repaired
136
+ # Extract data only for the specific distribution being checked
137
+ dist_data = None
138
+ for dist in json_data["urls"]:
139
+ if ra.sha256 == dist["digests"]["sha256"]:
140
+ dist_data = dist
141
+ break
142
+ if not dist_data:
143
+ continue
144
+
145
+ new_data = parse_metadata(json_data["info"], version, dist_data)
146
+ new_data.pop("url") # url belongs to RemoteArtifact
147
+ total_repaired += update_package_if_needed(
148
+ package, new_data, batch, set_of_update_fields
149
+ )
150
+ group_set.remove(package)
151
+ progress_report.increment()
152
+ # Store and track the unrepaired packages after all URLs are processed
153
+ pkgs_not_repaired.update([p.pk for p in group_set])
154
+ progress_report.increase_by(len(group_set))
155
+
156
+ if batch:
157
+ total_repaired += len(batch)
158
+ PythonPackageContent.objects.bulk_update(batch, set_of_update_fields)
159
+
160
+ return total_repaired, pkgs_not_repaired
161
+
162
+
163
+ def update_package_if_needed(
164
+ package: PythonPackageContent,
165
+ new_data: dict,
166
+ batch: list[PythonPackageContent],
167
+ set_of_update_fields: set[str],
168
+ ) -> int:
169
+ """
170
+ Compares the current package data with new data and updates the package
171
+ if needed ("batch" and "set_of_update_fields" are updated in-place).
172
+
173
+ Args:
174
+ package: Package to check and update.
175
+ new_data: A dict of new field values to compare against the package.
176
+ batch: A list of packages that were updated.
177
+ set_of_update_fields: A set of package field names that were updated.
178
+
179
+ Returns:
180
+ The count of repaired packages (increments in multiples of BULK_SIZE only).
181
+ """
182
+ total_repaired = 0
183
+ changed = False
184
+ for field, value in new_data.items():
185
+ if getattr(package, field) != value:
186
+ setattr(package, field, value)
187
+ set_of_update_fields.add(field)
188
+ changed = True
189
+ if changed:
190
+ batch.append(package)
191
+
192
+ if len(batch) == BULK_SIZE:
193
+ PythonPackageContent.objects.bulk_update(batch, set_of_update_fields)
194
+ total_repaired += BULK_SIZE
195
+ batch.clear()
196
+ set_of_update_fields.clear()
197
+
198
+ return total_repaired