spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
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