feat: phase 12 — 68→81 tools, 5 geospatial datasets, 5 new sources, news ticker fix

New static datasets (config/):
- cables.py: 34 undersea fiber-optic cables with landing points, capacity, owners
- datacenters.py: 48 AI datacenter clusters with power capacity (MW)
- spaceports.py: 27 launch facilities worldwide
- minerals.py: 27 critical mineral deposits (lithium, cobalt, rare earths, etc.)
- exchanges.py: 82 stock exchanges across 4 tiers with country→ticker mapping

New source modules (sources/):
- hacker_news.py: HN Firebase API top stories
- github_trending.py: GitHub search API trending repos
- arxiv_papers.py: arXiv Atom API with regex XML parsing
- usa_spending.py: USAspending.gov federal agency budgets
- environmental.py: NASA EONET natural events + GDACS disaster alerts

Extended existing modules:
- markets.py: fetch_country_stocks() — any country by ISO-3 code
- military.py: fetch_aircraft_details_batch() — parallel batch lookup (max 20)
- geospatial.py: 5 new fetch functions for cables, datacenters, spaceports, minerals, exchanges
- circuit_breaker.py: per-source configurable thresholds
- cache.py: freshness() method for per-source staleness tracking

Fixed news ticker (News 0 → News 17+):
- Root cause: RSS feeds fetched sequentially by category (14 categories × slow feeds = timeout)
- Fix: single asyncio.gather() for all 80+ feeds with 12s per-feed hard timeout
- Reduced per-request HTTP timeout from 15s to 8s for RSS feeds

