Skip to content

MetadataService

The metadata service exposes the UN Comtrade reference catalogues. Every accessor fetches a typed list of frozen dataclasses, caches the result on first use, and refreshes on a configurable cadence.

API reference

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

MetadataService

L3 Metadata Layer — reference catalogue service.

The service owns the dependency graph for metadata fetches: a HttpTransport for the network calls, and (when the cache subsystem lands) a MetadataCache for persistence. In this task scope the service holds only the transport.

The service is owned by ComtradeClient. Consumers should NOT instantiate it directly; use client.metadata instead.

Source code in un_comtrade/metadata.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
class MetadataService:
    """L3 Metadata Layer — reference catalogue service.

    The service owns the dependency graph for metadata
    fetches: a `HttpTransport` for the network calls,
    and (when the cache subsystem lands) a
    `MetadataCache` for persistence. In this task scope
    the service holds only the transport.

    The service is owned by `ComtradeClient`. Consumers
    should NOT instantiate it directly; use
    `client.metadata` instead.
    """

    def __init__(
        self,
        transport: "HttpTransport",
        *,
        cache: "MetadataCache | None" = None,
        parser: "MetadataParser | None" = None,
        base_path: str = DEFAULT_REFERENCE_BASE_PATH,
        downloader: "MetadataDownloader | None" = None,
    ) -> None:
        """Construct a metadata service.

        Parameters
        ----------
        transport
            The HTTP transport used to fetch reference
            catalogues from the upstream.
        cache
            Optional cache. When `None`, the service
            operates without persistence — every call hits
            the upstream.
        parser
            Optional parser. When `None`, the service
            constructs a default `MetadataParser` on first
            use (lazy).
        base_path
            The base URL path for reference catalogue
            endpoints. Defaults to the documented UN
            Comtrade path. Tests may override.
        downloader
            Optional pre-built `MetadataDownloader`. When
            `None`, the service constructs one lazily on
            first access via `service.downloader`.
        """
        self._transport: "HttpTransport" = transport
        self._cache: "MetadataCache | None" = cache
        self._parser: "MetadataParser | None" = parser
        self._base_path: str = base_path
        self._downloader: "MetadataDownloader | None" = downloader

    # ----- Properties -----------------------------------------------------

    @property
    def transport(self) -> "HttpTransport":
        """The HTTP transport used for upstream calls."""
        return self._transport

    @property
    def cache(self) -> "MetadataCache | None":
        """The cache, or `None` if caching is disabled."""
        return self._cache

    @property
    def base_path(self) -> str:
        """The base path for reference catalogue endpoints."""
        return self._base_path

    @property
    def downloader(self) -> "MetadataDownloader":
        """The download mechanism owned by this service.

        Constructed lazily on first access. The service
        retains ownership.
        """
        if self._downloader is None:
            self._downloader = MetadataDownloader(
                self._transport, base_path=self._base_path
            )
        return self._downloader

    @property
    def parser(self) -> "MetadataParser":
        """The parser owned by this service.

        Constructed lazily on first access. The service
        retains ownership.
        """
        if self._parser is None:
            # Local import to keep the module load path light.
            from .parser import MetadataParser

            self._parser = MetadataParser()
        return self._parser

    # ----- Reference: Countries (E01) --------------------------------------

    def get_countries(self) -> list[Country]:
        """M01 — Return the catalogue of reporter countries."""
        raise NotImplementedError("MetadataService.get_countries is not yet implemented")

    def get_country(self, country_code: int) -> Country:
        """M02 — Return a single country by its `country_code`."""
        raise NotImplementedError("MetadataService.get_country is not yet implemented")

    # ----- Reference: Partners (E01 partner role) -------------------------

    def get_partners(self) -> list[Partner]:
        """M03 — Return the catalogue of partner countries."""
        raise NotImplementedError("MetadataService.get_partners is not yet implemented")

    def get_partner(self, country_code: int) -> Partner:
        """M04 — Return a single partner by its `country_code`."""
        raise NotImplementedError("MetadataService.get_partner is not yet implemented")

    # ----- Reference: Classifications (E02) -------------------------------

    def get_classifications(self) -> list[Classification]:
        """M05 — Return the catalogue of classification systems."""
        raise NotImplementedError(
            "MetadataService.get_classifications is not yet implemented"
        )

    def get_classification(self, classification_code: str) -> Classification:
        """M06 — Return a single classification by its code."""
        raise NotImplementedError(
            "MetadataService.get_classification is not yet implemented"
        )

    # ----- Reference: Classification Editions (E03) ----------------------

    def get_classification_editions(self, classification_code: str) -> list[str]:
        """M07 — Return the editions of a classification."""
        raise NotImplementedError(
            "MetadataService.get_classification_editions is not yet implemented"
        )

    # ----- Reference: HS Codes (E04) -------------------------------------

    def get_hs_codes(self, edition: str) -> list[HSCode]:
        """M08 — Return the HS commodity codes for an edition."""
        raise NotImplementedError(
            "MetadataService.get_hs_codes is not yet implemented"
        )

    def get_hs_code(self, commodity_code: str, edition: str) -> HSCode:
        """M09 — Return a single HS code by its code and edition."""
        raise NotImplementedError(
            "MetadataService.get_hs_code is not yet implemented"
        )

    def search_hs(self, query: str, edition: str) -> list[HSCode]:
        """M10 — Search HS codes by a substring query."""
        raise NotImplementedError(
            "MetadataService.search_hs is not yet implemented"
        )

    # ----- Reference: Trade Flows (E05) -----------------------------------

    def get_trade_flows(self) -> list[TradeFlow]:
        """M11 — Return the catalogue of trade flow codes."""
        raise NotImplementedError(
            "MetadataService.get_trade_flows is not yet implemented"
        )

    # ----- Reference: Transport Modes (E06) -------------------------------

    def get_transport_modes(self) -> list[TransportMode]:
        """M12 — Return the catalogue of transport mode codes."""
        raise NotImplementedError(
            "MetadataService.get_transport_modes is not yet implemented"
        )

    # ----- Reference: Customs Procedures (E07) ----------------------------

    def get_customs_procedures(self) -> list[object]:
        """M13 — Return the catalogue of customs procedure codes.

        Returns a list of `CustomsProcedure` records. The
        model class is defined in a subsequent task; the
        signature uses `list[object]` here until the
        canonical model lands.
        """
        raise NotImplementedError(
            "MetadataService.get_customs_procedures is not yet implemented"
        )

    # ----- Reference: Quantity Units (E08) --------------------------------

    def get_quantity_units(self) -> list[object]:
        """M14 — Return the catalogue of quantity unit codes.

        Returns a list of `QuantityUnit` records. The model
        class is defined in a subsequent task; the signature
        uses `list[object]` here until the canonical model
        lands.
        """
        raise NotImplementedError(
            "MetadataService.get_quantity_units is not yet implemented"
        )

    # ----- Reference: Modes of Supply (E11) -------------------------------

    def get_modes_of_supply(self) -> list[object]:
        """M15 — Return the catalogue of mode-of-supply codes.

        Returns a list of `ModeOfSupply` records. The model
        class is defined in a subsequent task; the signature
        uses `list[object]` here until the canonical model
        lands.
        """
        raise NotImplementedError(
            "MetadataService.get_modes_of_supply is not yet implemented"
        )

    # ----- Reference: Frequencies (E09) -----------------------------------

    def get_frequencies(self) -> list[Frequency]:
        """M16 — Return the catalogue of frequency codes."""
        raise NotImplementedError(
            "MetadataService.get_frequencies is not yet implemented"
        )

    # ----- Reference: Data Items (auxiliary) ------------------------------

    def get_data_items(self) -> list[object]:
        """M17 — Return the catalogue of data-item (column) codes.

        Returns a list of `DataItem` records. The model class
        is defined in a subsequent task; the signature uses
        `list[object]` here until the canonical model lands.
        """
        raise NotImplementedError(
            "MetadataService.get_data_items is not yet implemented"
        )

    # ----- Reference: Generic metadata ------------------------------------

    def get_metadata(self, table_name: str) -> object:
        """M18 — Return a generic metadata collection by table name.

        Returns a `MetadataCollection` (E24). The result type
        is `object` until the envelope model lands.
        """
        return self._fetch_cached(self._resource_for_table(table_name))

    # ----- Catalogue fetchers (P2-001) ------------------------------------

    def _fetch_cached(self, resource_id: str, **params: object) -> list[Any]:
        """Cache-then-fetch-then-parse pipeline for a resource.

        1. Cache hit? Return the deserialised canonical list.
        2. Cache miss? Download the raw payload via the
           downloader, parse it via the parser, write the
           result back to the cache (serialised as a list
           of dicts), and return the canonical list.

        Raises
        ------
        ValueError
            When `resource_id` is not in `SUPPORTED_FETCHERS`.
        """
        if resource_id not in SUPPORTED_FETCHERS:
            raise ValueError(
                f"No fetcher implemented for resource {resource_id!r}; "
                f"supported: {sorted(SUPPORTED_FETCHERS)}"
            )

        # 1. Cache hit?
        if self._cache is not None:
            cached = self._cache.get(resource_id)
            if cached is not None and isinstance(cached, list):
                return self._reconstruct(resource_id, cached, **params)

        # 2. Cache miss — download.
        response = self.downloader.download(resource_id, **params)
        payload = response.json()

        # 3. Parse. R05 needs the `edition` kwarg, so call the
        # specific parser method directly rather than going
        # through the generic dispatch table.
        result = self._parse_for_resource(resource_id, payload, params)

        # 4. Cache (serialise as a list of dicts for JSON-friendly storage).
        if self._cache is not None:
            serialised = [m.to_dict() for m in result.records]
            self._cache.set(resource_id, serialised)

        # 5. Return.
        return list(result.records)

    def _parse_for_resource(self, resource_id: str, payload: Any, params: dict):
        """Dispatch parse calls that need path parameters.

        Mirrors `parser.parse(...)` for the supported
        resources but accepts extra kwargs for parameterised
        endpoints (R05 today).

        Returns a `ParseResult`-like duck type so the
        `_fetch_cached` caller can treat all resources
        uniformly.
        """
        records_list = self.parser._extract_data(payload)
        if resource_id == "R05":
            edition = str(params.get("edition", ""))
            records = self.parser.parse_r05_hs_edition(records_list, edition=edition)
            # Wrap the list in a ParseResult-shaped object so
            # `_fetch_cached` can read `.records` and `.skipped`.
            from .parser import ParseResult
            return ParseResult(records=list(records), skipped=0)
        return self.parser.parse(resource_id, payload)

    def _resource_for_table(self, table_name: str) -> str:
        """Map a user-facing table name to a resource id.

        Supports the common aliases used by the SDK spec
        ("Reporters", "Partners", etc.) plus the canonical
        resource ids.
        """
        aliases = {
            "Reporters": "R02",
            "Partners": "R03",
            "HSCombined": "R04",
            "HSEdition": "R05",
            "Frequency": "R09",
            "TradeFlows": "R10",
            "TransportModes": "R12",
            "QuantityUnits": "R14",
            "DataItems": "R15",
            "References": "R01",
        }
        if table_name in aliases:
            return aliases[table_name]
        if table_name in SUPPORTED_FETCHERS:
            return table_name
        raise ValueError(
            f"Unknown metadata table name: {table_name!r}; "
            f"supported: {sorted(aliases)}"
        )

    def _reconstruct(
        self, resource_id: str, items: list[dict], **params: object
    ) -> list[Any]:
        """Reconstruct canonical model instances from a cached list of dicts."""
        if resource_id == "R01":
            return [ReferenceEntry(**i) for i in items]
        if resource_id == "R02":
            return [Country(**self._country_kwargs(i)) for i in items]
        if resource_id == "R03":
            return [Partner(**self._country_kwargs(i)) for i in items]
        if resource_id == "R04":
            return [self._hs_code_kwargs(i, edition="combined") for i in items]
        if resource_id == "R05":
            edition = str(params.get("edition", ""))
            return [self._hs_code_kwargs(i, edition=edition) for i in items]
        if resource_id == "R09":
            return [Frequency(**i) for i in items]
        if resource_id == "R10":
            return [TradeFlow(**i) for i in items]
        if resource_id == "R12":
            return [TransportMode(**i) for i in items]
        if resource_id == "R14":
            return [QuantityUnit(**i) for i in items]
        if resource_id == "R15":
            return [DataItem(**i) for i in items]
        raise ValueError(f"No reconstructor for {resource_id}")

    @staticmethod
    def _country_kwargs(item: dict) -> dict:
        """Build kwargs for `Country` / `Partner` from a cached dict."""
        return dict(
            country_code=item["country_code"],
            iso_alpha2=item.get("iso_alpha2"),
            iso_alpha3=item.get("iso_alpha3"),
            display_name=item["display_name"],
            entry_effective_date=_parse_iso_date(item.get("entry_effective_date")),
            entry_expired_date=_parse_iso_date(item.get("entry_expired_date")),
        )

    @staticmethod
    def _hs_code_kwargs(item: dict, *, edition: str) -> dict:
        """Build kwargs for `HSCode` from a cached dict."""
        return dict(
            commodity_code=item["commodity_code"],
            classification_code="HS",
            edition=edition,
            display_name=item.get("display_name"),
        )

    # ----- Reference: Countries (E01) --------------------------------------

    def get_countries(self) -> list[Country]:
        """M01 — Return the catalogue of reporter countries."""
        return self._fetch_cached("R02")

    def get_country(self, country_code: int) -> Country | None:
        """M02 — Return a single country by its `country_code`."""
        for country in self.get_countries():
            if country.country_code == country_code:
                return country
        return None

    # ----- Reference: Partners (E01 partner role) -------------------------

    def get_partners(self) -> list[Partner]:
        """M03 — Return the catalogue of partner countries."""
        return self._fetch_cached("R03")

    def get_partner(self, country_code: int) -> Partner | None:
        """M04 — Return a single partner by its `country_code`."""
        for partner in self.get_partners():
            if partner.country_code == country_code:
                return partner
        return None

    # ----- Reference: Classifications (E02) -------------------------------

    def get_classifications(self) -> list[Classification]:
        """M05 — Return the catalogue of classification systems.

        Classifications are a small hard-coded set
        (HS, SITC, BEC, EBOPS) per the data model — there
        is no upstream endpoint. The cache is bypassed
        because the list is constant for the SDK's lifetime.
        """
        return [
            Classification(classification_code="HS", display_name="Harmonized System"),
            Classification(
                classification_code="SITC",
                display_name="Standard International Trade Classification",
            ),
            Classification(
                classification_code="BEC",
                display_name="Broad Economic Categories",
            ),
            Classification(
                classification_code="EBOPS",
                display_name="Extended Balance of Payments Services",
            ),
        ]

    def get_classification(self, classification_code: str) -> Classification | None:
        """M06 — Return a single classification by its code."""
        for c in self.get_classifications():
            if c.classification_code == classification_code:
                return c
        return None

    # ----- Reference: Classification Editions (E03) ----------------------

    def get_classification_editions(self, classification_code: str) -> list[str]:
        """M07 — Return the editions of a classification.

        Returns the documented HS editions (2022, 2017,
        2012, 2007, 2002, 1996, 1992) for `classification_code="HS"`.
        Other classifications return an empty list — their
        editions are documented in the spec but not yet
        exposed.
        """
        if classification_code == "HS":
            return ["HS2022", "HS2017", "HS2012", "HS2007", "HS2002", "HS1996", "HS1992"]
        return []

    # ----- Reference: HS Codes (E04) -------------------------------------

    def get_hs_codes(self, edition: str) -> list[HSCode]:
        """M08 — Return the HS commodity codes for an edition."""
        return self._fetch_cached("R05", edition=edition)

    def get_hs_code(self, commodity_code: str, edition: str) -> HSCode | None:
        """M09 — Return a single HS code by its code and edition."""
        for h in self.get_hs_codes(edition):
            if h.commodity_code == commodity_code:
                return h
        return None

    def search_hs(self, query: str, edition: str) -> list[HSCode]:
        """M10 — Search HS codes by a substring query (case-insensitive)."""
        codes = self.get_hs_codes(edition)
        q = query.lower()
        return [c for c in codes if c.display_name and q in c.display_name.lower()]

    # ----- Reference: Trade Flows (E05) -----------------------------------

    def get_trade_flows(self) -> list[TradeFlow]:
        """M11 — Return the catalogue of trade flow codes."""
        return self._fetch_cached("R10")

    # ----- Reference: Transport Modes (E06) -------------------------------

    def get_transport_modes(self) -> list[TransportMode]:
        """M12 — Return the catalogue of transport mode codes."""
        return self._fetch_cached("R12")

    # ----- Reference: Customs Procedures (E07) ----------------------------

    def get_customs_procedures(self) -> list[object]:
        """M13 — Return the catalogue of customs procedure codes.

        Not yet implemented — the `CustomsProcedure` model
        and the upstream parser land in a follow-up task.
        """
        raise NotImplementedError(
            "MetadataService.get_customs_procedures is not yet implemented"
        )

    # ----- Reference: Quantity Units (E08) --------------------------------

    def get_quantity_units(self) -> list[QuantityUnit]:
        """M14 — Return the catalogue of quantity unit codes."""
        return self._fetch_cached("R14")

    # ----- Reference: Modes of Supply (E11) -------------------------------

    def get_modes_of_supply(self) -> list[object]:
        """M15 — Return the catalogue of mode-of-supply codes.

        Not yet implemented — the `ModeOfSupply` model and
        the upstream parser land in a follow-up task.
        """
        raise NotImplementedError(
            "MetadataService.get_modes_of_supply is not yet implemented"
        )

    # ----- Reference: Frequencies (E09) -----------------------------------

    def get_frequencies(self) -> list[Frequency]:
        """M16 — Return the catalogue of frequency codes."""
        return self._fetch_cached("R09")

    # ----- Reference: Data Items (auxiliary) ------------------------------

    def get_data_items(self) -> list[DataItem]:
        """M17 — Return the catalogue of data-item (column) codes."""
        return self._fetch_cached("R15")

    # ----- Lifecycle ------------------------------------------------------

    def close(self) -> None:
        """Release the transport's underlying resources.

        Closes the transport only when the service created
        it. When the caller injected a transport, the
        caller retains ownership.
        """
        if self._downloader is not None:
            # The downloader is the only path that holds a
            # transport reference built by this service; the
            # caller-supplied transport case is covered by
            # `ComtradeClient.close` which owns its own
            # transport lifecycle.
            # No-op today (the downloader does not own the
            # transport); placeholder for future expansion.
            return

