Skip to content

Commit c52ecd7

Browse files
committed
Implement PEP 700
fixes: #996
1 parent f1052ef commit c52ecd7

File tree

7 files changed

+94
-8
lines changed

7 files changed

+94
-8
lines changed

CHANGES/996.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implemented PEP 700 support, adding `versions`, `size` and `upload-time` to the Simple JSON API.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Generated by Django 4.2.26 on 2025-11-11 21:43
2+
3+
from django.db import migrations, models, transaction
4+
5+
6+
def add_size_to_current_models(apps, schema_editor):
7+
"""Adds the size to current PythonPackageContent models."""
8+
PythonPackageContent = apps.get_model("python", "PythonPackageContent")
9+
RemoteArtifact = apps.get_model("core", "RemoteArtifact")
10+
package_bulk = []
11+
for python_package in PythonPackageContent.objects.only("pk", "size").iterator():
12+
content_artifact = python_package.contentartifact_set.first()
13+
if content_artifact.artifact:
14+
artifact = content_artifact.artifact
15+
else:
16+
artifact = RemoteArtifact.objects.filter(content_artifact=content_artifact).first()
17+
python_package.size = artifact.size or 0
18+
package_bulk.append(python_package)
19+
if len(package_bulk) == 100000:
20+
with transaction.atomic():
21+
PythonPackageContent.objects.bulk_update(
22+
package_bulk,
23+
[
24+
"size",
25+
],
26+
)
27+
package_bulk = []
28+
with transaction.atomic():
29+
PythonPackageContent.objects.bulk_update(
30+
package_bulk,
31+
[
32+
"size",
33+
],
34+
)
35+
36+
37+
class Migration(migrations.Migration):
38+
39+
dependencies = [
40+
("python", "0016_pythonpackagecontent_metadata_sha256"),
41+
]
42+
43+
operations = [
44+
migrations.AddField(
45+
model_name="pythonpackagecontent",
46+
name="size",
47+
field=models.BigIntegerField(default=0),
48+
),
49+
migrations.RunPython(add_size_to_current_models, migrations.RunPython.noop, elidable=True),
50+
]

pulp_python/app/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ class PythonPackageContent(Content):
193193
python_version = models.TextField()
194194
sha256 = models.CharField(db_index=True, max_length=64)
195195
metadata_sha256 = models.CharField(max_length=64, null=True)
196+
size = models.BigIntegerField(default=0)
196197
# yanked and yanked_reason are not implemented because they are mutable
197198

198199
# From pulpcore

pulp_python/app/pypi/views.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,9 @@ def parse_package(release_package):
302302
"sha256": release_package.digests.get("sha256", ""),
303303
"requires_python": release_package.requires_python,
304304
"metadata_sha256": (release_package.metadata_digests or {}).get("sha256"),
305+
"size": release_package.size,
306+
"upload_time": release_package.upload_time,
307+
"version": release_package.version,
305308
}
306309

307310
rfilter = get_remote_package_filter(remote)
@@ -343,12 +346,19 @@ def retrieve(self, request, path, package):
343346
return redirect(urljoin(self.base_content_url, f"{path}/simple/{normalized}/"))
344347
if content:
345348
packages = content.filter(name__normalize=normalized).values(
346-
"filename", "sha256", "metadata_sha256", "requires_python"
349+
"filename",
350+
"sha256",
351+
"metadata_sha256",
352+
"requires_python",
353+
"size",
354+
"pulp_created",
355+
"version",
347356
)
348357
local_releases = {
349358
p["filename"]: {
350359
**p,
351360
"url": urljoin(self.base_content_url, f"{path}/{p['filename']}"),
361+
"upload_time": p["pulp_created"],
352362
}
353363
for p in packages
354364
}

pulp_python/app/serializers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
277277
),
278278
read_only=True,
279279
)
280+
size = serializers.IntegerField(
281+
help_text=_("The size of the package in bytes."),
282+
read_only=True,
283+
)
280284
sha256 = serializers.CharField(
281285
default="",
282286
help_text=_("The SHA256 digest of this package."),
@@ -368,6 +372,7 @@ class Meta:
368372
"filename",
369373
"packagetype",
370374
"python_version",
375+
"size",
371376
"sha256",
372377
"metadata_sha256",
373378
)
@@ -421,6 +426,7 @@ def validate(self, data):
421426
data["artifact"] = artifact
422427
data["sha256"] = artifact.sha256
423428
data["relative_path"] = filename
429+
data["size"] = artifact.size
424430
data.update(parse_project_metadata(vars(metadata)))
425431
# Overwrite filename from metadata
426432
data["filename"] = filename

pulp_python/app/utils.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import json
88
from collections import defaultdict
99
from django.conf import settings
10+
from django.utils import timezone
1011
from jinja2 import Template
1112
from packaging.utils import canonicalize_name
1213
from packaging.requirements import Requirement
@@ -18,7 +19,7 @@
1819
"""TODO This serial constant is temporary until Python repositories implements serials"""
1920
PYPI_SERIAL_CONSTANT = 1000000000
2021

