tesorotools-python 0.0.52__tar.gz → 0.0.53__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.
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/PKG-INFO +1 -1
- tesorotools_python-0.0.53/src/tesorotools/providers/bde.py +479 -0
- tesorotools_python-0.0.52/src/tesorotools/providers/bde.py +0 -269
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/.gitignore +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/pyproject.toml +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/_build_context.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/_registry.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/_common.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/bar_line.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/barh_plot.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/box_plot.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/compact.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/intraday_plot.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/line_plot.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/matrix.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/plotly_backend.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/shock_plot.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/stacked.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/type_curve.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/vector_plot.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/waterfall.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/README.md +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/fonts/README.md +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/plots.yaml +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/template.docx +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/tesoro.mplstyle +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/data_sources/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/data_sources/debug.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/database/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/database/local.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/database/push.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/database/shared.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/dependencies/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/dependencies/node.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/dependencies/resolution.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/driver.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/manifest.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/offsets/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/offsets/offsets.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/offsets/outliers.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/orchestration.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/pipeline/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/pipeline/diagnose.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/pipeline/engine.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/pipeline/rules.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/providers/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/providers/base.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/providers/ecb.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/providers/imf_irfcl.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/providers/lseg.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/py.typed +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/content.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/images.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/section.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/subtitle.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/table.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/text.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/title.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/document.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/report.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/testing/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/testing/compare.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/utils/__init__.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/utils/config.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/utils/format.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/utils/globals.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/utils/matplotlib.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/utils/series.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/utils/shortcuts.py +0 -0
- {tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/utils/template.py +0 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"""Bank of Spain (Banco de Espana, BdE) data provider.
|
|
2
|
+
|
|
3
|
+
Downloads historical time series from the BdE public REST API.
|
|
4
|
+
No authentication required.
|
|
5
|
+
|
|
6
|
+
Install with::
|
|
7
|
+
|
|
8
|
+
pip install tesorotools-python[bde]
|
|
9
|
+
|
|
10
|
+
API reference
|
|
11
|
+
-------------
|
|
12
|
+
Endpoints:
|
|
13
|
+
https://app.bde.es/bierest/resources/srdatosapp/listaSeries
|
|
14
|
+
https://app.bde.es/bierest/resources/srdatosapp/favoritas
|
|
15
|
+
|
|
16
|
+
Query parameters (names are fixed by the BdE API):
|
|
17
|
+
``idioma`` : ``"es"`` | ``"en"``
|
|
18
|
+
``series`` : comma-separated series codes
|
|
19
|
+
``rango`` : temporal range; **the valid values depend on
|
|
20
|
+
the frequency of the series** (see below).
|
|
21
|
+
|
|
22
|
+
The ``rango`` parameter
|
|
23
|
+
-----------------------
|
|
24
|
+
The BdE API rejects a ``rango`` value that does not match the
|
|
25
|
+
series frequency (``codFrecuencia``). The official table is:
|
|
26
|
+
|
|
27
|
+
========== ==============================
|
|
28
|
+
Frequency Valid ``rango`` values
|
|
29
|
+
========== ==============================
|
|
30
|
+
Daily (D) ``3M``, ``12M``, ``36M``
|
|
31
|
+
Monthly (M) ``30M``, ``60M``, ``MAX``
|
|
32
|
+
Quarter (Q) ``30M``, ``60M``, ``MAX``
|
|
33
|
+
Annual (A) ``60M``, ``MAX``
|
|
34
|
+
========== ==============================
|
|
35
|
+
|
|
36
|
+
Every frequency additionally accepts a specific calendar year
|
|
37
|
+
(e.g. ``rango=2024``), which is the only frequency-agnostic
|
|
38
|
+
value. Daily series have **no** ``MAX``: their largest window
|
|
39
|
+
is 36 months, so longer histories are paginated year by year.
|
|
40
|
+
|
|
41
|
+
Because ``rango`` applies to the whole request, series of
|
|
42
|
+
different frequencies cannot share a request. This provider
|
|
43
|
+
therefore detects each code's frequency first (via the cheap
|
|
44
|
+
``favoritas`` endpoint) and downloads one frequency at a time.
|
|
45
|
+
|
|
46
|
+
Notes
|
|
47
|
+
-----
|
|
48
|
+
- ECB DCF series (credit stock data) are also available
|
|
49
|
+
through this API because the BdE redistributes them.
|
|
50
|
+
- The API accepts multiple series per request, but may
|
|
51
|
+
time out with too many. Downloads are batched.
|
|
52
|
+
- Timestamps include timezone info (UTC). They are
|
|
53
|
+
normalized to tz-naive midnight for pandas compatibility.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
import logging
|
|
59
|
+
import re
|
|
60
|
+
from typing import ClassVar, NotRequired, TypedDict, cast
|
|
61
|
+
|
|
62
|
+
import pandas as pd
|
|
63
|
+
import requests
|
|
64
|
+
|
|
65
|
+
from tesorotools.providers.base import DataProvider
|
|
66
|
+
|
|
67
|
+
logger = logging.getLogger(__name__)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class _BdeSeriesEntry(TypedDict):
|
|
71
|
+
"""Shape of a single series object in the ``listaSeries``
|
|
72
|
+
JSON response. ``informacion`` is optional metadata."""
|
|
73
|
+
|
|
74
|
+
serie: str
|
|
75
|
+
fechas: list[str]
|
|
76
|
+
valores: list[int | float | None]
|
|
77
|
+
decimales: int
|
|
78
|
+
codFrecuencia: str
|
|
79
|
+
informacion: NotRequired[list[dict[str, str]]]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_BASE_URL = "https://app.bde.es/bierest/resources/srdatosapp"
|
|
83
|
+
_LIST_URL = f"{_BASE_URL}/listaSeries"
|
|
84
|
+
_FAV_URL = f"{_BASE_URL}/favoritas"
|
|
85
|
+
|
|
86
|
+
DEFAULT_TIMEOUT = 30
|
|
87
|
+
BATCH_SIZE = 10
|
|
88
|
+
|
|
89
|
+
#: Frequency code the API treats specially: no ``MAX`` and a
|
|
90
|
+
#: distinct set of month-window ranges.
|
|
91
|
+
_DAILY = "D"
|
|
92
|
+
#: Largest month window the API offers for daily series.
|
|
93
|
+
_MAX_DAILY_MONTHS = 36
|
|
94
|
+
#: Extra months added when translating a start date into a
|
|
95
|
+
#: month-window range, so publication lag never truncates it.
|
|
96
|
+
_RANGE_BUFFER_MONTHS = 3
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class BdeProvider(DataProvider):
|
|
100
|
+
"""Provider that downloads series from the Bank of Spain.
|
|
101
|
+
|
|
102
|
+
The BdE API is public and requires no credentials.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
language
|
|
107
|
+
Language for metadata. ``"es"`` for Spanish,
|
|
108
|
+
``"en"`` for English.
|
|
109
|
+
timeout
|
|
110
|
+
Maximum seconds to wait per HTTP request.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
PROVIDER_NAME: ClassVar[str] = "bde"
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
*,
|
|
118
|
+
language: str = "es",
|
|
119
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
120
|
+
) -> None:
|
|
121
|
+
self._language = language
|
|
122
|
+
self._timeout = timeout
|
|
123
|
+
|
|
124
|
+
def fetch(
|
|
125
|
+
self,
|
|
126
|
+
codes: list[str],
|
|
127
|
+
start: str | None = None,
|
|
128
|
+
end: str | None = None,
|
|
129
|
+
) -> pd.DataFrame:
|
|
130
|
+
"""Download one or more series from the BdE.
|
|
131
|
+
|
|
132
|
+
Detects the frequency of every code, groups the codes
|
|
133
|
+
by frequency (series of different frequencies cannot
|
|
134
|
+
share a request), downloads each group with a valid
|
|
135
|
+
``rango`` for that frequency, and merges everything
|
|
136
|
+
into one DataFrame.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
codes
|
|
141
|
+
BdE series codes.
|
|
142
|
+
start
|
|
143
|
+
Earliest date to include (ISO format).
|
|
144
|
+
If ``None``, fetches the full history.
|
|
145
|
+
end
|
|
146
|
+
Ignored by this provider. The BdE API always
|
|
147
|
+
returns data up to the latest observation.
|
|
148
|
+
"""
|
|
149
|
+
if not codes:
|
|
150
|
+
return pd.DataFrame()
|
|
151
|
+
|
|
152
|
+
frequencies = self._detect_frequencies(codes)
|
|
153
|
+
missing = [c for c in codes if c not in frequencies]
|
|
154
|
+
if missing:
|
|
155
|
+
logger.warning(
|
|
156
|
+
"BdE: series not found: %s",
|
|
157
|
+
", ".join(missing),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
start_ts = pd.Timestamp(start) if start else None
|
|
161
|
+
now = pd.Timestamp.now()
|
|
162
|
+
frames: list[pd.DataFrame] = []
|
|
163
|
+
|
|
164
|
+
for freq, group in _group_by_frequency(codes, frequencies).items():
|
|
165
|
+
df = self._fetch_group(group, freq, start_ts, now)
|
|
166
|
+
if not df.empty:
|
|
167
|
+
frames.append(df)
|
|
168
|
+
|
|
169
|
+
if not frames:
|
|
170
|
+
return pd.DataFrame()
|
|
171
|
+
|
|
172
|
+
result = pd.concat(frames, axis=1)
|
|
173
|
+
result.sort_index(inplace=True)
|
|
174
|
+
result = result[[c for c in codes if c in result.columns]]
|
|
175
|
+
|
|
176
|
+
if start_ts is not None:
|
|
177
|
+
result = result.loc[result.index >= start_ts]
|
|
178
|
+
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
def is_available(self) -> bool:
|
|
182
|
+
"""Ping the BdE API to check connectivity."""
|
|
183
|
+
try:
|
|
184
|
+
resp = requests.get(
|
|
185
|
+
_FAV_URL,
|
|
186
|
+
params={
|
|
187
|
+
"idioma": self._language,
|
|
188
|
+
"series": "DN_1TI2TIE42",
|
|
189
|
+
},
|
|
190
|
+
timeout=5,
|
|
191
|
+
)
|
|
192
|
+
return resp.status_code == 200
|
|
193
|
+
except requests.RequestException:
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
# -- frequency detection ---------------------------------
|
|
197
|
+
|
|
198
|
+
def _detect_frequencies(self, codes: list[str]) -> dict[str, str]:
|
|
199
|
+
"""Return ``{code: codFrecuencia}`` for the codes the
|
|
200
|
+
API knows about.
|
|
201
|
+
|
|
202
|
+
Uses the lightweight ``favoritas`` endpoint (last datum
|
|
203
|
+
only), which returns the frequency of every series and
|
|
204
|
+
works for a mixed-frequency batch.
|
|
205
|
+
"""
|
|
206
|
+
frequencies: dict[str, str] = {}
|
|
207
|
+
for batch in _batches(codes, BATCH_SIZE):
|
|
208
|
+
resp = requests.get(
|
|
209
|
+
_FAV_URL,
|
|
210
|
+
params={
|
|
211
|
+
"idioma": self._language,
|
|
212
|
+
"series": ",".join(batch),
|
|
213
|
+
},
|
|
214
|
+
timeout=self._timeout,
|
|
215
|
+
)
|
|
216
|
+
resp.raise_for_status()
|
|
217
|
+
payload = resp.json()
|
|
218
|
+
_check_api_error(payload)
|
|
219
|
+
for entry in cast(list[dict[str, object]], payload):
|
|
220
|
+
code = entry.get("serie")
|
|
221
|
+
freq = entry.get("codFrecuencia")
|
|
222
|
+
if isinstance(code, str) and isinstance(freq, str):
|
|
223
|
+
frequencies[code] = freq
|
|
224
|
+
return frequencies
|
|
225
|
+
|
|
226
|
+
# -- per-frequency download ------------------------------
|
|
227
|
+
|
|
228
|
+
def _fetch_group(
|
|
229
|
+
self,
|
|
230
|
+
codes: list[str],
|
|
231
|
+
freq: str,
|
|
232
|
+
start: pd.Timestamp | None,
|
|
233
|
+
now: pd.Timestamp,
|
|
234
|
+
) -> pd.DataFrame:
|
|
235
|
+
"""Download a set of same-frequency codes."""
|
|
236
|
+
if freq == _DAILY and start is None:
|
|
237
|
+
first_year = self._earliest_year(codes, now)
|
|
238
|
+
ranges = _year_ranges(first_year, now.year)
|
|
239
|
+
else:
|
|
240
|
+
ranges = _plan_ranges(freq, start, now)
|
|
241
|
+
return self._download_ranges(codes, ranges)
|
|
242
|
+
|
|
243
|
+
def _earliest_year(self, codes: list[str], now: pd.Timestamp) -> int:
|
|
244
|
+
"""First observation year across a daily batch.
|
|
245
|
+
|
|
246
|
+
Reads the ``"Primer valor"`` metadata (format
|
|
247
|
+
``[DD/MM/YYYY]``) returned by ``listaSeries``. Falls
|
|
248
|
+
back to the last ``_MAX_DAILY_MONTHS`` when no year can
|
|
249
|
+
be parsed, so a metadata change never breaks the fetch.
|
|
250
|
+
"""
|
|
251
|
+
params = {
|
|
252
|
+
"idioma": self._language,
|
|
253
|
+
"series": ",".join(codes[:BATCH_SIZE]),
|
|
254
|
+
"rango": f"{_MAX_DAILY_MONTHS}M",
|
|
255
|
+
}
|
|
256
|
+
resp = requests.get(_LIST_URL, params=params, timeout=self._timeout)
|
|
257
|
+
resp.raise_for_status()
|
|
258
|
+
payload = resp.json()
|
|
259
|
+
_check_api_error(payload)
|
|
260
|
+
|
|
261
|
+
years = [
|
|
262
|
+
year
|
|
263
|
+
for entry in cast(list[_BdeSeriesEntry], payload)
|
|
264
|
+
if (year := _first_observation_year(entry)) is not None
|
|
265
|
+
]
|
|
266
|
+
if not years:
|
|
267
|
+
logger.warning(
|
|
268
|
+
"BdE: could not determine daily history start; "
|
|
269
|
+
"limiting to last %d months",
|
|
270
|
+
_MAX_DAILY_MONTHS,
|
|
271
|
+
)
|
|
272
|
+
return now.year - _MAX_DAILY_MONTHS // 12
|
|
273
|
+
return min(years)
|
|
274
|
+
|
|
275
|
+
def _download_ranges(
|
|
276
|
+
self,
|
|
277
|
+
codes: list[str],
|
|
278
|
+
ranges: list[str],
|
|
279
|
+
) -> pd.DataFrame:
|
|
280
|
+
"""Download ``codes`` for every ``rango`` in ``ranges``
|
|
281
|
+
and stack the results by date."""
|
|
282
|
+
per_range: list[pd.DataFrame] = []
|
|
283
|
+
for time_range in ranges:
|
|
284
|
+
batch_frames: list[pd.DataFrame] = []
|
|
285
|
+
for batch in _batches(codes, BATCH_SIZE):
|
|
286
|
+
df = self._download_batch(batch, time_range)
|
|
287
|
+
if not df.empty:
|
|
288
|
+
batch_frames.append(df)
|
|
289
|
+
if batch_frames:
|
|
290
|
+
per_range.append(pd.concat(batch_frames, axis=1))
|
|
291
|
+
|
|
292
|
+
if not per_range:
|
|
293
|
+
return pd.DataFrame()
|
|
294
|
+
|
|
295
|
+
combined = pd.concat(per_range, axis=0)
|
|
296
|
+
combined = combined[~combined.index.duplicated(keep="last")]
|
|
297
|
+
combined.sort_index(inplace=True)
|
|
298
|
+
return combined
|
|
299
|
+
|
|
300
|
+
def _download_batch(
|
|
301
|
+
self,
|
|
302
|
+
codes: list[str],
|
|
303
|
+
time_range: str,
|
|
304
|
+
) -> pd.DataFrame:
|
|
305
|
+
params = {
|
|
306
|
+
"idioma": self._language,
|
|
307
|
+
"series": ",".join(codes),
|
|
308
|
+
"rango": time_range,
|
|
309
|
+
}
|
|
310
|
+
logger.info(
|
|
311
|
+
"BdE: downloading %d series (rango=%s)...",
|
|
312
|
+
len(codes),
|
|
313
|
+
time_range,
|
|
314
|
+
)
|
|
315
|
+
resp = requests.get(
|
|
316
|
+
_LIST_URL,
|
|
317
|
+
params=params,
|
|
318
|
+
timeout=self._timeout,
|
|
319
|
+
)
|
|
320
|
+
resp.raise_for_status()
|
|
321
|
+
|
|
322
|
+
payload = resp.json()
|
|
323
|
+
_check_api_error(payload)
|
|
324
|
+
return _parse_series_list(cast(list[_BdeSeriesEntry], payload))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ----------------------------------------------------------
|
|
328
|
+
# Module-level helpers
|
|
329
|
+
# ----------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class _BdeErrorResponse(TypedDict, total=False):
|
|
333
|
+
errNum: int # noqa: N815
|
|
334
|
+
errMsgUsr: str # noqa: N815
|
|
335
|
+
errMsgDebug: str # noqa: N815
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _check_api_error(payload: object) -> None:
|
|
339
|
+
"""Raise RuntimeError if the API returned an error."""
|
|
340
|
+
if not isinstance(payload, dict):
|
|
341
|
+
return
|
|
342
|
+
if "errNum" not in payload:
|
|
343
|
+
return
|
|
344
|
+
err = cast(_BdeErrorResponse, payload)
|
|
345
|
+
raise RuntimeError(
|
|
346
|
+
f"BdE API error {err.get('errNum', '?')}: {err.get('errMsgUsr', '')}"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _group_by_frequency(
|
|
351
|
+
codes: list[str],
|
|
352
|
+
frequencies: dict[str, str],
|
|
353
|
+
) -> dict[str, list[str]]:
|
|
354
|
+
"""Group *codes* by their frequency, preserving order.
|
|
355
|
+
|
|
356
|
+
Codes absent from *frequencies* (unknown to the API) are
|
|
357
|
+
dropped; ``fetch`` has already warned about them.
|
|
358
|
+
"""
|
|
359
|
+
groups: dict[str, list[str]] = {}
|
|
360
|
+
for code in codes:
|
|
361
|
+
freq = frequencies.get(code)
|
|
362
|
+
if freq is None:
|
|
363
|
+
continue
|
|
364
|
+
groups.setdefault(freq, []).append(code)
|
|
365
|
+
return groups
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _plan_ranges(
|
|
369
|
+
freq: str,
|
|
370
|
+
start: pd.Timestamp | None,
|
|
371
|
+
now: pd.Timestamp,
|
|
372
|
+
) -> list[str]:
|
|
373
|
+
"""Pick the ``rango`` value(s) needed to cover *start* for a
|
|
374
|
+
series of frequency *freq*.
|
|
375
|
+
|
|
376
|
+
Returns a list because daily series older than
|
|
377
|
+
``_MAX_DAILY_MONTHS`` must be paginated one calendar year
|
|
378
|
+
at a time. Every other case is a single keyword range.
|
|
379
|
+
"""
|
|
380
|
+
if freq == _DAILY:
|
|
381
|
+
# Daily start is never None here: that case is resolved
|
|
382
|
+
# in _fetch_group, which knows the first observation.
|
|
383
|
+
months = _months_between(cast(pd.Timestamp, start), now)
|
|
384
|
+
if months <= 3:
|
|
385
|
+
return ["3M"]
|
|
386
|
+
if months <= 12:
|
|
387
|
+
return ["12M"]
|
|
388
|
+
if months <= _MAX_DAILY_MONTHS:
|
|
389
|
+
return [f"{_MAX_DAILY_MONTHS}M"]
|
|
390
|
+
return _year_ranges(cast(pd.Timestamp, start).year, now.year)
|
|
391
|
+
|
|
392
|
+
if start is None:
|
|
393
|
+
return ["MAX"]
|
|
394
|
+
|
|
395
|
+
months = _months_between(start, now)
|
|
396
|
+
# Annual series reject "30M"; their smallest window is 60M.
|
|
397
|
+
if freq != "A" and months <= 30:
|
|
398
|
+
return ["30M"]
|
|
399
|
+
if months <= 60:
|
|
400
|
+
return ["60M"]
|
|
401
|
+
return ["MAX"]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _year_ranges(first_year: int, last_year: int) -> list[str]:
|
|
405
|
+
"""Calendar-year ``rango`` values from *first_year* to
|
|
406
|
+
*last_year* (inclusive)."""
|
|
407
|
+
return [str(year) for year in range(first_year, last_year + 1)]
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _months_between(start: pd.Timestamp, now: pd.Timestamp) -> int:
|
|
411
|
+
"""Whole months from *start* to *now*, plus a lag buffer."""
|
|
412
|
+
return (
|
|
413
|
+
(now.year - start.year) * 12
|
|
414
|
+
+ (now.month - start.month)
|
|
415
|
+
+ _RANGE_BUFFER_MONTHS
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
_FIRST_YEAR_RE = re.compile(r"\[\d{1,2}\D\d{1,2}\D(\d{4})\]")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _first_observation_year(entry: _BdeSeriesEntry) -> int | None:
|
|
423
|
+
"""Parse the first-observation year from a series' metadata.
|
|
424
|
+
|
|
425
|
+
The ``"Primer valor"`` entry looks like ``"[04/01/1999] ..."``
|
|
426
|
+
or ``"[18.07.1989] ..."`` -- the day/month separator varies
|
|
427
|
+
by series, so any non-digit separator is accepted. Only
|
|
428
|
+
daily series (this helper's only caller) use that format.
|
|
429
|
+
"""
|
|
430
|
+
for info in entry.get("informacion", []):
|
|
431
|
+
if info.get("titulo") == "Primer valor":
|
|
432
|
+
match = _FIRST_YEAR_RE.search(info.get("descripcion", ""))
|
|
433
|
+
if match:
|
|
434
|
+
return int(match.group(1))
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _parse_series_list(
|
|
439
|
+
series_list: list[_BdeSeriesEntry],
|
|
440
|
+
) -> pd.DataFrame:
|
|
441
|
+
"""Convert the JSON series list into a DataFrame."""
|
|
442
|
+
columns: dict[str, pd.Series[float]] = {}
|
|
443
|
+
|
|
444
|
+
for entry in series_list:
|
|
445
|
+
code = entry["serie"]
|
|
446
|
+
raw_dates = entry["fechas"]
|
|
447
|
+
raw_values = entry["valores"]
|
|
448
|
+
|
|
449
|
+
if not raw_dates:
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
dates = pd.to_datetime(raw_dates, utc=True)
|
|
453
|
+
dates = dates.tz_localize(None).normalize()
|
|
454
|
+
values = [_to_float(v) for v in raw_values]
|
|
455
|
+
|
|
456
|
+
columns[code] = pd.Series(values, index=dates, name=code, dtype=float)
|
|
457
|
+
|
|
458
|
+
if not columns:
|
|
459
|
+
return pd.DataFrame()
|
|
460
|
+
|
|
461
|
+
df = pd.DataFrame(columns)
|
|
462
|
+
df.index.name = "date"
|
|
463
|
+
df.sort_index(inplace=True)
|
|
464
|
+
return df
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _to_float(raw: int | float | None) -> float | None:
|
|
468
|
+
"""Convert a raw API value to float, or None."""
|
|
469
|
+
if raw is None:
|
|
470
|
+
return None
|
|
471
|
+
try:
|
|
472
|
+
return float(raw)
|
|
473
|
+
except (ValueError, TypeError):
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _batches(items: list[str], size: int) -> list[list[str]]:
|
|
478
|
+
"""Split a list into sublists of at most ``size``."""
|
|
479
|
+
return [items[i : i + size] for i in range(0, len(items), size)]
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
"""Bank of Spain (Banco de Espana, BdE) data provider.
|
|
2
|
-
|
|
3
|
-
Downloads historical time series from the BdE public REST API.
|
|
4
|
-
No authentication required.
|
|
5
|
-
|
|
6
|
-
Install with::
|
|
7
|
-
|
|
8
|
-
pip install tesorotools-python[bde]
|
|
9
|
-
|
|
10
|
-
API reference
|
|
11
|
-
-------------
|
|
12
|
-
Endpoint:
|
|
13
|
-
https://app.bde.es/bierest/resources/srdatosapp/listaSeries
|
|
14
|
-
|
|
15
|
-
Query parameters (names are fixed by the BdE API):
|
|
16
|
-
``idioma`` : ``"es"`` | ``"en"``
|
|
17
|
-
``series`` : comma-separated series codes
|
|
18
|
-
``rango`` : ``"MAX"`` | ``"30M"`` | ``"60M"``
|
|
19
|
-
|
|
20
|
-
Notes
|
|
21
|
-
-----
|
|
22
|
-
- ECB DCF series (credit stock data) are also available
|
|
23
|
-
through this API because the BdE redistributes them.
|
|
24
|
-
- The API accepts multiple series per request, but may
|
|
25
|
-
time out with too many. Downloads are batched.
|
|
26
|
-
- Timestamps include timezone info (UTC). They are
|
|
27
|
-
normalized to tz-naive midnight for pandas compatibility.
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
from __future__ import annotations
|
|
31
|
-
|
|
32
|
-
import logging
|
|
33
|
-
from typing import ClassVar, TypedDict, cast
|
|
34
|
-
|
|
35
|
-
import pandas as pd
|
|
36
|
-
import requests
|
|
37
|
-
|
|
38
|
-
from tesorotools.providers.base import DataProvider
|
|
39
|
-
|
|
40
|
-
logger = logging.getLogger(__name__)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class _BdeSeriesEntry(TypedDict):
|
|
44
|
-
"""Shape of a single series object in the BdE API
|
|
45
|
-
JSON response."""
|
|
46
|
-
|
|
47
|
-
serie: str
|
|
48
|
-
fechas: list[str]
|
|
49
|
-
valores: list[int | float | None]
|
|
50
|
-
decimales: int
|
|
51
|
-
codFrecuencia: str
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
_LIST_URL = "https://app.bde.es/bierest/resources/srdatosapp/listaSeries"
|
|
55
|
-
_PING_URL = "https://app.bde.es/bierest/resources/srdatosapp/favoritas"
|
|
56
|
-
|
|
57
|
-
DEFAULT_TIMEOUT = 30
|
|
58
|
-
BATCH_SIZE = 10
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class BdeProvider(DataProvider):
|
|
62
|
-
"""Provider that downloads series from the Bank of Spain.
|
|
63
|
-
|
|
64
|
-
The BdE API is public and requires no credentials.
|
|
65
|
-
|
|
66
|
-
Parameters
|
|
67
|
-
----------
|
|
68
|
-
language
|
|
69
|
-
Language for metadata. ``"es"`` for Spanish,
|
|
70
|
-
``"en"`` for English.
|
|
71
|
-
timeout
|
|
72
|
-
Maximum seconds to wait per HTTP request.
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
PROVIDER_NAME: ClassVar[str] = "bde"
|
|
76
|
-
|
|
77
|
-
def __init__(
|
|
78
|
-
self,
|
|
79
|
-
*,
|
|
80
|
-
language: str = "es",
|
|
81
|
-
timeout: int = DEFAULT_TIMEOUT,
|
|
82
|
-
) -> None:
|
|
83
|
-
self._language = language
|
|
84
|
-
self._timeout = timeout
|
|
85
|
-
|
|
86
|
-
def fetch(
|
|
87
|
-
self,
|
|
88
|
-
codes: list[str],
|
|
89
|
-
start: str | None = None,
|
|
90
|
-
end: str | None = None,
|
|
91
|
-
) -> pd.DataFrame:
|
|
92
|
-
"""Download one or more series from the BdE.
|
|
93
|
-
|
|
94
|
-
Splits the series into batches of ``BATCH_SIZE``,
|
|
95
|
-
downloads each batch in a single HTTP request, and
|
|
96
|
-
merges the results into one DataFrame.
|
|
97
|
-
|
|
98
|
-
Parameters
|
|
99
|
-
----------
|
|
100
|
-
codes
|
|
101
|
-
BdE series codes.
|
|
102
|
-
start
|
|
103
|
-
Earliest date to include (ISO format).
|
|
104
|
-
If ``None``, fetches the full history.
|
|
105
|
-
end
|
|
106
|
-
Ignored by this provider. The BdE API always
|
|
107
|
-
returns data up to the latest observation.
|
|
108
|
-
"""
|
|
109
|
-
if not codes:
|
|
110
|
-
return pd.DataFrame()
|
|
111
|
-
|
|
112
|
-
time_range = _compute_time_range(start)
|
|
113
|
-
frames: list[pd.DataFrame] = []
|
|
114
|
-
|
|
115
|
-
for batch in _batches(codes, BATCH_SIZE):
|
|
116
|
-
df = self._download_batch(batch, time_range)
|
|
117
|
-
if not df.empty:
|
|
118
|
-
frames.append(df)
|
|
119
|
-
|
|
120
|
-
if not frames:
|
|
121
|
-
return pd.DataFrame()
|
|
122
|
-
|
|
123
|
-
result = pd.concat(frames, axis=1)
|
|
124
|
-
result.sort_index(inplace=True)
|
|
125
|
-
|
|
126
|
-
if start:
|
|
127
|
-
cutoff = pd.Timestamp(start)
|
|
128
|
-
result = result.loc[result.index >= cutoff]
|
|
129
|
-
|
|
130
|
-
return result
|
|
131
|
-
|
|
132
|
-
def is_available(self) -> bool:
|
|
133
|
-
"""Ping the BdE API to check connectivity."""
|
|
134
|
-
try:
|
|
135
|
-
resp = requests.get(
|
|
136
|
-
_PING_URL,
|
|
137
|
-
params={
|
|
138
|
-
"idioma": self._language,
|
|
139
|
-
"series": "DN_1TI2TIE42",
|
|
140
|
-
},
|
|
141
|
-
timeout=5,
|
|
142
|
-
)
|
|
143
|
-
return resp.status_code == 200
|
|
144
|
-
except requests.RequestException:
|
|
145
|
-
return False
|
|
146
|
-
|
|
147
|
-
def _download_batch(
|
|
148
|
-
self,
|
|
149
|
-
codes: list[str],
|
|
150
|
-
time_range: str,
|
|
151
|
-
) -> pd.DataFrame:
|
|
152
|
-
params = {
|
|
153
|
-
"idioma": self._language,
|
|
154
|
-
"series": ",".join(codes),
|
|
155
|
-
"rango": time_range,
|
|
156
|
-
}
|
|
157
|
-
logger.info("BdE: downloading %d series...", len(codes))
|
|
158
|
-
resp = requests.get(
|
|
159
|
-
_LIST_URL,
|
|
160
|
-
params=params,
|
|
161
|
-
timeout=self._timeout,
|
|
162
|
-
)
|
|
163
|
-
resp.raise_for_status()
|
|
164
|
-
|
|
165
|
-
payload = resp.json()
|
|
166
|
-
_check_api_error(payload)
|
|
167
|
-
return _parse_series_list(cast(list[_BdeSeriesEntry], payload), codes)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
# ----------------------------------------------------------
|
|
171
|
-
# Module-level helpers
|
|
172
|
-
# ----------------------------------------------------------
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
class _BdeErrorResponse(TypedDict, total=False):
|
|
176
|
-
errNum: int # noqa: N815
|
|
177
|
-
errMsgUsr: str # noqa: N815
|
|
178
|
-
errMsgDebug: str # noqa: N815
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def _check_api_error(payload: object) -> None:
|
|
182
|
-
"""Raise RuntimeError if the API returned an error."""
|
|
183
|
-
if not isinstance(payload, dict):
|
|
184
|
-
return
|
|
185
|
-
if "errNum" not in payload:
|
|
186
|
-
return
|
|
187
|
-
err = cast(_BdeErrorResponse, payload)
|
|
188
|
-
raise RuntimeError(
|
|
189
|
-
f"BdE API error {err.get('errNum', '?')}: {err.get('errMsgUsr', '')}"
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def _compute_time_range(start: str | None) -> str:
|
|
194
|
-
"""Translate a start date into a BdE API range value.
|
|
195
|
-
|
|
196
|
-
The BdE API only accepts ``"30M"``, ``"60M"``, or
|
|
197
|
-
``"MAX"`` for the ``rango`` parameter. This function
|
|
198
|
-
picks the smallest value that covers the requested
|
|
199
|
-
start date.
|
|
200
|
-
"""
|
|
201
|
-
if start is None:
|
|
202
|
-
return "MAX"
|
|
203
|
-
|
|
204
|
-
start_ts = pd.Timestamp(start)
|
|
205
|
-
now = pd.Timestamp.now()
|
|
206
|
-
months_needed = (
|
|
207
|
-
(now.year - start_ts.year) * 12
|
|
208
|
-
+ (now.month - start_ts.month)
|
|
209
|
-
+ 3 # buffer for publication lag
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
if months_needed <= 30:
|
|
213
|
-
return "30M"
|
|
214
|
-
if months_needed <= 60:
|
|
215
|
-
return "60M"
|
|
216
|
-
return "MAX"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _parse_series_list(
|
|
220
|
-
series_list: list[_BdeSeriesEntry],
|
|
221
|
-
requested: list[str],
|
|
222
|
-
) -> pd.DataFrame:
|
|
223
|
-
"""Convert the JSON series list into a DataFrame."""
|
|
224
|
-
columns: dict[str, pd.Series[float]] = {}
|
|
225
|
-
|
|
226
|
-
for entry in series_list:
|
|
227
|
-
code = entry["serie"]
|
|
228
|
-
raw_dates = entry["fechas"]
|
|
229
|
-
raw_values = entry["valores"]
|
|
230
|
-
|
|
231
|
-
if not raw_dates:
|
|
232
|
-
logger.warning("BdE: %s returned no data", code)
|
|
233
|
-
continue
|
|
234
|
-
|
|
235
|
-
dates = pd.to_datetime(raw_dates, utc=True)
|
|
236
|
-
dates = dates.tz_localize(None).normalize()
|
|
237
|
-
values = [_to_float(v) for v in raw_values]
|
|
238
|
-
|
|
239
|
-
columns[code] = pd.Series(values, index=dates, name=code, dtype=float)
|
|
240
|
-
|
|
241
|
-
missing = set(requested) - set(columns)
|
|
242
|
-
if missing:
|
|
243
|
-
logger.warning(
|
|
244
|
-
"BdE: series not found: %s",
|
|
245
|
-
", ".join(sorted(missing)),
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
if not columns:
|
|
249
|
-
return pd.DataFrame()
|
|
250
|
-
|
|
251
|
-
df = pd.DataFrame(columns)
|
|
252
|
-
df.index.name = "date"
|
|
253
|
-
df.sort_index(inplace=True)
|
|
254
|
-
return df
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def _to_float(raw: int | float | None) -> float | None:
|
|
258
|
-
"""Convert a raw API value to float, or None."""
|
|
259
|
-
if raw is None:
|
|
260
|
-
return None
|
|
261
|
-
try:
|
|
262
|
-
return float(raw)
|
|
263
|
-
except (ValueError, TypeError):
|
|
264
|
-
return None
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
def _batches(items: list[str], size: int) -> list[list[str]]:
|
|
268
|
-
"""Split a list into sublists of at most ``size``."""
|
|
269
|
-
return [items[i : i + size] for i in range(0, len(items), size)]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/barh_plot.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/intraday_plot.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/line_plot.py
RENAMED
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/plotly_backend.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/shock_plot.py
RENAMED
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/type_curve.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/vector_plot.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/artists/waterfall.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/fonts/README.md
RENAMED
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/template.docx
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/assets/tesoro.mplstyle
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/data_sources/__init__.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/data_sources/debug.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/database/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/dependencies/__init__.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/dependencies/node.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/dependencies/resolution.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/pipeline/__init__.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/pipeline/diagnose.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/providers/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/providers/imf_irfcl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/__init__.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/content.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/images.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/section.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/subtitle.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/table.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/text.py
RENAMED
|
File without changes
|
{tesorotools_python-0.0.52 → tesorotools_python-0.0.53}/src/tesorotools/render/content/title.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|