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.
- {pulp_python-3.15.1 → pulp_python-3.17.0}/CHANGES.md +23 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/PKG-INFO +4 -3
- pulp_python-3.17.0/README.md +9 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/__init__.py +1 -1
- pulp_python-3.17.0/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +34 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/models.py +50 -33
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/serializers.py +107 -74
- pulp_python-3.17.0/pulp_python/app/tasks/repair.py +198 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/utils.py +77 -29
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +32 -2
- pulp_python-3.17.0/pulp_python/tests/functional/api/test_repair.py +226 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/constants.py +6 -5
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/PKG-INFO +4 -3
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/SOURCES.txt +1 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/requires.txt +1 -1
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pyproject.toml +4 -4
- pulp_python-3.15.1/README.md +0 -8
- pulp_python-3.15.1/pulp_python/app/tasks/repair.py +0 -89
- pulp_python-3.15.1/pulp_python/tests/functional/api/test_repair.py +0 -126
- {pulp_python-3.15.1 → pulp_python-3.17.0}/COMMITMENT +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/COPYRIGHT +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/LICENSE +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/MANIFEST.in +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/functest_requirements.txt +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/__init__.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/global_access_conditions.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/management/__init__.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/management/commands/__init__.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0001_initial.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/migrations/__init__.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/modelresource.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/pypi/__init__.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/pypi/serializers.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/pypi/views.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/replica.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/settings.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/tasks/__init__.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/tasks/publish.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/tasks/sync.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/tasks/upload.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/urls.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/viewsets.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/pytest_plugin.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/__init__.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/__init__.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/__init__.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_domains.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/api/test_sync.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/functional/utils.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/unit/__init__.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python/tests/unit/test_models.py +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/dependency_links.txt +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/entry_points.txt +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/pulp_python.egg-info/top_level.txt +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/setup.cfg +0 -0
- {pulp_python-3.15.1 → pulp_python-3.17.0}/test_requirements.txt +0 -0
- {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.
|
|
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.
|
|
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
|
-
](https://pypi.python.org/pypi/pulp_python)
|
|
33
|
+
[](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
|
+
[](https://pypi.python.org/pypi/pulp_python)
|
|
4
|
+
[](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).
|
|
@@ -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
|
-
|
|
143
|
+
Core Metadata:
|
|
144
|
+
https://packaging.python.org/en/latest/specifications/core-metadata/
|
|
144
145
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"""
|
|
148
|
-
|
|
149
|
-
PROTECTED_FROM_RECLAIM = False
|
|
146
|
+
Release metadata (JSON API):
|
|
147
|
+
https://docs.pypi.org/api/json/
|
|
150
148
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
173
|
-
requires_python = models.TextField()
|
|
173
|
+
obsoletes_dist = models.JSONField(default=list)
|
|
174
174
|
project_url = models.TextField()
|
|
175
|
-
|
|
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
|
-
|
|
182
|
-
|
|
178
|
+
requires_dist = models.JSONField(default=list)
|
|
179
|
+
requires_python = models.TextField()
|
|
180
|
+
# Version 2.1
|
|
183
181
|
description_content_type = models.TextField()
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
82
|
+
author_email = serializers.CharField(
|
|
104
83
|
required=False, allow_blank=True,
|
|
105
|
-
help_text=_('
|
|
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
|
-
|
|
90
|
+
home_page = serializers.CharField(
|
|
112
91
|
required=False, allow_blank=True,
|
|
113
|
-
help_text=_('
|
|
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
|
-
|
|
99
|
+
license = serializers.CharField(
|
|
122
100
|
required=False, allow_blank=True,
|
|
123
|
-
help_text=_('
|
|
101
|
+
help_text=_('Text indicating the license covering the distribution')
|
|
124
102
|
)
|
|
125
|
-
|
|
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=_('
|
|
113
|
+
help_text=_('A comma-separated list of platform specifications, '
|
|
114
|
+
'summarizing the operating systems supported by the package.')
|
|
128
115
|
)
|
|
129
|
-
|
|
116
|
+
summary = serializers.CharField(
|
|
130
117
|
required=False, allow_blank=True,
|
|
131
|
-
help_text=_('
|
|
132
|
-
' separated with newlines.')
|
|
118
|
+
help_text=_('A one-line summary of what the package does.')
|
|
133
119
|
)
|
|
134
|
-
|
|
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=_('
|
|
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
|
-
|
|
148
|
-
required=False,
|
|
149
|
-
help_text=_('
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
165
|
-
required=False,
|
|
166
|
-
help_text=_('A
|
|
167
|
-
'
|
|
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
|
-
|
|
170
|
-
required=False,
|
|
171
|
-
help_text=_('
|
|
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
|
-
|
|
179
|
-
required=False,
|
|
180
|
-
help_text=_('
|
|
181
|
-
'
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
191
|
+
# Version 2.2
|
|
192
|
+
dynamic = serializers.JSONField(
|
|
190
193
|
required=False, default=list,
|
|
191
|
-
help_text=_('A JSON list containing
|
|
192
|
-
'
|
|
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
|
-
|
|
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
|
|
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
|
-
'
|
|
246
|
-
'
|
|
247
|
-
'
|
|
248
|
-
'
|
|
249
|
-
'requires_dist', '
|
|
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
|