21-
SIMPLE_API_VERSION = "1.0"
22+
SIMPLE_API_VERSION = "1.1"
2223

2324
simple_index_template = """<!DOCTYPE html>
2425
<html>
@@ -161,6 +162,7 @@ def parse_metadata(project, version, distribution):
161162
package["sha256"] = distribution.get("digests", {}).get("sha256") or ""
162163
package["python_version"] = distribution.get("python_version") or ""
163164
package["requires_python"] = distribution.get("requires_python") or ""
165+
package["size"] = distribution.get("size") or 0
164166

165167
return package
166168

@@ -223,6 +225,7 @@ def artifact_to_python_content_data(filename, artifact, domain=None):
223225
metadata = get_project_metadata_from_file(temp_file.name)
224226
data = parse_project_metadata(vars(metadata))
225227
data["sha256"] = artifact.sha256
228+
data["size"] = artifact.size
226229
data["filename"] = filename
227230
data["pulp_domain"] = domain or artifact.pulp_domain
228231
data["_pulp_domain"] = data["pulp_domain"]
@@ -403,7 +406,6 @@ def find_artifact():
403406
components.insert(2, domain.name)
404407
url = "/".join(components)
405408
md5 = artifact.md5 if artifact and artifact.md5 else ""
406-
size = artifact.size if artifact and artifact.size else 0
407409
return {
408410
"comment_text": "",
409411
"digests": {"md5": md5, "sha256": content.sha256},
@@ -414,7 +416,7 @@ def find_artifact():
414416
"packagetype": content.packagetype,
415417
"python_version": content.python_version,
416418
"requires_python": content.requires_python or None,
417-
"size": size,
419+
"size": content.size,
418420
"upload_time": str(content.pulp_created),
419421
"upload_time_iso_8601": str(content.pulp_created.isoformat()),
420422
"url": url,
@@ -471,20 +473,32 @@ def write_simple_detail_json(project_name, project_packages):
471473
{"sha256": package["metadata_sha256"]} if package["metadata_sha256"] else False
472474
),
473475
# yanked and yanked_reason are not implemented because they are mutable
476+
# (v1.1, PEP 700)
477+
"size": package["size"],
478+
"upload-time": format_upload_time(package["upload_time"]),
474479
# TODO in the future:
475-
# size, upload-time (v1.1, PEP 700)
476480
# core-metadata (PEP 7.14)
477481
# provenance (v1.3, PEP 740)
478482
}
479483
for package in project_packages
480484
],
485+
# (v1.1, PEP 700)
486+
"versions": sorted(set(package["version"] for package in project_packages)),
481487
# TODO in the future:
482-
# versions (v1.1, PEP 700)
483488
# alternate-locations (v1.2, PEP 708)
484489
# project-status (v1.4, PEP 792 - pypi and docs differ)
485490
}
486491

487492

493+
def format_upload_time(upload_time):
494+
"""Formats the upload time to be in Zulu time. UTC with Z suffix"""
495+
if upload_time:
496+
if upload_time.tzinfo:
497+
dt = upload_time.astimezone(timezone.utc)
498+
return dt.isoformat().replace("+00:00", "Z")
499+
return None
500+
501+
488502
class PackageIncludeFilter:
489503
"""A special class to help filter Package's based on a remote's include/exclude"""
490504

pulp_python/tests/functional/api/test_pypi_simple_json_api.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
PYTHON_WHEEL_URL,
1212
)
1313

14-
API_VERSION = "1.0"
14+
API_VERSION = "1.1"
1515
PYPI_SERIAL_CONSTANT = 1000000000
1616

1717
PYPI_TEXT_HTML = "text/html"
@@ -69,6 +69,7 @@ def test_simple_json_detail_api(
6969
assert data["meta"] == {"api-version": API_VERSION, "_last-serial": PYPI_SERIAL_CONSTANT}
7070
assert data["name"] == "shelf-reader"
7171
assert data["files"]
72+
assert data["versions"] == ["0.1"]
7273

7374
# Check data of a wheel
7475
file_whl = next(
@@ -83,7 +84,8 @@ def test_simple_json_detail_api(
8384
assert file_whl["data-dist-info-metadata"] == {
8485
"sha256": "ed333f0db05d77e933a157b7225b403ada9a2f93318d77b41b662eba78bac350"
8586
}
86-
87+
assert file_whl["size"] == 22455
88+
assert file_whl["upload-time"] is not None
8789
# Check data of a tarball
8890
file_tar = next((i for i in data["files"] if i["filename"] == "shelf-reader-0.1.tar.gz"), None)
8991
assert file_tar is not None, "tar file not found"
@@ -93,6 +95,8 @@ def test_simple_json_detail_api(
9395
}
9496
assert file_tar["requires-python"] is None
9597
assert file_tar["data-dist-info-metadata"] is False
98+
assert file_tar["size"] == 19097
99+
assert file_tar["upload-time"] is not None
96100

97101

98102
@pytest.mark.parallel

0 commit comments

Comments
 (0)