Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion apps/agent/src/trip_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
40 changes: 40 additions & 0 deletions apps/agent/tests/test_trip_license_validation_regression.py
Original file line number Diff line number Diff line change
@@ -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