tesorotools-python 0.0.44__tar.gz → 0.0.45__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/PKG-INFO +4 -1
  2. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/pyproject.toml +1 -0
  3. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/providers/__init__.py +11 -4
  4. tesorotools_python-0.0.45/src/tesorotools/providers/imf_irfcl.py +242 -0
  5. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/.gitignore +0 -0
  6. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/__init__.py +0 -0
  7. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/_build_context.py +0 -0
  8. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/_registry.py +0 -0
  9. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/artists/__init__.py +0 -0
  10. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/artists/_common.py +0 -0
  11. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/artists/barh_plot.py +0 -0
  12. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/artists/line_plot.py +0 -0
  13. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/artists/stacked.py +0 -0
  14. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/artists/type_curve.py +0 -0
  15. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/README.md +0 -0
  16. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
  17. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
  18. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
  19. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
  20. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
  21. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
  22. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
  23. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
  24. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/fonts/README.md +0 -0
  25. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/plots.yaml +0 -0
  26. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/assets/tesoro.mplstyle +0 -0
  27. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/data_sources/__init__.py +0 -0
  28. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/data_sources/debug.py +0 -0
  29. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/database/__init__.py +0 -0
  30. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/database/local.py +0 -0
  31. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/database/push.py +0 -0
  32. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/database/shared.py +0 -0
  33. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/dependencies/__init__.py +0 -0
  34. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/dependencies/node.py +0 -0
  35. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/dependencies/resolution.py +0 -0
  36. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/driver.py +0 -0
  37. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/manifest.py +0 -0
  38. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/offsets/__init__.py +0 -0
  39. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/offsets/offsets.py +0 -0
  40. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/offsets/outliers.py +0 -0
  41. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/orchestration.py +0 -0
  42. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/pipeline/__init__.py +0 -0
  43. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/pipeline/diagnose.py +0 -0
  44. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/pipeline/engine.py +0 -0
  45. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/pipeline/rules.py +0 -0
  46. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/providers/base.py +0 -0
  47. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/providers/bde.py +0 -0
  48. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/providers/ecb.py +0 -0
  49. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/providers/lseg.py +0 -0
  50. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/py.typed +0 -0
  51. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/render/__init__.py +0 -0
  52. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/render/content/__init__.py +0 -0
  53. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/render/content/content.py +0 -0
  54. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/render/content/images.py +0 -0
  55. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/render/content/section.py +0 -0
  56. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/render/content/subtitle.py +0 -0
  57. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/render/content/table.py +0 -0
  58. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/render/content/text.py +0 -0
  59. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/render/content/title.py +0 -0
  60. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/render/report.py +0 -0
  61. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/testing/__init__.py +0 -0
  62. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/testing/compare.py +0 -0
  63. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/utils/__init__.py +0 -0
  64. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/utils/config.py +0 -0
  65. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/utils/format.py +0 -0
  66. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/utils/globals.py +0 -0
  67. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/utils/matplotlib.py +0 -0
  68. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/utils/series.py +0 -0
  69. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/utils/shortcuts.py +0 -0
  70. {tesorotools_python-0.0.44 → tesorotools_python-0.0.45}/src/tesorotools/utils/template.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tesorotools-python
3
- Version: 0.0.44
3
+ Version: 0.0.45
4
4
  Requires-Python: >=3.13
5
5
  Requires-Dist: babel>=2.17
6
6
  Requires-Dist: matplotlib>=3.10
@@ -16,5 +16,8 @@ Provides-Extra: bde
16
16
  Requires-Dist: requests>=2.31; extra == 'bde'
17
17
  Provides-Extra: ecb
18
18
  Requires-Dist: requests>=2.31; extra == 'ecb'
19
+ Provides-Extra: imf
20
+ Requires-Dist: requests>=2.31; extra == 'imf'
21
+ Requires-Dist: truststore>=0.10; extra == 'imf'
19
22
  Provides-Extra: lseg
20
23
  Requires-Dist: lseg-data>=2.1; extra == 'lseg'
