Skip to content

Models

The canonical data model is the typed boundary between the upstream wire format and the consumer's view of the world. Every record is immutable (frozen=True); monetary values are Decimal; dates are ISO-8601 strings; enums are frozenset.

API reference

The full reference is generated from the SDK's docstrings via mkdocstrings.

models

Metadata models for the UN Comtrade Python SDK.

This package provides frozen, validated dataclasses for the canonical metadata entities described in 006_DATA_MODEL.md:

  • Country / Partner (E01)
  • Classification (E02)
  • HSCode (E04, HS-specialized commodity code)
  • TradeFlow (E05)
  • TransportMode (E06)
  • Frequency (E09)

And the canonical trade-record models from §3.12 (P2-003):

  • Reporter (record-embedded)
  • Partner (record-embedded; aliased to TradePartner at the package level to avoid clashing with the catalog Partner)
  • Commodity (record-embedded)
  • TradeFlow (record-embedded; aliased to RecordTradeFlow at the package level)
  • TradeValue
  • Quantity
  • TradeRecord

Per the task scope (P1-011 + P2-003) this module contains models only: no transport, no metadata download, no API integration. Validation is enforced in __post_init__ per ADR-0013 and the documented rules in the data-model specification.

Country dataclass

Bases: BaseModel

E01 Country — political entity.

Primary key: country_code (non-negative integer). Validation per 006_DATA_MODEL.md §3.1.

Source code in un_comtrade/models/country.py
@dataclass(frozen=True)
class Country(BaseModel):
    """E01 Country — political entity.

    Primary key: `country_code` (non-negative integer).
    Validation per `006_DATA_MODEL.md` §3.1.
    """

    country_code: int
    iso_alpha2: str | None
    iso_alpha3: str | None
    display_name: str
    entry_effective_date: date | None = None
    entry_expired_date: date | None = None

    def __post_init__(self) -> None:
        _validate_country_fields(
            self.country_code,
            self.iso_alpha2,
            self.iso_alpha3,
            self.display_name,
            self.entry_effective_date,
            self.entry_expired_date,
        )

Partner dataclass

Bases: BaseModel

E01 Country in the partner role.

Distinct type from Country so dataclass equality treats them separately — Country(699, ...) != Partner(699, ...). Shape and validation are identical.

Source code in un_comtrade/models/country.py
@dataclass(frozen=True)
class Partner(BaseModel):
    """E01 Country in the partner role.

    Distinct type from `Country` so dataclass equality
    treats them separately — `Country(699, ...) !=
    Partner(699, ...)`. Shape and validation are
    identical.
    """

    country_code: int
    iso_alpha2: str | None
    iso_alpha3: str | None
    display_name: str
    entry_effective_date: date | None = None
    entry_expired_date: date | None = None

    def __post_init__(self) -> None:
        _validate_country_fields(
            self.country_code,
            self.iso_alpha2,
            self.iso_alpha3,
            self.display_name,
            self.entry_effective_date,
            self.entry_expired_date,
        )

Classification dataclass

Bases: BaseModel

E02 Classification — product classification system.

Primary key: classification_code (string). Validation per 006_DATA_MODEL.md §3.2.

Source code in un_comtrade/models/classification.py
@dataclass(frozen=True)
class Classification(BaseModel):
    """E02 Classification — product classification system.

    Primary key: `classification_code` (string).
    Validation per `006_DATA_MODEL.md` §3.2.
    """

    classification_code: str
    display_name: str

    def __post_init__(self) -> None:
        if not isinstance(self.classification_code, str):
            raise TypeError(
                f"classification_code must be a str; got "
                f"{type(self.classification_code).__name__}"
            )
        if not self.classification_code.strip():
            raise ValueError("classification_code must be a non-empty string")
        if self.classification_code not in _VALID_CLASSIFICATIONS:
            raise ValueError(
                f"classification_code must be one of "
                f"{sorted(_VALID_CLASSIFICATIONS)}; got "
                f"{self.classification_code!r}"
            )
        if not isinstance(self.display_name, str) or not self.display_name.strip():
            raise ValueError("display_name must be a non-empty string")

DataItem dataclass

Bases: BaseModel

A single reference-catalogue column / variable.

Primary key: data_item (string). Validation per the upstream JSON shape (dataItem field).

Source code in un_comtrade/models/data_item.py
@dataclass(frozen=True)
class DataItem(BaseModel):
    """A single reference-catalogue column / variable.

    Primary key: `data_item` (string). Validation per the
    upstream JSON shape (`dataItem` field).
    """

    data_item: str
    description: str

    def __post_init__(self) -> None:
        if not isinstance(self.data_item, str) or not self.data_item.strip():
            raise ValueError("data_item must be a non-empty string")
        if not isinstance(self.description, str) or not self.description.strip():
            raise ValueError("description must be a non-empty string")

Frequency dataclass

Bases: BaseModel

E09 Frequency — time granularity of a query or record.

Primary key: frequency_code (string). Validation per 006_DATA_MODEL.md §3.9.

Source code in un_comtrade/models/frequency.py
@dataclass(frozen=True)
class Frequency(BaseModel):
    """E09 Frequency — time granularity of a query or record.

    Primary key: `frequency_code` (string).
    Validation per `006_DATA_MODEL.md` §3.9.
    """

    frequency_code: str
    display_name: str

    def __post_init__(self) -> None:
        if not isinstance(self.frequency_code, str):
            raise TypeError(
                f"frequency_code must be a str; got "
                f"{type(self.frequency_code).__name__}"
            )
        if self.frequency_code not in _VALID_FREQUENCY_CODES:
            raise ValueError(
                f"frequency_code must be one of {sorted(_VALID_FREQUENCY_CODES)}; "
                f"got {self.frequency_code!r}"
            )
        if not isinstance(self.display_name, str) or not self.display_name.strip():
            raise ValueError("display_name must be a non-empty string")

