tesorotools-python 0.0.24__tar.gz → 0.0.25__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 (67) hide show
  1. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/.gitignore +7 -1
  2. tesorotools_python-0.0.25/PKG-INFO +18 -0
  3. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/pyproject.toml +15 -12
  4. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/__init__.py +3 -0
  5. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/barh_plot.py +0 -3
  6. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/line_plot.py +24 -13
  7. tesorotools_python-0.0.25/src/tesorotools/artists/stacked.py +264 -0
  8. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/type_curve.py +8 -47
  9. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/convert.py +1 -1
  10. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/data_sources/lseg.py +26 -6
  11. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/database/push.py +0 -11
  12. tesorotools_python-0.0.25/src/tesorotools/dependencies/node.py +48 -0
  13. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/dependencies/resolution.py +39 -17
  14. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/offsets/offsets.py +69 -6
  15. tesorotools_python-0.0.25/src/tesorotools/offsets/outliers.py +25 -0
  16. tesorotools_python-0.0.25/src/tesorotools/pipeline/engine.py +77 -0
  17. tesorotools_python-0.0.25/src/tesorotools/pipeline/rules.py +153 -0
  18. tesorotools_python-0.0.25/src/tesorotools/providers/__init__.py +0 -0
  19. tesorotools_python-0.0.25/src/tesorotools/providers/base.py +72 -0
  20. tesorotools_python-0.0.25/src/tesorotools/providers/bde.py +267 -0
  21. tesorotools_python-0.0.25/src/tesorotools/py.typed +0 -0
  22. tesorotools_python-0.0.25/src/tesorotools/render/content/__init__.py +0 -0
  23. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/series.py +4 -4
  24. tesorotools_python-0.0.24/PKG-INFO +0 -16
  25. tesorotools_python-0.0.24/src/tesorotools/dependencies/functions.py +0 -11
  26. tesorotools_python-0.0.24/src/tesorotools/dependencies/node.py +0 -35
  27. tesorotools_python-0.0.24/src/tesorotools/offsets/outliers.py +0 -15
  28. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/__init__.py +0 -0
  29. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/barh.md +0 -0
  30. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/artists/table.py +0 -0
  31. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/README.md +0 -0
  32. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
  33. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
  34. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
  35. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
  36. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
  37. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
  38. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
  39. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
  40. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/fonts/README.md +0 -0
  41. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/plots.yaml +0 -0
  42. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/assets/tesoro.mplstyle +0 -0
  43. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/data_sources/README.md +0 -0
  44. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/data_sources/__init__.py +0 -0
  45. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/data_sources/debug.py +0 -0
  46. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/database/__init__.py +0 -0
  47. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/database/local.py +0 -0
  48. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/dependencies/__init__.py +0 -0
  49. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/main.py +0 -0
  50. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/offsets/__init__.py +0 -0
  51. {tesorotools_python-0.0.24/src/tesorotools/render/content → tesorotools_python-0.0.25/src/tesorotools/pipeline}/__init__.py +0 -0
  52. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/__init__.py +0 -0
  53. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/content.py +0 -0
  54. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/images.py +0 -0
  55. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/section.py +0 -0
  56. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/subtitle.py +0 -0
  57. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/table.py +0 -0
  58. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/text.py +0 -0
  59. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/content/title.py +0 -0
  60. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/render/report.py +0 -0
  61. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/__init__.py +0 -0
  62. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/config.py +0 -0
  63. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/format.py +0 -0
  64. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/globals.py +0 -0
  65. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/matplotlib.py +0 -0
  66. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/shortcuts.py +0 -0
  67. {tesorotools_python-0.0.24 → tesorotools_python-0.0.25}/src/tesorotools/utils/template.py +0 -0
@@ -21,4 +21,10 @@ dist/
21
21
  test/
22
22
 
23
23
  # font files
