Ask flickr for each photo's details and its largest size

Two unsigned json calls per photo: getInfo for title, owner, dates and
licence, getSizes for the renditions. Sizes come back ordered best-first so
a missing original can fall through to the next. Not-found and permission
errors become PhotoGone so the run can name the wiki pages affected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bobandClaude Fable 5.1 committed 2026-09-10 12:07:44 +01:00
1 parent b68d477938
commit fc434a3d9b
2 files changed
+224

No files matched your search

+112
View File
@@ -421,6 +421,14 @@ class Retrying(TestCase):
path = os.path.join(self.dir, "p.part")
self.assertEqual(rb.download("http://t/p.jpg", "ctx", path), ("image/jpeg", 1000))
self.assertEqual(os.path.getsize(path), 1000)
def test_pace_sleeps_before_a_flickr_call(self):
waits = []
rb.SLEEP = waits.append
rb.PACE = 2.0
rb.OPEN = FakeOpen(FakeResponse({"stat": "ok"}))
rb.flickrcall("flickr.test.echo", {}, "KEY")
self.assertEqual(waits, [2.0])
def test_download_refuses_an_oversized_body(self):
rb.DOWNLOAD_CAP = 10
rb.OPEN = FakeOpen(FakeResponse(b"x" * 100))
@@ -429,3 +437,107 @@ class Retrying(TestCase):
rb.download("http://t/p.jpg", "ctx", path)
self.assertIn("10", str(cm.exception))
self.assertEqual(len(rb.OPEN.requests), 1) # not a retryable failure
def test_debug_redacts_the_flickr_key(self):
rb.DEBUG = True
rb.OPEN = FakeOpen(FakeResponse({"stat": "ok"}))
rb.flickrcall("flickr.test.echo", {}, "SECRETKEY")
logged = self.stderr.getvalue()
self.assertIn("api_key=<redacted>", logged)
self.assertNotIn("SECRETKEY", logged)
def flickr_info(photoid="111", licence=5, realname="Real Name"):
return {"stat": "ok", "photo": {
"id": photoid, "license": str(licence), "originalformat": "jpg",
"owner": {"nsid": "12345678@N00", "username": "handle", "realname": realname},
"title": {"_content": "A title"}, "description": {"_content": "Words"},
"dates": {"taken": "2009-07-22 14:03:11", "posted": "1248345600"},
"urls": {"url": [{"type": "photopage",
"_content": "https://www.flickr.com/photos/12345678@N00/%s/" % photoid}]},
}}
def flickr_sizes(*sizes):
return {"stat": "ok", "sizes": {"size": [
{"label": l, "width": w, "height": h, "source": s, "media": "photo"}
for (l, w, h, s) in sizes]}}
class Flickr(TestCase):
def test_call_builds_unsigned_json_request(self):
rb.OPEN = FakeOpen(FakeResponse({"stat": "ok", "x": 1}))
self.assertEqual(rb.flickrcall("flickr.test.echo", {"a": "b"}, "KEY")["x"], 1)
url = rb.OPEN.requests[0].full_url
q = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)
self.assertEqual(q["method"], ["flickr.test.echo"])
self.assertEqual(q["api_key"], ["KEY"])
self.assertEqual(q["format"], ["json"])
self.assertEqual(q["nojsoncallback"], ["1"])
self.assertEqual(q["a"], ["b"])
def test_api_failure_raises(self):
rb.OPEN = FakeOpen(FakeResponse({"stat": "fail", "code": 100, "message": "Invalid API Key"}))
with self.assertRaises(rb.BackupError) as cm:
rb.flickrcall("flickr.photos.getInfo", {"photo_id": "1"}, "k")
self.assertIn("Invalid API Key", str(cm.exception))
def test_transient_service_error_is_retried(self):
busy = {"stat": "fail", "code": 105, "message": "Service currently unavailable"}
rb.OPEN = FakeOpen(FakeResponse(busy), FakeResponse(busy),
FakeResponse({"stat": "ok", "x": 1}))
self.assertEqual(rb.flickrcall("flickr.test.echo", {}, "k")["x"], 1)
self.assertEqual(len(rb.OPEN.requests), 3)
self.assertEqual(rb.STATS["retries"], 2)
def test_transient_service_error_gives_up_after_attempts(self):
rb.ATTEMPTS = 2
busy = {"stat": "fail", "code": "106", "message": "Write operation failed"}
rb.OPEN = FakeOpen(FakeResponse(busy), FakeResponse(busy))
with self.assertRaises(rb.BackupError) as cm:
rb.flickrcall("flickr.test.echo", {}, "k")
self.assertIn("Write operation failed", str(cm.exception))
self.assertEqual(len(rb.OPEN.requests), 2)
def test_missing_or_private_photo_is_gone(self):
for code in (1, 2):
rb.OPEN = FakeOpen(FakeResponse({"stat": "fail", "code": code, "message": "nope"}))
with self.assertRaises(rb.PhotoGone):
rb.getinfo("1", "k")
def test_getinfo_flattens(self):
rb.OPEN = FakeOpen(FakeResponse(flickr_info()))
info = rb.getinfo("111", "k")
self.assertEqual(info["id"], "111")
self.assertEqual(info["owner"], "12345678@N00")
self.assertEqual(info["owner_name"], "handle")
self.assertEqual(info["owner_realname"], "Real Name")
self.assertEqual(info["title"], "A title")
self.assertEqual(info["taken"], "2009-07-22 14:03:11")
self.assertEqual(info["posted"], "2009-07-23T10:40:00Z")
self.assertEqual(info["licence_id"], 5)
self.assertEqual(info["page"], "https://www.flickr.com/photos/12345678@N00/111/")
self.assertEqual(info["original_format"], "jpg")
def test_getsizes_normalises_numbers(self):
rb.OPEN = FakeOpen(FakeResponse(flickr_sizes(("Small", "240", "180", "https://x/s.jpg"))))
sizes = rb.getsizes("111", "k")
self.assertEqual(sizes[0]["width"], 240)
self.assertEqual(sizes[0]["label"], "Small")
def test_picksizes_prefers_original_then_area(self):
sizes = [
{"label": "Small", "width": 240, "height": 180, "source": "s", "media": "photo"},
{"label": "Large", "width": 1024, "height": 768, "source": "l", "media": "photo"},
{"label": "Original", "width": 800, "height": 600, "source": "o", "media": "photo"},
{"label": "Video Player", "width": 640, "height": 480, "source": "v", "media": "video"},
]
self.assertEqual([s["label"] for s in rb.picksizes(sizes)], ["Original", "Large", "Small"])
def test_picksizes_empty(self):
self.assertEqual(rb.picksizes([]), [])
def test_extensionfor(self):
self.assertEqual(rb.extensionfor("https://x/1_o.png", ""), ".png")
self.assertEqual(rb.extensionfor("https://x/1_o.jpeg", ""), ".jpg")
self.assertEqual(rb.extensionfor("https://x/1_o", "image/gif"), ".gif")
self.assertEqual(rb.extensionfor("https://x/1_o.weird", "text/html"), ".jpg")