HSCode dataclass

Bases: BaseModel

E04 CommodityCode specialized to the HS classification.

Primary key (composite): (commodity_code, classification_code, edition). Validation per 006_DATA_MODEL.md §3.4.

The model is specialized to HS in this task scope; future editions of the model MAY generalize to other classifications (SITC, BEC, EBOPS).

Source code in un_comtrade/models/hs_code.py
@dataclass(frozen=True)
class HSCode(BaseModel):
    """E04 CommodityCode specialized to the HS classification.

    Primary key (composite): `(commodity_code,
    classification_code, edition)`. Validation per
    `006_DATA_MODEL.md` §3.4.

    The model is specialized to HS in this task scope;
    future editions of the model MAY generalize to
    other classifications (SITC, BEC, EBOPS).
    """

    commodity_code: str
    classification_code: str
    edition: str
    display_name: str | None = None

    def __post_init__(self) -> None:
        if not isinstance(self.commodity_code, str):
            raise TypeError(
                f"commodity_code must be a str; got "
                f"{type(self.commodity_code).__name__}"
            )
        if not self.commodity_code.strip():
            raise ValueError("commodity_code must be a non-empty string")
        if (
            self.commodity_code != _TOTAL
            and not _HS_CODE_PATTERN.fullmatch(self.commodity_code)
        ):
            raise ValueError(
                f"HS commodity_code must be {repr(_TOTAL)} or "
                f"2/4/6/8/10 digits; got {self.commodity_code!r}"
            )
        if self.classification_code != "HS":
            raise ValueError(
                f"HSCode is specialized to HS; "
                f"got classification_code={self.classification_code!r}"
            )
        if not isinstance(self.edition, str) or not self.edition.strip():
            raise ValueError("edition must be a non-empty string")
        if self.display_name is not None and (
            not isinstance(self.display_name, str) or not self.display_name.strip()
        ):
            raise ValueError("display_name, if provided, must be a non-empty string")

QuantityUnit dataclass

Bases: BaseModel

E08 QuantityUnit — unit of measurement for quantities.

Primary key: qty_unit_code (integer; -1 represents the TOTAL / not-applicable aggregate). Validation per 006_DATA_MODEL.md §3.8.

Source code in un_comtrade/models/quantity_unit.py
@dataclass(frozen=True)
class QuantityUnit(BaseModel):
    """E08 QuantityUnit — unit of measurement for quantities.

    Primary key: `qty_unit_code` (integer; `-1` represents
    the TOTAL / not-applicable aggregate). Validation per
    `006_DATA_MODEL.md` §3.8.
    """

    qty_unit_code: int
    qty_abbr: str
    qty_description: str

    def __post_init__(self) -> None:
        if isinstance(self.qty_unit_code, bool) or not isinstance(
            self.qty_unit_code, int
        ):
            raise TypeError(
                f"qty_unit_code must be an int; got {type(self.qty_unit_code).__name__}"
            )
        if not isinstance(self.qty_abbr, str) or not self.qty_abbr.strip():
            raise ValueError("qty_abbr must be a non-empty string")
        if not isinstance(self.qty_description, str) or not self.qty_description.strip():
            raise ValueError("qty_description must be a non-empty string")

ReferenceEntry dataclass

Bases: BaseModel

R01 row of the reference list.

Primary key: (category, variable) — both fields together identify the row. Validation enforces non-empty strings and a non-empty fileuri.

Source code in un_comtrade/models/reference_entry.py
@dataclass(frozen=True)
class ReferenceEntry(BaseModel):
    """R01 row of the reference list.

    Primary key: `(category, variable)` — both fields
    together identify the row. Validation enforces
    non-empty strings and a non-empty fileuri.
    """

    category: str
    variable: str
    description: str
    fileuri: str

    def __post_init__(self) -> None:
        for name in ("category", "variable", "description", "fileuri"):
            value = getattr(self, name)
            if not isinstance(value, str) or not value.strip():
                raise ValueError(f"{name} must be a non-empty string")

TradeResponse dataclass

Bases: BaseModel

E22 Response — canonical success envelope.

Wraps the upstream's documented response shape:

  • elapsed_seconds — non-negative number.
  • count — non-negative integer; per the data model contract this SHALL equal the number of records upstream reported (len(records)).
  • records — list of canonical TradeRecord instances. Empty list when no records match. Distinct from the upstream's data field; the canonical name is records per PCR §10 ("canonical renames data to records").
  • error — non-empty string on failure, empty string on success.
  • upstream_url — the URL the request was sent to; useful for diagnostics and for consumers that want to replay the call.
  • request — opaque request metadata (E21 payload) when the caller supplied one.
  • skipped — number of records the parser dropped (validation failures or duplicates). Defaults to 0 when no parser ran.
