Skip to content

Exchange rates API

The BNR rate client and its value types.

anafpy.bnr.client

Async client for BNR's published foreign-exchange reference rates.

The one non-ANAF source in anafpy (DESIGN.md §17). Romanian tax filings are denominated in lei, so a foreign-currency invoice has to be converted before it can be declared — e-Transport's value_ron, e-Factura's BT-111, D301's curs_valutar. BNR is who publishes the reference rate that conversion uses, and this client is a rate source, not a ruling: it returns what BNR published on a date. Which date a given filing must use is the caller's question, not this module's answer.

BNR publishes static XML documents on curs.bnr.ro, no auth and no key (schema at https://curs.bnr.ro/xsd/nbrfxrates.xsd):

  • nbrfxrates.xml — the current rates (one day);
  • nbrfxrates10days.xml — the last ten banking days;
  • files/xml/years/nbrfxrates<YYYY>.xml — one year, back to 2005, regenerated daily so the current year reaches yesterday's close.

Two things shape the code. Rates exist per banking day, published just after 13:00, so a request for a weekend, a holiday, or this morning has no rate of its own: :meth:BnrClient.get_rates resolves to the latest day on or before the one asked for and reports that day in :attr:~anafpy.bnr.models.FxRateSet.date — it never labels Friday's rate as Sunday's. And BNR asks callers to cache rather than re-fetch, and to read these files instead of scraping the site pages; hence the smallest-sufficient document per query (1.8 KB for today, 14 KB for the last fortnight, the 350 KB year archive only for older dates) and an in-process cache keyed to how mutable each document actually is.

BnrClient

BnrClient(*, http: AsyncClient | None = None, timeout: float = 30.0)

Bases: HttpClientBase

Reads BNR's published exchange-rate XML (curs.bnr.ro).

No credentials, no test/prod split — like :class:~anafpy.public.client.PublicClient, but a different publisher. Use it as an async context manager so an owned client closes cleanly; an injected client must carry a non-empty base_url.

Fetched documents are cached in-process, because BNR asks callers to store what they take rather than re-fetch it. The lifetime follows what BNR can still change: a closed year's archive is final and never expires, while a document BNR is still updating — today's file, the ten-day window, the current year — is re-read after :data:_LIVE_TTL. Concurrent misses share one fetch, and a failed fetch is not cached.

resolve_date

resolve_date(date: date | str | None = None) -> date

The day a lookup is for: the caller's, or today in Romania.

The one definition of that default. A caller who has to report which day it asked about — the MCP tool does, so a banking-day fallback is visible — must not compute it a second time: two clocks drift apart, and the one that drifts is the one nothing tests.

Raises:

Type Description
AnafConfigError

date is not an ISO date.

get_rates async

get_rates(date: date | str | None = None) -> FxRateSet

The reference rates in force for date (default: today in Romania).

Rates are published per banking day just after 13:00, so the result is the latest set BNR published on or before date — read :attr:~anafpy.bnr.models.FxRateSet.date to see which day that was rather than assuming it is the one requested.

Raises:

Type Description
AnafConfigError

date is unparseable or predates BNR's archive.

AnafResponseError

BNR served something that is not a rate document, or published nothing on or before date.

convert async

convert(amount: Decimal | int | str, currency: str, *, date: date | str | None = None) -> Conversion

Convert one amount to RON at date's published rate.

Convenience over :meth:get_rates plus :meth:~anafpy.bnr.models.FxRateSet.convert. To convert several amounts at one rate — an invoice's lines — fetch the set once and call its convert per amount, so every line demonstrably shares a rate.

anafpy.bnr.models

Value types for BNR's published foreign-exchange reference rates.

BNR quotes lei per unit of foreign currency, with a multiplier attribute on the small-denomination currencies: <Rate currency="HUF" multiplier="100"> 1.4430</Rate> means 100 HUF = 1.4430 RON, not 1 HUF. Keeping quoted and multiplier exactly as published — and dividing only inside :meth:FxRateSet.convert — is what stops that factor-of-100 from being lost in a rounded intermediate.

:attr:FxRateSet.date is the date BNR published, which is not always the date that was asked for: rates exist per banking day, so a Sunday, a holiday, or a moment before the 13:00 publication resolves to the last published day. The set carries the real date so a caller can say which day it actually got.

FxRate

Bases: BaseModel

One currency's reference quotation, as BNR published it.

lei_per_unit property

lei_per_unit: Decimal

Lei for one unit — quoted with the multiplier applied.

Exact: the multiplier is always a power of ten, so the division adds digits rather than losing them.

FxRateSet

Bases: BaseModel

Every reference rate BNR published on one banking day.

currencies property

currencies: list[str]

The quoted currency codes, sorted.

rate

rate(currency: str) -> FxRate | None

The rate for currency, or None if BNR does not quote it.

convert

convert(amount: Decimal | int | str, currency: str) -> Conversion

Convert amount of currency into RON at this day's rate.

The multiplier is applied and the result rounded to two decimals half away from zero — once, on the final figure. The base currency converts one-for-one (BNR quotes no rate for it).

Raises:

Type Description
AnafConfigError

BNR does not quote currency on this day. The message lists what it does quote, so a wrong code self-heals.

Conversion

Bases: BaseModel

One amount converted to RON at a published BNR reference rate.