transport property

transport: 'HttpTransport'

The HTTP transport used for upstream calls.

cache property

cache: 'MetadataCache | None'

The cache, or None if caching is disabled.

base_path property

base_path: str

The base path for reference catalogue endpoints.

downloader property

downloader: 'MetadataDownloader'

The download mechanism owned by this service.

Constructed lazily on first access. The service retains ownership.

parser property

parser: 'MetadataParser'

The parser owned by this service.

Constructed lazily on first access. The service retains ownership.

get_metadata

get_metadata(table_name: str) -> object

M18 — Return a generic metadata collection by table name.

Returns a MetadataCollection (E24). The result type is object until the envelope model lands.

Source code in un_comtrade/metadata.py
def get_metadata(self, table_name: str) -> object:
    """M18 — Return a generic metadata collection by table name.

    Returns a `MetadataCollection` (E24). The result type
    is `object` until the envelope model lands.
    """
    return self._fetch_cached(self._resource_for_table(table_name))

get_countries

get_countries() -> list[Country]

M01 — Return the catalogue of reporter countries.

Source code in un_comtrade/metadata.py
def get_countries(self) -> list[Country]:
    """M01 — Return the catalogue of reporter countries."""
    return self._fetch_cached("R02")

get_country

get_country(country_code: int) -> Country | None