Source code in un_comtrade/models/response.py
@dataclass(frozen=True)
class TradeResponse(BaseModel):
    """E22 Response — canonical success envelope.

    Wraps the upstream's documented response shape:

    - `elapsed_seconds` — non-negative number.
    - `count` — non-negative integer; per the data
      model contract this SHALL equal the number of
      records upstream reported (`len(records)`).
    - `records` — list of canonical `TradeRecord`
      instances. Empty list when no records match.
      Distinct from the upstream's `data` field; the
      canonical name is `records` per PCR §10
      ("canonical renames `data` to `records`").
    - `error` — non-empty string on failure, empty
      string on success.
    - `upstream_url` — the URL the request was sent
      to; useful for diagnostics and for consumers
      that want to replay the call.
    - `request` — opaque request metadata (E21
      payload) when the caller supplied one.
    - `skipped` — number of records the parser dropped
      (validation failures or duplicates). Defaults
      to `0` when no parser ran.
    """

    elapsed_seconds: float
    count: int
    records: list["TradeRecord"] = field(default_factory=list)
    error: str = ""
    upstream_url: str = ""
    request: dict[str, Any] | None = None
    skipped: int = 0

    def __post_init__(self) -> None:
        if isinstance(self.elapsed_seconds, bool) or not isinstance(
            self.elapsed_seconds, (int, float)
        ):
            raise TypeError(
                f"elapsed_seconds must be a number; got "
                f"{type(self.elapsed_seconds).__name__}"
            )
        if self.elapsed_seconds < 0:
            raise ValueError(
                f"elapsed_seconds must be non-negative; got "
                f"{self.elapsed_seconds}"
            )
        if isinstance(self.count, bool) or not isinstance(self.count, int):
            raise TypeError(
                f"count must be an int; got {type(self.count).__name__}"
            )
        if self.count < 0:
            raise ValueError(
                f"count must be non-negative; got {self.count}"
            )
        if not isinstance(self.records, list):
            raise TypeError(
                f"records must be a list; got "
                f"{type(self.records).__name__}"
            )
        if not isinstance(self.error, str):
            raise TypeError(
                f"error must be a str; got {type(self.error).__name__}"
            )
        if not isinstance(self.upstream_url, str):
            raise TypeError(
                f"upstream_url must be a str; got "
                f"{type(self.upstream_url).__name__}"
            )
        if self.request is not None and not isinstance(self.request, dict):
            raise TypeError(
                f"request must be a dict or None; got "
                f"{type(self.request).__name__}"
            )
        if isinstance(self.skipped, bool) or not isinstance(
            self.skipped, int
        ):
            raise TypeError(
                f"skipped must be an int; got {type(self.skipped).__name__}"
            )
        if self.skipped < 0:
            raise ValueError(
                f"skipped must be non-negative; got {self.skipped}"
            )

Commodity dataclass

Bases: BaseModel

Commodity as it appears on a trade record.

commodity_code is the HS code (2/4/6 digits) or a tariffline extension (8/10 digits), or "TOTAL" (the wildcard that selects every commodity). The display name is optional because the upstream returns null when includeDesc=false.

Source code in un_comtrade/models/trade.py
@dataclass(frozen=True)
class Commodity(BaseModel):
    """Commodity as it appears on a trade record.

    `commodity_code` is the HS code (2/4/6 digits) or a
    tariffline extension (8/10 digits), or `"TOTAL"`
    (the wildcard that selects every commodity). The
    display name is optional because the upstream
    returns `null` when `includeDesc=false`.
    """

    commodity_code: str
    name: str | None

    def __post_init__(self) -> None:
        if not isinstance(self.commodity_code, str):
            raise TypeError(
                f"commodity_code must be a str; got "
                f"{type(self.commodity_code).__name__}"
            )
        if not self.commodity_code.strip():
            raise ValueError("commodity_code must be non-empty")
        if (
            self.commodity_code != _TOTAL_COMMODITY
            and not _HS_CODE_PATTERN.fullmatch(self.commodity_code)
        ):
            raise ValueError(
                f"commodity_code must be {repr(_TOTAL_COMMODITY)} "
                f"or 2/4/6/8/10 digits; got {self.commodity_code!r}"
            )
        _require_optional_str(self.name, field="name")

TradePartner dataclass

Bases: BaseModel

Partner as it appears on a trade record.

partner_code=0 with iso3="W00" and name="World" is the documented sentinel for the World aggregate (PCR Q13, 006_DATA_MODEL.md §4.12). The model accepts any non-negative int but does NOT enforce that the World sentinel is paired with the matching iso3/name — that is the upstream's responsibility.

Source code in un_comtrade/models/trade.py
@dataclass(frozen=True)
class Partner(BaseModel):
    """Partner as it appears on a trade record.

    `partner_code=0` with `iso3="W00"` and `name="World"`
    is the documented sentinel for the World aggregate
    (PCR Q13, `006_DATA_MODEL.md` §4.12). The model
    accepts any non-negative int but does NOT enforce
    that the World sentinel is paired with the matching
    iso3/name — that is the upstream's responsibility.
    """

    partner_code: int
    iso3: str | None
    name: str | None

    def __post_init__(self) -> None:
        _require_non_negative_int(
            self.partner_code, field="partner_code"
        )
        _require_iso3_or_world(self.iso3, field="iso3")
        _require_optional_str(self.name, field="name")

    @property
    def is_world(self) -> bool:
        """Return True if this partner is the World sentinel."""
        return self.partner_code == _WORLD_PARTNER_CODE

is_world property

is_world: bool

Return True if this partner is the World sentinel.

Quantity dataclass

Bases: BaseModel

Quantity section of a trade record.

Carries the primary quantity, the alternate quantity (when reported by the upstream), and their unit codes / abbreviations. All numeric values are Decimal. The unit code -1 is the documented "no unit" sentinel and is accepted as-is (PCR Q28).

Estimation flags default to False because the upstream omits the field when the value is exact.

