"""The complete immutable country profile."""
from __future__ import annotations
from dataclasses import dataclass
import json
from typing import Any
from ._serialize import _jsonable
from ._display import friendly_repr
from .reference import CountryCodes, Currency, Demonym, Language, LocalizedName, NationalAnthem, NationalMotto, PostalCodeFormat, SourceReference, Timezone, TravelBasics
from .geography import ClimateProfile, Coordinate, ElevationPoint, Geography, Lake, PhysicalGeography, River
from .places import Capital, City
from .results import CountryReference
from .learning import CountryDiscoveryCard
from .units import SummaryText
[docs]
@dataclass(frozen=True, slots=True)
class Country:
"""A sourced, immutable country profile from the offline atlas.
``official_name`` is the canonical English identity from UN M49.
``formal_name`` is the sourced English long or formal form when the
country or area is covered by the formal-name source layer.
"""
name: str
official_name: str | None
names: tuple[LocalizedName, ...]
aliases: tuple[str, ...]
codes: CountryCodes
flag: str
geography: Geography
capitals: tuple[Capital, ...]
major_cities: tuple[City, ...]
sources: tuple[SourceReference, ...]
local_names: tuple[LocalizedName, ...] = ()
population: int | None = None
currency: Currency | None = None
languages: tuple[Language, ...] = ()
calling_codes: tuple[str, ...] = ()
top_level_domain: str | None = None
observed_timezones: tuple[str, ...] = ()
formal_name: str | None = None
anthems: tuple[NationalAnthem, ...] = ()
mottos: tuple[NationalMotto, ...] = ()
demonyms: tuple[Demonym, ...] = ()
timezones: tuple[Timezone, ...] = ()
postal_code: PostalCodeFormat | None = None
travel: TravelBasics | None = None
@property
def alpha2(self) -> str:
"""Return the ISO alpha-2 code."""
return self.codes.alpha2
@property
def alpha3(self) -> str | None:
"""Return the ISO alpha-3 code."""
return self.codes.alpha3
@property
def continent(self) -> str | None:
"""Return the broad continent classification."""
return self.geography.continent
@property
def region(self) -> str | None:
"""Return the UN region classification."""
return self.geography.region
@property
def subregion(self) -> str | None:
"""Return the UN subregion classification."""
return self.geography.subregion
@property
def area_km2(self) -> float | None:
"""Return sourced total area in square kilometres, when available."""
return self.geography.area.total_km2
@property
def land_area_km2(self) -> float | None:
"""Return sourced land area in square kilometres, when available."""
return self.geography.area.land_km2
@property
def water_area_km2(self) -> float | None:
"""Return sourced inland-water area in square kilometres, when available."""
return self.geography.area.water_km2
@property
def water_percent(self) -> float | None:
"""Return the derived percentage of total area recorded as water."""
return self.geography.area.water_percent
@property
def physical(self) -> PhysicalGeography:
"""Return structured physical-geography facts for this profile."""
return self.geography.physical
@property
def coastline_km(self) -> float | None:
"""Return sourced coastline length in kilometres, when available."""
return self.physical.coastline_km
@property
def mean_elevation_m(self) -> float | None:
"""Return sourced mean elevation above sea level, when available."""
return self.physical.mean_elevation_m
@property
def highest_point(self) -> ElevationPoint | None:
"""Return the sourced named highest point, when available."""
return self.physical.highest_point
@property
def lowest_point(self) -> ElevationPoint | None:
"""Return the sourced named lowest point, when available."""
return self.physical.lowest_point
@property
def rivers(self) -> tuple[River, ...]:
"""Return source-listed major rivers; this is not an exhaustive inventory."""
return self.physical.rivers
@property
def lakes(self) -> tuple[Lake, ...]:
"""Return source-listed major lakes; this is not an exhaustive inventory."""
return self.physical.lakes
@property
def climate(self) -> ClimateProfile:
"""Return the climate summary and represented Köppen-Geiger classes."""
return self.physical.climate
@property
def is_coastal(self) -> bool | None:
"""Return whether the source reports a positive coastline, or ``None``."""
return self.physical.is_coastal
@property
def is_landlocked(self) -> bool | None:
"""Return whether the source reports zero coastline, or ``None``."""
return self.physical.is_landlocked
@property
def flag_emoji(self) -> str | None:
"""Return the regional-indicator flag derived from the alpha-2 code.
Emoji appearance depends on the operating system, font, and application.
``None`` is returned if a profile ever lacks a valid two-letter code.
The existing ``flag`` attribute is the same value.
"""
return self.flag if len(self.alpha2) == 2 and self.alpha2.isalpha() else None
@property
def has_distinct_formal_name(self) -> bool:
"""Return whether the sourced English formal form differs from ``name``.
``False`` also covers records outside the current formal-name source
scope. Inspect ``formal_name`` directly when that distinction matters.
"""
return bool(self.formal_name and self.formal_name.casefold() != self.name.casefold())
@property
def population_density(self) -> float | None:
"""Return snapshot population per square kilometre when calculable.
This is a transparent ratio of ``population`` to ``area_km2``,
not a separately sourced official statistic. ``None`` represents a
missing population, missing area, or non-positive area.
"""
area = self.area_km2
if self.population is None or area is None or area <= 0:
return None
return self.population / area
@property
def language_codes(self) -> tuple[str, ...]:
"""Return the captured country language codes as an immutable tuple."""
return tuple(language.code for language in self.languages)
@property
def currency_code(self) -> str | None:
"""Return the captured currency code, or ``None`` when unavailable."""
return self.currency.code if self.currency else None
@property
def anthem(self) -> NationalAnthem | None:
"""Return the first sourced anthem-title record, when available."""
return self.anthems[0] if self.anthems else None
@property
def motto(self) -> NationalMotto | None:
"""Return the first reviewed source-listed motto, when available."""
return self.mottos[0] if self.mottos else None
@property
def demonym(self) -> Demonym | None:
"""Return the English demonym record, when available."""
return next(
(record for record in self.demonyms if record.language_code == "en"),
self.demonyms[0] if self.demonyms else None,
)
@property
def timezone_ids(self) -> tuple[str, ...]:
"""Return all captured country timezone identifiers."""
return tuple(timezone.id for timezone in self.timezones)
@property
def major_city_count(self) -> int:
"""Return the number of populated-place records bundled for this profile."""
return len(self.major_cities)
@property
def capital(self) -> Capital | None:
"""Return the primary capital, or ``None`` when unavailable."""
return next((capital for capital in self.capitals if capital.primary), None)
@property
def capital_coordinates(self) -> Coordinate | None:
"""Return the primary capital's coordinates, when a capital is available."""
return self.capital.coordinates if self.capital else None
@property
def local_name_languages(self) -> tuple[str, ...]:
"""Return language codes represented by sourced local identity records."""
return tuple(name.language_code for name in self.local_names if name.language_code)
[docs]
def local_name(self, language_code: str) -> LocalizedName | None:
"""Return the complete sourced local record for ``language_code``.
Matching is case-insensitive. ``None`` means that this dataset does not
contain a selected record for that language; it does not mean the
language or local name does not exist. No translation, English
fallback, or romanization is invented.
"""
normalized = language_code.casefold()
return next((name for name in self.local_names if name.language_code == normalized), None)
[docs]
def name_in(self, language_code: str) -> str | None:
"""Return the sourced short local name, without fallback."""
match = self.local_name(language_code)
return match.short_name if match else None
[docs]
def official_name_in(self, language_code: str) -> str | None:
"""Return the reviewed formal local name, without fallback.
This is populated only for ``national_official`` local records. It is
separate from the English ``formal_name`` profile field.
"""
match = self.local_name(language_code)
return match.formal_name if match else None
[docs]
def romanized_name_in(self, language_code: str) -> str | None:
"""Return a source-provided romanized short name, without generating one."""
match = self.local_name(language_code)
return match.romanized_short_name if match else None
[docs]
def romanized_official_name_in(self, language_code: str) -> str | None:
"""Return a source-provided romanized formal name, without generating one."""
match = self.local_name(language_code)
return match.romanized_official_name if match else None
[docs]
def reference(self) -> CountryReference:
"""Return a compact immutable reference suitable for results and prompts."""
return CountryReference(self.name, self.alpha2, self.alpha3, self.codes.numeric)
[docs]
def discovery_card(self) -> CountryDiscoveryCard:
"""Return a compact educational view built entirely from this profile.
The card is safe to retain after the originating :class:`Atlas` closes
and can be serialized with :meth:`CountryDiscoveryCard.to_dict` or
:meth:`CountryDiscoveryCard.to_json`.
"""
capital = self.capital
return CountryDiscoveryCard(
country=self.reference(),
flag_emoji=self.flag_emoji,
official_name=self.official_name,
formal_name=self.formal_name,
capital=capital.name if capital else None,
capital_coordinates=capital.coordinates if capital else None,
continent=self.continent,
region=self.region,
subregion=self.subregion,
population=self.population,
area_km2=self.area_km2,
population_density=self.population_density,
currency=self.currency,
language_codes=self.language_codes,
calling_codes=self.calling_codes,
top_level_domain=self.top_level_domain,
observed_timezones=self.observed_timezones,
local_names=self.local_names,
major_city_count=self.major_city_count,
source_ids=tuple(source.id for source in self.sources),
anthem_title=self.anthem.title if self.anthem else None,
motto_text=self.motto.text if self.motto else None,
demonym=self.demonym.noun if self.demonym else None,
timezone_ids=self.timezone_ids,
coastline_km=self.coastline_km,
highest_point=self.highest_point,
climate_zone_codes=self.climate.zone_codes,
)
@property
def summary(self) -> SummaryText:
"""Return a readable, multiline introduction to this country profile.
``print(country.summary)`` shows useful classroom facts and leaves out
unavailable values. To show the local name in a particular language,
call it with a language code: ``country.summary(local_language="fr")``.
Without one, the selected non-English local identity is used when
present. This is for reading; use :meth:`to_dict` when field names and
machine-readable values matter.
"""
return SummaryText(self._summary(), self._summary)
def _summary(self, *, local_language: str | None = None) -> str:
"""Build the text behind :attr:`summary`."""
local_name = (
self.local_name(local_language)
if local_language is not None
else next(
(
name
for name in self.local_names
if name.short_name.casefold() != self.name.casefold()
),
None,
)
)
heading = f"{self.flag_emoji or ''} {self.name}".strip()
if local_name is not None and local_name.short_name.casefold() != self.name.casefold():
heading += f" · {local_name.short_name}"
lines = [heading]
if self.has_distinct_formal_name:
lines.append(f"Formal name: {self.formal_name}")
if self.capital is not None:
lines.append(f"Capital: {self.capital.name}")
location = tuple(
value for value in (self.continent, self.subregion or self.region) if value
)
if location:
lines.append(f"Location: {' · '.join(dict.fromkeys(location))}")
if self.population is not None:
lines.append(f"Population snapshot: {self.population:,}")
if self.currency is not None:
currency = self.currency.name or self.currency.code
details = [self.currency.code]
if self.currency.symbol and self.currency.symbol != self.currency.code:
details.append(self.currency.symbol)
lines.append(f"Currency: {currency} ({', '.join(details)})")
if self.languages:
languages = ", ".join(
f"{language.name} ({language.code})"
if language.name and language.name != language.code
else language.code
for language in self.languages
)
lines.append(f"Languages: {languages}")
if self.anthem is not None:
anthem = self.anthem.title
if self.anthem.english_title and self.anthem.english_title != anthem:
anthem += f" · {self.anthem.english_title}"
lines.append(f"Anthem title: {anthem}")
if self.motto is not None:
motto = self.motto.text
if self.motto.english_text and self.motto.english_text != motto:
motto += f" · {self.motto.english_text}"
lines.append(f"Motto: {motto}")
if self.travel is not None and self.travel.summary:
lines.append(f"Travel: {self.travel.summary}")
if self.highest_point is not None:
lines.append(
f"Highest point: {self.highest_point.name} "
f"({self.highest_point.elevation_m:,.0f} m)"
)
if self.climate.dominant_zone is not None:
zone = self.climate.dominant_zone
lines.append(f"Dominant climate class: {zone.code} · {zone.name}")
if self.rivers:
lines.append(
"Source-listed rivers: "
+ ", ".join(river.name for river in self.rivers[:3])
)
if self.lakes:
lines.append(
"Source-listed lakes: "
+ ", ".join(lake.name for lake in self.lakes[:3])
)
return "\n".join(lines)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Serialize this profile to JSON-compatible primitives."""
data: dict[str, Any] = _jsonable(self)
return data
[docs]
def to_json(self, indent: int | None = None) -> str:
"""Serialize this profile as JSON."""
return json.dumps(self.to_dict(), ensure_ascii=False, indent=indent)
def _repr_html_(self) -> str:
"""Show the profile as a fact card in Jupyter and other notebook tools.
The card is the :meth:`summary` as an accessible table. It sets no colors,
so it follows the notebook's light or dark theme.
"""
from html import escape
heading, *lines = self.summary.splitlines()
rows = "".join(
'<tr><th scope="row" style="text-align:left;padding:2px 14px 2px 0;'
f'font-weight:600;vertical-align:top">{escape(label)}</th>'
f'<td style="text-align:left;padding:2px 0">{escape(value)}</td></tr>'
for label, _, value in (line.partition(": ") for line in lines)
)
return (
'<div class="pyworldatlas-country" style="font-family:system-ui,sans-serif;'
'line-height:1.45;max-width:46em">'
f'<p style="font-size:1.25em;font-weight:650;margin:0 0 6px">{escape(heading)}</p>'
f'<table style="border-collapse:collapse;margin:0">'
f"<caption style=\"text-align:left;opacity:0.75;padding-bottom:4px\">"
f"Country profile for {escape(self.name)} ({escape(self.alpha2)})</caption>"
f"{rows}</table></div>"
)
def __str__(self) -> str:
return self.name
def __repr__(self) -> str:
return friendly_repr(self, f"name={self.name!r}, alpha2={self.alpha2!r}")