M02 — Return a single country by its country_code.

Source code in un_comtrade/metadata.py
def get_country(self, country_code: int) -> Country | None:
    """M02 — Return a single country by its `country_code`."""
    for country in self.get_countries():
        if country.country_code == country_code:
            return country
    return None

get_partners

get_partners() -> list[Partner]

M03 — Return the catalogue of partner countries.

Source code in un_comtrade/metadata.py
def get_partners(self) -> list[Partner]:
    """M03 — Return the catalogue of partner countries."""
    return self._fetch_cached("R03")

get_partner

get_partner(country_code: int) -> Partner | None

M04 — Return a single partner by its country_code.

Source code in un_comtrade/metadata.py
def get_partner(self, country_code: int) -> Partner | None:
    """M04 — Return a single partner by its `country_code`."""
    for partner in self.get_partners():
        if partner.country_code == country_code:
            return partner
    return None

get_classifications

get_classifications() -> list[Classification]

M05 — Return the catalogue of classification systems.

Classifications are a small hard-coded set (HS, SITC, BEC, EBOPS) per the data model — there is no upstream endpoint. The cache is bypassed because the list is constant for the SDK's lifetime.

Source code in un_comtrade/metadata.py
def get_classifications(self) -> list[Classification]:
    """M05 — Return the catalogue of classification systems.

    Classifications are a small hard-coded set
    (HS, SITC, BEC, EBOPS) per the data model — there
    is no upstream endpoint. The cache is bypassed
    because the list is constant for the SDK's lifetime.
    """
    return [
        Classification(classification_code="HS", display_name="Harmonized System"),
        Classification(
            classification_code="SITC",
            display_name="Standard International Trade Classification",
        ),
        Classification(
            classification_code="BEC",
            display_name="Broad Economic Categories",
        ),
        Classification(
            classification_code="EBOPS",
            display_name="Extended Balance of Payments Services",
        ),
    ]

