Skip to content

Commit efd0b81

Browse files
authored
Merge branch 'main' into quote-remote-release-commands
2 parents 66a733e + 0fd6e16 commit efd0b81

2 files changed

Lines changed: 105 additions & 12 deletions

File tree

add_to_pydotorg.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import re
3030
import subprocess
3131
import sys
32-
from collections.abc import Generator
32+
from collections.abc import Generator, Iterable
3333
from functools import cache
3434
from os import path
3535
from typing import Any, NoReturn
@@ -307,19 +307,54 @@ def post_object(base_url: str, objtype: str, datadict: dict[str, Any]) -> int:
307307
"""Create a new API object."""
308308
resp = session.post(f"{base_url}downloads/{objtype}/", data=json.dumps(datadict))
309309
if resp.status_code != 201:
310+
messages = [f"Creating {objtype} failed: {resp.status_code}"]
310311
try:
311312
info = json.loads(resp.text)
312-
print(info.get("error_message", "No error message."))
313-
print(info.get("traceback", ""))
314-
except: # noqa: E722
313+
except json.JSONDecodeError:
315314
pass
316-
print(f"Creating {objtype} failed: {resp.status_code}")
317-
return -1
315+
else:
316+
if isinstance(info, dict):
317+
error_message = info.get("error_message")
318+
traceback = info.get("traceback")
319+
if error_message:
320+
messages.append(str(error_message))
321+
if traceback:
322+
messages.append(str(traceback))
323+
raise RuntimeError("\n".join(messages))
318324
newloc = resp.headers["Location"]
319325
pk = int(newloc.strip("/").split("/")[-1])
320326
return pk
321327

322328

329+
def delete_object(base_url: str, objtype: str, pk: int) -> None:
330+
"""Delete an existing API object."""
331+
resp = session.delete(f"{base_url}downloads/{objtype}/{pk}/")
332+
if resp.status_code != 204:
333+
raise RuntimeError(f"Deleting {objtype} {pk} failed: {resp.status_code}")
334+
335+
336+
def create_release_files(base_url: str, file_dicts: Iterable[dict[str, Any]]) -> int:
337+
"""Create ReleaseFile objects and clean up this run's rows on failure."""
338+
created_pks: list[int] = []
339+
try:
340+
for file_dict in file_dicts:
341+
file_pk = post_object(base_url, "release_file", file_dict)
342+
created_pks.append(file_pk)
343+
print("Created as id =", file_pk)
344+
except Exception as create_error:
345+
cleanup_errors = []
346+
for file_pk in reversed(created_pks):
347+
try:
348+
delete_object(base_url, "release_file", file_pk)
349+
except Exception as cleanup_error:
350+
cleanup_errors.append(f"{file_pk}: {cleanup_error}")
351+
if cleanup_errors:
352+
message = "Failed to clean up partially created release files:\n"
353+
raise RuntimeError(message + "\n".join(cleanup_errors)) from create_error
354+
raise
355+
return len(created_pks)
356+
357+
323358
def sign_release_files_with_sigstore(
324359
ftp_root: str, release: str, release_files: list[tuple[str, str, str, bool, str]]
325360
) -> None:
@@ -452,7 +487,6 @@ def main() -> None:
452487

453488
release_files = list(list_files(args.ftp_root, rel))
454489
sign_release_files_with_sigstore(args.ftp_root, rel, release_files)
455-
n = 0
456490
file_dicts = {}
457491
for rfile, file_desc, os_slug, add_download, add_desc in release_files:
458492
if not os_slug:
@@ -470,11 +504,7 @@ def main() -> None:
470504
resp = session.delete(f"{args.base_url}downloads/release_file/?release={rel_pk}")
471505
if resp.status_code != 204:
472506
raise RuntimeError(f"deleting previous releases failed: {resp.status_code}")
473-
for file_dict in file_dicts.values():
474-
file_pk = post_object(args.base_url, "release_file", file_dict)
475-
if file_pk >= 0:
476-
print("Created as id =", file_pk)
477-
n += 1
507+
n = create_release_files(args.base_url, file_dicts.values())
478508
print(f"Done - {n} files added")
479509

480510

tests/test_add_to_pydotorg.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
from pathlib import Path
3+
from typing import Any
34

45
import pytest
56
from pyfakefs.fake_filesystem import FakeFilesystem
@@ -77,6 +78,68 @@ def test_build_file_dict(tmp_path: Path) -> None:
7778
}
7879

7980

81+
def test_post_object_raises_on_failure(monkeypatch: pytest.MonkeyPatch) -> None:
82+
class Response:
83+
status_code = 500
84+
text = '{"error_message": "validation failed", "traceback": "details"}'
85+
86+
def fake_post(*args: Any, **kwargs: Any) -> Response:
87+
return Response()
88+
89+
monkeypatch.setattr(add_to_pydotorg.session, "post", fake_post)
90+
91+
with pytest.raises(RuntimeError, match="validation failed"):
92+
add_to_pydotorg.post_object(
93+
"https://example.invalid/api/v1/", "release_file", {"slug": "bad"}
94+
)
95+
96+
97+
def test_delete_object_uses_session(monkeypatch: pytest.MonkeyPatch) -> None:
98+
class Response:
99+
status_code = 204
100+
101+
calls: list[str] = []
102+
103+
def fake_delete(url: str) -> Response:
104+
calls.append(url)
105+
return Response()
106+
107+
monkeypatch.setattr(add_to_pydotorg.session, "delete", fake_delete)
108+
109+
add_to_pydotorg.delete_object(
110+
"https://example.invalid/api/v1/", "release_file", 123
111+
)
112+
113+
assert calls == ["https://example.invalid/api/v1/downloads/release_file/123/"]
114+
115+
116+
def test_create_release_files_cleans_up_created_rows(
117+
monkeypatch: pytest.MonkeyPatch,
118+
) -> None:
119+
events: list[tuple[str, str | int]] = []
120+
121+
def fake_post_object(base_url: str, objtype: str, datadict: dict[str, Any]) -> int:
122+
slug = str(datadict["slug"])
123+
events.append(("post", slug))
124+
if slug == "bad":
125+
raise RuntimeError("create failed")
126+
return 101
127+
128+
def fake_delete_object(base_url: str, objtype: str, pk: int) -> None:
129+
events.append(("delete", pk))
130+
131+
monkeypatch.setattr(add_to_pydotorg, "post_object", fake_post_object)
132+
monkeypatch.setattr(add_to_pydotorg, "delete_object", fake_delete_object)
133+
134+
with pytest.raises(RuntimeError, match="create failed"):
135+
add_to_pydotorg.create_release_files(
136+
"https://example.invalid/api/v1/",
137+
[{"slug": "ok"}, {"slug": "bad"}],
138+
)
139+
140+
assert events == [("post", "ok"), ("post", "bad"), ("delete", 101)]
141+
142+
80143
@pytest.mark.parametrize(
81144
["release", "expected"],
82145
[

0 commit comments

Comments
 (0)