From ea9b9bd81c1f93c9236a7d435c9feb60bbb671b2 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 21:15:06 -0400 Subject: [PATCH] fix(agent): recognize supported Commons photo license names --- apps/agent/src/trip_images.py | 27 ++++++++++++- ...test_trip_license_validation_regression.py | 40 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 apps/agent/tests/test_trip_license_validation_regression.py diff --git a/apps/agent/src/trip_images.py b/apps/agent/src/trip_images.py index 103826a..67bba1e 100644 --- a/apps/agent/src/trip_images.py +++ b/apps/agent/src/trip_images.py @@ -11,6 +11,31 @@ IMAGE_HOSTS = {"upload.wikimedia.org", "thumb.wikimedia.org"} +# Exact published version/port combinations, not their Cartesian product. +# https://commons.wikimedia.org/wiki/Commons:Creative_Commons_copyright_tags +CC_ATTRIBUTION_PORTS = { + "1.0": frozenset({"", "FI", "IL", "NL"}), + "2.0": frozenset({"", *"AT AU BE BR CA CL DE ES FR HR IT JP KR NL PL TW UK ZA".split()}), + "2.1": frozenset("AU CA ES JP".split()), + "2.5": frozenset({"", *"AR AU BG BR CA CH CN CO DK ES HR HU IL IN IT MK MT MX MY NL PE PL PT SCOTLAND SE SI TW ZA".split()}), + "3.0": frozenset({"", *"AM AT AU AZ BR CA CH CL CN CR CZ DE EC EE EG ES FR GE GR GT HK HR IE IGO IT LU NL NO NZ PH PL PR PT RO RS SG TH TW UG US VE VN ZA".split()}), + "4.0": frozenset({""}), +} +CC_SHAREALIKE_PORTS = {"1.0": frozenset({"", "FI", "NL"}), "2.0": frozenset({"JP"})} + + +def supported_license(name: str) -> bool: + normalized = " ".join(name.upper().split()) + if normalized in {"PUBLIC DOMAIN", "CC0", "CC0 1.0"}: + return True + match = re.fullmatch(r"CC (BY(?:-SA)?|SA) (\d\.\d)(?: ([A-Z]+))?", normalized) + if not match: + return False + family, version, port = match.groups() + ports = CC_SHAREALIKE_PORTS if family == "SA" else CC_ATTRIBUTION_PORTS + return (port or "") in ports.get(version, ()) + + def fetch_card(title: str, client: httpx.Client) -> dict: summary = client.get("https://en.wikipedia.org/api/rest_v1/page/summary/" + quote(title, safe="")) summary.raise_for_status() @@ -39,7 +64,7 @@ def plain(key): return unescape(re.sub(r"<[^>]*>", "", info.get(key, {}).get("value", ""))).strip()[:500] license_name = plain("LicenseShortName") artist = plain("Artist") - if not artist or not license_name or not (license_name.startswith("CC") or license_name == "Public domain"): + if not artist or not license_name or not supported_license(license_name): raise ValueError("Reusable image attribution was unavailable.") return { "status": "available", "place": title, "image_url": image, diff --git a/apps/agent/tests/test_trip_license_validation_regression.py b/apps/agent/tests/test_trip_license_validation_regression.py new file mode 100644 index 0000000..de43f7c --- /dev/null +++ b/apps/agent/tests/test_trip_license_validation_regression.py @@ -0,0 +1,40 @@ +import httpx +import pytest + +from src.trip_images import fetch_card + + +def card_for(license_name): + image = "https://upload.wikimedia.org/wikipedia/commons/a/ab/Test.jpg" + + def handle(request): + if "summary" in request.url.path: + return httpx.Response(200, json={"thumbnail": {"source": image}, "originalimage": {"source": image}}) + return httpx.Response(200, json={"query": {"pages": {"1": {"imageinfo": [{"extmetadata": { + "Artist": {"value": "A Photographer"}, "LicenseShortName": {"value": license_name}, + }}]}}}}) + + with httpx.Client(transport=httpx.MockTransport(handle)) as client: + return fetch_card("Coast", client) + + +@pytest.mark.parametrize("license_name", [ + "CC BY-NC 4.0", "CC BY-ND 4.0", "CC BY-NC-SA 4.0", + "CC All rights reserved", "CC BY-SA 99.0", "CC BY 3.0 ZZ", "CC SA 4.0", + "CC BY-SA 1.0 IGO", "CC BY 1.0 US", "CC BY 2.1", "CC BY-SA 3.0 JP", "CC BY-SA 2.0 IGO", +]) +def test_unknown_and_restricted_license_names_are_not_marked_reusable(license_name): + with pytest.raises(ValueError, match="Reusable image attribution"): + card_for(license_name) + + +@pytest.mark.parametrize("license_name", [ + "CC BY 4.0", "CC BY-SA 3.0", "CC by 2.0 fr", "CC BY-SA 2.1 JP", + "CC SA 1.0", "CC0", "CC0 1.0", "Public domain", + "CC BY 2.1 ES", "CC BY-SA 2.1 CA", "CC BY 3.0 EE", "CC BY-SA 3.0 GE", + "CC BY 3.0 IE", "CC SA 1.0 FI", "CC SA 2.0 JP", +]) +def test_supported_commons_names_keep_the_original_credit(license_name): + card = card_for(license_name) + assert card["status"] == "available" + assert card["license"] == license_name