get_classification

get_classification(
    classification_code: str,
) -> Classification | None

M06 — Return a single classification by its code.

Source code in un_comtrade/metadata.py
def get_classification(self, classification_code: str) -> Classification | None:
    """M06 — Return a single classification by its code."""
    for c in self.get_classifications():
        if c.classification_code == classification_code:
            return c
    return None

get_classification_editions

get_classification_editions(
    classification_code: str,
) -> list[str]

M07 — Return the editions of a classification.

Returns the documented HS editions (2022, 2017, 2012, 2007, 2002, 1996, 1992) for classification_code="HS". Other classifications return an empty list — their editions are documented in the spec but not yet exposed.

Source code in un_comtrade/metadata.py
def get_classification_editions(self, classification_code: str) -> list[str]:
    """M07 — Return the editions of a classification.

    Returns the documented HS editions (2022, 2017,
    2012, 2007, 2002, 1996, 1992) for `classification_code="HS"`.
    Other classifications return an empty list — their
    editions are documented in the spec but not yet
    exposed.
    """
    if classification_code == "HS":
        return ["HS2022", "HS2017", "HS2012", "HS2007", "HS2002", "HS1996", "HS1992"]
    return []

get_hs_codes

get_hs_codes(edition: str) -> list[HSCode]

M08 — Return the HS commodity codes for an edition.

