Skip to content

Commit 2552b38

Browse files
authored
Merge pull request #885 from jobselko/metadata_support
Fully support the latest core metadata (up to 2.4)
2 parents 44090d1 + cdd1323 commit 2552b38

File tree

8 files changed

+270
-141
lines changed

8 files changed

+270
-141
lines changed

CHANGES/689.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added full support for the latest core metadata (up to 2.4).
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
]

pulp_python/app/models.py

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -140,49 +140,66 @@ class PythonPackageContent(Content):
140140
"""
141141
A Content Type representing Python's Distribution Package.
142142
143-
As defined in pep-0426 and pep-0345.
143+
Core Metadata:
144+
https://packaging.python.org/en/latest/specifications/core-metadata/
144145
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/
150148
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
168155
author = models.TextField()
169156
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
170171
maintainer = models.TextField()
171172
maintainer_email = models.TextField()
172-
license = models.TextField()
173-
requires_python = models.TextField()
173+
obsoletes_dist = models.JSONField(default=list)
174174
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)
178176
provides_dist = models.JSONField(default=list)
179-
obsoletes_dist = models.JSONField(default=list)
180177
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
183181
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",)
186203

187204
@staticmethod
188205
def init_from_artifact_and_relative_path(artifact, relative_path):

pulp_python/app/serializers.py

Lines changed: 107 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -72,69 +72,69 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
7272
"""
7373
A Serializer for PythonPackageContent.
7474
"""
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.')
10281
)
103-
summary = serializers.CharField(
82+
author_email = serializers.CharField(
10483
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. ')
10685
)
10786
description = serializers.CharField(
10887
required=False, allow_blank=True,
10988
help_text=_('A longer description of the package that can run to several paragraphs.')
11089
)
111-
description_content_type = serializers.CharField(
90+
home_page = serializers.CharField(
11291
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.')
11593
)
11694
keywords = serializers.CharField(
11795
required=False, allow_blank=True,
11896
help_text=_('Additional keywords to be used to assist searching for the '
11997
'package in a larger catalog.')
12098
)
121-
home_page = serializers.CharField(
99+
license = serializers.CharField(
122100
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')
124102
)
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(
126112
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.')
128115
)
129-
author = serializers.CharField(
116+
summary = serializers.CharField(
130117
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.')
133119
)
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(
135130
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.')
137132
)
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
138138
maintainer = serializers.CharField(
139139
required=False, allow_blank=True,
140140
help_text=_('The maintainer\'s name at a minimum; '
@@ -144,14 +144,11 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
144144
required=False, allow_blank=True,
145145
help_text=_('The maintainer\'s e-mail address.')
146146
)
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.')
155152
)
156153
project_url = serializers.CharField(
157154
required=False, allow_blank=True,
@@ -161,39 +158,73 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
161158
required=False, default=dict,
162159
help_text=_('A dictionary of labels and URLs for the project.')
163160
)
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.')
168165
)
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.')
172170
)
173171
requires_dist = serializers.JSONField(
174172
required=False, default=list,
175173
help_text=_('A JSON list containing names of some other distutils project '
176174
'required by this distribution.')
177175
)
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.')
182180
)
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(
184188
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.')
188190
)
189-
requires_external = serializers.JSONField(
191+
# Version 2.2
192+
dynamic = serializers.JSONField(
190193
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.')
193197
)
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(
195204
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.'),
197228
)
198229

199230
def deferred_validate(self, data):
@@ -242,11 +273,13 @@ def retrieve(self, validated_data):
242273

243274
class Meta:
244275
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'
250283
)
251284
model = python_models.PythonPackageContent
252285

0 commit comments

Comments
 (0)