Source code in un_comtrade/models/trade.py
@dataclass(frozen=True)
class Quantity(BaseModel):
    """Quantity section of a trade record.

    Carries the primary quantity, the alternate quantity
    (when reported by the upstream), and their unit codes
    / abbreviations. All numeric values are `Decimal`.
    The unit code `-1` is the documented "no unit"
    sentinel and is accepted as-is (PCR Q28).

    Estimation flags default to `False` because the
    upstream omits the field when the value is exact.
    """

    qty: Decimal | None
    qty_unit_code: int
    qty_unit_abbr: str | None
    is_estimated: bool
    alt_qty: Decimal | None
    alt_qty_unit_code: int | None
    alt_qty_unit_abbr: str | None
    is_alt_qty_estimated: bool

    def __post_init__(self) -> None:
        if isinstance(self.qty_unit_code, bool) or not isinstance(
            self.qty_unit_code, int
        ):
            raise TypeError(
                f"qty_unit_code must be an int; got "
                f"{type(self.qty_unit_code).__name__}"
            )
        _require_non_negative_decimal(self.qty, field="qty")
        _require_optional_str(self.qty_unit_abbr, field="qty_unit_abbr")
        if not isinstance(self.is_estimated, bool):
            raise TypeError(
                f"is_estimated must be a bool; got "
                f"{type(self.is_estimated).__name__}"
            )
        _require_non_negative_decimal(self.alt_qty, field="alt_qty")
        if self.alt_qty_unit_code is not None:
            if isinstance(
                self.alt_qty_unit_code, bool
            ) or not isinstance(self.alt_qty_unit_code, int):
                raise TypeError(
                    f"alt_qty_unit_code must be an int; got "
                    f"{type(self.alt_qty_unit_code).__name__}"
                )
        _require_optional_str(
            self.alt_qty_unit_abbr, field="alt_qty_unit_abbr"
        )
        if not isinstance(self.is_alt_qty_estimated, bool):
            raise TypeError(
                f"is_alt_qty_estimated must be a bool; got "
                f"{type(self.is_alt_qty_estimated).__name__}"
            )

Reporter dataclass

Bases: BaseModel

Reporter as it appears on a trade record.

Smaller than the catalog Country model: only the code, ISO alpha-3 code (if provided), and display name. No effective dates, no entry metadata.

reporter_code is the upstream's reporterCode integer (e.g. 699 for India).

Source code in un_comtrade/models/trade.py
@dataclass(frozen=True)
class Reporter(BaseModel):
    """Reporter as it appears on a trade record.

    Smaller than the catalog `Country` model: only the
    code, ISO alpha-3 code (if provided), and display
    name. No effective dates, no entry metadata.

    `reporter_code` is the upstream's `reporterCode`
    integer (e.g. `699` for India).
    """

    reporter_code: int
    iso3: str | None
    name: str | None

    def __post_init__(self) -> None:
        _require_non_negative_int(
            self.reporter_code, field="reporter_code"
        )
        _require_iso3_or_world(self.iso3, field="iso3")
        _require_optional_str(self.name, field="name")

RecordTradeFlow dataclass

Bases: BaseModel

Trade flow as it appears on a trade record.

Distinct from the catalog TradeFlow model in models/trade_flow.py: the record-embedded variant carries only the code and display name, no taxonomy metadata.

flow_code must be one of M / X / RX / RM.

Source code in un_comtrade/models/trade.py
@dataclass(frozen=True)
class TradeFlow(BaseModel):
    """Trade flow as it appears on a trade record.

    Distinct from the catalog `TradeFlow` model in
    `models/trade_flow.py`: the record-embedded variant
    carries only the code and display name, no taxonomy
    metadata.

    `flow_code` must be one of `M` / `X` / `RX` / `RM`.
    """

    flow_code: str
    flow_name: str | None

    def __post_init__(self) -> None:
        if not isinstance(self.flow_code, str):
            raise TypeError(
                f"flow_code must be a str; got "
                f"{type(self.flow_code).__name__}"
            )
        if self.flow_code not in _VALID_FLOW_CODES:
            raise ValueError(
                f"flow_code must be one of "
                f"{sorted(_VALID_FLOW_CODES)}; got {self.flow_code!r}"
            )
        _require_optional_str(self.flow_name, field="flow_name")

TradeRecord dataclass

Bases: BaseModel

E12 TradeRecord — a single trade observation.

Composes the record-embedded dimension models (Reporter / Partner / Commodity / TradeFlow) and the value-bearing sub-models (TradeValue / Quantity) with the upstream's metadata fields. Frozen and validated.

Validation rules enforced (per 006_DATA_MODEL.md §4.12):

  • type_code{"C", "S"}.
  • frequency_code{"A", "M"}.
  • ref_year in 1900..2100.
  • ref_month in {1..12, 52} (52 = annual).
  • period matches YYYY or YYYYMM.
  • classification_code, edition, period, customs_code, mos_code are non-empty strings.
  • mot_code is a non-negative int.
  • legacy_estimation_flag is a non-negative int.
  • net_weight_kg, gross_weight_kg are non-negative Decimals (or None).
