Source code for aeat.adapters.outbound.fx._ecb_provider
"""ECB euro reference-rate exchange-rate provider.Implements the :class:`domain.currency.ExchangeRateProvider` protocol over abundled snapshot of the European Central Bank euro foreign-exchange referencerates (``eurofxref`` XML). The ECB rates are the official exchange rate ofSpanish law (Ley 46/1998 art. 36) accepted for IRPF, IVA, and PGC conversion —see the ``ledger-fx-conversion`` ADR.The ECB publishes EUR-base quotes (``1 EUR = rate CCY``). The:class:`domain.currency.CurrencyNormalizationService` expects``get_eur_rate`` to return CCY->EUR (so ``eur = amount * rate``), so this providerreturns ``1 / ecb_rate``. The ECB publishes only on TARGET working days, so alookup for a non-publication date falls back to the most-recent prior publisheddate."""from__future__importannotationsfrombisectimportbisect_rightfromdatetimeimportdatefromdecimalimportDecimalfromfunctoolsimportlru_cachefrompathlibimportPathfromdefusedxmlimportElementTreefrom....core.external_constantsimportUTF_8_ENCODINGfrom....core.parsingimportparse_iso8601_date_BUNDLED_RATES=Path(__file__).resolve().parents[3]/"_data"/"fx"/"eurofxref-bundled.xml"
[docs]classEcbReferenceRateProvider:"""EUR reference-rate provider backed by a bundled ECB ``eurofxref`` snapshot."""def__init__(self,*,rates_path:Path|None=None)->None:path=rates_pathor_BUNDLED_RATES# date -> {currency: ecb_eur_base_rate}; sorted date index for fallback.self._by_date:dict[date,dict[str,Decimal]]=_parse_eurofxref(path)self._dates:list[date]=sorted(self._by_date)
[docs]defget_eur_rate(self,currency:str,rate_date:date)->Decimal|None:"""Return the CCY->EUR rate for ``rate_date`` (or most-recent prior). Returns ``None`` for EUR (handled natively upstream) or an unknown currency / a date earlier than the snapshot's first published date. """code=currency.upper()ifcode=="EUR":returnDecimal("1")effective=self._effective_date(rate_date)ifeffectiveisNone:returnNoneecb_rate=self._by_date[effective].get(code)ifecb_rateisNoneorecb_rate==0:returnNone# ECB quotes EUR-base (1 EUR = ecb_rate CCY); CCY->EUR is the inverse.returnDecimal("1")/ecb_rate
def_effective_date(self,rate_date:date)->date|None:"""Most-recent published date on or before ``rate_date`` (working-day fallback)."""index=bisect_right(self._dates,rate_date)ifindex==0:returnNonereturnself._dates[index-1]
def_parse_eurofxref(path:Path)->dict[date,dict[str,Decimal]]:"""Parse an ECB ``eurofxref`` XML file into ``{date: {currency: rate}}``. Namespace-agnostic: the ECB feed uses default + ``gesmes`` namespaces, so elements are matched by the local ``Cube`` tag name rather than a fixed qualified name. """root=ElementTree.fromstring(path.read_text(encoding=UTF_8_ENCODING))by_date:dict[date,dict[str,Decimal]]={}fornodeinroot.iter():ifnotnode.tag.endswith("Cube"):continuetime_attr=node.get("time")iftime_attrisNone:continueday=parse_iso8601_date(time_attr)ifdayisNone:continuerates:dict[str,Decimal]={}forchildinnode:ifnotchild.tag.endswith("Cube"):continuecurrency=child.get("currency")rate=child.get("rate")ifcurrencyandrate:rates[currency.upper()]=Decimal(rate)ifrates:by_date[day]=ratesreturnby_date
[docs]@lru_cache(maxsize=1)defdefault_ecb_rate_provider()->EcbReferenceRateProvider:"""Return the process-wide :class:`EcbReferenceRateProvider` over the bundled ECB snapshot (cached)."""returnEcbReferenceRateProvider()