Source code in un_comtrade/metadata.py
def get_hs_codes(self, edition: str) -> list[HSCode]:
    """M08 — Return the HS commodity codes for an edition."""
    return self._fetch_cached("R05", edition=edition)

get_hs_code

get_hs_code(
    commodity_code: str, edition: str
) -> HSCode | None

M09 — Return a single HS code by its code and edition.

Source code in un_comtrade/metadata.py
def get_hs_code(self, commodity_code: str, edition: str) -> HSCode | None:
    """M09 — Return a single HS code by its code and edition."""
    for h in self.get_hs_codes(edition):
        if h.commodity_code == commodity_code:
            return h
    return None

search_hs

search_hs(query: str, edition: str) -> list[HSCode]

M10 — Search HS codes by a substring query (case-insensitive).

Source code in un_comtrade/metadata.py
def search_hs(self, query: str, edition: str) -> list[HSCode]:
    """M10 — Search HS codes by a substring query (case-insensitive)."""
    codes = self.get_hs_codes(edition)
    q = query.lower()
    return [c for c in codes if c.display_name and q in c.display_name.lower()]

get_trade_flows

get_trade_flows() -> list[TradeFlow]

M11 — Return the catalogue of trade flow codes.

Source code in un_comtrade/metadata.py
def get_trade_flows(self) -> list[TradeFlow]:
    """M11 — Return the catalogue of trade flow codes."""
    return self._fetch_cached("R10")

