All checks were successful
secrets-guard / encrypted (pull_request) Successful in 6s
PR build (required check) / changes (pull_request) Successful in 12s
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Successful in 1m24s
PR build (required check) / build-backend (pull_request) Successful in 1m32s
PR build (required check) / gate (pull_request) Successful in 1s
Introduce backend/city_copy.json: original, researched per-city editorial copy (a lede, 2-4 body paragraphs, and a records-page intro), keyed by slug next to the existing Wikipedia cities_flavor.json. Covers 83 of the 90 cities that had no flavor blurb at all; each entry carries the independent sources it was written from and reviewed:false pending a human spot-check. Wire it end to end: - cities.copy(slug) loader, mirroring cities.flavor() (lazy, tolerant of a missing file so pages render fine until copy exists). - A `copy` field on the city and records content payloads. PAYLOAD_VER p2->p3 so cached content rows rebuild with the new field instead of serving the old shape. - city.html.j2 renders the editorial lede + body in place of the Wikipedia blurb, falling back to the flavor blurb (with its "via Wikipedia" credit) wherever copy isn't written yet. records.html.j2 gains a per-city intro line it never had before. Additive and backward-compatible: no API contract bump, and any slug without a copy entry keeps its existing behavior.
121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
"""Access to the curated city set (backend/cities.json) that gets crawlable
|
|
climate pages. Loaded once, lazily; regenerate the JSON with gen_cities.py."""
|
|
import json
|
|
import os
|
|
|
|
import paths
|
|
|
|
_PATH = paths.CITIES_JSON
|
|
_FLAVOR_PATH = paths.CITIES_FLAVOR_JSON
|
|
_COPY_PATH = paths.CITY_COPY_JSON
|
|
_CITIES: list[dict] | None = None
|
|
_BY_SLUG: dict[str, dict] | None = None
|
|
_FLAVOR: dict[str, dict] | None = None
|
|
_COPY: dict[str, dict] | None = None
|
|
_DUPES: dict[str, dict[str, int]] | None = None
|
|
|
|
|
|
def _load() -> list[dict]:
|
|
global _CITIES, _BY_SLUG
|
|
if _CITIES is None:
|
|
with open(_PATH, encoding="utf-8") as f:
|
|
_CITIES = json.load(f)
|
|
_BY_SLUG = {c["slug"]: c for c in _CITIES}
|
|
return _CITIES
|
|
|
|
|
|
def all_cities() -> list[dict]:
|
|
return _load()
|
|
|
|
|
|
def all_slugs() -> list[str]:
|
|
return [c["slug"] for c in _load()]
|
|
|
|
|
|
def get(slug: str) -> dict | None:
|
|
"""The city for a slug, or None (→ 404)."""
|
|
_load()
|
|
return _BY_SLUG.get(slug)
|
|
|
|
|
|
def flavor(slug: str) -> dict | None:
|
|
"""A city's descriptive blurb {extract, url, title} from cities_flavor.json, or
|
|
None when we have no confident match (the page renders fine without it)."""
|
|
global _FLAVOR
|
|
if _FLAVOR is None:
|
|
try:
|
|
with open(_FLAVOR_PATH, encoding="utf-8") as f:
|
|
_FLAVOR = json.load(f)
|
|
except (OSError, ValueError):
|
|
_FLAVOR = {}
|
|
return _FLAVOR.get(slug)
|
|
|
|
|
|
def copy(slug: str) -> dict | None:
|
|
"""A city's editorial copy {lede, body, records_intro, sources, ...} from
|
|
city_copy.json, or None when it hasn't been written yet (the page falls back
|
|
to the Wikipedia flavor blurb). Mirrors flavor() — lazy, tolerant of a
|
|
missing/malformed file."""
|
|
global _COPY
|
|
if _COPY is None:
|
|
try:
|
|
with open(_COPY_PATH, encoding="utf-8") as f:
|
|
_COPY = json.load(f)
|
|
except (OSError, ValueError):
|
|
_COPY = {}
|
|
return _COPY.get(slug)
|
|
|
|
|
|
def title_name(city: dict) -> str:
|
|
"""The shortest label that still names this city unambiguously — for page
|
|
titles, where display_name()'s region + country spend the width Google gives
|
|
us before it ever reaches the payload ("… in July").
|
|
|
|
'Seattle' for the 968 cities whose name is unique; 'London, GB' when the name
|
|
recurs in another country; 'Columbus, Ohio' when it recurs *within* one
|
|
country, where the country code would collide too and hand two different
|
|
pages the same title.
|
|
"""
|
|
name = city["name"]
|
|
dupes = _dupe_names().get(name)
|
|
if not dupes:
|
|
return name
|
|
if dupes.get(city.get("country_code")) == 1:
|
|
return f"{name}, {city['country_code']}"
|
|
return f"{name}, {city['admin1']}" if city.get("admin1") else name
|
|
|
|
|
|
def _dupe_names() -> dict[str, dict[str, int]]:
|
|
"""{name: {country_code: how many cities share both}} for duplicated names
|
|
only. Built once alongside the city list."""
|
|
global _DUPES
|
|
if _DUPES is None:
|
|
counts: dict[str, dict[str, int]] = {}
|
|
for c in _load():
|
|
counts.setdefault(c["name"], {})
|
|
cc = c.get("country_code")
|
|
counts[c["name"]][cc] = counts[c["name"]].get(cc, 0) + 1
|
|
_DUPES = {n: by_cc for n, by_cc in counts.items() if sum(by_cc.values()) > 1}
|
|
return _DUPES
|
|
|
|
|
|
def display_name(city: dict) -> str:
|
|
"""Human label: 'Seattle, Washington, United States' (drops repeated admin1)."""
|
|
parts = [city["name"]]
|
|
if city.get("admin1") and city["admin1"] != city["name"]:
|
|
parts.append(city["admin1"])
|
|
if city.get("country"):
|
|
parts.append(city["country"])
|
|
return ", ".join(parts)
|
|
|
|
|
|
def by_country() -> dict[str, list[dict]]:
|
|
"""Cities grouped by country, both the countries and the cities within each
|
|
ordered alphabetically — for the /climate hub's crawlable link graph + search."""
|
|
groups: dict[str, list[dict]] = {}
|
|
for c in _load():
|
|
key = c.get("country") or c.get("country_code") or "Other"
|
|
groups.setdefault(key, []).append(c)
|
|
for v in groups.values():
|
|
v.sort(key=lambda x: x["name"].lower())
|
|
return dict(sorted(groups.items(), key=lambda kv: kv[0].lower()))
|