Source code in un_comtrade/models/trade.py
@dataclass(frozen=True)
class TradeRecord(BaseModel):
    """E12 TradeRecord — a single trade observation.

    Composes the record-embedded dimension models
    (Reporter / Partner / Commodity / TradeFlow) and the
    value-bearing sub-models (TradeValue / Quantity) with
    the upstream's metadata fields. Frozen and validated.

    Validation rules enforced (per `006_DATA_MODEL.md` §4.12):

    - `type_code` ∈ `{"C", "S"}`.
    - `frequency_code` ∈ `{"A", "M"}`.
    - `ref_year` in 1900..2100.
    - `ref_month` in {1..12, 52} (52 = annual).
    - `period` matches `YYYY` or `YYYYMM`.
    - `classification_code`, `edition`, `period`,
      `customs_code`, `mos_code` are non-empty strings.
    - `mot_code` is a non-negative int.
    - `legacy_estimation_flag` is a non-negative int.
    - `net_weight_kg`, `gross_weight_kg` are non-negative
      Decimals (or None).
    """

    # Identifier / metadata
    type_code: str
    frequency_code: str
    classification_code: str
    classification_search_code: str | None
    edition: str
    is_original_classification: bool | None
    # Period
    ref_period_id: int | None
    ref_year: int
    ref_month: int
    period: str
    # Subjects (composed)
    reporter: Reporter
    partner: Partner
    partner2: Partner | None
    flow: TradeFlow
    commodity: Commodity
    # Procedural
    customs_code: str
    customs_name: str | None
    mos_code: str
    mot_code: int
    mot_name: str | None
    # Values / quantities
    quantity: Quantity
    net_weight_kg: Decimal | None
    is_net_weight_estimated: bool
    gross_weight_kg: Decimal | None
    is_gross_weight_estimated: bool
    trade_value: TradeValue
    # Flags
    legacy_estimation_flag: int
    is_reported: bool
    is_aggregate: bool
    # Provenance (derived / upstream metadata; opaque)
    provenance: dict[str, Any] | None

    def __post_init__(self) -> None:
        # type_code
        if not isinstance(self.type_code, str):
            raise TypeError(
                f"type_code must be a str; got "
                f"{type(self.type_code).__name__}"
            )
        if self.type_code not in _VALID_TYPE_CODES:
            raise ValueError(
                f"type_code must be one of "
                f"{sorted(_VALID_TYPE_CODES)}; got {self.type_code!r}"
            )
        # frequency_code
        if not isinstance(self.frequency_code, str):
            raise TypeError(
                f"frequency_code must be a str; got "
                f"{type(self.frequency_code).__name__}"
            )
        if self.frequency_code not in _VALID_FREQUENCY_CODES:
            raise ValueError(
                f"frequency_code must be one of "
                f"{sorted(_VALID_FREQUENCY_CODES)}; got "
                f"{self.frequency_code!r}"
            )
        # classification_code
        _require_optional_str(
            self.classification_code,
            field="classification_code",
            allow_empty=False,
        )
        if not self.classification_code or not isinstance(
            self.classification_code, str
        ):
            raise ValueError("classification_code must be non-empty")
        _require_optional_str(
            self.classification_search_code,
            field="classification_search_code",
        )
        _require_optional_str(self.edition, field="edition")
        if self.is_original_classification is not None and not isinstance(
            self.is_original_classification, bool
        ):
            raise TypeError(
                f"is_original_classification must be a bool or None; got "
                f"{type(self.is_original_classification).__name__}"
            )
        # ref_period_id
        if self.ref_period_id is not None:
            if isinstance(self.ref_period_id, bool) or not isinstance(
                self.ref_period_id, int
            ):
                raise TypeError(
                    f"ref_period_id must be an int or None; got "
                    f"{type(self.ref_period_id).__name__}"
                )
            if self.ref_period_id < 0:
                raise ValueError(
                    f"ref_period_id must be non-negative; got "
                    f"{self.ref_period_id}"
                )
        # ref_year
        if isinstance(self.ref_year, bool) or not isinstance(
            self.ref_year, int
        ):
            raise TypeError(
                f"ref_year must be an int; got "
                f"{type(self.ref_year).__name__}"
            )
        if not _MIN_REF_YEAR <= self.ref_year <= _MAX_REF_YEAR:
            raise ValueError(
                f"ref_year must be in "
                f"{_MIN_REF_YEAR}..{_MAX_REF_YEAR}; got {self.ref_year}"
            )
        # ref_month
        if isinstance(self.ref_month, bool) or not isinstance(
            self.ref_month, int
        ):
            raise TypeError(
                f"ref_month must be an int; got "
                f"{type(self.ref_month).__name__}"
            )
        if self.ref_month not in _VALID_REF_MONTHS:
            raise ValueError(
                f"ref_month must be 1..12 or 52 (annual sentinel); "
                f"got {self.ref_month}"
            )
        # period
        if not isinstance(self.period, str):
            raise TypeError(
                f"period must be a str; got "
                f"{type(self.period).__name__}"
            )
        if not _PERIOD_PATTERN.fullmatch(self.period):
            raise ValueError(
                f"period must match YYYY or YYYYMM; got {self.period!r}"
            )
        # customs / mot / mos
        _require_optional_str(
            self.customs_code, field="customs_code", allow_empty=False
        )
        _require_optional_str(self.customs_name, field="customs_name")
        _require_optional_str(
            self.mos_code, field="mos_code", allow_empty=False
        )
        _require_non_negative_int(self.mot_code, field="mot_code")
        _require_optional_str(self.mot_name, field="mot_name")
        # weights
        _require_non_negative_decimal(
            self.net_weight_kg, field="net_weight_kg"
        )
        if not isinstance(self.is_net_weight_estimated, bool):
            raise TypeError(
                f"is_net_weight_estimated must be a bool; got "
                f"{type(self.is_net_weight_estimated).__name__}"
            )
        _require_non_negative_decimal(
            self.gross_weight_kg, field="gross_weight_kg"
        )
        if not isinstance(self.is_gross_weight_estimated, bool):
            raise TypeError(
                f"is_gross_weight_estimated must be a bool; got "
                f"{type(self.is_gross_weight_estimated).__name__}"
            )
        # legacy_estimation_flag
        if isinstance(
            self.legacy_estimation_flag, bool
        ) or not isinstance(self.legacy_estimation_flag, int):
            raise TypeError(
                f"legacy_estimation_flag must be an int; got "
                f"{type(self.legacy_estimation_flag).__name__}"
            )
        if self.legacy_estimation_flag < 0:
            raise ValueError(
                f"legacy_estimation_flag must be non-negative; got "
                f"{self.legacy_estimation_flag}"
            )
        # is_reported / is_aggregate
        if not isinstance(self.is_reported, bool):
            raise TypeError(
                f"is_reported must be a bool; got "
                f"{type(self.is_reported).__name__}"
            )
        if not isinstance(self.is_aggregate, bool):
            raise TypeError(
                f"is_aggregate must be a bool; got "
                f"{type(self.is_aggregate).__name__}"
            )
        # provenance
        if self.provenance is not None and not isinstance(
            self.provenance, dict
        ):
            raise TypeError(
                f"provenance must be a dict or None; got "
                f"{type(self.provenance).__name__}"
            )