@@ -27,6 +27,7 @@ dependencies = [
27
27
  [project.optional-dependencies]
28
28
  bde = ["requests>=2.31"]
29
29
  ecb = ["requests>=2.31"]
30
+ imf = ["requests>=2.31", "truststore>=0.10"]
30
31
  lseg = ["lseg-data>=2.1"]
31
32
 
32
33
  [dependency-groups]
@@ -1,9 +1,10 @@
1
1
  """Public provider API.
2
2
 
3
- ``BdeProvider``, ``EcbProvider`` and ``LSEGProvider`` depend on
4
- the optional ``[bde]`` / ``[ecb]`` / ``[lseg]`` extras and are
5
- imported lazily through ``__getattr__``; importing
6
- ``tesorotools.providers`` itself does not require those extras.
3
+ ``BdeProvider``, ``EcbProvider``, ``ImfIrfclProvider`` and
4
+ ``LSEGProvider`` depend on the optional ``[bde]`` / ``[ecb]`` /
5
+ ``[imf]`` / ``[lseg]`` extras and are imported lazily through
6
+ ``__getattr__``; importing ``tesorotools.providers`` itself does not
7
+ require those extras.
7
8
  """
8
9
 
9
10
  from typing import TYPE_CHECKING, Any
@@ -17,12 +18,14 @@ from tesorotools.providers.base import (
17
18
  if TYPE_CHECKING:
18
19
  from tesorotools.providers.bde import BdeProvider
19
20
  from tesorotools.providers.ecb import EcbProvider
21
+ from tesorotools.providers.imf_irfcl import ImfIrfclProvider
20
22
  from tesorotools.providers.lseg import LSEGProvider
21
23
 
22
24
  __all__ = [
23
25
  "BdeProvider",
24
26
  "DataProvider",
25
27
  "EcbProvider",
28
+ "ImfIrfclProvider",
26
29
  "LSEGProvider",
27
30
  "RegistryProtocol",
28
31
  "bootstrap_providers",
@@ -38,6 +41,10 @@ def __getattr__(name: str) -> Any:
38
41
  from tesorotools.providers.ecb import EcbProvider
39
42
 
40
43
  return EcbProvider
44
+ if name == "ImfIrfclProvider":
45
+ from tesorotools.providers.imf_irfcl import ImfIrfclProvider
46
+
47
+ return ImfIrfclProvider
41
48
  if name == "LSEGProvider":
42
49
  from tesorotools.providers.lseg import LSEGProvider
43
50
 
@@ -0,0 +1,242 @@
1
+ """IMF IRFCL data provider via the official SDMX 3.0 REST API.
2
+
3
+ Downloads time series from the IMF's IRFCL dataflow (International
4
+ Reserves and Foreign Currency Liquidity), which publishes official
5
+ reserve assets per country, gold included. No authentication
6
+ required.
7
+
8
+ Install with the ``imf`` optional extra::
9
+
10
+ uv pip install "tesorotools-python[imf]"
11
+
12
+ Indicators (verified against the API in jun-2026):
13
+
14
+ * ``IRFCLDT1_IRFCL56V_FTO`` — gold, VOLUME in fine troy ounces. This
15
+ is the **physical stock** (despite the IMF label saying "millions of
16
+ ounces", the raw value comes in ounces: Spain ≈ 9,053,750 oz ≈
17
+ 281.6 t).
18
+ * ``IRFCLDT1_IRFCL56_USD`` — gold value in USD (approx. market value).
19
+ * ``IRFCLDT1_IRFCL65_USD`` — TOTAL official reserve assets in USD
20
+ (denominator of the % gold represents over reserves).
21
+
22
+ API base
23
+ --------
24
+ ``https://api.imf.org/external/sdmx/3.0/data/dataflow/IMF.STA/IRFCL/~/{key}``
25
+
26
+ Code convention (4 dimensions of DSD_IRFCL_PUB)
27
+ -----------------------------------------------
28
+ ::
29
+
30
+ {COUNTRY}.{INDICATOR}.{SECTOR}.{FREQUENCY}
31
+
32
+ * COUNTRY ISO-3 country code (``ESP``, ``USA``, ``DEU`` …).
33
+ * INDICATOR the IRFCL indicator (``IRFCLDT1_IRFCL56V_FTO`` …).
34
+ * SECTOR ``*`` (wildcard). Countries report gold under different
35
+ sectors (Spain ``S1X``, the US ``S1XS1311`` …); the
36
+ wildcard captures them all. When a key returns several
37
+ series (same gold total under different sectors) they are
38
+ collapsed by date in :meth:`_fetch_one` (last write wins;
39
+ values coincide).
40
+ * FREQUENCY ``M`` (monthly; reserves are published every month).
41
+
42
+ Keys are passed verbatim to the API; this provider does not parse them.
43
+
44
+ Date label
45
+ ----------
46
+ IRFCL labels the period and the observation is the end-of-period
47
+ balance. We map to the real end of period:
48
+
49
+ * ``"2026"`` → 2026-12-31
50
+ * ``"2026-Q1"`` → 2026-03-31
51
+ * ``"2026-M04"`` → 2026-04-30
52
+
53
+ Corporate TLS
54
+ -------------
55
+ ``truststore`` is enabled on import (if installed) so the provider
56
+ works behind MINECO's TLS proxy without ``verify=False``.
57
+ """
58
+
59
+ from __future__ import annotations
60
+
61
+ import logging
62
+ from typing import ClassVar
63
+
64
+ import pandas as pd
65
+ import requests
66
+
67
+ from tesorotools.providers.base import DataProvider
68
+
69
+ logger = logging.getLogger(__name__)
70
+
71
+
72
+ def _enable_truststore_once() -> bool:
73
+ """Enable truststore (the OS cert store) if available."""
74
+ try:
75
+ import truststore # type: ignore[import-not-found]
76
+ except ImportError:
77
+ return False
78
+ truststore.inject_into_ssl() # pyright: ignore[reportUnknownMemberType]
79
+ return True
80
+
81
+
82
+ _TRUSTSTORE_ACTIVE = _enable_truststore_once()
83
+
84
+ _API_BASE = (
85
+ "https://api.imf.org/external/sdmx/3.0/data/dataflow/IMF.STA/IRFCL/~"
86
+ )
87
+ _DEFAULT_TIMEOUT = 60
88
+ _DEFAULT_START = "2000"
89
+ _HEADERS = {"Accept": "application/vnd.sdmx.data+json"}
90
+
91
+
92
+ class ImfIrfclProvider(DataProvider):
93
+ """Provider that downloads IMF IRFCL series via SDMX 3.0.
94
+
95
+ One HTTP request per key. Series with no observations (a country
96
+ that does not report that indicator) are skipped with a warning
97
+ instead of aborting the whole download.
98
+
99
+ Parameters
100
+ ----------
101
+ timeout
102
+ Max seconds per HTTP request.
103
+ session
104
+ Optional pre-built ``requests.Session``. Useful for tests or
105
+ for custom retry policies.
106
+ """
107
+
108
+ PROVIDER_NAME: ClassVar[str] = "imf_irfcl"
109
+
110
+ def __init__(
111
+ self,
112
+ *,
113
+ timeout: int = _DEFAULT_TIMEOUT,
114
+ session: requests.Session | None = None,
115
+ ) -> None:
116
+ self._timeout = timeout
117
+ self._session = session or requests.Session()
118
+
119
+ def fetch(
120
+ self,
121
+ codes: list[str],
122
+ start: str | None = None,
123
+ end: str | None = None,
124
+ ) -> pd.DataFrame:
125
+ """Download a list of IRFCL keys and return a wide DataFrame.
126
+
127
+ Parameters
128
+ ----------
129
+ codes
130
+ List of IRFCL keys (``"{COUNTRY}.{INDICATOR}.{SECTOR}.{FREQ}"``).
131
+ start
132
+ Start period (e.g. ``"2024"``). If ``None`` defaults to
133
+ ``"2000"``.
134
+ end
135
+ End date as ISO string. If ``None`` up to latest.
136
+
137
+ Returns
138
+ -------
139
+ pd.DataFrame
140
+ Wide DataFrame. Index = dates (tz-naive), columns = the
141
+ requested codes. Missing observations are NaN.
142
+ """
143
+ if not codes:
144
+ return pd.DataFrame()
145
+
146
+ cols: dict[str, pd.Series[float]] = {}
147
+ for code in codes:
148
+ try:
149
+ series = self._fetch_one(code, start=start)
150
+ except requests.RequestException as e:
151
+ logger.warning("IRFCL: failed downloading %s (%s)", code, e)
152
+ continue
153
+ if series is not None and not series.empty:
154
+ cols[code] = series
155
+
156
+ if not cols:
157
+ return pd.DataFrame()
158
+
159
+ df = pd.DataFrame(cols).sort_index()
160
+ df.index.name = "date"
161
+ if end:
162
+ df = df.loc[df.index <= pd.Timestamp(end)]
163
+ return df
164
+
165
+ def is_available(self) -> bool:
166
+ """Check whether the IMF API responds.
167
+
168
+ Hits a small ESP gold-volume query.
169
+ """
170
+ try:
171
+ r = self._session.get(
172
+ f"{_API_BASE}/ESP.IRFCLDT1_IRFCL56V_FTO.*.M?startPeriod=2024",
173
+ timeout=10,
174
+ headers=_HEADERS,
175
+ )
176
+ return r.status_code == 200
177
+ except requests.RequestException:
178
+ return False
179
+
180
+ def _fetch_one(
181
+ self, code: str, start: str | None = None
182
+ ) -> "pd.Series[float] | None":
183
+ """Download a single IRFCL series and return it as a Series."""
184
+ url = f"{_API_BASE}/{code}?startPeriod={start or _DEFAULT_START}"
185
+ r = self._session.get(url, timeout=self._timeout, headers=_HEADERS)
186
+ if r.status_code != 200:
187
+ logger.warning("IRFCL: HTTP %d for %s", r.status_code, code)
188
+ return None
189
+ payload = r.json()
190
+ data_sets = payload.get("data", {}).get("dataSets", [])
191
+ structures = payload.get("data", {}).get("structures", [])
192
+ if not data_sets or not structures:
193
+ return None
194
+ time_dim = structures[0].get("dimensions", {}).get("observation", [])
195
+ if not time_dim:
196
+ return None
197
+ time_values = [t["value"] for t in time_dim[0].get("values", [])]
198
+ series_dict = data_sets[0].get("series", {})
199
+
200
+ data: dict[pd.Timestamp, float] = {}
201
+ for _, series in series_dict.items():
202
+ observations = series.get("observations", {})
203
+ for idx_str, payload_obs in observations.items():
204
+ if not payload_obs or payload_obs[0] is None:
205
+ continue
206
+ try:
207
+ value = float(payload_obs[0])
208
+ except (TypeError, ValueError):
209
+ continue
210
+ idx = int(idx_str)
211
+ if idx >= len(time_values):
212
+ continue
213
+ ts = _parse_period(time_values[idx])
214
+ if ts is not None:
215
+ data[ts] = value
216
+
217
+ if not data:
218
+ return None
219
+ return pd.Series(data, dtype="float64").sort_index()
220
+
221
+
222
+ def _parse_period(label: str) -> pd.Timestamp | None:
223
+ """Convert an IRFCL label to the **real end of period**.
224
+
225
+ * ``"YYYY"`` → December 31 of that year.
226
+ * ``"YYYY-QN"`` → last day of the quarter.
227
+ * ``"YYYY-MNN"`` → last day of the month.
228
+ """
229
+ try:
230
+ if "-Q" in label:
231
+ period = pd.Period(label.replace("-Q", "Q"), freq="Q")
232
+ return period.end_time.normalize()
233
+ if "-M" in label: # "YYYY-MNN"
234
+ year_str, month_str = label.split("-M")
235
+ period = pd.Period(
236
+ freq="M", year=int(year_str), month=int(month_str)
237
+ )
238
+ return period.end_time.normalize()
239
+ year = int(label)
240
+ return pd.Timestamp(year=year, month=12, day=31)
241
+ except (ValueError, TypeError):
242
+ return None