Skip to content

Commit 46e3c60

Browse files
authored
Merge pull request #1011 from gerrod3/pep700
Implement PEP 700
2 parents 3af3ef2 + c52ecd7 commit 46e3c60

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
@@ -305,6 +305,9 @@ def parse_package(release_package):
305305
"sha256": release_package.digests.get("sha256", ""),
306306
"requires_python": release_package.requires_python,
307307
"metadata_sha256": (release_package.metadata_digests or {}).get("sha256"),
308+
"size": release_package.size,
309+
"upload_time": release_package.upload_time,
310+
"version": release_package.version,
308311
}
309312

310313
rfilter = get_remote_package_filter(remote)
@@ -346,12 +349,19 @@ def retrieve(self, request, path, package):
346349
return redirect(urljoin(self.base_content_url, f"{path}/simple/{normalized}/"))
347350
if content:
348351
packages = content.filter(name__normalize=normalized).values(
349-
"filename", "sha256", "metadata_sha256", "requires_python"
352+
"filename",
353+
"sha256",
354+
"metadata_sha256",
355+
"requires_python",
356+
"size",
357+
"pulp_created",
358+
"version",
350359
)
351360
local_releases = {
352361
p["filename"]: {
353362
**p,
354363
"url": urljoin(self.base_content_url, f"{path}/{p['filename']}"),
364+
"upload_time": p["pulp_created"],
355365
}
356366
for p in packages
357367
}

pulp_python/app/serializers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,10 @@ class PythonPackageContentSerializer(core_serializers.SingleArtifactContentUploa
281281
),
282282
read_only=True,
283283
)
284+
size = serializers.IntegerField(
285+
help_text=_("The size of the package in bytes."),
286+
read_only=True,
287+
)
284288
sha256 = serializers.CharField(
285289
default="",
286290
help_text=_("The SHA256 digest of this package."),
@@ -372,6 +376,7 @@ class Meta:
372376
"filename",
373377
"packagetype",
374378
"python_version",
379+
"size",
375380
"sha256",
376381
"metadata_sha256",
377382
)
@@ -425,6 +430,7 @@ def validate(self, data):
425430
data["artifact"] = artifact
426431
data["sha256"] = artifact.sha256
427432
data["relative_path"] = filename
433+
data["size"] = artifact.size
428434
data.update(parse_project_metadata(vars(metadata)))
429435
# Overwrite filename from metadata
430436
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)