TradeValue dataclass

Bases: BaseModel

Monetary value triplet for a trade record.

All values are USD (per 006_DATA_MODEL.md §4.12) and use Decimal for exact arithmetic. The upstream may return null for cif_value and fob_value (when not applicable to the flow direction); primary_value is required.

Per 006_DATA_MODEL.md §3.12, all values SHALL be non-negative when present.

Source code in un_comtrade/models/trade.py
@dataclass(frozen=True)
class TradeValue(BaseModel):
    """Monetary value triplet for a trade record.

    All values are USD (per `006_DATA_MODEL.md` §4.12)
    and use `Decimal` for exact arithmetic. The
    upstream may return `null` for `cif_value` and
    `fob_value` (when not applicable to the flow
    direction); `primary_value` is required.

    Per `006_DATA_MODEL.md` §3.12, all values SHALL be
    non-negative when present.
    """

    primary_value: Decimal
    fob_value: Decimal | None
    cif_value: Decimal | None

    def __post_init__(self) -> None:
        if not isinstance(self.primary_value, Decimal):
            raise TypeError(
                f"primary_value must be a Decimal; got "
                f"{type(self.primary_value).__name__}"
            )
        if self.primary_value.is_nan():
            raise ValueError("primary_value must not be NaN")
        if self.primary_value.is_signed() and self.primary_value != 0:
            raise ValueError(
                f"primary_value must be non-negative; got "
                f"{self.primary_value}"
            )
        _require_non_negative_decimal(
            self.fob_value, field="fob_value"
        )
        _require_non_negative_decimal(
            self.cif_value, field="cif_value"
        )

TradeFlow dataclass

Bases: BaseModel

E05 TradeFlow — direction of a trade.

Primary key: flow_code (string). Validation per 006_DATA_MODEL.md §3.5.

Source code in un_comtrade/models/trade_flow.py
@dataclass(frozen=True)
class TradeFlow(BaseModel):
    """E05 TradeFlow — direction of a trade.

    Primary key: `flow_code` (string).
    Validation per `006_DATA_MODEL.md` §3.5.
    """

    flow_code: str
    display_name: str

    def __post_init__(self) -> None:
        if not isinstance(self.flow_code, str):
            raise TypeError(
                f"flow_code must be a str; got {type(self.flow_code).__name__}"
            )
        if self.flow_code not in _VALID_FLOW_CODES:
            raise ValueError(
                f"flow_code must be one of {sorted(_VALID_FLOW_CODES)}; "
                f"got {self.flow_code!r}"
            )
        if not isinstance(self.display_name, str) or not self.display_name.strip():
            raise ValueError("display_name must be a non-empty string")

TransportMode dataclass

Bases: BaseModel

E06 TransportMode — mode of transport.

Primary key: mot_code (integer; 0 represents the TOTAL aggregate). Validation per 006_DATA_MODEL.md §3.6.

Source code in un_comtrade/models/transport_mode.py
@dataclass(frozen=True)
class TransportMode(BaseModel):
    """E06 TransportMode — mode of transport.

    Primary key: `mot_code` (integer; `0` represents the
    TOTAL aggregate). Validation per `006_DATA_MODEL.md`
    §3.6.
    """

    mot_code: int
    display_name: str

    def __post_init__(self) -> None:
        # Reject bool (a subclass of int) to keep the type
        # contract clean.
        if isinstance(self.mot_code, bool) or not isinstance(self.mot_code, int):
            raise TypeError(
                f"mot_code must be an int; got {type(self.mot_code).__name__}"
            )
        if self.mot_code < 0:
            raise ValueError(
                f"mot_code must be a non-negative integer; got {self.mot_code}"
            )
        if not isinstance(self.display_name, str) or not self.display_name.strip():
            raise ValueError("display_name must be a non-empty string")

config

Configuration subsystem for the UN Comtrade Python SDK.

This module owns the configuration contract declared in 007_SDK_SPECIFICATION.md §8 and the configuration strategy in 010_INFRASTRUCTURE_SPECIFICATION.md §3.

The configuration is: - a typed, immutable object (@dataclass(frozen=True)) - loaded from one or more sources in priority order: 1. explicit construction argument 2. environment variable 3. default value - validated at construction; invalid values raise ConfigurationError - never mutated after construction

No HTTP, transport, retry, timeout, logging, or business logic lives in this module.

ConfigurationError

Bases: ComtradeError, ValueError

Raised when the configuration is invalid at construction time.

Per 010_INFRASTRUCTURE_SPECIFICATION.md §3.5, the configuration is validated at construction. An invalid configuration raises ConfigurationError before the first call is issued.

Source code in un_comtrade/exceptions.py
class ConfigurationError(ComtradeError, ValueError):
    """Raised when the configuration is invalid at construction time.

    Per `010_INFRASTRUCTURE_SPECIFICATION.md` §3.5, the configuration
    is validated at construction. An invalid configuration raises
    `ConfigurationError` before the first call is issued.
    """

Configuration dataclass

Immutable SDK configuration.

Per 007_SDK_SPECIFICATION.md §8 and 010_INFRASTRUCTURE_SPECIFICATION.md §3.