24
- *.otf
24
+ *.otf
25
+
26
+ # coverage
27
+ .coverage
28
+
29
+ # ruff
30
+ .ruff_cache/
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: tesorotools-python
3
+ Version: 0.0.25
4
+ Requires-Python: >=3.13
5
+ Requires-Dist: babel>=2.17
6
+ Requires-Dist: eikon>=1.1
7
+ Requires-Dist: lseg-data>=2.0
8
+ Requires-Dist: matplotlib>=3.10
9
+ Requires-Dist: openpyxl>=3.1
10
+ Requires-Dist: pandas>=2.2
11
+ Requires-Dist: psycopg[binary]>=3.1
12
+ Requires-Dist: pyarrow>=18.0
13
+ Requires-Dist: python-docx>=1.1
14
+ Requires-Dist: pywin32>=311; sys_platform == 'win32'
15
+ Requires-Dist: pyyaml>=6.0
16
+ Requires-Dist: sqlalchemy>=2.0
17
+ Provides-Extra: bde
18
+ Requires-Dist: requests>=2.31; extra == 'bde'
@@ -1,31 +1,34 @@
1
1
  [project]
2
2
  name = "tesorotools-python"
3
3
  requires-python = ">=3.13"
4
- version = "0.0.24"
4
+ version = "0.0.25"
5
5
  dependencies = [
6
6
  # database and ORM
7
- "psycopg[binary]",
7
+ "psycopg[binary]>=3.1",
8
8
  "SQLAlchemy>=2.0",
9
9
 
10
10
  # data analysis
11
- "pandas",
12
- "pyarrow",
13
- "openpyxl",
11
+ "pandas>=2.2",
12
+ "pyarrow>=18.0",
13
+ "openpyxl>=3.1",
14
14
 
15
15
  # utils
16
- "PyYAML",
17
- "babel",
18
- "eikon",
19
- "lseg-data",
16
+ "PyYAML>=6.0",
17
+ "babel>=2.17",
18
+ "eikon>=1.1",
19
+ "lseg-data>=2.0",
20
20
 
21
21
  # data visualization
22
- "matplotlib",
23
- "python-docx",
22
+ "matplotlib>=3.10",
23
+ "python-docx>=1.1",
24
24
 
25
25
  # os dependencies
26
- "pywin32>=311; sys_platform == 'win32'"
26
+ "pywin32>=311; sys_platform == 'win32'",
27
27
  ]
28
28
 
29
+ [project.optional-dependencies]
30
+ bde = ["requests>=2.31"]
31
+
29
32
  [dependency-groups]