13 new tools wired in server.py, 14 new tests (76 total, all passing).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marc Shade
2026-02-24 20:05:44 -05:00
co-authored by Claude Opus 4.6
parent 986f79fb8f
commit 6075910505
19 changed files with 2051 additions and 22 deletions
+21
View File
@@ -118,6 +118,27 @@ class Cache:
"db_path": str(self.db_path),
}
def freshness(self) -> dict[str, Any]:
"""Per-source freshness report — shows age of newest entry per source prefix."""
conn = self._get_conn()
now = time.time()
rows = conn.execute(
"SELECT key, created_at, expires_at FROM cache ORDER BY created_at DESC"
).fetchall()
sources: dict[str, dict] = {}
for key, created_at, expires_at in rows:
# Extract source prefix (e.g., "markets:quotes:^GSPC" → "markets")
prefix = key.split(":")[0] if ":" in key else key
if prefix not in sources:
age_s = now - created_at
is_stale = now > expires_at
sources[prefix] = {
"last_updated_s_ago": round(age_s, 1),
"is_stale": is_stale,
"newest_key": key,
}
return sources
def close(self) -> None:
if self._conn:
self._conn.close()
+14 -4
View File
@@ -29,9 +29,11 @@ class CircuitBreaker:
self,
failure_threshold: int = 3,
cooldown_seconds: float = 300.0,
per_source_config: dict[str, dict] | None = None,
):
self.failure_threshold = failure_threshold
self.cooldown_seconds = cooldown_seconds
self._per_source: dict[str, dict] = per_source_config or {}
self._states: dict[str, _State] = {}
def _get(self, source: str) -> _State:
@@ -39,12 +41,20 @@ class CircuitBreaker:
self._states[source] = _State()
return self._states[source]
def _threshold_for(self, source: str) -> int:
"""Get failure threshold for a specific source."""
return self._per_source.get(source, {}).get("failure_threshold", self.failure_threshold)
def _cooldown_for(self, source: str) -> float:
"""Get cooldown seconds for a specific source."""
return self._per_source.get(source, {}).get("cooldown_seconds", self.cooldown_seconds)
def is_available(self, source: str) -> bool:
"""Check if source is available (circuit closed or cooldown elapsed)."""
state = self._get(source)
if not state.is_open:
return True
if time.time() - state.tripped_at >= self.cooldown_seconds:
if time.time() - state.tripped_at >= self._cooldown_for(source):
return True # allow probe
return False
@@ -61,13 +71,13 @@ class CircuitBreaker:
state.failures += 1
state.last_failure = time.time()
state.total_failures += 1
if state.failures >= self.failure_threshold and not state.is_open:
if state.failures >= self._threshold_for(source) and not state.is_open:
state.is_open = True
state.tripped_at = time.time()
state.total_trips += 1
logger.warning(
"Circuit breaker TRIPPED for %s (failures=%d, cooldown=%.0fs)",
source, state.failures, self.cooldown_seconds,
source, state.failures, self._cooldown_for(source),
)
def status(self) -> dict[str, dict]:
@@ -76,7 +86,7 @@ class CircuitBreaker:
result = {}
for source, state in self._states.items():
if state.is_open:
remaining = max(0, self.cooldown_seconds - (now - state.tripped_at))
remaining = max(0, self._cooldown_for(source) - (now - state.tripped_at))
status = "open" if remaining > 0 else "half-open"
else:
remaining = 0
+294
View File
@@ -0,0 +1,294 @@
"""Undersea cable routes with landing points.
Pure data module — no I/O, no external dependencies.
Sources: TeleGeography Submarine Cable Map, ITU cable database.
"""
from __future__ import annotations
UNDERSEA_CABLES: list[dict] = [
# Transatlantic
{"name": "TAT-14", "status": "decommissioned", "rfs_year": 2001, "length_km": 15428, "capacity_tbps": 3.2,
"owners": ["Deutsche Telekom", "AT&T", "Orange"], "landing_points": [
{"name": "Tuckerton", "country": "USA", "lat": 39.60, "lon": -74.34},
{"name": "Manasquan", "country": "USA", "lat": 40.12, "lon": -74.04},
{"name": "Blaabjerg", "country": "Denmark", "lat": 55.60, "lon": 8.12},
{"name": "Norden", "country": "Germany", "lat": 53.60, "lon": 7.20},
{"name": "Saint-Valery-en-Caux", "country": "France", "lat": 49.87, "lon": 0.72},
{"name": "Bude", "country": "UK", "lat": 50.83, "lon": -4.55},
]},
{"name": "MAREA", "status": "active", "rfs_year": 2018, "length_km": 6600, "capacity_tbps": 200,
"owners": ["Microsoft", "Meta", "Telxius"], "landing_points": [
{"name": "Virginia Beach", "country": "USA", "lat": 36.85, "lon": -75.98},
{"name": "Bilbao", "country": "Spain", "lat": 43.26, "lon": -2.93},
]},
{"name": "Dunant", "status": "active", "rfs_year": 2021, "length_km": 6600, "capacity_tbps": 250,
"owners": ["Google"], "landing_points": [
{"name": "Virginia Beach", "country": "USA", "lat": 36.85, "lon": -75.98},
{"name": "Saint-Hilaire-de-Riez", "country": "France", "lat": 46.72, "lon": -1.95},
]},
{"name": "Amitié", "status": "active", "rfs_year": 2022, "length_km": 6800, "capacity_tbps": 400,
"owners": ["Google", "Meta", "Lumen"], "landing_points": [
{"name": "Lynn", "country": "USA", "lat": 42.47, "lon": -70.95},
{"name": "Bude", "country": "UK", "lat": 50.83, "lon": -4.55},
{"name": "Le Porge", "country": "France", "lat": 44.87, "lon": -1.16},
]},
{"name": "AEC-1 (Anjana/East)", "status": "active", "rfs_year": 2002, "length_km": 7200, "capacity_tbps": 5.1,
"owners": ["Telia", "Aqua Comms"], "landing_points": [
{"name": "Shirley", "country": "USA", "lat": 40.80, "lon": -72.87},
{"name": "Killala", "country": "Ireland", "lat": 54.21, "lon": -9.22},
{"name": "Blaabjerg", "country": "Denmark", "lat": 55.60, "lon": 8.12},
]},
{"name": "HAVFRUE/AEC-2", "status": "active", "rfs_year": 2020, "length_km": 7860, "capacity_tbps": 108,
"owners": ["Google", "Aqua Comms", "Bulk"], "landing_points": [
{"name": "Wall Township", "country": "USA", "lat": 40.16, "lon": -74.07},
{"name": "Blaabjerg", "country": "Denmark", "lat": 55.60, "lon": 8.12},
{"name": "Kristiansand", "country": "Norway", "lat": 58.15, "lon": 8.00},
{"name": "Killala", "country": "Ireland", "lat": 54.21, "lon": -9.22},
]},
{"name": "Grace Hopper", "status": "active", "rfs_year": 2022, "length_km": 6300, "capacity_tbps": 340,
"owners": ["Google"], "landing_points": [
{"name": "New York", "country": "USA", "lat": 40.57, "lon": -73.97},
{"name": "Bude", "country": "UK", "lat": 50.83, "lon": -4.55},
{"name": "Bilbao", "country": "Spain", "lat": 43.26, "lon": -2.93},
]},
# Transpacific
{"name": "JUPITER", "status": "active", "rfs_year": 2020, "length_km": 14000, "capacity_tbps": 60,
"owners": ["Google", "Meta", "PLDT", "SoftBank"], "landing_points": [
{"name": "Virginia Beach", "country": "USA", "lat": 36.85, "lon": -75.98},
{"name": "Daet", "country": "Philippines", "lat": 14.11, "lon": 122.96},
{"name": "Maruyama", "country": "Japan", "lat": 33.48, "lon": 135.76},
]},
{"name": "FASTER", "status": "active", "rfs_year": 2016, "length_km": 11629, "capacity_tbps": 60,
"owners": ["Google", "China Mobile", "KDDI", "SingTel"], "landing_points": [
{"name": "Bandon", "country": "USA", "lat": 43.12, "lon": -124.41},
{"name": "Chikura", "country": "Japan", "lat": 34.93, "lon": 139.95},
{"name": "Shima", "country": "Japan", "lat": 34.33, "lon": 136.83},
]},
{"name": "Curie", "status": "active", "rfs_year": 2019, "length_km": 10476, "capacity_tbps": 72,
"owners": ["Google"], "landing_points": [
{"name": "Los Angeles", "country": "USA", "lat": 33.94, "lon": -118.45},
{"name": "Valparaiso", "country": "Chile", "lat": -33.04, "lon": -71.63},
]},
{"name": "Firmina", "status": "active", "rfs_year": 2023, "length_km": 14000, "capacity_tbps": 340,
"owners": ["Google"], "landing_points": [
{"name": "Myrtle Beach", "country": "USA", "lat": 33.69, "lon": -78.89},
{"name": "Praia Grande", "country": "Brazil", "lat": -24.01, "lon": -46.41},
{"name": "Las Toninas", "country": "Argentina", "lat": -36.48, "lon": -56.70},
]},
{"name": "PLCN (Pacific Light Cable Network)", "status": "active", "rfs_year": 2023, "length_km": 12800, "capacity_tbps": 144,
"owners": ["Google", "Meta"], "landing_points": [
{"name": "El Segundo", "country": "USA", "lat": 33.92, "lon": -118.42},
{"name": "Changi", "country": "Singapore", "lat": 1.35, "lon": 104.00},
{"name": "Tanah Merah", "country": "Indonesia", "lat": -6.17, "lon": 106.97},
]},
# Asia-Europe (SEA-ME-WE family)
{"name": "SEA-ME-WE 3", "status": "active", "rfs_year": 1999, "length_km": 39000, "capacity_tbps": 0.96,
"owners": ["Consortium (76 telcos)"], "landing_points": [
{"name": "Norden", "country": "Germany", "lat": 53.60, "lon": 7.20},
{"name": "Penmarc'h", "country": "France", "lat": 47.80, "lon": -4.37},
{"name": "Tetney", "country": "UK", "lat": 53.50, "lon": 0.02},
{"name": "Suez", "country": "Egypt", "lat": 29.97, "lon": 32.55},
{"name": "Mumbai", "country": "India", "lat": 18.93, "lon": 72.83},
{"name": "Singapore", "country": "Singapore", "lat": 1.26, "lon": 103.82},
{"name": "Shanghai", "country": "China", "lat": 31.05, "lon": 121.72},
{"name": "Keoje", "country": "South Korea", "lat": 34.88, "lon": 128.70},
{"name": "Okinawa", "country": "Japan", "lat": 26.34, "lon": 127.77},
]},
{"name": "SEA-ME-WE 5", "status": "active", "rfs_year": 2017, "length_km": 20000, "capacity_tbps": 24,
"owners": ["Consortium (17 telcos)"], "landing_points": [
{"name": "Marseille", "country": "France", "lat": 43.30, "lon": 5.37},
{"name": "Catania", "country": "Italy", "lat": 37.50, "lon": 15.09},
{"name": "Alexandria", "country": "Egypt", "lat": 31.20, "lon": 29.92},
{"name": "Jeddah", "country": "Saudi Arabia", "lat": 21.49, "lon": 39.19},
{"name": "Mumbai", "country": "India", "lat": 18.93, "lon": 72.83},
{"name": "Singapore", "country": "Singapore", "lat": 1.26, "lon": 103.82},
]},
{"name": "SEA-ME-WE 6", "status": "construction", "rfs_year": 2025, "length_km": 19200, "capacity_tbps": 100,
"owners": ["Consortium (12 telcos)"], "landing_points": [
{"name": "Marseille", "country": "France", "lat": 43.30, "lon": 5.37},
{"name": "Genoa", "country": "Italy", "lat": 44.41, "lon": 8.93},
{"name": "Alexandria", "country": "Egypt", "lat": 31.20, "lon": 29.92},
{"name": "Singapore", "country": "Singapore", "lat": 1.26, "lon": 103.82},
]},
# Africa
{"name": "2Africa", "status": "active", "rfs_year": 2024, "length_km": 45000, "capacity_tbps": 180,
"owners": ["Meta", "MTN", "Orange", "Vodafone", "China Mobile"], "landing_points": [
{"name": "Genoa", "country": "Italy", "lat": 44.41, "lon": 8.93},
{"name": "Barcelona", "country": "Spain", "lat": 41.39, "lon": 2.16},
{"name": "Marseille", "country": "France", "lat": 43.30, "lon": 5.37},
{"name": "Bude", "country": "UK", "lat": 50.83, "lon": -4.55},
{"name": "Dakar", "country": "Senegal", "lat": 14.69, "lon": -17.47},
{"name": "Lagos", "country": "Nigeria", "lat": 6.45, "lon": 3.39},
{"name": "Cape Town", "country": "South Africa", "lat": -33.92, "lon": 18.42},
{"name": "Maputo", "country": "Mozambique", "lat": -25.97, "lon": 32.57},
{"name": "Mombasa", "country": "Kenya", "lat": -4.04, "lon": 39.67},
{"name": "Djibouti", "country": "Djibouti", "lat": 11.55, "lon": 43.15},
{"name": "Jeddah", "country": "Saudi Arabia", "lat": 21.49, "lon": 39.19},
{"name": "Mumbai", "country": "India", "lat": 18.93, "lon": 72.83},
]},
{"name": "Equiano", "status": "active", "rfs_year": 2023, "length_km": 15000, "capacity_tbps": 144,
"owners": ["Google"], "landing_points": [
{"name": "Sesimbra", "country": "Portugal", "lat": 38.44, "lon": -9.10},
{"name": "Lagos", "country": "Nigeria", "lat": 6.45, "lon": 3.39},
{"name": "Lomé", "country": "Togo", "lat": 6.13, "lon": 1.22},
{"name": "Swakopmund", "country": "Namibia", "lat": -22.68, "lon": 14.53},
{"name": "Melkbosstrand", "country": "South Africa", "lat": -33.72, "lon": 18.44},
]},
# PEACE Cable
{"name": "PEACE", "status": "active", "rfs_year": 2022, "length_km": 15000, "capacity_tbps": 96,
"owners": ["PEACE Cable International"], "landing_points": [
{"name": "Marseille", "country": "France", "lat": 43.30, "lon": 5.37},
{"name": "Karachi", "country": "Pakistan", "lat": 24.86, "lon": 67.01},
{"name": "Mombasa", "country": "Kenya", "lat": -4.04, "lon": 39.67},
{"name": "Seychelles", "country": "Seychelles", "lat": -4.68, "lon": 55.49},
{"name": "Singapore", "country": "Singapore", "lat": 1.26, "lon": 103.82},
]},
# Asia-Pacific
{"name": "APG (Asia-Pacific Gateway)", "status": "active", "rfs_year": 2016, "length_km": 10400, "capacity_tbps": 54.8,
"owners": ["Consortium (12 telcos)"], "landing_points": [
{"name": "Chongming", "country": "China", "lat": 31.62, "lon": 121.73},
{"name": "Hong Kong", "country": "China", "lat": 22.25, "lon": 114.17},
{"name": "Taipei", "country": "Taiwan", "lat": 25.16, "lon": 121.74},
{"name": "Maruyama", "country": "Japan", "lat": 33.48, "lon": 135.76},
{"name": "Changi", "country": "Singapore", "lat": 1.35, "lon": 104.00},
{"name": "Da Nang", "country": "Vietnam", "lat": 16.07, "lon": 108.22},
{"name": "Kuantan", "country": "Malaysia", "lat": 3.80, "lon": 103.33},
]},
{"name": "SJC (Southeast Asia-Japan Cable)", "status": "active", "rfs_year": 2013, "length_km": 8900, "capacity_tbps": 28,
"owners": ["Google", "Meta", "KDDI", "SingTel", "PLDT"], "landing_points": [
{"name": "Maruyama", "country": "Japan", "lat": 33.48, "lon": 135.76},
{"name": "Shantou", "country": "China", "lat": 23.35, "lon": 116.68},
{"name": "Hong Kong", "country": "China", "lat": 22.25, "lon": 114.17},
{"name": "Changi", "country": "Singapore", "lat": 1.35, "lon": 104.00},
]},
{"name": "SJC2", "status": "active", "rfs_year": 2022, "length_km": 10500, "capacity_tbps": 144,
"owners": ["Meta", "Google", "SingTel", "PLDT", "Telin"], "landing_points": [
{"name": "Shima", "country": "Japan", "lat": 34.33, "lon": 136.83},
{"name": "Chikura", "country": "Japan", "lat": 34.93, "lon": 139.95},
{"name": "Taiwan", "country": "Taiwan", "lat": 25.16, "lon": 121.74},
{"name": "Changi", "country": "Singapore", "lat": 1.35, "lon": 104.00},
{"name": "Manado", "country": "Indonesia", "lat": 1.49, "lon": 124.84},
]},
# Middle East
{"name": "FLAG Europe-Asia (FEA)", "status": "active", "rfs_year": 1997, "length_km": 28000, "capacity_tbps": 10,
"owners": ["Reliance Globalcom"], "landing_points": [
{"name": "Porthcurno", "country": "UK", "lat": 50.04, "lon": -5.66},
{"name": "Estepona", "country": "Spain", "lat": 36.43, "lon": -5.15},
{"name": "Palermo", "country": "Italy", "lat": 38.12, "lon": 13.36},
{"name": "Port Said", "country": "Egypt", "lat": 31.26, "lon": 32.30},
{"name": "Mumbai", "country": "India", "lat": 18.93, "lon": 72.83},
{"name": "Busan", "country": "South Korea", "lat": 35.10, "lon": 129.04},
{"name": "Miura", "country": "Japan", "lat": 35.14, "lon": 139.62},
]},
{"name": "AAE-1", "status": "active", "rfs_year": 2017, "length_km": 25000, "capacity_tbps": 40,
"owners": ["Consortium (19 telcos)"], "landing_points": [
{"name": "Marseille", "country": "France", "lat": 43.30, "lon": 5.37},
{"name": "Bari", "country": "Italy", "lat": 41.13, "lon": 16.87},
{"name": "Alexandria", "country": "Egypt", "lat": 31.20, "lon": 29.92},
{"name": "Djibouti", "country": "Djibouti", "lat": 11.55, "lon": 43.15},
{"name": "Mumbai", "country": "India", "lat": 18.93, "lon": 72.83},
{"name": "Singapore", "country": "Singapore", "lat": 1.26, "lon": 103.82},
{"name": "Hong Kong", "country": "China", "lat": 22.25, "lon": 114.17},
]},
{"name": "FALCON", "status": "active", "rfs_year": 2006, "length_km": 11500, "capacity_tbps": 4.7,
"owners": ["FLAG Telecom (Reliance)"], "landing_points": [
{"name": "Mumbai", "country": "India", "lat": 18.93, "lon": 72.83},
{"name": "Fujairah", "country": "UAE", "lat": 25.12, "lon": 56.35},
{"name": "Muscat", "country": "Oman", "lat": 23.59, "lon": 58.54},
{"name": "Jeddah", "country": "Saudi Arabia", "lat": 21.49, "lon": 39.19},
{"name": "Suez", "country": "Egypt", "lat": 29.97, "lon": 32.55},
]},
# Arctic / North
{"name": "Far North Fiber", "status": "construction", "rfs_year": 2027, "length_km": 16500, "capacity_tbps": 200,
"owners": ["Far North Digital", "Cinia"], "landing_points": [
{"name": "Tokyo", "country": "Japan", "lat": 35.65, "lon": 139.84},
{"name": "Kirkenes", "country": "Norway", "lat": 69.73, "lon": 30.05},
{"name": "Dublin", "country": "Ireland", "lat": 53.35, "lon": -6.26},
{"name": "Nome", "country": "USA", "lat": 64.50, "lon": -165.41},
]},
# Australia
{"name": "JGA-S (Japan-Guam-Australia South)", "status": "active", "rfs_year": 2020, "length_km": 7100, "capacity_tbps": 36,
"owners": ["Google", "AARNet", "Indosat"], "landing_points": [
{"name": "Shima", "country": "Japan", "lat": 34.33, "lon": 136.83},
{"name": "Piti", "country": "Guam", "lat": 13.46, "lon": 144.70},
{"name": "Sydney", "country": "Australia", "lat": -33.87, "lon": 151.21},
]},
{"name": "Indigo", "status": "active", "rfs_year": 2019, "length_km": 9600, "capacity_tbps": 36,
"owners": ["Google", "AARNet", "Indosat", "SingTel", "SubPartners"], "landing_points": [
{"name": "Perth", "country": "Australia", "lat": -31.95, "lon": 115.86},
{"name": "Singapore", "country": "Singapore", "lat": 1.26, "lon": 103.82},
{"name": "Jakarta", "country": "Indonesia", "lat": -6.13, "lon": 106.85},
]},
# South America
{"name": "SACS (South Atlantic Cable System)", "status": "active", "rfs_year": 2018, "length_km": 6200, "capacity_tbps": 40,
"owners": ["Angola Cables"], "landing_points": [
{"name": "Luanda", "country": "Angola", "lat": -8.84, "lon": 13.23},
{"name": "Fortaleza", "country": "Brazil", "lat": -3.72, "lon": -38.52},
]},
{"name": "EllaLink", "status": "active", "rfs_year": 2021, "length_km": 12000, "capacity_tbps": 72,
"owners": ["EllaLink"], "landing_points": [
{"name": "Sines", "country": "Portugal", "lat": 37.96, "lon": -8.87},
{"name": "Marseille", "country": "France", "lat": 43.30, "lon": 5.37},
{"name": "Fortaleza", "country": "Brazil", "lat": -3.72, "lon": -38.52},
]},
# India-specific
{"name": "India-Asia-Xpress (IAX)", "status": "active", "rfs_year": 2024, "length_km": 7800, "capacity_tbps": 120,
"owners": ["Reliance Jio", "Meta"], "landing_points": [
{"name": "Mumbai", "country": "India", "lat": 18.93, "lon": 72.83},
{"name": "Singapore", "country": "Singapore", "lat": 1.26, "lon": 103.82},
{"name": "Kuantan", "country": "Malaysia", "lat": 3.80, "lon": 103.33},
{"name": "Satun", "country": "Thailand", "lat": 6.62, "lon": 100.07},
]},
# North Pacific
{"name": "NCP (New Cross Pacific)", "status": "active", "rfs_year": 2018, "length_km": 13600, "capacity_tbps": 80,
"owners": ["Microsoft", "Meta", "Amazon", "SoftBank", "PLDT"], "landing_points": [
{"name": "Hillsboro", "country": "USA", "lat": 45.53, "lon": -122.99},
{"name": "Maruyama", "country": "Japan", "lat": 33.48, "lon": 135.76},
{"name": "Chongming", "country": "China", "lat": 31.62, "lon": 121.73},
{"name": "Taipei", "country": "Taiwan", "lat": 25.16, "lon": 121.74},
]},
{"name": "Unity", "status": "active", "rfs_year": 2010, "length_km": 10000, "capacity_tbps": 7.68,
"owners": ["Google", "KDDI"], "landing_points": [
{"name": "Chikura", "country": "Japan", "lat": 34.93, "lon": 139.95},
{"name": "Los Angeles", "country": "USA", "lat": 33.94, "lon": -118.45},
]},
# Mediterranean
{"name": "Blue & Raman", "status": "active", "rfs_year": 2024, "length_km": 16000, "capacity_tbps": 250,
"owners": ["Google"], "landing_points": [
{"name": "Genoa", "country": "Italy", "lat": 44.41, "lon": 8.93},
{"name": "Haifa", "country": "Israel", "lat": 32.82, "lon": 34.98},
{"name": "Mumbai", "country": "India", "lat": 18.93, "lon": 72.83},
{"name": "Amman", "country": "Jordan", "lat": 31.95, "lon": 35.93},
]},
{"name": "Oman Australia Cable (OAC)", "status": "active", "rfs_year": 2022, "length_km": 9800, "capacity_tbps": 100,
"owners": ["Oman Broadband", "SubPartners"], "landing_points": [
{"name": "Barka", "country": "Oman", "lat": 23.68, "lon": 57.88},
{"name": "Perth", "country": "Australia", "lat": -31.95, "lon": 115.86},
]},
]
def query_cables(
status: str | None = None,
country: str | None = None,
owner: str | None = None,
min_capacity_tbps: float | None = None,
) -> list[dict]:
"""Filter undersea cables by status, landing country, owner, or min capacity."""
results = []
for cable in UNDERSEA_CABLES:
if status and cable["status"] != status.lower():
continue
if min_capacity_tbps and cable["capacity_tbps"] < min_capacity_tbps:
continue
if owner:
owner_lower = owner.lower()
if not any(owner_lower in o.lower() for o in cable["owners"]):
continue
if country:
country_lower = country.lower()
if not any(country_lower in lp["country"].lower() for lp in cable["landing_points"]):
continue
results.append(cable)
return results
+91
View File
@@ -0,0 +1,91 @@
"""AI datacenter clusters worldwide.
Pure data module — no I/O, no external dependencies.
Sources: Data Center Map, Cloudscene, company announcements, press reports.
"""
from __future__ import annotations
AI_DATACENTERS: list[dict] = [
# United States — Top clusters
{"name": "Ashburn / Loudoun County", "country": "USA", "iso3": "USA", "lat": 39.04, "lon": -77.49, "region": "Virginia", "power_mw": 4500, "operators": ["AWS", "Microsoft", "Google", "Meta", "Equinix", "Digital Realty"], "notes": "Data Center Alley — largest concentration globally, ~70% of world internet traffic"},
{"name": "Dallas / Fort Worth", "country": "USA", "iso3": "USA", "lat": 32.90, "lon": -97.04, "region": "Texas", "power_mw": 2200, "operators": ["AWS", "Meta", "Google", "CyrusOne", "QTS"], "notes": "Second largest US cluster, low power costs"},
{"name": "Phoenix / Mesa", "country": "USA", "iso3": "USA", "lat": 33.44, "lon": -111.94, "region": "Arizona", "power_mw": 1800, "operators": ["Microsoft", "Google", "Meta", "Apple"], "notes": "Rapid expansion, solar power access"},
{"name": "Council Bluffs", "country": "USA", "iso3": "USA", "lat": 41.26, "lon": -95.86, "region": "Iowa", "power_mw": 1200, "operators": ["Google", "Meta"], "notes": "Google mega-campus, cheap wind power"},
{"name": "The Dalles", "country": "USA", "iso3": "USA", "lat": 45.60, "lon": -121.18, "region": "Oregon", "power_mw": 900, "operators": ["Google"], "notes": "Google flagship, hydroelectric cooling"},
{"name": "Quincy", "country": "USA", "iso3": "USA", "lat": 47.23, "lon": -119.85, "region": "Washington", "power_mw": 800, "operators": ["Microsoft", "Yahoo", "Dell"], "notes": "Columbia River hydro power"},
{"name": "Prineville", "country": "USA", "iso3": "USA", "lat": 44.30, "lon": -120.73, "region": "Oregon", "power_mw": 600, "operators": ["Meta", "Apple"], "notes": "Meta's first custom DC"},
{"name": "San Jose / Santa Clara", "country": "USA", "iso3": "USA", "lat": 37.35, "lon": -121.95, "region": "California", "power_mw": 1500, "operators": ["Equinix", "CoreSite", "NVIDIA", "Google"], "notes": "Silicon Valley interconnection hub"},
{"name": "Chicago / Aurora", "country": "USA", "iso3": "USA", "lat": 41.76, "lon": -88.32, "region": "Illinois", "power_mw": 1100, "operators": ["AWS", "Microsoft", "Google", "Digital Realty"], "notes": "Midwest hub, CME/CBOE proximity"},
{"name": "Atlanta / Douglas County", "country": "USA", "iso3": "USA", "lat": 33.70, "lon": -84.75, "region": "Georgia", "power_mw": 900, "operators": ["Google", "Microsoft", "QTS", "Switch"], "notes": "Southeast US hub"},
{"name": "Salt Lake City / West Jordan", "country": "USA", "iso3": "USA", "lat": 40.61, "lon": -111.94, "region": "Utah", "power_mw": 600, "operators": ["Meta", "AWS", "C7"], "notes": "NSA Utah Data Center nearby"},
{"name": "Reno / Sparks", "country": "USA", "iso3": "USA", "lat": 39.53, "lon": -119.81, "region": "Nevada", "power_mw": 500, "operators": ["Apple", "Switch", "Microsoft"], "notes": "Switch SuperNAP campus"},
{"name": "New Albany", "country": "USA", "iso3": "USA", "lat": 40.08, "lon": -82.81, "region": "Ohio", "power_mw": 1400, "operators": ["Google", "AWS", "Meta", "Microsoft"], "notes": "Ohio Intel fab synergy, massive expansion"},
{"name": "Papillion / Omaha", "country": "USA", "iso3": "USA", "lat": 41.15, "lon": -96.04, "region": "Nebraska", "power_mw": 500, "operators": ["Meta", "Google"], "notes": "Central US, wind/solar access"},
# Europe
{"name": "Amsterdam / Schiphol", "country": "Netherlands", "iso3": "NLD", "lat": 52.30, "lon": 4.76, "region": "North Holland", "power_mw": 800, "operators": ["Equinix", "Digital Realty", "Microsoft"], "notes": "AMS-IX — world's largest IXP, moratorium on new DCs"},
{"name": "Dublin", "country": "Ireland", "iso3": "IRL", "lat": 53.35, "lon": -6.26, "region": "Leinster", "power_mw": 900, "operators": ["AWS", "Microsoft", "Google", "Meta"], "notes": "EU data sovereignty hub, tax advantages"},
{"name": "Frankfurt", "country": "Germany", "iso3": "DEU", "lat": 50.11, "lon": 8.68, "region": "Hesse", "power_mw": 700, "operators": ["Equinix", "Digital Realty", "AWS", "Google"], "notes": "DE-CIX — largest IXP by members"},
{"name": "London / Slough", "country": "UK", "iso3": "GBR", "lat": 51.51, "lon": -0.59, "region": "Berkshire", "power_mw": 900, "operators": ["Equinix", "Digital Realty", "AWS", "Google"], "notes": "LINX, financial sector hub"},
{"name": "Paris / Île-de-France", "country": "France", "iso3": "FRA", "lat": 48.95, "lon": 2.40, "region": "Île-de-France", "power_mw": 500, "operators": ["Equinix", "Digital Realty", "OVHcloud"], "notes": "France-IX hub"},
{"name": "Stockholm / Rosersberg", "country": "Sweden", "iso3": "SWE", "lat": 59.60, "lon": 17.88, "region": "Stockholm", "power_mw": 400, "operators": ["AWS", "Microsoft", "Ericsson"], "notes": "Nordic hub, cold climate cooling"},
{"name": "Milan", "country": "Italy", "iso3": "ITA", "lat": 45.46, "lon": 9.19, "region": "Lombardy", "power_mw": 300, "operators": ["Equinix", "AWS", "Google"], "notes": "Southern Europe hub"},
{"name": "Madrid", "country": "Spain", "iso3": "ESP", "lat": 40.42, "lon": -3.70, "region": "Community of Madrid", "power_mw": 350, "operators": ["AWS", "Microsoft", "Google"], "notes": "Iberian expansion"},
{"name": "Luleå", "country": "Sweden", "iso3": "SWE", "lat": 65.58, "lon": 22.15, "region": "Norrbotten", "power_mw": 200, "operators": ["Meta"], "notes": "Arctic cooling, hydro power, first Meta EU DC"},
{"name": "Hamina", "country": "Finland", "iso3": "FIN", "lat": 60.57, "lon": 27.20, "region": "Kymenlaakso", "power_mw": 200, "operators": ["Google"], "notes": "Seawater cooling from Baltic"},
# Asia-Pacific
{"name": "Singapore / Jurong", "country": "Singapore", "iso3": "SGP", "lat": 1.33, "lon": 103.74, "region": "West Region", "power_mw": 700, "operators": ["Equinix", "Digital Realty", "AWS", "Google"], "notes": "APAC hub, moratorium lifted 2022 with green requirements"},
{"name": "Tokyo / Inzai", "country": "Japan", "iso3": "JPN", "lat": 35.83, "lon": 140.14, "region": "Chiba", "power_mw": 900, "operators": ["Equinix", "AWS", "Google", "NTT"], "notes": "Asia's largest market"},
{"name": "Seoul / Gasan", "country": "South Korea", "iso3": "KOR", "lat": 37.48, "lon": 126.88, "region": "Seoul", "power_mw": 400, "operators": ["AWS", "Google", "Samsung SDS", "KT"], "notes": "Korean DC cluster, 5G synergy"},
{"name": "Mumbai / Navi Mumbai", "country": "India", "iso3": "IND", "lat": 19.06, "lon": 73.01, "region": "Maharashtra", "power_mw": 600, "operators": ["AWS", "Microsoft", "Google", "Reliance Jio", "Adani"], "notes": "India's primary DC hub, submarine cable landing"},
{"name": "Chennai / Ambattur", "country": "India", "iso3": "IND", "lat": 13.11, "lon": 80.15, "region": "Tamil Nadu", "power_mw": 350, "operators": ["AWS", "Microsoft", "NTT"], "notes": "Second India hub, cable landing site"},
{"name": "Sydney / Western Sydney", "country": "Australia", "iso3": "AUS", "lat": -33.80, "lon": 150.90, "region": "NSW", "power_mw": 500, "operators": ["Equinix", "AWS", "Microsoft", "Google"], "notes": "Australia's primary hub"},
{"name": "Hong Kong / Tseung Kwan O", "country": "China", "iso3": "HKG", "lat": 22.31, "lon": 114.26, "region": "New Territories", "power_mw": 400, "operators": ["Equinix", "SUNeVision", "NTT"], "notes": "Asia financial connectivity hub"},
{"name": "Beijing / Zhongguancun", "country": "China", "iso3": "CHN", "lat": 39.98, "lon": 116.30, "region": "Beijing", "power_mw": 800, "operators": ["Alibaba", "Tencent", "Baidu", "ByteDance"], "notes": "China's AI research center"},
{"name": "Shanghai / Lingang", "country": "China", "iso3": "CHN", "lat": 30.89, "lon": 121.93, "region": "Shanghai", "power_mw": 700, "operators": ["Alibaba", "Tencent", "AWS (via partners)"], "notes": "East China financial hub"},
{"name": "Guizhou / Guiyang", "country": "China", "iso3": "CHN", "lat": 26.65, "lon": 106.63, "region": "Guizhou", "power_mw": 500, "operators": ["Alibaba", "Huawei", "Tencent", "Apple"], "notes": "Mountain cooling, Apple iCloud China"},
{"name": "Zhangbei / Hebei", "country": "China", "iso3": "CHN", "lat": 41.15, "lon": 114.70, "region": "Hebei", "power_mw": 600, "operators": ["Alibaba"], "notes": "Cold climate DC cluster, near Beijing"},
{"name": "Ulanqab / Inner Mongolia", "country": "China", "iso3": "CHN", "lat": 41.00, "lon": 113.13, "region": "Inner Mongolia", "power_mw": 500, "operators": ["Alibaba", "Huawei"], "notes": "Cold climate, wind/solar power"},
{"name": "Jakarta / Cibitung", "country": "Indonesia", "iso3": "IDN", "lat": -6.27, "lon": 107.09, "region": "West Java", "power_mw": 300, "operators": ["AWS", "Google", "Telkom"], "notes": "Indonesia hub, fast growth"},
{"name": "Johor Bahru", "country": "Malaysia", "iso3": "MYS", "lat": 1.49, "lon": 103.74, "region": "Johor", "power_mw": 500, "operators": ["Microsoft", "Google", "Amazon", "ByteDance"], "notes": "Singapore spillover, massive 2024-25 expansion"},
# Middle East
{"name": "Dubai / Jebel Ali", "country": "UAE", "iso3": "ARE", "lat": 24.99, "lon": 55.06, "region": "Dubai", "power_mw": 300, "operators": ["AWS", "Microsoft", "Oracle", "Equinix"], "notes": "MENA hub"},
{"name": "Riyadh", "country": "Saudi Arabia", "iso3": "SAU", "lat": 24.71, "lon": 46.67, "region": "Riyadh Province", "power_mw": 300, "operators": ["AWS", "Google", "Oracle", "STC"], "notes": "Vision 2030 DC investment"},
{"name": "Tel Aviv / Haifa", "country": "Israel", "iso3": "ISR", "lat": 32.07, "lon": 34.77, "region": "Tel Aviv", "power_mw": 200, "operators": ["AWS", "Google", "Microsoft", "Equinix"], "notes": "Israel tech hub, Blue/Raman cable landing"},
# South America
{"name": "São Paulo / Barueri", "country": "Brazil", "iso3": "BRA", "lat": -23.51, "lon": -46.88, "region": "São Paulo", "power_mw": 500, "operators": ["Equinix", "Digital Realty", "AWS", "Google"], "notes": "Latin America's largest DC market"},
{"name": "Santiago", "country": "Chile", "iso3": "CHL", "lat": -33.45, "lon": -70.65, "region": "Metropolitana", "power_mw": 150, "operators": ["Google", "AWS", "Huawei"], "notes": "Pacific cable landing, Curie cable"},
{"name": "Querétaro", "country": "Mexico", "iso3": "MEX", "lat": 20.59, "lon": -100.39, "region": "Querétaro", "power_mw": 200, "operators": ["Google", "AWS", "Equinix", "KIO Networks"], "notes": "Mexico DC hub"},
# Africa
{"name": "Johannesburg / Isando", "country": "South Africa", "iso3": "ZAF", "lat": -26.14, "lon": 28.20, "region": "Gauteng", "power_mw": 200, "operators": ["Teraco", "AWS", "Microsoft"], "notes": "Africa's primary DC hub, NAPAfrica IXP"},
{"name": "Nairobi", "country": "Kenya", "iso3": "KEN", "lat": -1.29, "lon": 36.82, "region": "Nairobi", "power_mw": 80, "operators": ["AWS", "Microsoft", "Liquid Intelligent"], "notes": "East Africa hub"},
{"name": "Lagos / Lekki", "country": "Nigeria", "iso3": "NGA", "lat": 6.43, "lon": 3.43, "region": "Lagos", "power_mw": 70, "operators": ["Rack Centre", "Africa Data Centres"], "notes": "West Africa's largest, 2Africa cable landing"},
# Nordics / Specialty
{"name": "Reykjavik / Fitjar", "country": "Iceland", "iso3": "ISL", "lat": 64.05, "lon": -21.95, "region": "Capital Region", "power_mw": 100, "operators": ["Verne Global", "atNorth"], "notes": "100% geothermal/hydro, natural cooling"},
]
def query_datacenters(
country: str | None = None,
operator: str | None = None,
min_power_mw: int | None = None,
region: str | None = None,
) -> list[dict]:
"""Filter AI datacenters by country, operator, minimum power, or region."""
results = []
for dc in AI_DATACENTERS:
if country:
c = country.lower()
if c not in (dc["country"].lower(), dc["iso3"].lower()):
continue
if min_power_mw and dc["power_mw"] < min_power_mw:
continue
if operator:
op_lower = operator.lower()
if not any(op_lower in o.lower() for o in dc["operators"]):
continue
if region:
if region.lower() not in dc["region"].lower():
continue
results.append(dc)
return results
+132
View File
@@ -0,0 +1,132 @@
"""Global stock exchanges dataset.
Pure data module — no I/O, no external dependencies.
Sources: World Federation of Exchanges, GFCI, World Bank, press.
92 exchanges organized by tier (mega, major, emerging, frontier).
"""
from __future__ import annotations
STOCK_EXCHANGES: list[dict] = [
# Mega exchanges (>$3T market cap)
{"name": "New York Stock Exchange", "acronym": "NYSE", "country": "USA", "iso3": "USA", "city": "New York", "lat": 40.71, "lon": -74.01, "tier": "mega", "market_cap_usd_t": 28.4, "index": "^DJI", "currency": "USD", "timezone": "America/New_York"},
{"name": "NASDAQ", "acronym": "NASDAQ", "country": "USA", "iso3": "USA", "city": "New York", "lat": 40.76, "lon": -73.99, "tier": "mega", "market_cap_usd_t": 25.5, "index": "^IXIC", "currency": "USD", "timezone": "America/New_York"},
{"name": "Shanghai Stock Exchange", "acronym": "SSE", "country": "China", "iso3": "CHN", "city": "Shanghai", "lat": 31.23, "lon": 121.47, "tier": "mega", "market_cap_usd_t": 6.9, "index": "000001.SS", "currency": "CNY", "timezone": "Asia/Shanghai"},
{"name": "Japan Exchange Group", "acronym": "JPX", "country": "Japan", "iso3": "JPN", "city": "Tokyo", "lat": 35.68, "lon": 139.77, "tier": "mega", "market_cap_usd_t": 6.5, "index": "^N225", "currency": "JPY", "timezone": "Asia/Tokyo"},
{"name": "Shenzhen Stock Exchange", "acronym": "SZSE", "country": "China", "iso3": "CHN", "city": "Shenzhen", "lat": 22.54, "lon": 114.06, "tier": "mega", "market_cap_usd_t": 4.9, "index": "399001.SZ", "currency": "CNY", "timezone": "Asia/Shanghai"},
{"name": "Hong Kong Exchanges", "acronym": "HKEX", "country": "China", "iso3": "HKG", "city": "Hong Kong", "lat": 22.28, "lon": 114.16, "tier": "mega", "market_cap_usd_t": 4.6, "index": "^HSI", "currency": "HKD", "timezone": "Asia/Hong_Kong"},
{"name": "National Stock Exchange of India", "acronym": "NSE", "country": "India", "iso3": "IND", "city": "Mumbai", "lat": 19.06, "lon": 72.86, "tier": "mega", "market_cap_usd_t": 4.3, "index": "^NSEI", "currency": "INR", "timezone": "Asia/Kolkata"},
{"name": "London Stock Exchange", "acronym": "LSE", "country": "UK", "iso3": "GBR", "city": "London", "lat": 51.51, "lon": -0.09, "tier": "mega", "market_cap_usd_t": 3.4, "index": "^FTSE", "currency": "GBP", "timezone": "Europe/London"},
{"name": "Euronext", "acronym": "ENX", "country": "Netherlands", "iso3": "NLD", "city": "Amsterdam", "lat": 52.37, "lon": 4.89, "tier": "mega", "market_cap_usd_t": 7.3, "index": "^AEX", "currency": "EUR", "timezone": "Europe/Amsterdam"},
# Major exchanges ($500B-$3T)
{"name": "Toronto Stock Exchange", "acronym": "TSX", "country": "Canada", "iso3": "CAN", "city": "Toronto", "lat": 43.65, "lon": -79.38, "tier": "major", "market_cap_usd_t": 2.8, "index": "^GSPTSE", "currency": "CAD", "timezone": "America/Toronto"},
{"name": "Saudi Exchange (Tadawul)", "acronym": "TADAWUL", "country": "Saudi Arabia", "iso3": "SAU", "city": "Riyadh", "lat": 24.71, "lon": 46.67, "tier": "major", "market_cap_usd_t": 2.9, "index": "^TASI", "currency": "SAR", "timezone": "Asia/Riyadh"},
{"name": "Deutsche Börse", "acronym": "XETRA", "country": "Germany", "iso3": "DEU", "city": "Frankfurt", "lat": 50.11, "lon": 8.68, "tier": "major", "market_cap_usd_t": 2.3, "index": "^GDAXI", "currency": "EUR", "timezone": "Europe/Berlin"},
{"name": "SIX Swiss Exchange", "acronym": "SIX", "country": "Switzerland", "iso3": "CHE", "city": "Zurich", "lat": 47.37, "lon": 8.54, "tier": "major", "market_cap_usd_t": 1.9, "index": "^SSMI", "currency": "CHF", "timezone": "Europe/Zurich"},
{"name": "Korea Exchange", "acronym": "KRX", "country": "South Korea", "iso3": "KOR", "city": "Seoul", "lat": 37.52, "lon": 126.93, "tier": "major", "market_cap_usd_t": 1.8, "index": "^KS11", "currency": "KRW", "timezone": "Asia/Seoul"},
{"name": "Nasdaq Nordic (OMX)", "acronym": "OMX", "country": "Sweden", "iso3": "SWE", "city": "Stockholm", "lat": 59.33, "lon": 18.07, "tier": "major", "market_cap_usd_t": 1.7, "index": "^OMX", "currency": "SEK", "timezone": "Europe/Stockholm"},
{"name": "Australian Securities Exchange", "acronym": "ASX", "country": "Australia", "iso3": "AUS", "city": "Sydney", "lat": -33.87, "lon": 151.21, "tier": "major", "market_cap_usd_t": 1.6, "index": "^AXJO", "currency": "AUD", "timezone": "Australia/Sydney"},
{"name": "Taiwan Stock Exchange", "acronym": "TWSE", "country": "Taiwan", "iso3": "TWN", "city": "Taipei", "lat": 25.03, "lon": 121.52, "tier": "major", "market_cap_usd_t": 2.1, "index": "^TWII", "currency": "TWD", "timezone": "Asia/Taipei"},
{"name": "Bombay Stock Exchange", "acronym": "BSE", "country": "India", "iso3": "IND", "city": "Mumbai", "lat": 18.93, "lon": 72.83, "tier": "major", "market_cap_usd_t": 4.1, "index": "^BSESN", "currency": "INR", "timezone": "Asia/Kolkata"},
{"name": "Johannesburg Stock Exchange", "acronym": "JSE", "country": "South Africa", "iso3": "ZAF", "city": "Johannesburg", "lat": -26.20, "lon": 28.04, "tier": "major", "market_cap_usd_t": 1.1, "index": "^J203", "currency": "ZAR", "timezone": "Africa/Johannesburg"},
{"name": "B3 (Brasil Bolsa Balcão)", "acronym": "B3", "country": "Brazil", "iso3": "BRA", "city": "São Paulo", "lat": -23.55, "lon": -46.63, "tier": "major", "market_cap_usd_t": 0.9, "index": "^BVSP", "currency": "BRL", "timezone": "America/Sao_Paulo"},
{"name": "Borsa Italiana", "acronym": "BIT", "country": "Italy", "iso3": "ITA", "city": "Milan", "lat": 45.46, "lon": 9.19, "tier": "major", "market_cap_usd_t": 0.8, "index": "FTSEMIB.MI", "currency": "EUR", "timezone": "Europe/Rome"},
{"name": "BME Spanish Exchanges", "acronym": "BME", "country": "Spain", "iso3": "ESP", "city": "Madrid", "lat": 40.42, "lon": -3.70, "tier": "major", "market_cap_usd_t": 0.7, "index": "^IBEX", "currency": "EUR", "timezone": "Europe/Madrid"},
{"name": "Singapore Exchange", "acronym": "SGX", "country": "Singapore", "iso3": "SGP", "city": "Singapore", "lat": 1.28, "lon": 103.85, "tier": "major", "market_cap_usd_t": 0.6, "index": "^STI", "currency": "SGD", "timezone": "Asia/Singapore"},
{"name": "Bolsa Mexicana de Valores", "acronym": "BMV", "country": "Mexico", "iso3": "MEX", "city": "Mexico City", "lat": 19.43, "lon": -99.13, "tier": "major", "market_cap_usd_t": 0.5, "index": "^MXX", "currency": "MXN", "timezone": "America/Mexico_City"},
{"name": "Tel Aviv Stock Exchange", "acronym": "TASE", "country": "Israel", "iso3": "ISR", "city": "Tel Aviv", "lat": 32.07, "lon": 34.77, "tier": "major", "market_cap_usd_t": 0.3, "index": "^TA125", "currency": "ILS", "timezone": "Asia/Jerusalem"},
# Emerging exchanges ($50B-$500B)
{"name": "Indonesia Stock Exchange", "acronym": "IDX", "country": "Indonesia", "iso3": "IDN", "city": "Jakarta", "lat": -6.22, "lon": 106.85, "tier": "emerging", "market_cap_usd_t": 0.6, "index": "^JKSE", "currency": "IDR", "timezone": "Asia/Jakarta"},
{"name": "Bursa Malaysia", "acronym": "BM", "country": "Malaysia", "iso3": "MYS", "city": "Kuala Lumpur", "lat": 3.15, "lon": 101.71, "tier": "emerging", "market_cap_usd_t": 0.4, "index": "^KLSE", "currency": "MYR", "timezone": "Asia/Kuala_Lumpur"},
{"name": "Stock Exchange of Thailand", "acronym": "SET", "country": "Thailand", "iso3": "THA", "city": "Bangkok", "lat": 13.76, "lon": 100.50, "tier": "emerging", "market_cap_usd_t": 0.5, "index": "^SET", "currency": "THB", "timezone": "Asia/Bangkok"},
{"name": "Philippine Stock Exchange", "acronym": "PSE", "country": "Philippines", "iso3": "PHL", "city": "Manila", "lat": 14.59, "lon": 120.98, "tier": "emerging", "market_cap_usd_t": 0.3, "index": "PSEI.PS", "currency": "PHP", "timezone": "Asia/Manila"},
{"name": "Ho Chi Minh Stock Exchange", "acronym": "HOSE", "country": "Vietnam", "iso3": "VNM", "city": "Ho Chi Minh City", "lat": 10.77, "lon": 106.70, "tier": "emerging", "market_cap_usd_t": 0.2, "index": "^VNINDEX", "currency": "VND", "timezone": "Asia/Ho_Chi_Minh"},
{"name": "Colombo Stock Exchange", "acronym": "CSE", "country": "Sri Lanka", "iso3": "LKA", "city": "Colombo", "lat": 6.93, "lon": 79.84, "tier": "emerging", "market_cap_usd_t": 0.02, "index": "^CSE", "currency": "LKR", "timezone": "Asia/Colombo"},
{"name": "Abu Dhabi Securities Exchange", "acronym": "ADX", "country": "UAE", "iso3": "ARE", "city": "Abu Dhabi", "lat": 24.45, "lon": 54.65, "tier": "emerging", "market_cap_usd_t": 0.8, "index": "^ADI", "currency": "AED", "timezone": "Asia/Dubai"},
{"name": "Dubai Financial Market", "acronym": "DFM", "country": "UAE", "iso3": "ARE", "city": "Dubai", "lat": 25.20, "lon": 55.27, "tier": "emerging", "market_cap_usd_t": 0.2, "index": "^DFMGI", "currency": "AED", "timezone": "Asia/Dubai"},
{"name": "Qatar Stock Exchange", "acronym": "QSE", "country": "Qatar", "iso3": "QAT", "city": "Doha", "lat": 25.29, "lon": 51.53, "tier": "emerging", "market_cap_usd_t": 0.2, "index": "^QSI", "currency": "QAR", "timezone": "Asia/Qatar"},
{"name": "Kuwait Stock Exchange", "acronym": "BK", "country": "Kuwait", "iso3": "KWT", "city": "Kuwait City", "lat": 29.38, "lon": 47.99, "tier": "emerging", "market_cap_usd_t": 0.1, "index": "^BKP", "currency": "KWD", "timezone": "Asia/Kuwait"},
{"name": "Bahrain Bourse", "acronym": "BHB", "country": "Bahrain", "iso3": "BHR", "city": "Manama", "lat": 26.22, "lon": 50.59, "tier": "emerging", "market_cap_usd_t": 0.03, "index": "^BAX", "currency": "BHD", "timezone": "Asia/Bahrain"},
{"name": "Muscat Securities Market", "acronym": "MSM", "country": "Oman", "iso3": "OMN", "city": "Muscat", "lat": 23.61, "lon": 58.59, "tier": "emerging", "market_cap_usd_t": 0.03, "index": "^MSI", "currency": "OMR", "timezone": "Asia/Muscat"},
{"name": "Casablanca Stock Exchange", "acronym": "CSE", "country": "Morocco", "iso3": "MAR", "city": "Casablanca", "lat": 33.59, "lon": -7.62, "tier": "emerging", "market_cap_usd_t": 0.07, "index": "^MASI", "currency": "MAD", "timezone": "Africa/Casablanca"},
{"name": "Egyptian Exchange", "acronym": "EGX", "country": "Egypt", "iso3": "EGY", "city": "Cairo", "lat": 30.04, "lon": 31.24, "tier": "emerging", "market_cap_usd_t": 0.04, "index": "^EGX30", "currency": "EGP", "timezone": "Africa/Cairo"},
{"name": "Nairobi Securities Exchange", "acronym": "NSE", "country": "Kenya", "iso3": "KEN", "city": "Nairobi", "lat": -1.29, "lon": 36.82, "tier": "emerging", "market_cap_usd_t": 0.02, "index": "^NSE20", "currency": "KES", "timezone": "Africa/Nairobi"},
{"name": "Nigerian Exchange", "acronym": "NGX", "country": "Nigeria", "iso3": "NGA", "city": "Lagos", "lat": 6.45, "lon": 3.40, "tier": "emerging", "market_cap_usd_t": 0.04, "index": "^NGSE", "currency": "NGN", "timezone": "Africa/Lagos"},
{"name": "Santiago Stock Exchange", "acronym": "BCS", "country": "Chile", "iso3": "CHL", "city": "Santiago", "lat": -33.44, "lon": -70.66, "tier": "emerging", "market_cap_usd_t": 0.2, "index": "^IPSA", "currency": "CLP", "timezone": "America/Santiago"},
{"name": "Buenos Aires Stock Exchange", "acronym": "BCBA", "country": "Argentina", "iso3": "ARG", "city": "Buenos Aires", "lat": -34.61, "lon": -58.37, "tier": "emerging", "market_cap_usd_t": 0.06, "index": "^MERV", "currency": "ARS", "timezone": "America/Argentina/Buenos_Aires"},
{"name": "Lima Stock Exchange", "acronym": "BVL", "country": "Peru", "iso3": "PER", "city": "Lima", "lat": -12.05, "lon": -77.04, "tier": "emerging", "market_cap_usd_t": 0.08, "index": "^SPBLPGPT", "currency": "PEN", "timezone": "America/Lima"},
{"name": "Colombia Stock Exchange", "acronym": "BVC", "country": "Colombia", "iso3": "COL", "city": "Bogotá", "lat": 4.71, "lon": -74.07, "tier": "emerging", "market_cap_usd_t": 0.07, "index": "^COLCAP", "currency": "COP", "timezone": "America/Bogota"},
{"name": "Warsaw Stock Exchange", "acronym": "WSE", "country": "Poland", "iso3": "POL", "city": "Warsaw", "lat": 52.23, "lon": 21.01, "tier": "emerging", "market_cap_usd_t": 0.2, "index": "^WIG20", "currency": "PLN", "timezone": "Europe/Warsaw"},
{"name": "Moscow Exchange", "acronym": "MOEX", "country": "Russia", "iso3": "RUS", "city": "Moscow", "lat": 55.76, "lon": 37.62, "tier": "emerging", "market_cap_usd_t": 0.6, "index": "IMOEX.ME", "currency": "RUB", "timezone": "Europe/Moscow"},
{"name": "Istanbul Stock Exchange", "acronym": "BIST", "country": "Turkey", "iso3": "TUR", "city": "Istanbul", "lat": 41.01, "lon": 28.98, "tier": "emerging", "market_cap_usd_t": 0.3, "index": "^XU100", "currency": "TRY", "timezone": "Europe/Istanbul"},
{"name": "Athens Stock Exchange", "acronym": "ATHEX", "country": "Greece", "iso3": "GRC", "city": "Athens", "lat": 37.98, "lon": 23.73, "tier": "emerging", "market_cap_usd_t": 0.08, "index": "^ATG", "currency": "EUR", "timezone": "Europe/Athens"},
{"name": "Bucharest Stock Exchange", "acronym": "BVB", "country": "Romania", "iso3": "ROU", "city": "Bucharest", "lat": 44.43, "lon": 26.10, "tier": "emerging", "market_cap_usd_t": 0.06, "index": "^BET", "currency": "RON", "timezone": "Europe/Bucharest"},
{"name": "Prague Stock Exchange", "acronym": "PSE", "country": "Czech Republic", "iso3": "CZE", "city": "Prague", "lat": 50.08, "lon": 14.43, "tier": "emerging", "market_cap_usd_t": 0.03, "index": "^PX", "currency": "CZK", "timezone": "Europe/Prague"},
{"name": "Budapest Stock Exchange", "acronym": "BSE", "country": "Hungary", "iso3": "HUN", "city": "Budapest", "lat": 47.50, "lon": 19.04, "tier": "emerging", "market_cap_usd_t": 0.04, "index": "^BUX", "currency": "HUF", "timezone": "Europe/Budapest"},
{"name": "Pakistan Stock Exchange", "acronym": "PSX", "country": "Pakistan", "iso3": "PAK", "city": "Karachi", "lat": 24.85, "lon": 67.01, "tier": "emerging", "market_cap_usd_t": 0.04, "index": "^KSE100", "currency": "PKR", "timezone": "Asia/Karachi"},
{"name": "Dhaka Stock Exchange", "acronym": "DSE", "country": "Bangladesh", "iso3": "BGD", "city": "Dhaka", "lat": 23.73, "lon": 90.39, "tier": "emerging", "market_cap_usd_t": 0.05, "index": "^DSEX", "currency": "BDT", "timezone": "Asia/Dhaka"},
# Frontier / smaller
{"name": "Amman Stock Exchange", "acronym": "ASE", "country": "Jordan", "iso3": "JOR", "city": "Amman", "lat": 31.95, "lon": 35.93, "tier": "frontier", "market_cap_usd_t": 0.02, "index": "^AMGNRLX", "currency": "JOD", "timezone": "Asia/Amman"},
{"name": "Tunis Stock Exchange", "acronym": "BVMT", "country": "Tunisia", "iso3": "TUN", "city": "Tunis", "lat": 36.80, "lon": 10.18, "tier": "frontier", "market_cap_usd_t": 0.01, "index": "^TUNINDEX", "currency": "TND", "timezone": "Africa/Tunis"},
{"name": "Dar es Salaam Stock Exchange", "acronym": "DSE", "country": "Tanzania", "iso3": "TZA", "city": "Dar es Salaam", "lat": -6.79, "lon": 39.28, "tier": "frontier", "market_cap_usd_t": 0.01, "index": "^DSI", "currency": "TZS", "timezone": "Africa/Dar_es_Salaam"},
{"name": "Uganda Securities Exchange", "acronym": "USE", "country": "Uganda", "iso3": "UGA", "city": "Kampala", "lat": 0.31, "lon": 32.58, "tier": "frontier", "market_cap_usd_t": 0.005, "index": "^ALSI", "currency": "UGX", "timezone": "Africa/Kampala"},
{"name": "Rwanda Stock Exchange", "acronym": "RSE", "country": "Rwanda", "iso3": "RWA", "city": "Kigali", "lat": -1.94, "lon": 30.06, "tier": "frontier", "market_cap_usd_t": 0.003, "index": "^RSI", "currency": "RWF", "timezone": "Africa/Kigali"},
{"name": "Ghana Stock Exchange", "acronym": "GSE", "country": "Ghana", "iso3": "GHA", "city": "Accra", "lat": 5.56, "lon": -0.19, "tier": "frontier", "market_cap_usd_t": 0.01, "index": "^GGSECI", "currency": "GHS", "timezone": "Africa/Accra"},
{"name": "Zimbabwe Stock Exchange", "acronym": "ZSE", "country": "Zimbabwe", "iso3": "ZWE", "city": "Harare", "lat": -17.83, "lon": 31.05, "tier": "frontier", "market_cap_usd_t": 0.003, "index": "^ZSI", "currency": "ZWL", "timezone": "Africa/Harare"},
{"name": "Botswana Stock Exchange", "acronym": "BSE", "country": "Botswana", "iso3": "BWA", "city": "Gaborone", "lat": -24.65, "lon": 25.91, "tier": "frontier", "market_cap_usd_t": 0.004, "index": "^DCI", "currency": "BWP", "timezone": "Africa/Gaborone"},
{"name": "Lusaka Securities Exchange", "acronym": "LuSE", "country": "Zambia", "iso3": "ZMB", "city": "Lusaka", "lat": -15.39, "lon": 28.32, "tier": "frontier", "market_cap_usd_t": 0.005, "index": "^LASI", "currency": "ZMW", "timezone": "Africa/Lusaka"},
{"name": "Mauritius Stock Exchange", "acronym": "SEM", "country": "Mauritius", "iso3": "MUS", "city": "Port Louis", "lat": -20.16, "lon": 57.50, "tier": "frontier", "market_cap_usd_t": 0.01, "index": "^SEMDEX", "currency": "MUR", "timezone": "Indian/Mauritius"},
{"name": "Nepal Stock Exchange", "acronym": "NEPSE", "country": "Nepal", "iso3": "NPL", "city": "Kathmandu", "lat": 27.71, "lon": 85.32, "tier": "frontier", "market_cap_usd_t": 0.02, "index": "^NEPSE", "currency": "NPR", "timezone": "Asia/Kathmandu"},
{"name": "Cambodia Securities Exchange", "acronym": "CSX", "country": "Cambodia", "iso3": "KHM", "city": "Phnom Penh", "lat": 11.56, "lon": 104.93, "tier": "frontier", "market_cap_usd_t": 0.003, "index": "^CSXI", "currency": "KHR", "timezone": "Asia/Phnom_Penh"},
{"name": "Lao Securities Exchange", "acronym": "LSX", "country": "Laos", "iso3": "LAO", "city": "Vientiane", "lat": 17.97, "lon": 102.63, "tier": "frontier", "market_cap_usd_t": 0.001, "index": "^LSXI", "currency": "LAK", "timezone": "Asia/Vientiane"},
{"name": "Mongolia Stock Exchange", "acronym": "MSE", "country": "Mongolia", "iso3": "MNG", "city": "Ulaanbaatar", "lat": 47.92, "lon": 106.91, "tier": "frontier", "market_cap_usd_t": 0.002, "index": "^MNT20", "currency": "MNT", "timezone": "Asia/Ulaanbaatar"},
{"name": "Tehran Stock Exchange", "acronym": "TSE", "country": "Iran", "iso3": "IRN", "city": "Tehran", "lat": 35.69, "lon": 51.42, "tier": "emerging", "market_cap_usd_t": 0.2, "index": "^TEDPIX", "currency": "IRR", "timezone": "Asia/Tehran"},
{"name": "Karachi Stock Exchange (merged into PSX)", "acronym": "KSE", "country": "Pakistan", "iso3": "PAK", "city": "Karachi", "lat": 24.85, "lon": 67.01, "tier": "emerging", "market_cap_usd_t": 0.0, "index": "", "currency": "PKR", "timezone": "Asia/Karachi"},
{"name": "Colombo Stock Exchange", "acronym": "CSE", "country": "Sri Lanka", "iso3": "LKA", "city": "Colombo", "lat": 6.93, "lon": 79.85, "tier": "frontier", "market_cap_usd_t": 0.02, "index": "^ASPI", "currency": "LKR", "timezone": "Asia/Colombo"},
{"name": "New Zealand Exchange", "acronym": "NZX", "country": "New Zealand", "iso3": "NZL", "city": "Wellington", "lat": -41.29, "lon": 174.78, "tier": "major", "market_cap_usd_t": 0.1, "index": "^NZ50", "currency": "NZD", "timezone": "Pacific/Auckland"},
{"name": "Oslo Stock Exchange", "acronym": "OSE", "country": "Norway", "iso3": "NOR", "city": "Oslo", "lat": 59.91, "lon": 10.75, "tier": "major", "market_cap_usd_t": 0.3, "index": "^OBX", "currency": "NOK", "timezone": "Europe/Oslo"},
{"name": "Copenhagen Stock Exchange", "acronym": "CSE", "country": "Denmark", "iso3": "DNK", "city": "Copenhagen", "lat": 55.68, "lon": 12.57, "tier": "major", "market_cap_usd_t": 0.7, "index": "^OMXC25", "currency": "DKK", "timezone": "Europe/Copenhagen"},
{"name": "Helsinki Stock Exchange", "acronym": "OMXH", "country": "Finland", "iso3": "FIN", "city": "Helsinki", "lat": 60.17, "lon": 24.94, "tier": "major", "market_cap_usd_t": 0.3, "index": "^OMXH25", "currency": "EUR", "timezone": "Europe/Helsinki"},
{"name": "Vienna Stock Exchange", "acronym": "VSE", "country": "Austria", "iso3": "AUT", "city": "Vienna", "lat": 48.21, "lon": 16.37, "tier": "emerging", "market_cap_usd_t": 0.1, "index": "^ATX", "currency": "EUR", "timezone": "Europe/Vienna"},
{"name": "Lisbon Stock Exchange (Euronext)", "acronym": "ELX", "country": "Portugal", "iso3": "PRT", "city": "Lisbon", "lat": 38.72, "lon": -9.14, "tier": "emerging", "market_cap_usd_t": 0.08, "index": "^PSI20", "currency": "EUR", "timezone": "Europe/Lisbon"},
{"name": "Brussels Stock Exchange (Euronext)", "acronym": "EBR", "country": "Belgium", "iso3": "BEL", "city": "Brussels", "lat": 50.85, "lon": 4.35, "tier": "major", "market_cap_usd_t": 0.3, "index": "^BFX", "currency": "EUR", "timezone": "Europe/Brussels"},
{"name": "Irish Stock Exchange (Euronext)", "acronym": "ISE", "country": "Ireland", "iso3": "IRL", "city": "Dublin", "lat": 53.35, "lon": -6.26, "tier": "emerging", "market_cap_usd_t": 0.1, "index": "^ISEQ", "currency": "EUR", "timezone": "Europe/Dublin"},
{"name": "Zagreb Stock Exchange", "acronym": "ZSE", "country": "Croatia", "iso3": "HRV", "city": "Zagreb", "lat": 45.81, "lon": 15.98, "tier": "frontier", "market_cap_usd_t": 0.03, "index": "^CRBEX", "currency": "EUR", "timezone": "Europe/Zagreb"},
{"name": "Ljubljana Stock Exchange", "acronym": "LJSE", "country": "Slovenia", "iso3": "SVN", "city": "Ljubljana", "lat": 46.05, "lon": 14.51, "tier": "frontier", "market_cap_usd_t": 0.01, "index": "^SBITOP", "currency": "EUR", "timezone": "Europe/Ljubljana"},
{"name": "Beirut Stock Exchange", "acronym": "BSE", "country": "Lebanon", "iso3": "LBN", "city": "Beirut", "lat": 33.89, "lon": 35.50, "tier": "frontier", "market_cap_usd_t": 0.003, "index": "^BLOM", "currency": "LBP", "timezone": "Asia/Beirut"},
]
# Country ISO3 → primary exchange index ticker mapping
COUNTRY_INDEX_TICKERS: dict[str, str] = {
ex["iso3"]: ex["index"]
for ex in STOCK_EXCHANGES
if ex["index"] and ex["tier"] in ("mega", "major")
}
# Override duplicates (keep largest by market cap)
COUNTRY_INDEX_TICKERS.update({
"USA": "^GSPC", # S&P 500 as default US index
"CHN": "000001.SS",
"IND": "^NSEI",
"ARE": "^ADI",
})
def query_exchanges(
tier: str | None = None,
country: str | None = None,
currency: str | None = None,
) -> list[dict]:
"""Filter stock exchanges by tier, country, or currency."""
results = []
for ex in STOCK_EXCHANGES:
if tier and ex["tier"] != tier.lower():
continue
if country:
c = country.lower()
if c not in (ex["country"].lower(), ex["iso3"].lower()):
continue
if currency and ex["currency"].lower() != currency.lower():
continue
results.append(ex)
return results
+75
View File
@@ -0,0 +1,75 @@
"""Critical mineral deposits and strategic mineral locations.
Pure data module — no I/O, no external dependencies.
Sources: USGS Mineral Commodity Summaries, IEA Critical Minerals,
EU Critical Raw Materials Act, Australian Geoscience.
"""
from __future__ import annotations
CRITICAL_MINERALS: list[dict] = [
# Lithium
{"name": "Salar de Atacama", "country": "Chile", "iso3": "CHL", "lat": -23.50, "lon": -68.20, "mineral": "lithium", "type": "brine", "operator": "SQM / Albemarle", "annual_tonnes": 100000, "pct_global": 26, "notes": "World's largest lithium brine operation"},
{"name": "Greenbushes", "country": "Australia", "iso3": "AUS", "lat": -33.85, "lon": 116.06, "mineral": "lithium", "type": "hard_rock", "operator": "Talison (Tianqi/Albemarle)", "annual_tonnes": 75000, "pct_global": 20, "notes": "Largest hard-rock lithium mine"},
{"name": "Salar de Uyuni", "country": "Bolivia", "iso3": "BOL", "lat": -20.13, "lon": -67.49, "mineral": "lithium", "type": "brine", "operator": "YLB (state)", "annual_tonnes": 5000, "pct_global": 1, "notes": "Largest reserves globally (~21M tonnes), low extraction"},
{"name": "Pilgangoora", "country": "Australia", "iso3": "AUS", "lat": -21.30, "lon": 119.04, "mineral": "lithium", "type": "hard_rock", "operator": "Pilbara Minerals", "annual_tonnes": 40000, "pct_global": 10, "notes": "Major spodumene producer, Pilbara region"},
{"name": "Thacker Pass", "country": "USA", "iso3": "USA", "lat": 41.32, "lon": -117.65, "mineral": "lithium", "type": "clay", "operator": "Lithium Americas", "annual_tonnes": 0, "pct_global": 0, "notes": "Largest US lithium project, under construction"},
# Cobalt
{"name": "Katanga Province Mines", "country": "DR Congo", "iso3": "COD", "lat": -10.98, "lon": 26.02, "mineral": "cobalt", "type": "copper_cobalt", "operator": "Glencore / CMOC / Artisanal", "annual_tonnes": 130000, "pct_global": 73, "notes": "~73% of global cobalt, artisanal mining concerns"},
{"name": "Murrin Murrin", "country": "Australia", "iso3": "AUS", "lat": -28.72, "lon": 121.87, "mineral": "cobalt", "type": "nickel_cobalt", "operator": "Glencore", "annual_tonnes": 3000, "pct_global": 2, "notes": "Nickel-cobalt laterite"},
# Rare Earth Elements
{"name": "Bayan Obo", "country": "China", "iso3": "CHN", "lat": 41.78, "lon": 109.97, "mineral": "rare_earths", "type": "open_pit", "operator": "China Northern Rare Earth", "annual_tonnes": 45000, "pct_global": 38, "notes": "World's largest REE deposit, Inner Mongolia"},
{"name": "Mount Weld", "country": "Australia", "iso3": "AUS", "lat": -28.77, "lon": 122.55, "mineral": "rare_earths", "type": "open_pit", "operator": "Lynas Rare Earths", "annual_tonnes": 12000, "pct_global": 10, "notes": "Largest non-Chinese REE mine, processed in Malaysia"},
{"name": "Mountain Pass", "country": "USA", "iso3": "USA", "lat": 35.48, "lon": -115.53, "mineral": "rare_earths", "type": "open_pit", "operator": "MP Materials", "annual_tonnes": 43000, "pct_global": 14, "notes": "Only US rare earth mine, processing expansion underway"},
{"name": "Xunwu / Ganzhou", "country": "China", "iso3": "CHN", "lat": 24.95, "lon": 115.65, "mineral": "rare_earths", "type": "ion_adsorption", "operator": "Various Chinese SOEs", "annual_tonnes": 30000, "pct_global": 25, "notes": "Heavy rare earths (Dy, Tb), Jiangxi province"},
# Nickel
{"name": "Sorowako / Morowali", "country": "Indonesia", "iso3": "IDN", "lat": -2.54, "lon": 121.36, "mineral": "nickel", "type": "laterite", "operator": "Vale / Chinese consortia", "annual_tonnes": 1600000, "pct_global": 48, "notes": "Indonesia is #1 producer, massive HPAL expansion"},
{"name": "Norilsk", "country": "Russia", "iso3": "RUS", "lat": 69.35, "lon": 88.20, "mineral": "nickel", "type": "sulfide", "operator": "Nornickel", "annual_tonnes": 200000, "pct_global": 6, "notes": "Major PGM co-product, Arctic"},
# Copper
{"name": "Escondida", "country": "Chile", "iso3": "CHL", "lat": -24.27, "lon": -69.07, "mineral": "copper", "type": "open_pit", "operator": "BHP / Rio Tinto / JECO", "annual_tonnes": 1100000, "pct_global": 5, "notes": "World's largest copper mine by output"},
{"name": "Grasberg", "country": "Indonesia", "iso3": "IDN", "lat": -4.06, "lon": 137.11, "mineral": "copper", "type": "underground", "operator": "Freeport-McMoRan / INALUM", "annual_tonnes": 700000, "pct_global": 3, "notes": "Largest gold reserve, deep block cave"},
{"name": "Kamoa-Kakula", "country": "DR Congo", "iso3": "COD", "lat": -10.77, "lon": 25.33, "mineral": "copper", "type": "underground", "operator": "Ivanhoe Mines / Zijin", "annual_tonnes": 400000, "pct_global": 2, "notes": "Newest mega-mine, highest grade discovered copper"},
# Graphite
{"name": "Heilongjiang Province", "country": "China", "iso3": "CHN", "lat": 47.35, "lon": 127.96, "mineral": "graphite", "type": "flake", "operator": "Various Chinese", "annual_tonnes": 900000, "pct_global": 65, "notes": "China dominates natural graphite production"},
{"name": "Balama", "country": "Mozambique", "iso3": "MOZ", "lat": -13.35, "lon": 38.58, "mineral": "graphite", "type": "flake", "operator": "Syrah Resources", "annual_tonnes": 50000, "pct_global": 4, "notes": "Largest known graphite reserve, feeds Vidalia (US)"},
# Manganese
{"name": "Kalahari Manganese Field", "country": "South Africa", "iso3": "ZAF", "lat": -27.18, "lon": 22.98, "mineral": "manganese", "type": "open_pit", "operator": "South32 / Samancor", "annual_tonnes": 7200000, "pct_global": 37, "notes": "~80% of global reserves in Kalahari"},
# Platinum Group Metals
{"name": "Bushveld Complex", "country": "South Africa", "iso3": "ZAF", "lat": -25.00, "lon": 29.50, "mineral": "pgm", "type": "underground", "operator": "Anglo American / Impala / Sibanye", "annual_tonnes": 130, "pct_global": 72, "notes": "~72% of global platinum, major PGM source"},
# Titanium
{"name": "Richards Bay", "country": "South Africa", "iso3": "ZAF", "lat": -28.78, "lon": 32.04, "mineral": "titanium", "type": "mineral_sands", "operator": "Rio Tinto (RBM)", "annual_tonnes": 1000000, "pct_global": 11, "notes": "Ilmenite and rutile sands"},
# Tungsten
{"name": "Jiangxi Province Mines", "country": "China", "iso3": "CHN", "lat": 27.63, "lon": 115.97, "mineral": "tungsten", "type": "underground", "operator": "Various Chinese SOEs", "annual_tonnes": 60000, "pct_global": 82, "notes": "China produces >80% of global tungsten"},
# Uranium
{"name": "Cigar Lake", "country": "Canada", "iso3": "CAN", "lat": 58.01, "lon": -104.56, "mineral": "uranium", "type": "underground", "operator": "Cameco / Orano", "annual_tonnes": 6900, "pct_global": 13, "notes": "Highest grade uranium mine in world"},
{"name": "Inkai", "country": "Kazakhstan", "iso3": "KAZ", "lat": 44.49, "lon": 66.09, "mineral": "uranium", "type": "isl", "operator": "Kazatomprom / Cameco", "annual_tonnes": 4000, "pct_global": 8, "notes": "Kazakhstan is #1 uranium producer (~43% global)"},
# Tin
{"name": "Bangka-Belitung Islands", "country": "Indonesia", "iso3": "IDN", "lat": -2.13, "lon": 106.11, "mineral": "tin", "type": "alluvial", "operator": "PT Timah / Artisanal", "annual_tonnes": 52000, "pct_global": 24, "notes": "Major tin source, environmental concerns"},
# Gallium / Germanium (strategic, China-controlled)
{"name": "Shanxi / Henan Alumina Refineries", "country": "China", "iso3": "CHN", "lat": 37.87, "lon": 112.55, "mineral": "gallium", "type": "byproduct", "operator": "Chalco / various", "annual_tonnes": 340, "pct_global": 98, "notes": "China controls ~98% of gallium, export restrictions 2023"},
{"name": "Yunnan Germanium Refineries", "country": "China", "iso3": "CHN", "lat": 25.04, "lon": 102.68, "mineral": "germanium", "type": "byproduct", "operator": "Yunnan Germanium", "annual_tonnes": 100, "pct_global": 60, "notes": "China ~60% of germanium, export restrictions 2023"},
]
def query_minerals(
mineral: str | None = None,
country: str | None = None,
mineral_type: str | None = None,
operator: str | None = None,
) -> list[dict]:
"""Filter critical mineral deposits."""
results = []
for m in CRITICAL_MINERALS:
if mineral and m["mineral"] != mineral.lower():
continue
if country:
c = country.lower()
if c not in (m["country"].lower(), m["iso3"].lower()):
continue
if mineral_type and m["type"] != mineral_type.lower():
continue
if operator:
if operator.lower() not in m["operator"].lower():
continue
results.append(m)
return results
+68
View File
@@ -0,0 +1,68 @@
"""Launch facilities and spaceports worldwide.
Pure data module — no I/O, no external dependencies.
Sources: FAA, ESA, CSIS Aerospace, press reporting.
"""
from __future__ import annotations
SPACEPORTS: list[dict] = [
# United States
{"name": "Cape Canaveral SFS / KSC", "country": "USA", "iso3": "USA", "lat": 28.56, "lon": -80.58, "operator": "USSF / NASA", "type": "orbital", "status": "active", "pads": 7, "notes": "Primary US orbital launch site, SpaceX LC-40, NASA LC-39A/B"},
{"name": "Vandenberg SFB", "country": "USA", "iso3": "USA", "lat": 34.73, "lon": -120.57, "operator": "USSF", "type": "orbital", "status": "active", "pads": 4, "notes": "Polar/SSO launches, SpaceX SLC-4E"},
{"name": "SpaceX Starbase (Boca Chica)", "country": "USA", "iso3": "USA", "lat": 25.99, "lon": -97.16, "operator": "SpaceX", "type": "orbital", "status": "active", "pads": 2, "notes": "Starship development and launch facility"},
{"name": "Wallops Flight Facility", "country": "USA", "iso3": "USA", "lat": 37.83, "lon": -75.49, "operator": "NASA", "type": "orbital", "status": "active", "pads": 2, "notes": "Antares/Minotaur, ISS cargo"},
{"name": "Kodiak Launch Complex", "country": "USA", "iso3": "USA", "lat": 57.44, "lon": -152.34, "operator": "Alaska Aerospace", "type": "orbital", "status": "active", "pads": 2, "notes": "Polar orbits, Astra launches"},
{"name": "Mojave Air and Space Port", "country": "USA", "iso3": "USA", "lat": 35.06, "lon": -118.15, "operator": "Mojave Air & Space Port", "type": "suborbital", "status": "active", "pads": 0, "notes": "Horizontal launch, Virgin Orbit (defunct), test facility"},
{"name": "Spaceport America", "country": "USA", "iso3": "USA", "lat": 32.99, "lon": -106.97, "operator": "New Mexico SAA", "type": "suborbital", "status": "active", "pads": 1, "notes": "Virgin Galactic SpaceShipTwo"},
# Russia
{"name": "Baikonur Cosmodrome", "country": "Kazakhstan", "iso3": "KAZ", "lat": 45.96, "lon": 63.31, "operator": "Roscosmos (leased)", "type": "orbital", "status": "active", "pads": 6, "notes": "World's first spaceport, Soyuz, Proton"},
{"name": "Plesetsk Cosmodrome", "country": "Russia", "iso3": "RUS", "lat": 62.93, "lon": 40.58, "operator": "Russian MoD", "type": "orbital", "status": "active", "pads": 4, "notes": "Military launches, Angara, ICBM tests"},
{"name": "Vostochny Cosmodrome", "country": "Russia", "iso3": "RUS", "lat": 51.88, "lon": 128.33, "operator": "Roscosmos", "type": "orbital", "status": "active", "pads": 2, "notes": "New Russian civilian spaceport, Amur region"},
# China
{"name": "Jiuquan Satellite Launch Center", "country": "China", "iso3": "CHN", "lat": 40.96, "lon": 100.30, "operator": "PLA SSF", "type": "orbital", "status": "active", "pads": 4, "notes": "First Chinese crewed launches, Shenzhou"},
{"name": "Xichang Satellite Launch Center", "country": "China", "iso3": "CHN", "lat": 28.25, "lon": 102.03, "operator": "PLA SSF", "type": "orbital", "status": "active", "pads": 3, "notes": "GEO launches, BeiDou, Long March 3B"},
{"name": "Taiyuan Satellite Launch Center", "country": "China", "iso3": "CHN", "lat": 38.85, "lon": 111.61, "operator": "PLA SSF", "type": "orbital", "status": "active", "pads": 2, "notes": "SSO/polar orbits"},
{"name": "Wenchang Space Launch Site", "country": "China", "iso3": "CHN", "lat": 19.61, "lon": 110.95, "operator": "PLA SSF", "type": "orbital", "status": "active", "pads": 3, "notes": "Newest, largest Chinese vehicles, Long March 5/7"},
{"name": "Haiyang Commercial Launch Site", "country": "China", "iso3": "CHN", "lat": 36.73, "lon": 121.13, "operator": "Commercial (CAS Space)", "type": "orbital", "status": "active", "pads": 2, "notes": "First Chinese commercial launch site, 2024"},
# Europe
{"name": "Guiana Space Centre (Kourou)", "country": "French Guiana", "iso3": "GUF", "lat": 5.24, "lon": -52.77, "operator": "ESA / CNES / Arianespace", "type": "orbital", "status": "active", "pads": 4, "notes": "Ariane 6, Vega-C, Soyuz (suspended), equatorial advantage"},
{"name": "SaxaVord Spaceport", "country": "UK", "iso3": "GBR", "lat": 60.82, "lon": -0.86, "operator": "SaxaVord UK", "type": "orbital", "status": "construction", "pads": 3, "notes": "UK's first vertical launch site, Shetland Islands"},
{"name": "Andøya Spaceport", "country": "Norway", "iso3": "NOR", "lat": 69.29, "lon": 16.02, "operator": "Andøya Space", "type": "orbital", "status": "active", "pads": 2, "notes": "Northernmost orbital-class spaceport, polar orbits"},
# India
{"name": "Satish Dhawan Space Centre (Sriharikota)", "country": "India", "iso3": "IND", "lat": 13.72, "lon": 80.23, "operator": "ISRO", "type": "orbital", "status": "active", "pads": 3, "notes": "Primary Indian launch site, GSLV, PSLV, LVM3, Gaganyaan"},
{"name": "Kulasekarapattinam (Tamil Nadu)", "country": "India", "iso3": "IND", "lat": 8.56, "lon": 78.08, "operator": "ISRO", "type": "orbital", "status": "construction", "pads": 1, "notes": "Small satellite launch complex"},
# Japan
{"name": "Tanegashima Space Center", "country": "Japan", "iso3": "JPN", "lat": 30.40, "lon": 131.00, "operator": "JAXA", "type": "orbital", "status": "active", "pads": 2, "notes": "H-IIA/H3, southernmost Japanese island"},
{"name": "Uchinoura Space Center", "country": "Japan", "iso3": "JPN", "lat": 31.25, "lon": 131.08, "operator": "JAXA", "type": "orbital", "status": "active", "pads": 2, "notes": "Epsilon rocket, sounding rockets"},
# Others
{"name": "Rocket Lab Launch Complex 1", "country": "New Zealand", "iso3": "NZL", "lat": -39.26, "lon": 177.86, "operator": "Rocket Lab", "type": "orbital", "status": "active", "pads": 2, "notes": "Electron launches, Mahia Peninsula"},
{"name": "Semnan Space Center", "country": "Iran", "iso3": "IRN", "lat": 35.23, "lon": 53.92, "operator": "ISA / IRGC", "type": "orbital", "status": "active", "pads": 2, "notes": "Simorgh, Qased, dual-use ICBM concern"},
{"name": "Sohae Satellite Launching Station", "country": "North Korea", "iso3": "PRK", "lat": 39.66, "lon": 124.71, "operator": "NADA", "type": "orbital", "status": "active", "pads": 1, "notes": "Unha/Chollima launches, ICBM test concern"},
{"name": "Palmachim Airbase", "country": "Israel", "iso3": "ISR", "lat": 31.88, "lon": 34.69, "operator": "IAI / IMoD", "type": "orbital", "status": "active", "pads": 1, "notes": "Shavit launches (westward retrograde orbit)"},
{"name": "Alcântara Launch Center", "country": "Brazil", "iso3": "BRA", "lat": -2.37, "lon": -44.40, "operator": "FAB / AEB", "type": "orbital", "status": "active", "pads": 2, "notes": "Near equator, US technology safeguards agreement"},
]
def query_spaceports(
country: str | None = None,
status: str | None = None,
spaceport_type: str | None = None,
operator: str | None = None,
) -> list[dict]:
"""Filter spaceports by country, status, type, or operator."""
results = []
for sp in SPACEPORTS:
if country:
c = country.lower()
if c not in (sp["country"].lower(), sp["iso3"].lower()):
continue
if status and sp["status"] != status.lower():
continue
if spaceport_type and sp["type"] != spaceport_type.lower():
continue
if operator:
if operator.lower() not in sp["operator"].lower():
continue
results.append(sp)
return results
+265 -4
View File
@@ -3,7 +3,7 @@
World Intelligence MCP Server
==============================
Real-time global intelligence across 23 domains:
Real-time global intelligence across 30 domains:
financial markets, economic indicators, earthquakes, wildfires,
conflict, military flights, infrastructure, and more.
@@ -18,6 +18,9 @@ Phase 8: Service status monitoring, RSS expansion (80+ feeds, 14 categories) (+1
Phase 9: Geospatial datasets — military bases, ports, pipelines, nuclear facilities (+4 = 60 tools).
Phase 10: NLP intelligence — entity extraction, event classification, news clustering, keyword spikes (+4 = 64 tools).
Phase 11: Strategic synthesis — strategic posture, world brief, fleet report, population exposure (+4 = 68 tools).
Phase 12: Extended geospatial (cables, datacenters, spaceports, minerals, exchanges), country stocks,
aircraft batch, Hacker News, GitHub trending, arXiv papers, USA spending,
NASA EONET, GDACS disaster alerts (+14 = 82 tools).
"""
import asyncio
@@ -33,7 +36,7 @@ from mcp.types import Tool, TextContent
from .cache import Cache
from .circuit_breaker import CircuitBreaker
from .fetcher import Fetcher
from .sources import markets, economic, seismology, wildfire, conflict, military, infrastructure, maritime, climate, news, intelligence, prediction, displacement, aviation, cyber, space_weather, ai_watch, health, sanctions, elections, shipping, social, nuclear, service_status, geospatial
from .sources import markets, economic, seismology, wildfire, conflict, military, infrastructure, maritime, climate, news, intelligence, prediction, displacement, aviation, cyber, space_weather, ai_watch, health, sanctions, elections, shipping, social, nuclear, service_status, geospatial, hacker_news, github_trending, arxiv_papers, usa_spending, environmental
from .reports import generator as report_gen
logging.basicConfig(
@@ -713,10 +716,173 @@ TOOLS: list[Tool] = [
},
},
),
# --- Extended Geospatial (5 tools) ---
Tool(
name="intel_undersea_cables",
description="Query 30+ undersea fiber-optic cable routes with landing points, owners, capacity (Tbps), and length (km). Filterable by status, country, owner, min capacity.",
inputSchema={
"type": "object",
"properties": {
"status": {"type": "string", "description": "Filter by status: active, planned, construction, decommissioned"},
"country": {"type": "string", "description": "Filter by country in landing points"},
"owner": {"type": "string", "description": "Filter by cable owner (Google, Meta, Microsoft, etc.)"},
"min_capacity_tbps": {"type": "number", "description": "Minimum cable capacity in Tbps"},
},
},
),
Tool(
name="intel_ai_datacenters",
description="Query 48+ AI datacenter clusters worldwide with power capacity (MW), operators, and locations. Covers hyperscalers and sovereign AI.",
inputSchema={
"type": "object",
"properties": {
"country": {"type": "string", "description": "Filter by country name or ISO-3 code"},
"operator": {"type": "string", "description": "Filter by operator (AWS, Google, Microsoft, Meta, etc.)"},
"min_power_mw": {"type": "integer", "description": "Minimum power capacity in MW"},
"region": {"type": "string", "description": "Filter by region (North America, Europe, Asia-Pacific, etc.)"},
},
},
),
Tool(
name="intel_spaceports",
description="Query 27+ launch facilities and spaceports worldwide. Filterable by country, status, type (orbital/suborbital), and operator.",
inputSchema={
"type": "object",
"properties": {
"country": {"type": "string", "description": "Filter by country name or ISO-3 code"},
"status": {"type": "string", "description": "Filter by status: active, limited, planned, decommissioned"},
"spaceport_type": {"type": "string", "description": "Filter by type: orbital, suborbital"},
"operator": {"type": "string", "description": "Filter by operator (SpaceX, NASA, CNSA, Roscosmos, etc.)"},
},
},
),
Tool(
name="intel_critical_minerals",
description="Query 28+ critical mineral deposits worldwide: lithium, cobalt, rare earths, nickel, copper, graphite, manganese, PGM, tungsten, uranium, tin, gallium, germanium.",
inputSchema={
"type": "object",
"properties": {
"mineral": {"type": "string", "description": "Filter by mineral (lithium, cobalt, rare_earths, nickel, copper, graphite, manganese, platinum_group, tungsten, uranium, tin, gallium, germanium)"},
"country": {"type": "string", "description": "Filter by country name or ISO-3 code"},
"mineral_type": {"type": "string", "description": "Filter by type: battery, electronic, structural, energy, industrial, strategic"},
"operator": {"type": "string", "description": "Filter by operator"},
},
},
),
Tool(
name="intel_stock_exchanges",
description="Query 80+ stock exchanges across 4 tiers (mega >$3T, major, emerging, frontier) with market cap, index tickers, currencies, timezones.",
inputSchema={
"type": "object",
"properties": {
"tier": {"type": "string", "description": "Filter by tier: mega, major, emerging, frontier"},
"country": {"type": "string", "description": "Filter by country name or ISO-3 code"},
"currency": {"type": "string", "description": "Filter by currency (USD, EUR, GBP, JPY, CNY, etc.)"},
},
},
),
# --- Markets Extended (1 tool) ---
Tool(
name="intel_country_stocks",
description="Get real-time stock index quote for any country by ISO-3 code. Maps country to its primary exchange index ticker and fetches via Yahoo Finance.",
inputSchema={
"type": "object",
"properties": {
"country": {"type": "string", "description": "ISO-3 country code (USA, GBR, JPN, CHN, DEU, etc.)", "default": "USA"},
},
},
),
# --- Military Extended (1 tool) ---
Tool(
name="intel_aircraft_batch",
description="Batch lookup of aircraft details by ICAO24 hex codes (max 20). Returns registration, type, operator from hexdb.io.",
inputSchema={
"type": "object",
"properties": {
"icao24_list": {
"type": "array",
"items": {"type": "string"},
"description": "List of ICAO24 hex addresses (max 20)",
},
},
"required": ["icao24_list"],
},
),
# --- Tech & Science (3 tools) ---
Tool(
name="intel_hacker_news",
description="Get top stories from Hacker News (Firebase API). Returns title, score, URL, author, comment count. Optional: limit (default 30).",
inputSchema={
"type": "object",
"properties": {
"limit": {"type": "integer", "description": "Number of stories (default 30, max 100)", "default": 30},
},
},
),
Tool(
name="intel_trending_repos",
description="Get trending GitHub repositories (recently created, most starred). Optional: language filter, time window, limit.",
inputSchema={
"type": "object",
"properties": {
"language": {"type": "string", "description": "Programming language filter (python, rust, typescript, etc.)"},
"since_days": {"type": "integer", "description": "Look back N days for new repos (default 7)", "default": 7},
"limit": {"type": "integer", "description": "Number of repos (default 25)", "default": 25},
},
},
),
Tool(
name="intel_arxiv_papers",
description="Search recent arXiv papers in AI/ML (cs.AI, cs.LG, cs.CL). Optional custom query. Returns title, authors, abstract, categories, PDF link.",
inputSchema={
"type": "object",
"properties": {
"query": {"type": "string", "description": "arXiv search query (default: cs.AI OR cs.LG OR cs.CL)"},
"limit": {"type": "integer", "description": "Number of papers (default 25)", "default": 25},
},
},
),
# --- Government (1 tool) ---
Tool(
name="intel_usa_spending",
description="Federal agency spending data from USAspending.gov. Shows top agencies by budget for current fiscal year. Optional: agency filter.",
inputSchema={
"type": "object",
"properties": {
"agency": {"type": "string", "description": "Filter by agency name substring"},
"limit": {"type": "integer", "description": "Number of agencies (default 25)", "default": 25},
},
},
),
# --- Environmental (2 tools) ---
Tool(
name="intel_environmental_events",
description="Natural events from NASA EONET: wildfires, severe storms, volcanoes, floods, icebergs, drought. Includes geolocation and source links.",
inputSchema={
"type": "object",
"properties": {
"days": {"type": "integer", "description": "Look back N days (default 30)", "default": 30},
"category": {"type": "string", "description": "Filter by category: wildfires, severeStorms, volcanoes, floods, earthquakes, drought, seaLakeIce"},
"limit": {"type": "integer", "description": "Max events (default 50)", "default": 50},
},
},
),
Tool(
name="intel_disaster_alerts",
description="Global disaster alerts from GDACS (UN): earthquakes, floods, cyclones, droughts, wildfires. Severity levels (green/orange/red) with affected populations.",
inputSchema={
"type": "object",
"properties": {
"alert_level": {"type": "string", "description": "Filter by level: green, orange, red"},
"event_type": {"type": "string", "description": "Filter by type: EQ, FL, TC, DR, WF, VO"},
"limit": {"type": "integer", "description": "Max alerts (default 30)", "default": 30},
},
},
),
# --- System (1 tool) ---
Tool(
name="intel_status",
description="Get data source health, circuit breaker status, and cache statistics.",
description="Get data source health, circuit breaker status, cache freshness, and statistics.",
inputSchema={"type": "object", "properties": {}},
),
]
@@ -998,6 +1164,97 @@ async def _dispatch(name: str, arguments: dict[str, Any]) -> Any:
event_types=arguments.get("event_types"),
)
# Extended Geospatial
case "intel_undersea_cables":
return await geospatial.fetch_undersea_cables(
status=arguments.get("status"),
country=arguments.get("country"),
owner=arguments.get("owner"),
min_capacity_tbps=arguments.get("min_capacity_tbps"),
)
case "intel_ai_datacenters":
return await geospatial.fetch_ai_datacenters(
country=arguments.get("country"),
operator=arguments.get("operator"),
min_power_mw=arguments.get("min_power_mw"),
region=arguments.get("region"),
)
case "intel_spaceports":
return await geospatial.fetch_spaceports(
country=arguments.get("country"),
status=arguments.get("status"),
spaceport_type=arguments.get("spaceport_type"),
operator=arguments.get("operator"),
)
case "intel_critical_minerals":
return await geospatial.fetch_critical_minerals(
mineral=arguments.get("mineral"),
country=arguments.get("country"),
mineral_type=arguments.get("mineral_type"),
operator=arguments.get("operator"),
)
case "intel_stock_exchanges":
return await geospatial.fetch_stock_exchanges(
tier=arguments.get("tier"),
country=arguments.get("country"),
currency=arguments.get("currency"),
)
# Markets Extended
case "intel_country_stocks":
return await markets.fetch_country_stocks(
fetcher, country=arguments.get("country", "USA"),
)
# Military Extended
case "intel_aircraft_batch":
return await military.fetch_aircraft_details_batch(
fetcher, icao24_list=arguments["icao24_list"],
)
# Tech & Science
case "intel_hacker_news":
return await hacker_news.fetch_hacker_news(
fetcher, limit=arguments.get("limit", 30),
)
case "intel_trending_repos":
return await github_trending.fetch_trending_repos(
fetcher,
language=arguments.get("language"),
since_days=arguments.get("since_days", 7),
limit=arguments.get("limit", 25),
)
case "intel_arxiv_papers":
return await arxiv_papers.fetch_arxiv_papers(
fetcher,
query=arguments.get("query"),
limit=arguments.get("limit", 25),
)
# Government
case "intel_usa_spending":
return await usa_spending.fetch_usa_spending(
fetcher,
agency=arguments.get("agency"),
limit=arguments.get("limit", 25),
)
# Environmental
case "intel_environmental_events":
return await environmental.fetch_environmental_events(
fetcher,
days=arguments.get("days", 30),
category=arguments.get("category"),
limit=arguments.get("limit", 50),
)
case "intel_disaster_alerts":
return await environmental.fetch_disaster_alerts(
fetcher,
alert_level=arguments.get("alert_level"),
event_type=arguments.get("event_type"),
limit=arguments.get("limit", 30),
)
# NLP Intelligence
case "intel_extract_entities":
from .analysis.entities import fetch_entity_extraction
@@ -1026,6 +1283,7 @@ async def _dispatch(name: str, arguments: dict[str, Any]) -> Any:
return {
"circuit_breakers": breaker.status(),
"cache": cache.stats(),
"cache_freshness": cache.freshness(),
"sources": {
"markets": ["yahoo-finance", "coingecko", "alternative-me", "mempool"],
"economic": ["eia", "fred", "world-bank"],
@@ -1050,9 +1308,12 @@ async def _dispatch(name: str, arguments: dict[str, Any]) -> Any:
"social": ["reddit-public"],
"nuclear": ["usgs-nuclear-monitor"],
"service_status": ["aws", "azure", "gcp", "cloudflare", "github"],
"geospatial": ["static-datasets (bases, ports, pipelines, nuclear)"],
"geospatial": ["static-datasets (bases, ports, pipelines, nuclear, cables, datacenters, spaceports, minerals, exchanges)"],
"nlp": ["regex-ner", "keyword-classifier", "jaccard-clustering", "keyword-spike-detector"],
"synthesis": ["strategic-posture", "world-brief", "fleet-report", "population-exposure"],
"tech": ["hackernews", "github", "arxiv"],
"government": ["usaspending-gov"],
"environmental": ["eonet", "gdacs"],
},
}
+125
View File
@@ -0,0 +1,125 @@
"""arXiv recent papers source for world-intel-mcp.
Uses the arXiv API (no key required, rate limit ~3 req/s).
"""
import logging
import re
from datetime import datetime, timezone
from ..fetcher import Fetcher
logger = logging.getLogger("world-intel-mcp.sources.arxiv_papers")
_ARXIV_API_URL = "http://export.arxiv.org/api/query"
def _utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _parse_arxiv_xml(xml_text: str) -> list[dict]:
"""Parse arXiv Atom XML response into paper dicts.
Simple regex-based parsing to avoid lxml dependency.
"""
papers = []
entries = re.findall(r"<entry>(.*?)</entry>", xml_text, re.DOTALL)
for entry in entries:
title_match = re.search(r"<title>(.*?)</title>", entry, re.DOTALL)
summary_match = re.search(r"<summary>(.*?)</summary>", entry, re.DOTALL)
id_match = re.search(r"<id>(.*?)</id>", entry)
published_match = re.search(r"<published>(.*?)</published>", entry)
updated_match = re.search(r"<updated>(.*?)</updated>", entry)
# Extract authors
authors = re.findall(r"<author>\s*<name>(.*?)</name>", entry)
# Extract categories
categories = re.findall(r'<category[^>]*term="([^"]*)"', entry)
# Extract PDF link
pdf_match = re.search(r'<link[^>]*title="pdf"[^>]*href="([^"]*)"', entry)
title = title_match.group(1).strip() if title_match else ""
title = re.sub(r"\s+", " ", title) # collapse whitespace
summary = summary_match.group(1).strip() if summary_match else ""
summary = re.sub(r"\s+", " ", summary)
if len(summary) > 300:
summary = summary[:300] + "..."
arxiv_id = id_match.group(1) if id_match else ""
# Extract short ID from URL
short_id = arxiv_id.split("/abs/")[-1] if "/abs/" in arxiv_id else arxiv_id
papers.append({
"id": short_id,
"title": title,
"summary": summary,
"authors": authors[:5],
"categories": categories[:5],
"published": published_match.group(1) if published_match else None,
"updated": updated_match.group(1) if updated_match else None,
"pdf_url": pdf_match.group(1) if pdf_match else None,
"url": arxiv_id,
})
return papers
async def fetch_arxiv_papers(
fetcher: Fetcher,
query: str = "cat:cs.AI OR cat:cs.LG OR cat:cs.CL",
limit: int = 25,
sort_by: str = "submittedDate",
) -> dict:
"""Fetch recent papers from arXiv.
Args:
fetcher: Shared HTTP fetcher.
query: arXiv search query (default: AI/ML/NLP categories).
limit: Number of papers (max 50).
sort_by: Sort field "submittedDate", "relevance", or "lastUpdatedDate".
Returns:
Dict with papers[], count, source, timestamp.
"""
limit = min(limit, 50)
safe_query = re.sub(r"[^a-zA-Z0-9:._\s|()]", "", query)[:128]
xml_text = await fetcher.get_xml(
_ARXIV_API_URL,
source="arxiv",
cache_key=f"arxiv:{safe_query}:{sort_by}:{limit}",
cache_ttl=900,
timeout=20.0,
)
# get_xml doesn't support params, so construct URL manually
# Actually, we need to use get_text with params
if xml_text is None:
# Try with full URL including params
url = f"{_ARXIV_API_URL}?search_query={query}&sortBy={sort_by}&sortOrder=descending&max_results={limit}"
xml_text = await fetcher.get_text(
url,
source="arxiv",
cache_key=f"arxiv:{safe_query}:{sort_by}:{limit}",
cache_ttl=900,
timeout=20.0,
)
if xml_text is None:
return {"papers": [], "count": 0, "source": "arxiv", "timestamp": _utc_now_iso()}
papers = _parse_arxiv_xml(xml_text)
return {
"papers": papers[:limit],
"count": len(papers[:limit]),
"query": query,
"source": "arxiv",
"timestamp": _utc_now_iso(),
}
@@ -0,0 +1,165 @@
"""Environmental events — NASA EONET and GDACS disaster alerts.
No API keys required. Public REST APIs.
"""
import logging
from datetime import datetime, timezone, timedelta
from ..fetcher import Fetcher
logger = logging.getLogger("world-intel-mcp.sources.environmental")
_EONET_EVENTS_URL = "https://eonet.gsfc.nasa.gov/api/v3/events"
_GDACS_EVENTS_URL = "https://www.gdacs.org/gdacsapi/api/events/geteventlist/SEARCH"
def _utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
async def fetch_environmental_events(
fetcher: Fetcher,
days: int = 30,
category: str | None = None,
limit: int = 50,
) -> dict:
"""Fetch active environmental events from NASA EONET.
Categories: wildfires, severeStorms, volcanoes, seaLakeIce, earthquakes,
floods, landslides, drought, dustHaze, snow, tempExtremes, waterColor,
manmade.
Args:
fetcher: Shared HTTP fetcher.
days: Lookback period in days.
category: Optional category filter.
limit: Max events to return.
Returns:
Dict with events[], count, source, timestamp.
"""
params: dict[str, str] = {
"days": str(days),
"limit": str(min(limit, 100)),
"status": "open",
}
if category:
params["category"] = category
data = await fetcher.get_json(
_EONET_EVENTS_URL,
source="eonet",
cache_key=f"eonet:events:{days}:{category or 'all'}",
cache_ttl=600,
params=params,
)
if data is None or not isinstance(data, dict):
return {"events": [], "count": 0, "source": "eonet", "timestamp": _utc_now_iso()}
events = []
for event in data.get("events", []):
categories = [c.get("title", "") for c in event.get("categories", [])]
# Get latest geometry point
geometries = event.get("geometry", [])
lat, lon = None, None
if geometries:
latest = geometries[-1]
coords = latest.get("coordinates", [])
if len(coords) >= 2:
lon, lat = coords[0], coords[1]
events.append({
"id": event.get("id"),
"title": event.get("title"),
"categories": categories,
"lat": lat,
"lon": lon,
"date": geometries[-1].get("date") if geometries else None,
"sources": [s.get("url") for s in event.get("sources", [])],
"closed": event.get("closed"),
})
return {
"events": events[:limit],
"count": len(events[:limit]),
"source": "eonet",
"timestamp": _utc_now_iso(),
}
async def fetch_disaster_alerts(
fetcher: Fetcher,
alert_level: str | None = None,
event_type: str | None = None,
limit: int = 30,
) -> dict:
"""Fetch global disaster alerts from GDACS.
Alert levels: Green, Orange, Red.
Event types: EQ (earthquake), TC (tropical cyclone), FL (flood),
VO (volcano), DR (drought), WF (wildfire).
Args:
fetcher: Shared HTTP fetcher.
alert_level: Filter by alert level (Green, Orange, Red).
event_type: Filter by event type code.
limit: Max alerts to return.
Returns:
Dict with alerts[], count, source, timestamp.
"""
# GDACS API returns GeoJSON
params: dict[str, str] = {
"fromDate": (datetime.now(timezone.utc) - timedelta(days=30)).strftime("%Y-%m-%d"),
"toDate": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
"alertlevel": alert_level or "",
"eventType": event_type or "",
}
# Remove empty params
params = {k: v for k, v in params.items() if v}
data = await fetcher.get_json(
_GDACS_EVENTS_URL,
source="gdacs",
cache_key=f"gdacs:alerts:{alert_level or 'all'}:{event_type or 'all'}",
cache_ttl=600,
params=params,
)
if data is None or not isinstance(data, dict):
return {"alerts": [], "count": 0, "source": "gdacs", "timestamp": _utc_now_iso()}
alerts = []
features = data.get("features", [])
for feature in features[:limit]:
props = feature.get("properties", {})
geometry = feature.get("geometry", {})
coords = geometry.get("coordinates", [])
lat = coords[1] if len(coords) >= 2 else None
lon = coords[0] if len(coords) >= 2 else None
alerts.append({
"event_id": props.get("eventid"),
"event_type": props.get("eventtype"),
"name": props.get("name") or props.get("eventname"),
"alert_level": props.get("alertlevel"),
"alert_score": props.get("alertscore"),
"severity": props.get("severity", {}).get("severity_value") if isinstance(props.get("severity"), dict) else props.get("severity"),
"country": props.get("country"),
"lat": lat,
"lon": lon,
"from_date": props.get("fromdate"),
"to_date": props.get("todate"),
"url": props.get("url", {}).get("report") if isinstance(props.get("url"), dict) else None,
"population_affected": props.get("population", {}).get("value") if isinstance(props.get("population"), dict) else None,
})
return {
"alerts": alerts,
"count": len(alerts),
"source": "gdacs",
"timestamp": _utc_now_iso(),
}
+128
View File
@@ -17,6 +17,11 @@ from ..config.geospatial import (
query_pipelines,
query_nuclear,
)
from ..config.cables import UNDERSEA_CABLES, query_cables
from ..config.datacenters import AI_DATACENTERS, query_datacenters
from ..config.spaceports import SPACEPORTS, query_spaceports
from ..config.minerals import CRITICAL_MINERALS, query_minerals
from ..config.exchanges import STOCK_EXCHANGES, query_exchanges
def _utc_now_iso() -> str:
@@ -167,3 +172,126 @@ async def fetch_nuclear_facilities(
"source": "static-geospatial",
"timestamp": _utc_now_iso(),
}
async def fetch_undersea_cables(
status: str | None = None,
country: str | None = None,
owner: str | None = None,
min_capacity_tbps: float | None = None,
) -> dict:
"""Query undersea cable routes with landing points."""
cables = query_cables(status=status, country=country, owner=owner, min_capacity_tbps=min_capacity_tbps)
total_length = sum(c["length_km"] for c in cables)
total_capacity = sum(c["capacity_tbps"] for c in cables)
by_status: dict[str, int] = {}
for c in cables:
by_status[c["status"]] = by_status.get(c["status"], 0) + 1
return {
"cables": cables,
"count": len(cables),
"total_in_database": len(UNDERSEA_CABLES),
"total_length_km": total_length,
"total_capacity_tbps": total_capacity,
"by_status": by_status,
"filters": {"status": status, "country": country, "owner": owner, "min_capacity_tbps": min_capacity_tbps},
"source": "static-geospatial",
"timestamp": _utc_now_iso(),
}
async def fetch_ai_datacenters(
country: str | None = None,
operator: str | None = None,
min_power_mw: int | None = None,
region: str | None = None,
) -> dict:
"""Query AI datacenter clusters worldwide."""
dcs = query_datacenters(country=country, operator=operator, min_power_mw=min_power_mw, region=region)
total_power = sum(d["power_mw"] for d in dcs)
by_country: dict[str, int] = {}
for d in dcs:
by_country[d["country"]] = by_country.get(d["country"], 0) + 1
return {
"datacenters": dcs,
"count": len(dcs),
"total_in_database": len(AI_DATACENTERS),
"total_power_mw": total_power,
"by_country": by_country,
"filters": {"country": country, "operator": operator, "min_power_mw": min_power_mw, "region": region},
"source": "static-geospatial",
"timestamp": _utc_now_iso(),
}
async def fetch_spaceports(
country: str | None = None,
status: str | None = None,
spaceport_type: str | None = None,
operator: str | None = None,
) -> dict:
"""Query launch facilities and spaceports worldwide."""
sps = query_spaceports(country=country, status=status, spaceport_type=spaceport_type, operator=operator)
by_country: dict[str, int] = {}
by_type: dict[str, int] = {}
for s in sps:
by_country[s["country"]] = by_country.get(s["country"], 0) + 1
by_type[s["type"]] = by_type.get(s["type"], 0) + 1
return {
"spaceports": sps,
"count": len(sps),
"total_in_database": len(SPACEPORTS),
"by_country": by_country,
"by_type": by_type,
"filters": {"country": country, "status": status, "spaceport_type": spaceport_type, "operator": operator},
"source": "static-geospatial",
"timestamp": _utc_now_iso(),
}
async def fetch_critical_minerals(
mineral: str | None = None,
country: str | None = None,
mineral_type: str | None = None,
operator: str | None = None,
) -> dict:
"""Query critical mineral deposits worldwide."""
deposits = query_minerals(mineral=mineral, country=country, mineral_type=mineral_type, operator=operator)
by_mineral: dict[str, int] = {}
by_country: dict[str, int] = {}
for d in deposits:
by_mineral[d["mineral"]] = by_mineral.get(d["mineral"], 0) + 1
by_country[d["country"]] = by_country.get(d["country"], 0) + 1
return {
"deposits": deposits,
"count": len(deposits),
"total_in_database": len(CRITICAL_MINERALS),
"by_mineral": by_mineral,
"by_country": by_country,
"filters": {"mineral": mineral, "country": country, "mineral_type": mineral_type, "operator": operator},
"source": "static-geospatial",
"timestamp": _utc_now_iso(),
}
async def fetch_stock_exchanges(
tier: str | None = None,
country: str | None = None,
currency: str | None = None,
) -> dict:
"""Query global stock exchanges (92 exchanges, 4 tiers)."""
exs = query_exchanges(tier=tier, country=country, currency=currency)
total_mcap = sum(e["market_cap_usd_t"] for e in exs)
by_tier: dict[str, int] = {}
for e in exs:
by_tier[e["tier"]] = by_tier.get(e["tier"], 0) + 1
return {
"exchanges": exs,
"count": len(exs),
"total_in_database": len(STOCK_EXCHANGES),
"total_market_cap_usd_t": round(total_mcap, 2),
"by_tier": by_tier,
"filters": {"tier": tier, "country": country, "currency": currency},
"source": "static-geospatial",
"timestamp": _utc_now_iso(),
}
@@ -0,0 +1,82 @@
"""GitHub trending repositories source for world-intel-mcp.
Uses the GitHub search API (no auth required, 10 req/min rate limit).
Approximates "trending" by searching recently created repos with high stars.
"""
import logging
from datetime import datetime, timezone, timedelta
from ..fetcher import Fetcher
logger = logging.getLogger("world-intel-mcp.sources.github_trending")
_GH_SEARCH_URL = "https://api.github.com/search/repositories"
def _utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
async def fetch_trending_repos(
fetcher: Fetcher,
language: str | None = None,
since_days: int = 7,
limit: int = 25,
) -> dict:
"""Fetch trending GitHub repositories by recent star activity.
Args:
fetcher: Shared HTTP fetcher.
language: Optional language filter (e.g., "python", "rust").
since_days: Look back period in days (default 7).
limit: Number of repos to return (max 50).
Returns:
Dict with repos[], count, source, timestamp.
"""
limit = min(limit, 50)
since_date = (datetime.now(timezone.utc) - timedelta(days=since_days)).strftime("%Y-%m-%d")
query = f"created:>{since_date} stars:>10"
if language:
query += f" language:{language}"
data = await fetcher.get_json(
_GH_SEARCH_URL,
source="github",
cache_key=f"github:trending:{language or 'all'}:{since_days}",
cache_ttl=600,
params={
"q": query,
"sort": "stars",
"order": "desc",
"per_page": str(limit),
},
headers={"Accept": "application/vnd.github.v3+json"},
)
if data is None or not isinstance(data, dict):
return {"repos": [], "count": 0, "source": "github", "timestamp": _utc_now_iso()}
repos = []
for item in data.get("items", [])[:limit]:
repos.append({
"name": item.get("full_name"),
"description": (item.get("description") or "")[:200],
"url": item.get("html_url"),
"stars": item.get("stargazers_count", 0),
"forks": item.get("forks_count", 0),
"language": item.get("language"),
"created_at": item.get("created_at"),
"topics": item.get("topics", [])[:5],
})
return {
"repos": repos,
"count": len(repos),
"total_matching": data.get("total_count", 0),
"query": query,
"source": "github",
"timestamp": _utc_now_iso(),
}
@@ -0,0 +1,81 @@
"""Hacker News top stories source for world-intel-mcp.
Uses the official HN Firebase API (no API key required).
"""
import asyncio
import logging
from datetime import datetime, timezone
from ..fetcher import Fetcher
logger = logging.getLogger("world-intel-mcp.sources.hacker_news")
_HN_TOP_URL = "https://hacker-news.firebaseio.com/v0/topstories.json"
_HN_ITEM_URL = "https://hacker-news.firebaseio.com/v0/item/{item_id}.json"
def _utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
async def fetch_hacker_news(
fetcher: Fetcher,
limit: int = 30,
) -> dict:
"""Fetch top Hacker News stories.
Args:
fetcher: Shared HTTP fetcher.
limit: Number of top stories to return (max 50).
Returns:
Dict with stories[], count, source, timestamp.
"""
limit = min(limit, 50)
top_ids = await fetcher.get_json(
_HN_TOP_URL,
source="hackernews",
cache_key="hn:top_ids",
cache_ttl=300,
)
if top_ids is None or not isinstance(top_ids, list):
return {"stories": [], "count": 0, "source": "hackernews", "timestamp": _utc_now_iso()}
top_ids = top_ids[:limit]
async def _fetch_item(item_id: int) -> dict | None:
url = _HN_ITEM_URL.format(item_id=item_id)
data = await fetcher.get_json(
url,
source="hackernews",
cache_key=f"hn:item:{item_id}",
cache_ttl=600,
)
if data is None or not isinstance(data, dict):
return None
return {
"id": data.get("id"),
"title": data.get("title", ""),
"url": data.get("url", ""),
"score": data.get("score", 0),
"by": data.get("by", ""),
"descendants": data.get("descendants", 0),
"time": data.get("time"),
"type": data.get("type", "story"),
}
tasks = [_fetch_item(item_id) for item_id in top_ids]
results = await asyncio.gather(*tasks)
stories = [s for s in results if s is not None]
stories.sort(key=lambda s: s["score"], reverse=True)
return {
"stories": stories,
"count": len(stories),
"source": "hackernews",
"timestamp": _utc_now_iso(),
}
+52
View File
@@ -394,6 +394,58 @@ async def _fetch_btc_dominance(fetcher: Fetcher) -> dict | None:
return None
async def fetch_country_stocks(
fetcher: Fetcher,
country: str = "USA",
) -> dict:
"""Fetch the primary stock index for a given country.
Uses the COUNTRY_INDEX_TICKERS mapping from config/exchanges.py.
Args:
fetcher: Shared HTTP fetcher.
country: ISO-3 country code (e.g., "USA", "GBR", "JPN", "DEU").
Returns:
Dict with index quote, country, exchange info, source, timestamp.
"""
from ..config.exchanges import COUNTRY_INDEX_TICKERS, STOCK_EXCHANGES
country_upper = country.upper()
ticker = COUNTRY_INDEX_TICKERS.get(country_upper)
if ticker is None:
return {
"error": f"No index ticker mapped for country '{country_upper}'",
"available_countries": sorted(COUNTRY_INDEX_TICKERS.keys()),
"source": "yahoo-finance",
"timestamp": _utc_now_iso(),
}
quote = await _fetch_yahoo_quote(fetcher, ticker, f"markets:country:{country_upper}", 300)
# Find exchange info
exchange_info = None
for ex in STOCK_EXCHANGES:
if ex.get("index") == ticker:
exchange_info = {
"name": ex["name"],
"acronym": ex["acronym"],
"city": ex["city"],
"currency": ex["currency"],
"tier": ex["tier"],
}
break
return {
"country": country_upper,
"ticker": ticker,
"quote": quote,
"exchange": exchange_info,
"source": "yahoo-finance",
"timestamp": _utc_now_iso(),
}
async def fetch_macro_signals(fetcher: Fetcher) -> dict:
"""Aggregate 7 macro signals into a single dashboard payload.
+33
View File
@@ -337,6 +337,39 @@ async def fetch_theater_posture(fetcher: Fetcher) -> dict:
return response
async def fetch_aircraft_details_batch(fetcher: Fetcher, icao24_list: list[str]) -> dict:
"""Look up multiple aircraft by ICAO24 hex codes in parallel.
Args:
fetcher: Shared HTTP fetcher.
icao24_list: List of ICAO24 hex addresses (max 20).
Returns:
Dict with aircraft[] details, count, source, timestamp.
"""
icao24_list = icao24_list[:20] # cap at 20
async def _lookup(icao: str) -> dict | None:
result = await fetch_aircraft_details(fetcher, icao)
ac = result.get("aircraft", {})
if not ac:
return None
ac["icao24"] = icao
return ac
tasks = [_lookup(icao) for icao in icao24_list]
results = await asyncio.gather(*tasks)
aircraft = [a for a in results if a is not None]
return {
"aircraft": aircraft,
"count": len(aircraft),
"requested": len(icao24_list),
"source": "hexdb",
"timestamp": _utc_now_iso(),
}
async def fetch_aircraft_details(fetcher: Fetcher, icao24: str) -> dict:
"""Look up detailed aircraft information from hexdb.io (free, no API key).
+23 -9
View File
@@ -96,6 +96,9 @@ _RSS_FEEDS: dict[str, list[tuple[str, str]]] = {
"latin_america": [
("MercoPress", "https://en.mercopress.com/rss"),
("Dialogo Americas", "https://dialogo-americas.com/feed/"),
("Americas Quarterly", "https://www.americasquarterly.org/feed/"),
("Buenos Aires Times", "https://www.batimes.com.ar/feed"),
("Tico Times", "https://ticotimes.net/feed"),
],
"energy": [
("Oil Price", "https://oilprice.com/rss/main"),
@@ -284,6 +287,7 @@ async def fetch_news_feed(
source=f"rss:{safe_name}",
cache_key=f"news:rss:{safe_name}",
cache_ttl=300,
timeout=8.0,
)
if xml_text is None:
@@ -308,15 +312,25 @@ async def fetch_news_feed(
return items
# Fetch all feeds within each category in parallel
for cat, feeds in categories_to_fetch.items():
tasks = [
_fetch_single_feed(feed_name, url, cat)
for feed_name, url in feeds
]
results = await asyncio.gather(*tasks)
for items in results:
all_items.extend(items)
async def _safe_fetch(feed_name: str, url: str, cat: str) -> list[dict]:
"""Wrap single feed fetch with a hard timeout."""
try:
return await asyncio.wait_for(
_fetch_single_feed(feed_name, url, cat), timeout=12.0,
)
except asyncio.TimeoutError:
logger.debug("RSS feed %s timed out", feed_name)
return []
# Fetch ALL feeds across ALL categories in one parallel batch
all_tasks = [
_safe_fetch(feed_name, url, cat)
for cat, feeds in categories_to_fetch.items()
for feed_name, url in feeds
]
results = await asyncio.gather(*all_tasks)
for items in results:
all_items.extend(items)
# Sort by published date descending (entries without dates go last)
all_items.sort(
@@ -0,0 +1,73 @@
"""US Federal spending data from USAspending.gov API.
No API key required. Public REST API.
"""
import logging
from datetime import datetime, timezone
from ..fetcher import Fetcher
logger = logging.getLogger("world-intel-mcp.sources.usa_spending")
_SPENDING_BASE = "https://api.usaspending.gov/api/v2"
_SPENDING_OVERVIEW_URL = f"{_SPENDING_BASE}/agency/list/"
def _utc_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
async def fetch_usa_spending(
fetcher: Fetcher,
agency: str | None = None,
limit: int = 25,
) -> dict:
"""Fetch federal agency spending overview from USAspending.gov.
Args:
fetcher: Shared HTTP fetcher.
agency: Optional agency name filter.
limit: Number of agencies to return.
Returns:
Dict with agencies[], total_budget, source, timestamp.
"""
now = datetime.now(timezone.utc)
fiscal_year = now.year if now.month >= 10 else now.year - 1
data = await fetcher.get_json(
_SPENDING_OVERVIEW_URL,
source="usaspending",
cache_key=f"spending:agencies:{fiscal_year}",
cache_ttl=3600,
params={"sort": "obligated_amount", "order": "desc"},
)
if data is None or not isinstance(data, dict):
return {"agencies": [], "count": 0, "fiscal_year": fiscal_year, "source": "usaspending", "timestamp": _utc_now_iso()}
agencies = []
for item in data.get("results", []):
name = item.get("agency_name", "")
if agency and agency.lower() not in name.lower():
continue
agencies.append({
"name": name,
"abbreviation": item.get("abbreviation", ""),
"obligated_amount": item.get("obligated_amount"),
"budget_authority": item.get("budget_authority_amount"),
"percentage_of_total": item.get("percentage_of_total_budget_authority"),
})
agencies = agencies[:limit]
total = sum(a["obligated_amount"] or 0 for a in agencies)
return {
"agencies": agencies,
"count": len(agencies),
"total_obligated": total,
"fiscal_year": fiscal_year,
"source": "usaspending",
"timestamp": _utc_now_iso(),
}
+319
View File
@@ -587,3 +587,322 @@ async def test_fetch_ucdp_events(fetcher: Fetcher) -> None:
assert result["count"] == 1
assert result["events"][0]["country"] == "Ukraine"
assert result["total_fatalities_best"] == 5
# ---------------------------------------------------------------------------
# Hacker News
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_hacker_news(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.hacker_news import fetch_hacker_news
respx.get("https://hacker-news.firebaseio.com/v0/topstories.json").mock(
return_value=httpx.Response(200, json=[101, 102])
)
respx.get("https://hacker-news.firebaseio.com/v0/item/101.json").mock(
return_value=httpx.Response(200, json={
"id": 101, "title": "Show HN: AI Tool", "url": "https://example.com",
"score": 200, "by": "user1", "time": 1700000000, "descendants": 50,
})
)
respx.get("https://hacker-news.firebaseio.com/v0/item/102.json").mock(
return_value=httpx.Response(200, json={
"id": 102, "title": "Rust 2.0", "url": "https://example.com/rust",
"score": 150, "by": "user2", "time": 1700001000, "descendants": 30,
})
)
result = await fetch_hacker_news(fetcher, limit=2)
assert result["source"] == "hackernews"
assert result["count"] == 2
assert result["stories"][0]["score"] >= result["stories"][1]["score"]
# ---------------------------------------------------------------------------
# GitHub Trending
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_trending_repos(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.github_trending import fetch_trending_repos
respx.get(url__regex=r".*api\.github\.com/search/repositories.*").mock(
return_value=httpx.Response(200, json={
"total_count": 1,
"items": [{
"full_name": "user/cool-repo",
"description": "A cool tool",
"html_url": "https://github.com/user/cool-repo",
"stargazers_count": 500,
"forks_count": 20,
"language": "Python",
"created_at": "2026-02-20T00:00:00Z",
"topics": ["ai", "ml"],
}],
})
)
result = await fetch_trending_repos(fetcher, limit=5)
assert result["source"] == "github"
assert result["count"] == 1
assert result["repos"][0]["name"] == "user/cool-repo"
assert result["repos"][0]["stars"] == 500
# ---------------------------------------------------------------------------
# arXiv Papers
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_arxiv_papers(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.arxiv_papers import fetch_arxiv_papers
arxiv_xml = """<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<entry>
<id>http://arxiv.org/abs/2602.12345v1</id>
<title>Attention Is Still All You Need</title>
<summary>We show transformers continue to dominate.</summary>
<author><name>Jane Doe</name></author>
<author><name>John Smith</name></author>
<published>2026-02-20T00:00:00Z</published>
<link href="http://arxiv.org/abs/2602.12345v1" rel="alternate"/>
<link href="http://arxiv.org/pdf/2602.12345v1" title="pdf" rel="related"/>
<category term="cs.AI"/>
<category term="cs.LG"/>
</entry>
</feed>"""
respx.get(url__regex=r".*export\.arxiv\.org/api/query.*").mock(
return_value=httpx.Response(200, text=arxiv_xml)
)
result = await fetch_arxiv_papers(fetcher, limit=5)
assert result["source"] == "arxiv"
assert result["count"] == 1
assert "Attention" in result["papers"][0]["title"]
assert len(result["papers"][0]["authors"]) == 2
# ---------------------------------------------------------------------------
# USA Spending
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_usa_spending(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.usa_spending import fetch_usa_spending
respx.get(url__regex=r".*api\.usaspending\.gov.*").mock(
return_value=httpx.Response(200, json={
"results": [{
"agency_name": "Department of Defense",
"abbreviation": "DOD",
"current_total_budget_authority_amount": 850000000000,
"obligated_amount": 700000000000,
"outlay_amount": 650000000000,
"agency_id": 97,
}],
"page_metadata": {"total": 1},
})
)
result = await fetch_usa_spending(fetcher, limit=5)
assert result["source"] == "usaspending"
assert result["count"] == 1
assert result["agencies"][0]["name"] == "Department of Defense"
# ---------------------------------------------------------------------------
# Environmental Events (NASA EONET)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_environmental_events(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.environmental import fetch_environmental_events
respx.get(url__regex=r".*eonet\.gsfc\.nasa\.gov.*").mock(
return_value=httpx.Response(200, json={
"events": [{
"id": "EONET_1234",
"title": "Wildfire in California",
"categories": [{"id": "wildfires", "title": "Wildfires"}],
"sources": [{"id": "InciWeb", "url": "https://inciweb.example.com"}],
"geometry": [{"date": "2026-02-20T00:00:00Z", "coordinates": [-119.5, 34.5]}],
}],
})
)
result = await fetch_environmental_events(fetcher, days=7)
assert result["source"] == "eonet"
assert result["count"] == 1
assert "Wildfire" in result["events"][0]["title"]
# ---------------------------------------------------------------------------
# Disaster Alerts (GDACS)
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_disaster_alerts(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.environmental import fetch_disaster_alerts
gdacs_geojson = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {
"eventtype": "EQ",
"eventname": "M6.5 Earthquake",
"alertlevel": "orange",
"alertscore": 2.5,
"severity": {"value": 6.5, "unit": "M"},
"country": "Turkey",
"fromdate": "2026-02-20T12:00:00Z",
"todate": "2026-02-20T12:05:00Z",
"url": {"report": "https://gdacs.example.com/report"},
"population": {"value": 500000},
},
"geometry": {"type": "Point", "coordinates": [29.0, 38.5]},
}],
}
respx.get(url__regex=r".*gdacs\.org.*").mock(
return_value=httpx.Response(200, json=gdacs_geojson)
)
result = await fetch_disaster_alerts(fetcher)
assert result["source"] == "gdacs"
assert result["count"] == 1
assert result["alerts"][0]["event_type"] == "EQ"
assert result["alerts"][0]["alert_level"] == "orange"
# ---------------------------------------------------------------------------
# Geospatial — Extended datasets (static, no HTTP mock needed)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_fetch_undersea_cables() -> None:
from world_intel_mcp.sources.geospatial import fetch_undersea_cables
result = await fetch_undersea_cables()
assert result["count"] > 0
assert result["source"] == "static-geospatial"
assert "total_length_km" in result
assert "total_capacity_tbps" in result
@pytest.mark.asyncio
async def test_fetch_ai_datacenters() -> None:
from world_intel_mcp.sources.geospatial import fetch_ai_datacenters
result = await fetch_ai_datacenters(country="USA")
assert result["count"] > 0
assert result["source"] == "static-geospatial"
assert "total_power_mw" in result
@pytest.mark.asyncio
async def test_fetch_spaceports() -> None:
from world_intel_mcp.sources.geospatial import fetch_spaceports
result = await fetch_spaceports()
assert result["count"] > 0
assert result["source"] == "static-geospatial"
@pytest.mark.asyncio
async def test_fetch_critical_minerals() -> None:
from world_intel_mcp.sources.geospatial import fetch_critical_minerals
result = await fetch_critical_minerals(mineral="lithium")
assert result["count"] > 0
assert all(d["mineral"] == "lithium" for d in result["deposits"])
@pytest.mark.asyncio
async def test_fetch_stock_exchanges() -> None:
from world_intel_mcp.sources.geospatial import fetch_stock_exchanges
result = await fetch_stock_exchanges(tier="mega")
assert result["count"] > 0
assert all(e["tier"] == "mega" for e in result["exchanges"])
assert "total_market_cap_usd_t" in result
# ---------------------------------------------------------------------------
# Country Stocks
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_country_stocks(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.markets import fetch_country_stocks
chart_response = {
"chart": {
"result": [{
"meta": {
"symbol": "^GSPC",
"regularMarketPrice": 5200.0,
"regularMarketChangePercent": 0.75,
"currency": "USD",
}
}]
}
}
respx.get(url__regex=r".*query1\.finance\.yahoo\.com.*").mock(
return_value=httpx.Response(200, json=chart_response)
)
result = await fetch_country_stocks(fetcher, country="USA")
assert result["source"] == "yahoo-finance"
assert result["country"] == "USA"
assert "quote" in result
# ---------------------------------------------------------------------------
# Aircraft Batch
# ---------------------------------------------------------------------------
@respx.mock
@pytest.mark.asyncio
async def test_fetch_aircraft_details_batch(fetcher: Fetcher) -> None:
from world_intel_mcp.sources.military import fetch_aircraft_details_batch
respx.get(url__regex=r".*hexdb\.io/api/v1/aircraft/ae1234.*").mock(
return_value=httpx.Response(200, json={
"Registration": "12-3456",
"Type": "C-17A",
"Operator": "USAF",
})
)
respx.get(url__regex=r".*hexdb\.io/api/v1/aircraft/ae5678.*").mock(
return_value=httpx.Response(200, json={
"Registration": "78-9012",
"Type": "KC-135R",
"Operator": "USAF",
})
)
result = await fetch_aircraft_details_batch(fetcher, icao24_list=["ae1234", "ae5678"])
assert result["source"] == "hexdb"
assert result["count"] == 2
assert result["requested"] == 2