get_transport_modes

get_transport_modes() -> list[TransportMode]

M12 — Return the catalogue of transport mode codes.

Source code in un_comtrade/metadata.py
def get_transport_modes(self) -> list[TransportMode]:
    """M12 — Return the catalogue of transport mode codes."""
    return self._fetch_cached("R12")

get_customs_procedures

get_customs_procedures() -> list[object]

M13 — Return the catalogue of customs procedure codes.

Not yet implemented — the CustomsProcedure model and the upstream parser land in a follow-up task.

Source code in un_comtrade/metadata.py
def get_customs_procedures(self) -> list[object]:
    """M13 — Return the catalogue of customs procedure codes.

    Not yet implemented — the `CustomsProcedure` model
    and the upstream parser land in a follow-up task.
    """
    raise NotImplementedError(
        "MetadataService.get_customs_procedures is not yet implemented"
    )

get_quantity_units

get_quantity_units() -> list[QuantityUnit]

M14 — Return the catalogue of quantity unit codes.

Source code in un_comtrade/metadata.py
def get_quantity_units(self) -> list[QuantityUnit]:
    """M14 — Return the catalogue of quantity unit codes."""
    return self._fetch_cached("R14")

get_modes_of_supply

get_modes_of_supply() -> list[object]

M15 — Return the catalogue of mode-of-supply codes.