30
33
  dev = [
31
34
  "ruff>=0.8",
@@ -1,6 +1,9 @@
1
1
  from tesorotools.artists.line_plot import Format, Legend, LinePlot
2
+ from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
2
3
  from tesorotools.utils.config import TemplateLoader
3
4
 
4
5
  TemplateLoader.add_constructor("!line_plot", LinePlot.from_yaml)
5
6
  TemplateLoader.add_constructor("!format", Format.from_yaml)
6
7
  TemplateLoader.add_constructor("!legend", Legend.from_yaml)
8
+ TemplateLoader.add_constructor("!stacked_area", StackedAreaPlot.from_yaml)
9
+ TemplateLoader.add_constructor("!stacked_bar", StackedBarPlot.from_yaml)
@@ -1,8 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- # Añadir anotaciones de tipos, el objetivo sería eliminar todos los diccionarios con anotaciones del tipo dict[str, Any] que dicen más bien poco
4
- # Tener cuidado para que nada de lo que está hecho con esta librería deje de funcionar
5
-
6
3
  from enum import Enum
7
4
  from pathlib import Path
8
5
  from typing import Any, Self
@@ -26,7 +26,7 @@ AX_CONFIG: dict[str, Any] = PLOT_CONFIG["ax"]
26
26
  FIG_CONFIG: dict[str, Any] = PLOT_CONFIG["figure"]
27
27
 
28
28
 
29
- def _style_spines(
29
+ def style_spines(
30
30
  ax: Axes,
31
31
  decimals: int,
32
32
  units: str,
@@ -62,7 +62,7 @@ def _style_spines(
62
62
  tick.set_markeredgecolor(color)
63
63
 
64
64
 
65
- def _style_baseline(
65
+ def style_baseline(
66
66
  ax: Axes,
67
67
  reference: float = 0,
68
68
  **baseline_config: Any,
@@ -106,8 +106,8 @@ def plot_line_chart(
106
106
  pass
107
107
 
108
108
  reference = 100 if base_100 else 0
109
- _style_spines(ax, **format, **AX_CONFIG["spines"])
110
- _style_baseline(ax, reference, **AX_CONFIG["baseline"])
109
+ style_spines(ax, **format, **AX_CONFIG["spines"])
110
+ style_baseline(ax, reference, **AX_CONFIG["baseline"])
111
111
  ax.legend( # type: ignore[reportUnknownMemberType]
112
112
  loc="upper center",
113
113
  bbox_to_anchor=(0.5, LINE_PLOT_CONFIG["legend_sep"]),
@@ -179,8 +179,8 @@ class LinePlot:
179
179
  def __init__(
180
180
  self,
181
181
  out_path: Path,
182
- data_path: Path,
183
- series: dict[str, str],
182
+ data_path: Path | None = None,
183
+ series: dict[str, str] | None = None,
184
184
  scale: float = 1,
185
185
  start_date: datetime.datetime | None = None,
186
186
  end_date: datetime.datetime | None = None,
@@ -189,17 +189,28 @@ class LinePlot:
189
189
  baseline: bool = False,
190
190
  format: Format | None = None,
191
191
  legend: Legend | None = None,
192
+ data: pd.DataFrame | None = None,
192
193
  ) -> None:
193
194
 
194
195
  if out_path.suffix != ".png":
195
196
  raise ValueError(f"The out file {out_path} should be a .png file")
196
197
  self.out_path = out_path
197
198
 
198
- if data_path.suffix != ".feather":
199
- raise ValueError(
200
- f"The data file {data_path} must be a .feather file"
201
- )
202
- self.data = pd.read_feather(data_path)
199
+ if data is not None and data_path is not None:
200
+ raise ValueError("Provide data or data_path, not both")
201
+ if data is not None:
202
+ self.data = data
203
+ elif data_path is not None:
204
+ if data_path.suffix != ".feather":
205
+ raise ValueError(
206
+ f"The data file {data_path} must be a .feather file"
207
+ )
208
+ self.data = pd.read_feather(data_path)
209
+ else:
210
+ raise ValueError("Provide data or data_path")
211
+
212
+ if series is None:
213
+ raise ValueError("series is required")
203
214
 
204
215
  self.base_100 = base_100
205
216
  self.annotate = annotate # unused for the moment
@@ -254,7 +265,7 @@ class LinePlot:
254
265
  pass
255
266
 
256
267
  assert self.format is not None
257
- _style_spines( # maybe make this function accept a Format object
268
+ style_spines( # maybe make this function accept a Format object
258
269
  ax,
259
270
  decimals=self.format.decimals,
260
271
  units=self.format.units,
@@ -262,7 +273,7 @@ class LinePlot:
262
273
  )
263
274
  if self.baseline:
264
275
  reference = 100 if self.base_100 else 0
265
- _style_baseline(ax, reference, **AX_CONFIG["baseline"])
276
+ style_baseline(ax, reference, **AX_CONFIG["baseline"])
266
277
 
267
278
  if self.legend is not None:
268
279
  ax.legend( # type: ignore[reportUnknownMemberType]
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Self
5
+
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ import pandas as pd
9
+ from matplotlib.axes import Axes
10
+ from matplotlib.figure import Figure
11
+ from yaml.nodes import MappingNode
12
+
13
+ from tesorotools.artists.line_plot import (
14
+ AX_CONFIG,
15
+ FIG_CONFIG,
16
+ Format,
17
+ Legend,
18
+ style_baseline,
19
+ style_spines,
20
+ )
21
+ from tesorotools.utils.config import TemplateLoader
22
+
23
+ _DEFAULT_NCOL = 5
24
+ _DEFAULT_SEP = -0.125
25
+
26
+
27
+ class StackedAreaPlot:
28
+ """Stacked area chart with the tesorotools visual style.
29
+
30
+ Parameters match ``LinePlot`` where applicable so that
31
+ chart configs can switch between types by changing a
32
+ single field.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ out_path: Path,
38
+ data: pd.DataFrame,
39
+ series: dict[str, str],
40
+ *,
41
+ scale: float = 1,
42
+ start_date: str | None = None,
43
+ end_date: str | None = None,
44
+ baseline: bool = False,
45
+ format: Format | None = None,
46
+ legend: Legend | None = None,
47
+ ) -> None:
48
+ if out_path.suffix != ".png":
49
+ raise ValueError(f"out_path must be .png: {out_path}")
50
+ self.out_path = out_path
51
+ self.data = data
52
+ self.series = series
53
+ self.scale = scale
54
+ self.start_date = start_date
55
+ self.end_date = end_date
56
+ self.baseline = baseline
57
+ self.format = format or Format()
58
+ self.legend = legend
59
+
60
+ @classmethod
61
+ def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
62
+ cfg: dict[str, Any] = loader.construct_mapping( # type: ignore[assignment]
63
+ node, deep=True
64
+ )
65
+ cfg.pop("id")
66
+ cfg["out_path"] = Path(cfg["out_path"])
67
+ cfg["data"] = pd.read_feather(cfg.pop("data_path"))
68
+ return cls(**cfg)
69
+
70
+ def plot(self) -> Axes:
71
+ start = (
72
+ pd.Timestamp(self.start_date)
73
+ if self.start_date
74
+ else self.data.index.min()
75
+ )
76
+ end = (
77
+ pd.Timestamp(self.end_date)
78
+ if self.end_date
79
+ else self.data.index.max()
80
+ )
81
+
82
+ plot_data = self.data.loc[start:end, list(self.series.keys())].dropna()
83
+ plot_data = plot_data * self.scale
84
+
85
+ fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
86
+ **FIG_CONFIG
87
+ )
88
+ ax: Axes = fig.add_subplot()
89
+
90
+ labels = list(self.series.values())
91
+ arrays: list[np.ndarray[tuple[int], np.dtype[np.float64]]] = [
92
+ plot_data[col].to_numpy(dtype=np.float64) for col in self.series
93
+ ]
94
+ ax.stackplot( # type: ignore[reportUnknownMemberType]
95
+ plot_data.index,
96
+ *arrays,
97
+ labels=labels,
98
+ alpha=0.85,
99
+ )
100
+
101
+ style_spines(
102
+ ax,
103
+ decimals=self.format.decimals,
104
+ units=self.format.units,
105
+ **AX_CONFIG["spines"],
106
+ )
107
+ if self.baseline:
108
+ style_baseline(ax, 0, **AX_CONFIG["baseline"])
109
+
110
+ ncol = self.legend.ncol if self.legend else _DEFAULT_NCOL
111
+ sep = self.legend.sep if self.legend else _DEFAULT_SEP
112
+ ax.legend( # type: ignore[reportUnknownMemberType]
113
+ loc="upper center",
114
+ bbox_to_anchor=(0.5, sep),
115
+ ncol=ncol,
116
+ )
117
+
118
+ fig.savefig( # type: ignore[reportUnknownMemberType]
119
+ self.out_path
120
+ )
121
+ plt.close(fig)
122
+ return ax
123
+
124
+
125
+ class StackedBarPlot:
126
+ """Stacked bar chart with the tesorotools visual style.
127
+
128
+ Positive and negative values are stacked separately so
129
+ that bars extend in both directions from the baseline.
130
+ """
131
+
132
+ def __init__(
133
+ self,
134
+ out_path: Path,
135
+ data: pd.DataFrame,
136
+ series: dict[str, str],
137
+ *,
138
+ scale: float = 1,
139
+ start_date: str | None = None,
140
+ end_date: str | None = None,
141
+ baseline: bool = True,
142
+ format: Format | None = None,
143
+ legend: Legend | None = None,
144
+ ) -> None:
145
+ if out_path.suffix != ".png":
146
+ raise ValueError(f"out_path must be .png: {out_path}")
147
+ self.out_path = out_path
148
+ self.data = data
149
+ self.series = series
150
+ self.scale = scale
151
+ self.start_date = start_date
152
+ self.end_date = end_date
153
+ self.baseline = baseline
154
+ self.format = format or Format()
155
+ self.legend = legend
156
+
157
+ @classmethod
158
+ def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
159
+ cfg: dict[str, Any] = loader.construct_mapping( # type: ignore[assignment]
160
+ node, deep=True
161
+ )
162
+ cfg.pop("id")
163
+ cfg["out_path"] = Path(cfg["out_path"])
164
+ cfg["data"] = pd.read_feather(cfg.pop("data_path"))
165
+ return cls(**cfg)
166
+
167
+ def plot(self) -> Axes:
168
+ start = (
169
+ pd.Timestamp(self.start_date)
170
+ if self.start_date
171
+ else self.data.index.min()
172
+ )
173
+ end = (
174
+ pd.Timestamp(self.end_date)
175
+ if self.end_date
176
+ else self.data.index.max()
177
+ )
178
+
179
+ plot_data = self.data.loc[start:end, list(self.series.keys())].dropna()
180
+ plot_data = plot_data * self.scale
181
+
182
+ fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
183
+ figsize=(12, 6), **FIG_CONFIG
184
+ )
185
+ ax: Axes = fig.add_subplot()
186
+
187
+ cols = list(self.series.keys())
188
+ labels = list(self.series.values())
189
+
190
+ x = np.arange(len(plot_data))
191
+ bar_width = 0.7
192
+
193
+ pos_bottom: np.ndarray[tuple[int], np.dtype[np.float64]] = np.zeros(
194
+ len(plot_data)
195
+ )
196
+ neg_bottom: np.ndarray[tuple[int], np.dtype[np.float64]] = np.zeros(
197
+ len(plot_data)
198
+ )
199
+
200
+ for col, label in zip(cols, labels):
201
+ values: np.ndarray[tuple[int], np.dtype[np.float64]] = plot_data[
202
+ col
203
+ ].to_numpy(dtype=np.float64)
204
+ pos: np.ndarray[tuple[int], np.dtype[np.float64]] = np.where(
205
+ values >= 0, values, 0.0
206
+ )
207
+ neg: np.ndarray[tuple[int], np.dtype[np.float64]] = np.where(
208
+ values < 0, values, 0.0
209
+ )
210
+
211
+ color = (
212
+ ax.bar( # type: ignore[reportUnknownMemberType]
213
+ x,
214
+ pos,
215
+ bottom=pos_bottom,
216
+ width=bar_width,
217
+ label=label,
218
+ )
219
+ .patches[0]
220
+ .get_facecolor()
221
+ )
222
+ ax.bar( # type: ignore[reportUnknownMemberType]
223
+ x,
224
+ neg,
225
+ bottom=neg_bottom,
226
+ width=bar_width,
227
+ color=color,
228
+ )
229
+ pos_bottom = pos_bottom + pos
230
+ neg_bottom = neg_bottom + neg
231
+
232
+ dates = plot_data.index
233
+ step = max(1, len(dates) // 12)
234
+ tick_pos = list(range(0, len(dates), step))
235
+ tick_labels = [dates[i].strftime("%Y") for i in tick_pos]
236
+ ax.set_xticks( # type: ignore[reportUnknownMemberType]
237
+ tick_pos
238
+ )
239
+ ax.set_xticklabels( # type: ignore[reportUnknownMemberType]
240
+ tick_labels
241
+ )
242
+
243
+ style_spines(
244
+ ax,
245
+ decimals=self.format.decimals,
246
+ units=self.format.units,
247
+ **AX_CONFIG["spines"],
248
+ )
249
+ if self.baseline:
250
+ style_baseline(ax, 0, **AX_CONFIG["baseline"])
251
+
252
+ ncol = self.legend.ncol if self.legend else _DEFAULT_NCOL
253
+ sep = self.legend.sep if self.legend else _DEFAULT_SEP
254
+ ax.legend( # type: ignore[reportUnknownMemberType]
255
+ loc="upper center",
256
+ bbox_to_anchor=(0.5, sep),
257
+ ncol=ncol,
258
+ )
259
+
260
+ fig.savefig( # type: ignore[reportUnknownMemberType]
261
+ self.out_path
262
+ )
263
+ plt.close(fig)
264
+ return ax
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- # pending to assure stylesheet data and fonts are only loaded once
4
-
5
3
  from pathlib import Path
6
4
  from typing import Any
7
5
 
@@ -9,9 +7,12 @@ import matplotlib.pyplot as plt
9
7
  import pandas as pd
10
8
  from matplotlib.axes import Axes
11
9
  from matplotlib.figure import Figure
12
- from matplotlib.ticker import FuncFormatter
13
10
  from pandas import Timestamp
14
11
 
12
+ from tesorotools.artists.line_plot import (
13
+ style_baseline,
14
+ style_spines,
15
+ )
15
16
  from tesorotools.utils.config import merge
16
17
  from tesorotools.utils.matplotlib import (
17
18
  PLOT_CONFIG,
@@ -26,54 +27,13 @@ FIG_CONFIG: dict[str, Any] = PLOT_CONFIG["figure"]
26
27
  load_fonts()
27
28
 
28
29
 
29
- def _style_spines(
30
- ax: Axes,
31
- decimals: int,
32
- units: str,
33
- *,
34
- color: str,
35
- linewidth: float,
36
- ) -> None:
37
- ax.grid( # type: ignore[reportUnknownMemberType]
38
- visible=True, axis="y"
39
- )
40
- for spine in ax.spines.values():
41
- spine.set_color(color)
42
- spine.set_linewidth(linewidth)
43
- ax.yaxis.tick_right()
44
-
45
- def _fmt_tick(y: float, _pos: int) -> str:
46
- return format_annotation(y, decimals, units)
47
-
48
- ax.yaxis.set_major_formatter(FuncFormatter(_fmt_tick))
49
- ax.tick_params( # type: ignore[reportUnknownMemberType]
50
- axis="both", which="major"
51
- )
30
+ def _rotate_xticks(ax: Axes) -> None:
52
31
  ax.set_xticks( # type: ignore[reportUnknownMemberType]
53
32
  ax.get_xticks(),
54
33
  ax.get_xticklabels(), # type: ignore[reportArgumentType]
55
34
  rotation=45,
56
35
  ha="right",
57
36
  )
58
- for tick in ax.get_xticklines():
59
- tick.set_markeredgecolor(color)
60
- for tick in ax.get_yticklines():
61
- tick.set_markeredgecolor(color)
62
-
63
-
64
- def _style_baseline(ax: Axes, **baseline_config: Any) -> None:
65
- color: str = baseline_config["color"]
66
- bottom_lim, top_lim = ax.get_ylim()
67
- ax.set_ylim(bottom=min(0, bottom_lim), top=max(0, top_lim))
68
- bottom_lim, top_lim = ax.get_ylim()
69
- if bottom_lim == 0:
70
- ax.spines["bottom"].set_edgecolor(color)
71
- elif top_lim == 0:
72
- ax.spines["top"].set_edgecolor(color)
73
- else:
74
- ax.axhline( # type: ignore[reportUnknownMemberType]
75
- y=0, **baseline_config
76
- )
77
37
 
78
38
 
79
39
  def _format_data(data: pd.DataFrame) -> dict[str, Any]:
@@ -204,8 +164,9 @@ def plot_type_curve(
204
164
  formatted_assets["current_date"],
205
165
  **merged_config["line"],
206
166
  )
207
- _style_spines(ax, **merged_config["yaxis"], **AX_CONFIG["spines"])
208
- _style_baseline(ax, **AX_CONFIG["baseline"])
167
+ style_spines(ax, **merged_config["yaxis"], **AX_CONFIG["spines"])
168
+ _rotate_xticks(ax)
169
+ style_baseline(ax, 0, **AX_CONFIG["baseline"])
209
170
  ax.legend( # type: ignore[reportUnknownMemberType]
210
171
  loc="upper center",
211
172
  bbox_to_anchor=(0.5, merged_config["legend_sep"]),
@@ -90,7 +90,7 @@ if __name__ == "__main__":
90
90
  ]
91
91
  independent_trimmed_df: pd.DataFrame = trim(independent_full_df) # type: ignore[assignment]
92
92
  dependent_trimmed_df: pd.DataFrame = compute_derivate_series(
93
- resolved_dict["dependent"], # type: ignore[arg-type]
93
+ resolved_dict["rules"], # type: ignore[arg-type]
94
94
  independent_trimmed_df,
95
95
  )
96
96
  offsets_config: Any = read_config(Path("examples") / "offsets.yaml")
@@ -1,9 +1,14 @@
1
+ import logging
1
2
  import time
2
3
  from pathlib import Path
3
4
 
4
5
  import lseg.data as ld
5
6
  import pandas as pd
6
7
 
8
+ logger = logging.getLogger(__name__)
9
+
10
+ MAX_RETRIES = 150
11
+
7
12
 
8
13
  def get_series(
9
14
  api_key: str,
@@ -16,7 +21,7 @@ def get_series(
16
21
  datapoint_limit: int = 2_000,
17
22
  cache_path: Path | None = None,
18
23
  ) -> pd.DataFrame:
19
- """Downloads data from LSEG given that you have a valid API key"""
24
+ """Downloads data from LSEG given a valid API key."""
20
25
  ld.open_session(app_key=api_key)
21
26
  fields = ["TIMESTAMP", "CLOSE"] if fields is None else fields
22
27
 
@@ -53,7 +58,10 @@ def get_series(
53
58
  if cache_file_path is None:
54
59
  partial_data.append(data)
55
60
  if downloaded_dates + download_step < len(dates_list):
56
- print(f"Waiting {cooldown} seconds for LSEG to cool down...")
61
+ logger.info(
62
+ "Waiting %d seconds for LSEG cooldown...",
63
+ cooldown,
64
+ )
57
65
  time.sleep(cooldown)
58
66
  downloaded_dates += download_step
59
67
  return data
@@ -67,10 +75,11 @@ def block_download(
67
75
  fields: list[str] | None = None,
68
76
  cooldown: int = 60,
69
77
  file_path: Path | None = None,
78
+ max_retries: int = MAX_RETRIES,
70
79
  ) -> pd.DataFrame:
71
80
  interval = "daily" if freq == "B" else freq
72
81
 
73
- while True:
82
+ for attempt in range(1, max_retries + 1):
74
83
  try:
75
84
  data: pd.DataFrame | None = ld.get_history( # type: ignore[reportUnknownMemberType]
76
85
  universe=series_id_list,
@@ -92,11 +101,22 @@ def block_download(
92
101
  data.to_csv(file_path)
93
102
  return data
94
103
  except ld.errors.LDError as e:
95
- print(f"LSEG error: {e}")
96
- print("This is probably not our fault")
97
- print(f"Waiting {cooldown} seconds for LSEG to cool down...")
104
+ logger.warning(
105
+ "LSEG error (attempt %d/%d): %s",
106
+ attempt,
107
+ max_retries,
108
+ e,
109
+ )
110
+ if attempt == max_retries:
111
+ raise
112
+ logger.info(
113
+ "Waiting %d seconds for LSEG cooldown...",
114
+ cooldown,
115
+ )
98
116
  time.sleep(cooldown)
99
117
 
118
+ raise RuntimeError("Unreachable")
119
+
100
120
 
101
121
  def concat_partial_data(
102
122
  cache_path: Path | None,
@@ -44,14 +44,9 @@ def flash_to_database_format(data: pd.DataFrame) -> pd.DataFrame:
44
44
  columns=["offset", "difference_type", "stat"]
45
45
  )
46
46
  database_data["value_meta"] = pd.NA
47
- print(database_data)
48
47
  return database_data
49
48
 
50
49
 
51
- def database_to_flash_format() -> None:
52
- pass
53
-
54
-
55
50
  def push_to_database(data: pd.DataFrame, conn_string: str, table: str) -> None:
56
51
  engine: Any = create_engine(url=conn_string)
57
52
  data.to_sql(
@@ -61,9 +56,3 @@ def push_to_database(data: pd.DataFrame, conn_string: str, table: str) -> None:
61
56
  chunksize=1000,
62
57
  index=False,
63
58
  )
64
-
65
-
66
- def pull_from_database(
67
- conn_string: str, start: str, end: str, series: list[str]
68
- ) -> None:
69
- _ = conn_string, start, end, series