Source code in un_comtrade/config.py
@dataclass(frozen=True)
class Configuration:
    """Immutable SDK configuration.

    Per `007_SDK_SPECIFICATION.md` §8 and
    `010_INFRASTRUCTURE_SPECIFICATION.md` §3.
    """

    # --- Authentication (8.1) ---------------------------------------------
    api_key: str | None = None

    # --- Transport (8.2) --------------------------------------------------
    base_url: str = DEFAULT_BASE_URL
    user_agent: str = DEFAULT_USER_AGENT
    timeout_seconds: float = float(DEFAULT_TIMEOUT_SECONDS)
    metadata_timeout_seconds: float = float(DEFAULT_METADATA_TIMEOUT_SECONDS)
    download_timeout_seconds: float = float(DEFAULT_DOWNLOAD_TIMEOUT_SECONDS)
    max_retries: int = DEFAULT_MAX_RETRIES
    initial_backoff_seconds: float = DEFAULT_INITIAL_BACKOFF_SECONDS
    backoff_multiplier: float = DEFAULT_BACKOFF_MULTIPLIER
    backoff_cap_seconds: float = DEFAULT_BACKOFF_CAP_SECONDS
    proxy_url: str | None = None

    # --- Caching (8.3) -----------------------------------------------------
    cache_enabled: bool = True
    cache_directory: Path = field(default_factory=_default_cache_directory)
    metadata_cache_ttl_seconds: int = DEFAULT_METADATA_CACHE_TTL_SECONDS
    trade_cache_ttl_seconds: int = DEFAULT_TRADE_CACHE_TTL_SECONDS

    # --- Logging (8.4) ----------------------------------------------------
    log_level: str = DEFAULT_LOG_LEVEL
    log_format: str = DEFAULT_LOG_FORMAT

    def __post_init__(self) -> None:
        """Validate the configuration immediately after construction.

        Also normalises types: ``cache_directory`` is coerced to
        :class:`pathlib.Path` so callers may pass either a string or a
        ``Path`` instance.
        """
        # Normalise cache_directory to Path (frozen dataclasses don't
        # auto-coerce, so we do it explicitly here).
        if not isinstance(self.cache_directory, Path):
            object.__setattr__(
                self, "cache_directory", Path(self.cache_directory)
            )
        _validate(self)

    # --- Mutators (documented; immutability rule permits these) -----------

    def with_api_key(self, api_key: str | None) -> "Configuration":
        """Return a new Configuration with the API key replaced."""
        return replace(self, api_key=api_key)

    def with_timeout_seconds(self, value: float) -> "Configuration":
        """Return a new Configuration with the request timeout replaced."""
        return replace(self, timeout_seconds=float(value))

with_api_key

with_api_key(api_key: str | None) -> 'Configuration'

Return a new Configuration with the API key replaced.

Source code in un_comtrade/config.py
def with_api_key(self, api_key: str | None) -> "Configuration":
    """Return a new Configuration with the API key replaced."""
    return replace(self, api_key=api_key)

with_timeout_seconds

with_timeout_seconds(value: float) -> 'Configuration'

Return a new Configuration with the request timeout replaced.

Source code in un_comtrade/config.py
def with_timeout_seconds(self, value: float) -> "Configuration":
    """Return a new Configuration with the request timeout replaced."""
    return replace(self, timeout_seconds=float(value))

load_configuration

load_configuration(
    *,
    api_key: str | None = None,
    base_url: str | None = None,
    user_agent: str | None = None,
    timeout_seconds: float | None = None,
    metadata_timeout_seconds: float | None = None,
    download_timeout_seconds: float | None = None,
    max_retries: int | None = None,
    initial_backoff_seconds: float | None = None,
    backoff_multiplier: float | None = None,
    backoff_cap_seconds: float | None = None,
    proxy_url: str | None = None,
    cache_enabled: bool | None = None,
    cache_directory: str | Path | None = None,
    metadata_cache_ttl_seconds: int | None = None,
    trade_cache_ttl_seconds: int | None = None,
    log_level: str | None = None,
    log_format: str | None = None,
    env: Mapping[str, str] | None = None,
) -> Configuration

Build a Configuration from explicit kwargs, environment, defaults.

Resolution priority (highest first): 1. Explicit kwargs passed to this function. 2. Environment variables (configurable via the env mapping, defaulting to os.environ). 3. Built-in defaults documented in the specifications.

The env parameter is provided for testability; production code should let it default to os.environ.

No I/O is performed; no network or filesystem access occurs.