Not yet implemented — the ModeOfSupply model and the upstream parser land in a follow-up task.

Source code in un_comtrade/metadata.py
def get_modes_of_supply(self) -> list[object]:
    """M15 — Return the catalogue of mode-of-supply codes.

    Not yet implemented — the `ModeOfSupply` model and
    the upstream parser land in a follow-up task.
    """
    raise NotImplementedError(
        "MetadataService.get_modes_of_supply is not yet implemented"
    )

get_frequencies

get_frequencies() -> list[Frequency]

M16 — Return the catalogue of frequency codes.

Source code in un_comtrade/metadata.py
def get_frequencies(self) -> list[Frequency]:
    """M16 — Return the catalogue of frequency codes."""
    return self._fetch_cached("R09")

get_data_items

get_data_items() -> list[DataItem]

M17 — Return the catalogue of data-item (column) codes.

Source code in un_comtrade/metadata.py
def get_data_items(self) -> list[DataItem]:
    """M17 — Return the catalogue of data-item (column) codes."""
    return self._fetch_cached("R15")

close

close() -> None

Release the transport's underlying resources.

Closes the transport only when the service created it. When the caller injected a transport, the caller retains ownership.

Source code in un_comtrade/metadata.py
def close(self) -> None:
    """Release the transport's underlying resources.

    Closes the transport only when the service created
    it. When the caller injected a transport, the
    caller retains ownership.
    """
    if self._downloader is not None:
        # The downloader is the only path that holds a
        # transport reference built by this service; the
        # caller-supplied transport case is covered by
        # `ComtradeClient.close` which owns its own
        # transport lifecycle.
        # No-op today (the downloader does not own the
        # transport); placeholder for future expansion.
        return

Examples

from un_comtrade import ComtradeClient

with ComtradeClient() as client:
    countries = client.metadata.get_countries()
    india = client.metadata.get_country(699)
    hs2 = client.metadata.get_hs_codes(level=2)