Skip to content

Commit 32c9075

Browse files
committed
Fail add_to_pydotorg on create errors
1 parent 6e6f19d commit 32c9075

2 files changed

Lines changed: 87 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
@@ -308,19 +308,54 @@ def post_object(base_url: str, objtype: str, datadict: dict[str, Any]) -> int:
308308
headers=headers,
309309
)
310310
if resp.status_code != 201:
311+
messages = [f"Creating {objtype} failed: {resp.status_code}"]
311312
try:
312313
info = json.loads(resp.text)
313-
print(info.get("error_message", "No error message."))
314-
print(info.get("traceback", ""))
315-
except: # noqa: E722
314+
except json.JSONDecodeError:
316315
pass
317-
print(f"Creating {objtype} failed: {resp.status_code}")
318-
return -1
316+
else:
317+
if isinstance(info, dict):
318+
error_message = info.get("error_message")
319+
traceback = info.get("traceback")
320+
if error_message:
321+
messages.append(str(error_message))
322+
if traceback:
323+
messages.append(str(traceback))
324+
raise RuntimeError("\n".join(messages))
319325
newloc = resp.headers["Location"]
320326
pk = int(newloc.strip("/").split("/")[-1])
321327
return pk
322328

323329

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

454489
release_files = list(list_files(args.ftp_root, rel))
455490
sign_release_files_with_sigstore(args.ftp_root, rel, release_files)
456-
n = 0
457491
file_dicts = {}
458492
for rfile, file_desc, os_slug, add_download, add_desc in release_files:
459493
if not os_slug:
@@ -473,11 +507,7 @@ def main() -> None:
473507
)
474508
if resp.status_code != 204:
475509
raise RuntimeError(f"deleting previous releases failed: {resp.status_code}")
476-
for file_dict in file_dicts.values():
477-
file_pk = post_object(args.base_url, "release_file", file_dict)
478-
if file_pk >= 0:
479-
print("Created as id =", file_pk)
480-
n += 1
510+
n = create_release_files(args.base_url, file_dicts.values())
481511
print(f"Done - {n} files added")
482512

483513

tests/test_add_to_pydotorg.py

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

45
import pytest
6+
import requests
57
from pyfakefs.fake_filesystem import FakeFilesystem
68

79
os.environ["AUTH_INFO"] = "test_username:test_api_key"
@@ -77,6 +79,49 @@ def test_build_file_dict(tmp_path: Path) -> None:
7779
}
7880

7981

82+
def test_post_object_raises_on_failure(monkeypatch: pytest.MonkeyPatch) -> None:
83+
class Response:
84+
status_code = 500
85+
text = '{"error_message": "validation failed", "traceback": "details"}'
86+
87+
def fake_post(*args: Any, **kwargs: Any) -> Response:
88+
return Response()
89+
90+
monkeypatch.setattr(requests, "post", fake_post)
91+
92+
with pytest.raises(RuntimeError, match="validation failed"):
93+
add_to_pydotorg.post_object(
94+
"https://example.invalid/api/v1/", "release_file", {"slug": "bad"}
95+
)
96+
97+
98+
def test_create_release_files_cleans_up_created_rows(
99+
monkeypatch: pytest.MonkeyPatch,
100+
) -> None:
101+
events: list[tuple[str, str | int]] = []
102+
103+
def fake_post_object(base_url: str, objtype: str, datadict: dict[str, Any]) -> int:
104+
slug = str(datadict["slug"])
105+
events.append(("post", slug))
106+
if slug == "bad":
107+
raise RuntimeError("create failed")
108+
return 101
109+
110+
def fake_delete_object(base_url: str, objtype: str, pk: int) -> None:
111+
events.append(("delete", pk))
112+
113+
monkeypatch.setattr(add_to_pydotorg, "post_object", fake_post_object)
114+
monkeypatch.setattr(add_to_pydotorg, "delete_object", fake_delete_object)
115+
116+
with pytest.raises(RuntimeError, match="create failed"):
117+
add_to_pydotorg.create_release_files(
118+
"https://example.invalid/api/v1/",
119+
[{"slug": "ok"}, {"slug": "bad"}],
120+
)
121+
122+
assert events == [("post", "ok"), ("post", "bad"), ("delete", 101)]
123+
124+
80125
@pytest.mark.parametrize(
81126
["release", "expected"],
82127
[

0 commit comments

Comments
 (0)