SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
22.7 KB · 363 lines python
Raw Blame History
1"""Build a small synthetic DuckDB snapshot for API tests (NEVER shipped).238 countries × 12 indicators × 1990–2024, values follow obvious deterministic formulas (so any test can recompute them),4plus IMF forecast rows (2025–2026), alternative-source rows, derived tables (latest, rankings, changes, events, similarity,5insights, country_dna, coverage), import_runs, validation_issues, search_index and meta — all from `schema.sql`.67Usage: `python tests/fixtures/make_fixture_db.py /tmp/atlas-fixture.duckdb`8"""9from __future__ import annotations1011import json12import math13import sys14from datetime import date, datetime15from pathlib import Path1617import duckdb1819ROOT = Path(__file__).resolve().parents[2]20SCHEMA = ROOT / "src" / "countryatlas" / "storage" / "schema.sql"2122COUNTRIES = ["CAN", "USA", "FRA", "DEU", "JPN", "IND", "BRA", "NGA"]23INDICATORS = ["population", "gdp", "gdp-per-capita", "gdp-growth", "inflation", "unemployment-rate", "life-expectancy",24              "median-age", "government-debt-pct-gdp", "co2-per-capita", "renewable-electricity-share", "internet-users",25              "gdp-per-capita-ppp", "population-growth"]26YEARS = list(range(1990, 2025))27RUN_ID = "fixture-20260911T000000"28BUILT_AT = "2026-09-11T00:00:00Z"29RETRIEVED = datetime(2026, 9, 10, 3, 20, 11)30SOURCE_UPDATED = datetime(2026, 7, 1, 0, 0, 0)3132# synthetic anchors (1990 population in millions, gdp per capita 1990 in US$, life expectancy 1990)33ANCHOR = {34    "CAN": dict(pop=27.7, gpc=21000, le=77.4, growth=1.0, base_unemp=8.0, ren=60, inet_year=1996),35    "USA": dict(pop=249.6, gpc=23900, le=75.2, growth=1.0, base_unemp=5.6, ren=11, inet_year=1995),36    "FRA": dict(pop=58.0, gpc=21800, le=76.7, growth=0.5, base_unemp=9.0, ren=15, inet_year=1997),37    "DEU": dict(pop=79.4, gpc=22200, le=75.3, growth=0.2, base_unemp=6.5, ren=4, inet_year=1997),38    "JPN": dict(pop=123.5, gpc=25400, le=78.8, growth=0.1, base_unemp=2.1, ren=12, inet_year=1996),39    "IND": dict(pop=870.5, gpc=370, le=57.9, growth=1.8, base_unemp=5.5, ren=25, inet_year=2002),40    "BRA": dict(pop=150.7, gpc=3100, le=65.3, growth=1.4, base_unemp=7.0, ren=93, inet_year=2000),41    "NGA": dict(pop=95.2, gpc=560, le=45.8, growth=2.6, base_unemp=4.0, ren=20, inet_year=2005),42}434445def value(country: str, indicator: str, year: int) -> float | None:46    a = ANCHOR[country]47    t = year - 199048    pop = a["pop"] * 1e6 * (1 + a["growth"] / 100) ** t49    gpc = a["gpc"] * (1.03 if country in ("CAN", "USA", "FRA", "DEU", "JPN") else 1.06) ** t50    if indicator == "population":51        return round(pop)52    if indicator == "population-growth":53        return a["growth"] - 0.01 * t54    if indicator == "gdp":55        return pop * gpc56    if indicator == "gdp-per-capita":57        return gpc58    if indicator == "gdp-per-capita-ppp":59        return gpc * (1.1 if country in ("CAN", "USA", "FRA", "DEU", "JPN") else 3.2)60    if indicator == "gdp-growth":61        base = 2.0 if country in ("CAN", "USA", "FRA", "DEU", "JPN") else 5.062        if year == 2009:63            return base - 5.0  # global recession → sign flip64        if year == 2020:65            return base - 8.066        return base + 0.8 * math.sin(t / 2.0)67    if indicator == "inflation":68        if year == 2022:69            return 8.0 if country != "JPN" else 2.570        return 2.0 + 0.5 * math.cos(t / 3.0) + (3.0 if country in ("IND", "BRA", "NGA") else 0)71    if indicator == "unemployment-rate":72        return a["base_unemp"] + (3.0 if year == 2020 else 0) + 0.5 * math.sin(t / 4.0)73    if indicator == "life-expectancy":74        return a["le"] + 0.22 * t - (0.8 if year in (2020, 2021) else 0)75    if indicator == "median-age":76        return (28 if country in ("IND", "NGA", "BRA") else 33) + 0.3 * t - (8 if country == "NGA" else 0)77    if indicator == "government-debt-pct-gdp":78        if year < 1995:79            return None  # deliberately missing early years80        return 40 + 1.2 * t + (60 if country == "JPN" else 0) + (10 if year >= 2020 else 0)81    if indicator == "co2-per-capita":82        base = {"CAN": 16, "USA": 20, "FRA": 6.5, "DEU": 11, "JPN": 9, "IND": 0.7, "BRA": 1.5, "NGA": 0.4}[country]83        return base * (0.99 ** t if base > 5 else 1.02 ** t)84    if indicator == "renewable-electricity-share":85        return min(99.0, a["ren"] + 0.6 * t)86    if indicator == "internet-users":87        if year < a["inet_year"]:88            return None89        return min(98.0, 100 / (1 + math.exp(-(year - a["inet_year"] - 8) / 2.5)))90    raise KeyError(indicator)919293def build(path: Path) -> Path:94    from countryatlas.registry import countries as reg_countries95    from countryatlas.registry import groups as reg_groups96    from countryatlas.registry import indicators as reg_indicators97    from countryatlas.registry import topics as reg_topics9899    path = Path(path)100    if path.exists():101        path.unlink()102    con = duckdb.connect(str(path))103    con.execute(SCHEMA.read_text())104105    # ---- countries106    cs = [c for c in reg_countries() if c.id in COUNTRIES]107    cols = ["id", "iso2", "iso3", "iso_numeric", "slug", "short_name", "official_name", "capital", "continent", "region_wb", "region_wb_name",108            "subregion", "income_group", "income_group_name", "currency_code", "currency_name", "area_km2", "latitude", "longitude",109            "flag_emoji", "un_member", "independent", "landlocked", "borders", "languages", "demonym", "status", "kind"]110    con.executemany(f"INSERT INTO countries ({', '.join(cols)}) VALUES ({', '.join('?' * len(cols))})",111                    [[getattr(c, k) for k in cols] for c in cs])112113    # ---- groups + members (restricted to fixture countries)114    members_rows = []115    for g in reg_groups():116        m = [x for x in g.members if x in COUNTRIES]117        con.execute("INSERT INTO groups VALUES (?, ?, ?, ?, ?, ?, ?)", [g.id, g.slug, g.name, g.kind, g.description, g.wb_code, len(m)])118        members_rows += [[g.id, x] for x in m]119    con.executemany("INSERT INTO group_members VALUES (?, ?)", members_rows)120121    # ---- sources122    sources = [123        ("worldbank", "World Bank", "World Bank Group", "https://data.worldbank.org/", "CC BY 4.0", "World Development Indicators, The World Bank",124         "https://api.worldbank.org/v2"),125        ("imf", "IMF", "International Monetary Fund", "https://data.imf.org/", "IMF Terms of Use", "World Economic Outlook, IMF", "https://www.imf.org/external/datamapper/api/v1"),126        ("owid", "Our World in Data", "Global Change Data Lab", "https://ourworldindata.org/", "CC BY 4.0", "Our World in Data", "https://ourworldindata.org/grapher"),127        ("who", "WHO", "World Health Organization", "https://www.who.int/data/gho", "CC BY-NC-SA 3.0 IGO", "WHO Global Health Observatory", "https://ghoapi.azureedge.net/api"),128    ]129    con.executemany("INSERT INTO sources (id, name, organization, url, licence, attribution, api_base, last_success_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",130                    [[*s, RETRIEVED] for s in sources])131132    # ---- indicators + indicator_sources133    inds = {i.slug: i for i in reg_indicators() if i.slug in INDICATORS}134    for slug in INDICATORS:135        i = inds[slug]136        con.execute(137            """INSERT INTO indicators (id, slug, name, short_name, description, topic, subtopic, unit, unit_short, frequency, precision, aggregation,138               higher_is_better, ranking_eligible, featured, format, scale, bounds_min, bounds_max, methodology, tags, per_capita_of)139               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",140            [i.slug, i.slug, i.name, i.short_name, i.description, i.topic, i.subtopic, i.unit, i.unit_short, i.frequency, i.precision,141             i.aggregation, i.higher_is_better, i.ranking_eligible, i.featured, i.format, i.scale, i.bounds[0], i.bounds[1] if len(i.bounds) > 1 else None,142             i.methodology, i.tags, i.per_capita_of])143        for s in i.sources:144            if s.connector not in {x[0] for x in sources}:145                continue146            con.execute("INSERT INTO indicator_sources (indicator_id, source_id, dataset, series_code, params, priority, transform, countries, notes) "147                        "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",148                        [i.slug, s.connector, s.dataset, s.code, json.dumps(s.params), s.priority, s.transform, s.countries, s.notes])149150    # ---- observations (primary source = first registry source; alt = second when present)151    obs_rows, alt_rows = [], []152    for slug in INDICATORS:153        i = inds[slug]154        primary = i.sources[0]155        alt = i.sources[1] if len(i.sources) > 1 and i.sources[1].connector in {x[0] for x in sources} else None156        for c in COUNTRIES:157            for y in YEARS:158                v = value(c, slug, y)159                if v is None:160                    continue161                status = "verified"162                if slug == "inflation" and y == 2022:163                    status = "warning"164                obs_rows.append([c, slug, date(y, 1, 1), y, "A", v, i.unit, primary.connector, primary.dataset, primary.code, False, False, 0,165                                 RETRIEVED, SOURCE_UPDATED, status, None])166                if alt is not None:167                    alt_rows.append([c, slug, date(y, 1, 1), y, "A", v * 1.01, i.unit, alt.connector, alt.dataset, alt.code, False, False, 0,168                                     RETRIEVED, SOURCE_UPDATED, "imported", None])169            # IMF forecasts for gdp / gdp-per-capita / gdp-growth / inflation170            if alt is not None and alt.connector == "imf":171                for y in (2025, 2026):172                    v = value(c, slug, 2024) * (1.03 ** (y - 2024)) if slug != "gdp-growth" else 2.2173                    obs_rows.append([c, slug, date(y, 1, 1), y, "A", v, i.unit, "imf", "WEO", alt.code, True, True, 0, RETRIEVED, SOURCE_UPDATED,174                                     "imported", json.dumps({"forecast": True})])175    ins = "INSERT INTO {t} VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"176    con.executemany(ins.format(t="observations"), sorted(obs_rows, key=lambda r: (r[1], r[0], r[2])))177    con.executemany(ins.format(t="observations_alt"), alt_rows)178179    # ---- latest (last non-forecast value; ranks within fixture countries)180    con.execute("""181    INSERT INTO latest182    WITH nf AS (SELECT * FROM observations WHERE NOT is_forecast AND value IS NOT NULL),183    cur AS (SELECT * FROM nf QUALIFY row_number() OVER (PARTITION BY country_id, indicator_id ORDER BY period DESC) = 1),184    prev AS (SELECT * FROM nf QUALIFY row_number() OVER (PARTITION BY country_id, indicator_id ORDER BY period DESC) = 2),185    ten AS (SELECT n.country_id, n.indicator_id, n.value FROM nf n JOIN cur ON cur.country_id = n.country_id AND cur.indicator_id = n.indicator_id186            AND n.year = cur.year - 10),187    ranked AS (188      SELECT cur.*, c.region_wb, c.income_group,189             rank() OVER (PARTITION BY cur.indicator_id, cur.year ORDER BY CASE WHEN i.higher_is_better = false THEN cur.value END ASC,190                          CASE WHEN i.higher_is_better = false THEN NULL ELSE cur.value END DESC) AS rw,191             count(*) OVER (PARTITION BY cur.indicator_id, cur.year) AS nw,192             rank() OVER (PARTITION BY cur.indicator_id, cur.year, c.region_wb ORDER BY CASE WHEN i.higher_is_better = false THEN cur.value END ASC,193                          CASE WHEN i.higher_is_better = false THEN NULL ELSE cur.value END DESC) AS rr,194             count(*) OVER (PARTITION BY cur.indicator_id, cur.year, c.region_wb) AS nr,195             rank() OVER (PARTITION BY cur.indicator_id, cur.year, c.income_group ORDER BY CASE WHEN i.higher_is_better = false THEN cur.value END ASC,196                          CASE WHEN i.higher_is_better = false THEN NULL ELSE cur.value END DESC) AS ri,197             count(*) OVER (PARTITION BY cur.indicator_id, cur.year, c.income_group) AS ni198      FROM cur JOIN countries c ON c.id = cur.country_id JOIN indicators i ON i.id = cur.indicator_id)199    SELECT r.country_id, r.indicator_id, r.period, r.year, r.frequency, r.value, p.period, p.value,200           r.value - p.value, CASE WHEN p.value <> 0 THEN (r.value - p.value) / abs(p.value) * 100 END,201           r.rw, r.nw, r.rr, r.nr, r.ri, r.ni, r.year, r.source_id, r.is_forecast, r.is_estimate, r.status,202           t.value, r.value - t.value, CASE WHEN t.value <> 0 THEN (r.value - t.value) / abs(t.value) * 100 END203    FROM ranked r204    LEFT JOIN prev p ON p.country_id = r.country_id AND p.indicator_id = r.indicator_id205    LEFT JOIN ten t ON t.country_id = r.country_id AND t.indicator_id = r.indicator_id206    """)207208    # ---- rankings (every year, ranking-eligible indicators)209    con.execute("""210    INSERT INTO rankings211    SELECT o.indicator_id, o.year, o.country_id, o.value,212           rank() OVER (PARTITION BY o.indicator_id, o.year ORDER BY CASE WHEN i.higher_is_better = false THEN o.value END ASC,213                        CASE WHEN i.higher_is_better = false THEN NULL ELSE o.value END DESC) AS rank,214           count(*) OVER (PARTITION BY o.indicator_id, o.year) AS n,215           percent_rank() OVER (PARTITION BY o.indicator_id, o.year ORDER BY o.value) AS pct_rank216    FROM observations o JOIN indicators i ON i.id = o.indicator_id217    WHERE NOT o.is_forecast AND o.value IS NOT NULL AND i.ranking_eligible218    """)219220    # ---- changes / events (simple deterministic detectors on the synthetic series)221    now = datetime(2026, 9, 11, 0, 0, 0)222    changes, events = [], []223    for c in COUNTRIES:224        for slug in INDICATORS:225            series = [(y, value(c, slug, y)) for y in YEARS if value(c, slug, y) is not None]226            if len(series) < 3:227                continue228            vals = [v for _, v in series]229            y, v = series[-1]230            py, pv = series[-2]231            d = v - pv232            dp = d / abs(pv) * 100 if pv else None233            if v == max(vals):234                changes.append([f"{c}:{slug}:{y}:record_high", c, slug, "record_high", date(y, 1, 1), y, v, max(vals[:-1]), d, dp, len(vals),235                                0.6, f"{c} reached a record high for {slug} in {y}.", json.dumps({"n_years": len(vals)}), now])236            if slug in ("inflation", "unemployment-rate", "gdp-growth") and abs(d) >= 1.0:237                sev = min(1.0, abs(d) / 5.0)238                changes.append([f"{c}:{slug}:{y}:yoy", c, slug, "yoy_jump" if d > 0 else "yoy_drop", date(y, 1, 1), y, v, pv, d, dp, 1, sev,239                                f"{slug} moved {d:+.1f} pts in {c} in {y}.", json.dumps({"floor": 1.0}), now])240            for (y0, v0), (y1, v1) in zip(series, series[1:]):241                if slug == "gdp-growth" and (v0 > 0 > v1 or v0 < 0 < v1):242                    events.append([f"{c}:{slug}:{y1}:sign_flip", c, slug, "sign_flip", date(y1, 1, 1), y1, v1, v0, v1 - v0, None, 1, 0.8,243                                   f"{c}'s GDP growth turned {'negative' if v1 < 0 else 'positive'} in {y1}.", None])244                if slug == "inflation" and v1 - v0 > 3:245                    events.append([f"{c}:{slug}:{y1}:yoy_jump", c, slug, "yoy_jump", date(y1, 1, 1), y1, v1, v0, v1 - v0, None, 1, 0.7,246                                   f"Inflation jumped {v1 - v0:+.1f} pts in {c} in {y1}.", None])247    con.executemany("INSERT INTO changes VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", changes)248    con.executemany("INSERT INTO events VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", events)249250    # ---- similarity (distance on log gdp pc + life expectancy)251    feats = {c: (math.log(value(c, "gdp-per-capita", 2024)), value(c, "life-expectancy", 2024) / 10) for c in COUNTRIES}252    sim_rows = []253    for mode in ("overall", "economic", "demographic"):254        for c in COUNTRIES:255            peers = []256            for p in COUNTRIES:257                if p == c:258                    continue259                d = math.dist(feats[c], feats[p])260                peers.append((p, 100 * math.exp(-d / 1.5), d))261            peers.sort(key=lambda x: -x[1])262            for rank, (p, score, d) in enumerate(peers, 1):263                contrib = {"gdp-per-capita": {"z_a": feats[c][0], "z_b": feats[p][0], "weight": 1, "contribution": abs(feats[c][0] - feats[p][0]) / d if d else 0},264                           "life-expectancy": {"z_a": feats[c][1], "z_b": feats[p][1], "weight": 1, "contribution": abs(feats[c][1] - feats[p][1]) / d if d else 0}}265                sim_rows.append([c, mode, p, score, rank, json.dumps(contrib)])266    con.executemany("INSERT INTO similarity VALUES (?, ?, ?, ?, ?, ?)", sim_rows)267268    # ---- insights269    ins_rows = []270    for c in COUNTRIES:271        p0, p1 = value(c, "population", 1990), value(c, "population", 2024)272        pct = (p1 / p0 - 1) * 100273        ins_rows.append([f"{c}:pop_growth_since", c, "pop_growth_since", f"{c}'s population grew {pct:.0f}% since 1990.", json.dumps({"pct": pct, "y0": 1990}),274                         ["population"], now])275        le = value(c, "life-expectancy", 2024)276        ins_rows.append([f"{c}:life_expectancy", c, "life_expectancy_level", f"Life expectancy in {c} is {le:.1f} years.", json.dumps({"value": le}),277                         ["life-expectancy"], now])278    con.executemany("INSERT INTO insights VALUES (?, ?, ?, ?, ?, ?, ?)", ins_rows)279280    # ---- country_dna (percentile ranks)281    def pct_rank(slug: str, c: str) -> float:282        vals = sorted(value(x, slug, 2024) for x in COUNTRIES)283        return 100.0 * vals.index(value(c, slug, 2024)) / (len(vals) - 1)284285    for c in COUNTRIES:286        dims = {"income": pct_rank("gdp-per-capita-ppp", c), "demographics": pct_rank("median-age", c), "emissions": pct_rank("co2-per-capita", c),287                "energy": pct_rank("renewable-electricity-share", c), "public_spending": pct_rank("government-debt-pct-gdp", c),288                "urbanization": None, "trade": None, "innovation": None, "education": pct_rank("internet-users", c)}289        con.execute("INSERT INTO country_dna VALUES (?, ?, ?)", [c, json.dumps(dims), 2024])290291    # ---- coverage292    con.execute("""293    INSERT INTO coverage294    SELECT country_id, count(DISTINCT indicator_id), count(*), max(year), 100.0 * count(DISTINCT indicator_id) / (SELECT count(*) FROM indicators), ?295    FROM observations WHERE NOT is_forecast GROUP BY country_id""", [now])296297    # ---- indicator coverage columns + sources counts298    con.execute("""299    UPDATE indicators SET300      n_countries = (SELECT count(DISTINCT country_id) FROM observations o WHERE o.indicator_id = indicators.id AND NOT is_forecast),301      n_observations = (SELECT count(*) FROM observations o WHERE o.indicator_id = indicators.id AND NOT is_forecast),302      first_year = (SELECT min(year) FROM observations o WHERE o.indicator_id = indicators.id),303      last_year = (SELECT max(year) FROM observations o WHERE o.indicator_id = indicators.id AND NOT is_forecast),304      latest_source_updated_at = (SELECT max(source_updated_at) FROM observations o WHERE o.indicator_id = indicators.id),305      primary_source_id = (SELECT source_id FROM observations o WHERE o.indicator_id = indicators.id GROUP BY source_id ORDER BY count(*) DESC LIMIT 1)306    """)307    con.execute("""308    UPDATE indicator_sources SET309      n_observations = (SELECT count(*) FROM observations o WHERE o.indicator_id = indicator_sources.indicator_id AND o.source_id = indicator_sources.source_id),310      n_countries = (SELECT count(DISTINCT country_id) FROM observations o WHERE o.indicator_id = indicator_sources.indicator_id AND o.source_id = indicator_sources.source_id),311      last_year = (SELECT max(year) FROM observations o WHERE o.indicator_id = indicator_sources.indicator_id AND o.source_id = indicator_sources.source_id),312      last_run_id = ?, last_status = 'ok'""", [RUN_ID])313    con.execute("UPDATE sources SET n_indicators = (SELECT count(DISTINCT indicator_id) FROM observations o WHERE o.source_id = sources.id), "314                "n_observations = (SELECT count(*) FROM observations o WHERE o.source_id = sources.id)")315316    # ---- import runs + validation issues317    runs = [318        [RUN_ID, "worldbank", "WDI", datetime(2026, 9, 10, 3, 15), datetime(2026, 9, 10, 3, 20), "ok", 12000, 11800, 11790, 3, 0, None, "raw/worldbank/WDI/2026-09-10"],319        [RUN_ID, "imf", "WEO", datetime(2026, 9, 10, 3, 20), datetime(2026, 9, 10, 3, 22), "ok", 4000, 3900, 3900, 0, 0, None, "raw/imf/WEO/2026-09-10"],320        [RUN_ID, "owid", "co2", datetime(2026, 9, 10, 3, 22), datetime(2026, 9, 10, 3, 23), "ok", 3000, 2900, 2900, 0, 0, None, "raw/owid/co2/2026-09-10"],321        [RUN_ID, "owid", "energy", datetime(2026, 9, 10, 3, 23), datetime(2026, 9, 10, 3, 24), "partial", 3000, 2000, 2000, 5, 0, "2 pages failed", "raw/owid/energy/2026-09-10"],322        [RUN_ID, "who", "GHO", datetime(2026, 9, 10, 3, 24), datetime(2026, 9, 10, 3, 25), "failed", 0, 0, 0, 0, 1, "HTTP 503", None],323        ["fixture-20260910T000000", "worldbank", "WDI", datetime(2026, 9, 9, 3, 15), datetime(2026, 9, 9, 3, 20), "ok", 12000, 11800, 11790, 1, 0, None, "raw/worldbank/WDI/2026-09-09"],324    ]325    con.executemany("INSERT INTO import_runs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", runs)326    con.executemany("INSERT INTO validation_issues VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [327        [RUN_ID, "worldbank", "inflation", "NGA", date(2022, 1, 1), "warning", "extreme_jump", "|Δ| = 6.0 > 4 × MAD"],328        [RUN_ID, "worldbank", "inflation", "BRA", date(2022, 1, 1), "warning", "extreme_jump", "|Δ| = 6.0 > 4 × MAD"],329        [RUN_ID, "owid", "renewable-electricity-share", None, None, "info", "partial_download", "2 pages failed, previous kept"],330        [RUN_ID, "who", "life-expectancy", None, None, "error", "schema_change", "HTTP 503"],331    ])332333    # ---- search index334    si = []335    for c in cs:336        si.append(["country", c.id, c.slug, c.short_name, f"{c.official_name} {c.iso2} {c.iso3} {c.capital}", f"Country · {c.region_wb_name}", 1.0])337    tnames = {t["id"]: t["name"] for t in reg_topics()["topics"]}338    for slug in INDICATORS:339        i = inds[slug]340        si.append(["indicator", slug, slug, i.name, f"{i.short_name or ''} {' '.join(i.tags)}", f"Indicator · {tnames.get(i.topic, i.topic)} · {i.unit}",341                   0.9 if i.featured else 0.7])342    for t in reg_topics()["topics"]:343        si.append(["topic", t["id"], t["id"], t["name"], t.get("short", ""), f"Topic · {len(t['indicators'])} indicators", 0.6])344    for g in reg_groups():345        si.append(["region", g.id, g.slug, g.name, g.wb_code or "", f"Region · {g.kind}", 0.6])346    for s in sources:347        si.append(["source", s[0], s[0], s[1], s[2], "Source", 0.4])348    con.executemany("INSERT INTO search_index VALUES (?, ?, ?, ?, ?, ?, ?)", si)349350    # ---- meta351    n_obs = con.execute("SELECT count(*) FROM observations").fetchone()[0]352    con.executemany("INSERT INTO meta VALUES (?, ?)", [353        ["build_run_id", RUN_ID], ["built_at", BUILT_AT], ["schema_version", "1"], ["indicator_count", str(len(INDICATORS))],354        ["country_count", str(len(COUNTRIES))], ["observation_count", str(n_obs)], ["fixture", "true"],355    ])356    con.close()357    return path358359360if __name__ == "__main__":361    out = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("/tmp/atlas-fixture.duckdb")362    print(build(out))363