Source code in un_comtrade/config.py
def load_configuration(
    *,
    api_key: str | None = None,
    base_url: str | None = None,
    user_agent: str | None = None,
    timeout_seconds: float | None = None,
    metadata_timeout_seconds: float | None = None,
    download_timeout_seconds: float | None = None,
    max_retries: int | None = None,
    initial_backoff_seconds: float | None = None,
    backoff_multiplier: float | None = None,
    backoff_cap_seconds: float | None = None,
    proxy_url: str | None = None,
    cache_enabled: bool | None = None,
    cache_directory: str | Path | None = None,
    metadata_cache_ttl_seconds: int | None = None,
    trade_cache_ttl_seconds: int | None = None,
    log_level: str | None = None,
    log_format: str | None = None,
    env: Mapping[str, str] | None = None,
) -> Configuration:
    """Build a `Configuration` from explicit kwargs, environment, defaults.

    Resolution priority (highest first):
    1. Explicit kwargs passed to this function.
    2. Environment variables (configurable via the ``env`` mapping,
       defaulting to ``os.environ``).
    3. Built-in defaults documented in the specifications.

    The `env` parameter is provided for testability; production code
    should let it default to ``os.environ``.

    No I/O is performed; no network or filesystem access occurs.
    """
    if env is None:
        env = os.environ

    def pick(kw_value, env_name, default, coerce=lambda x: x):
        # Explicit kwargs are assumed already typed; pass through unchanged.
        if kw_value is not None:
            return kw_value
        env_raw = env.get(env_name)
        if env_raw is not None and env_raw.strip() != "":
            return coerce(env_raw)
        # Default is the documented default value (string form); coerce
        # it to the right Python type (int / float / bool).
        return coerce(default)

    resolved_api_key = pick(api_key, ENV_API_KEY, _ENV_DEFAULTS[ENV_API_KEY])
    resolved_base_url = pick(base_url, ENV_BASE_URL, _ENV_DEFAULTS[ENV_BASE_URL])
    resolved_user_agent = pick(user_agent, ENV_USER_AGENT, _ENV_DEFAULTS[ENV_USER_AGENT])
    resolved_timeout = pick(timeout_seconds, ENV_TIMEOUT, _ENV_DEFAULTS[ENV_TIMEOUT],
                            lambda v: _coerce_positive_number(ENV_TIMEOUT, v))
    resolved_metadata_timeout = pick(
        metadata_timeout_seconds, ENV_METADATA_TIMEOUT, _ENV_DEFAULTS[ENV_METADATA_TIMEOUT],
        lambda v: _coerce_positive_number(ENV_METADATA_TIMEOUT, v),
    )
    resolved_download_timeout = pick(
        download_timeout_seconds, ENV_DOWNLOAD_TIMEOUT, _ENV_DEFAULTS[ENV_DOWNLOAD_TIMEOUT],
        lambda v: _coerce_positive_number(ENV_DOWNLOAD_TIMEOUT, v),
    )
    resolved_max_retries = pick(max_retries, ENV_MAX_RETRIES, _ENV_DEFAULTS[ENV_MAX_RETRIES],
                                lambda v: _coerce_non_negative_int(ENV_MAX_RETRIES, v))
    resolved_initial_backoff = pick(
        initial_backoff_seconds, ENV_INITIAL_BACKOFF, _ENV_DEFAULTS[ENV_INITIAL_BACKOFF],
        lambda v: _coerce_positive_number(ENV_INITIAL_BACKOFF, v),
    )
    resolved_backoff_multiplier = pick(
        backoff_multiplier, ENV_BACKOFF_MULTIPLIER, _ENV_DEFAULTS[ENV_BACKOFF_MULTIPLIER],
        lambda v: _coerce_positive_number(ENV_BACKOFF_MULTIPLIER, v),
    )
    resolved_backoff_cap = pick(
        backoff_cap_seconds, ENV_BACKOFF_CAP, _ENV_DEFAULTS[ENV_BACKOFF_CAP],
        lambda v: _coerce_positive_number(ENV_BACKOFF_CAP, v),
    )
    resolved_proxy = pick(proxy_url, ENV_PROXY, _ENV_DEFAULTS[ENV_PROXY])
    resolved_cache_enabled = pick(
        cache_enabled, ENV_CACHE_ENABLED, _ENV_DEFAULTS[ENV_CACHE_ENABLED],
        lambda v: _coerce_bool(ENV_CACHE_ENABLED, v),
    )
    resolved_log_level = pick(log_level, ENV_LOG_LEVEL, _ENV_DEFAULTS[ENV_LOG_LEVEL])

    # Cache directory: explicit path > env var > default
    if cache_directory is not None:
        resolved_cache_dir = Path(cache_directory).expanduser()
    else:
        env_cache_dir = env.get(ENV_CACHE_DIR)
        if env_cache_dir is not None and env_cache_dir.strip() != "":
            resolved_cache_dir = Path(env_cache_dir).expanduser()
        else:
            resolved_cache_dir = _default_cache_directory()

    resolved_metadata_ttl = pick(
        metadata_cache_ttl_seconds, "UN_COMTRADE_METADATA_CACHE_TTL",
        str(DEFAULT_METADATA_CACHE_TTL_SECONDS),
        lambda v: _coerce_non_negative_int("UN_COMTRADE_METADATA_CACHE_TTL", v),
    )
    resolved_trade_ttl = pick(
        trade_cache_ttl_seconds, "UN_COMTRADE_TRADE_CACHE_TTL",
        str(DEFAULT_TRADE_CACHE_TTL_SECONDS),
        lambda v: _coerce_non_negative_int("UN_COMTRADE_TRADE_CACHE_TTL", v),
    )
    resolved_log_format = pick(log_format, "UN_COMTRADE_LOG_FORMAT",
                                DEFAULT_LOG_FORMAT)

    return Configuration(
        api_key=resolved_api_key,
        base_url=resolved_base_url,
        user_agent=resolved_user_agent,
        timeout_seconds=resolved_timeout,
        metadata_timeout_seconds=resolved_metadata_timeout,
        download_timeout_seconds=resolved_download_timeout,
        max_retries=resolved_max_retries,
        initial_backoff_seconds=resolved_initial_backoff,
        backoff_multiplier=resolved_backoff_multiplier,
        backoff_cap_seconds=resolved_backoff_cap,
        proxy_url=resolved_proxy,
        cache_enabled=resolved_cache_enabled,
        cache_directory=resolved_cache_dir,
        metadata_cache_ttl_seconds=resolved_metadata_ttl,
        trade_cache_ttl_seconds=resolved_trade_ttl,
        log_level=resolved_log_level,
        log_format=resolved_log_format,
    )

Examples

from un_comtrade import ComtradeClient
from un_comtrade.config import Configuration

config = Configuration(api_key="your-key", retry_attempts=3)
with ComtradeClient(config) as client:
    exports = client.trade.get_exports(reporter_code=699, period="2022")
    record = exports.records[0]
    print(record.primary_value, record.partner_code, record.flow_code)