"""Thailand external-sector / trade factor — TradingEconomics Thailand. Source: https://tradingeconomics.com/thailand/current-account (server-rendered HTML, same infobox/infrastructure as the auto_credit source — the "related indicators" table lists the current value, previous value, unit and reporting period for Thailand's external account). Verified live: the current-account page's related-indicators table exposes (Jul 2026) Exports 34,781.10, Imports 38,399.60, Balance of Trade -3,611.00 USD mn and (Jun 2026) Current Account -3,473.85 USD mn. Replaces the stale BOT report-60 path (which only carried data through 2011) with a CURRENT, free, scrapeable source per the plan's "do not ship a wrong/stale series" gate. """ from __future__ import annotations import html import re from dataclasses import dataclass from typing import Optional from urllib.request import Request, urlopen _USER_AGENT = ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" ) _URL = "https://tradingeconomics.com/thailand/current-account" class ThaiTradeError(Exception): """Raised when the TradingEconomics Thailand page cannot be fetched/parsed.""" @dataclass(frozen=True) class ThaiTradeSnapshot: current_account_usdm: Optional[float] = None # USD million trade_balance_usdm: Optional[float] = None # USD million exports_usdm: Optional[float] = None # USD million imports_usdm: Optional[float] = None # USD million as_of: str = "" source: str = "tradingeconomics.thai-trade" def to_dict(self) -> dict: return { "source": self.source, "as_of": self.as_of, "current_account_usdm": self.current_account_usdm, "trade_balance_usdm": self.trade_balance_usdm, "exports_usdm": self.exports_usdm, "imports_usdm": self.imports_usdm, } def _fetch(url: str = _URL, timeout: float = 30.0) -> str: req = Request(url, headers={"User-Agent": _USER_AGENT, "Accept": "text/html"}) try: with urlopen(req, timeout=timeout) as resp: raw = resp.read() except Exception as exc: raise ThaiTradeError(f"failed to fetch {url}: {exc}") from exc try: return raw.decode("utf-8") except UnicodeDecodeError: return raw.decode("latin-1", "ignore") def _tables(html_text: str) -> list[str]: return re.findall(r"]*>(.*?)", html_text, re.S) def _cells(row_html: str) -> list[str]: return [ html.unescape(re.sub(r"<[^>]+>", "", td)).strip() for td in re.findall(r"]*>(.*?)", row_html, re.S) if td.strip() ] def _to_float(text: str) -> Optional[float]: text = text.replace(",", "").strip().replace("%", "") if not text or text in ("-", "N/A", "…"): return None try: return float(text) except ValueError: return None def parse_thai_trade_html(html_text: str) -> ThaiTradeSnapshot: """Parse the TradingEconomics Thailand current-account related table.""" current = trade = exports = imports = None as_of = "" for table in _tables(html_text): rows = re.findall(r"]*>(.*?)", table, re.S) for row_html in rows: cells = _cells(row_html) if not cells: continue label = cells[0].strip() if label == "Current Account": current = _to_float(cells[1]) if len(cells) > 4: as_of = cells[4] elif label == "Balance of Trade": trade = _to_float(cells[1]) elif label == "Exports" and exports is None: exports = _to_float(cells[1]) elif label == "Imports" and imports is None: imports = _to_float(cells[1]) if current is None and exports is None and imports is None: raise ThaiTradeError("no Thai trade series found in TradingEconomics page") return ThaiTradeSnapshot( current_account_usdm=current, trade_balance_usdm=trade, exports_usdm=exports, imports_usdm=imports, as_of=as_of, ) def fetch_thai_trade(timeout: float = 30.0) -> ThaiTradeSnapshot: return parse_thai_trade_html(_fetch(timeout=timeout))