tesorotools-python 0.0.35__tar.gz → 0.0.36__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 (75) hide show
  1. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/PKG-INFO +1 -1
  2. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/pyproject.toml +1 -1
  3. tesorotools_python-0.0.36/src/tesorotools/__init__.py +115 -0
  4. tesorotools_python-0.0.36/src/tesorotools/_registry.py +117 -0
  5. tesorotools_python-0.0.36/src/tesorotools/artists/__init__.py +25 -0
  6. tesorotools_python-0.0.36/src/tesorotools/artists/type_curve.py +229 -0
  7. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/convert.py +17 -3
  8. tesorotools_python-0.0.36/src/tesorotools/providers/__init__.py +32 -0
  9. tesorotools_python-0.0.36/src/tesorotools/render/__init__.py +22 -0
  10. tesorotools_python-0.0.36/src/tesorotools/utils/config.py +38 -0
  11. tesorotools_python-0.0.35/src/tesorotools/artists/__init__.py +0 -5
  12. tesorotools_python-0.0.35/src/tesorotools/artists/type_curve.py +0 -201
  13. tesorotools_python-0.0.35/src/tesorotools/providers/__init__.py +0 -0
  14. tesorotools_python-0.0.35/src/tesorotools/render/__init__.py +0 -0
  15. tesorotools_python-0.0.35/src/tesorotools/render/content/__init__.py +0 -0
  16. tesorotools_python-0.0.35/src/tesorotools/utils/config.py +0 -98
  17. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/.gitignore +0 -0
  18. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/artists/barh.md +0 -0
  19. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/artists/barh_plot.py +0 -0
  20. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/artists/line_plot.py +0 -0
  21. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/artists/stacked.py +0 -0
  22. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/artists/table.py +0 -0
  23. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/README.md +0 -0
  24. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
  25. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
  26. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
  27. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
  28. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
  29. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
  30. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
  31. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
  32. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/fonts/README.md +0 -0
  33. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/plots.yaml +0 -0
  34. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/assets/tesoro.mplstyle +0 -0
  35. {tesorotools_python-0.0.35/src/tesorotools → tesorotools_python-0.0.36/src/tesorotools/data_sources}/__init__.py +0 -0
  36. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/data_sources/debug.py +0 -0
  37. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/database/__init__.py +0 -0
  38. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/database/local.py +0 -0
  39. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/database/push.py +0 -0
  40. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/database/shared.py +0 -0
  41. {tesorotools_python-0.0.35/src/tesorotools/data_sources → tesorotools_python-0.0.36/src/tesorotools/dependencies}/__init__.py +0 -0
  42. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/dependencies/node.py +0 -0
  43. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/dependencies/resolution.py +0 -0
  44. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/driver.py +0 -0
  45. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/main.py +0 -0
  46. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/manifest.py +0 -0
  47. {tesorotools_python-0.0.35/src/tesorotools/dependencies → tesorotools_python-0.0.36/src/tesorotools/offsets}/__init__.py +0 -0
  48. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/offsets/offsets.py +0 -0
  49. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/offsets/outliers.py +0 -0
  50. {tesorotools_python-0.0.35/src/tesorotools/offsets → tesorotools_python-0.0.36/src/tesorotools/pipeline}/__init__.py +0 -0
  51. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/pipeline/diagnose.py +0 -0
  52. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/pipeline/engine.py +0 -0
  53. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/pipeline/rules.py +0 -0
  54. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/providers/base.py +0 -0
  55. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/providers/bde.py +0 -0
  56. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/providers/ecb.py +0 -0
  57. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/py.typed +0 -0
  58. {tesorotools_python-0.0.35/src/tesorotools/pipeline → tesorotools_python-0.0.36/src/tesorotools/render/content}/__init__.py +0 -0
  59. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/render/content/content.py +0 -0
  60. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/render/content/images.py +0 -0
  61. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/render/content/section.py +0 -0
  62. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/render/content/subtitle.py +0 -0
  63. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/render/content/table.py +0 -0
  64. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/render/content/text.py +0 -0
  65. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/render/content/title.py +0 -0
  66. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/render/report.py +0 -0
  67. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/testing/__init__.py +0 -0
  68. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/testing/compare.py +0 -0
  69. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/utils/__init__.py +0 -0
  70. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/utils/format.py +0 -0
  71. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/utils/globals.py +0 -0
  72. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/utils/matplotlib.py +0 -0
  73. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/utils/series.py +0 -0
  74. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/src/tesorotools/utils/shortcuts.py +0 -0
  75. {tesorotools_python-0.0.35 → tesorotools_python-0.0.36}/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.35
3
+ Version: 0.0.36
4
4
  Requires-Python: >=3.13
5
5
  Requires-Dist: babel>=2.17
6
6
  Requires-Dist: matplotlib>=3.10
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "tesorotools-python"
3
3
  requires-python = ">=3.13"
4
- version = "0.0.35"
4
+ version = "0.0.36"
5
5
  dependencies = [
6
6
  # database and ORM
7
7
  "psycopg[binary]>=3.1",
@@ -0,0 +1,115 @@
1
+ """tesorotools public API.
2
+
3
+ Importing ``tesorotools`` eagerly loads artist and render
4
+ classes (their modules run matplotlib/locale setup as side
5
+ effects) and registers their YAML tags via
6
+ ``_register_builtins``.
7
+
8
+ Provider subclasses gated by optional extras
9
+ (``BdeProvider`` requires ``[bde]``, ``EcbProvider``
10
+ requires ``[ecb]``) are exposed lazily through
11
+ ``__getattr__``; importing this module does not require the
12
+ extras to be installed.
13
+
14
+ Third parties extend the package via ``register_artist``,
15
+ ``register_tag``, and ``register_provider``.
16
+ """
17
+
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ if TYPE_CHECKING:
21
+ from tesorotools.providers.bde import BdeProvider
22
+ from tesorotools.providers.ecb import EcbProvider
23
+
24
+ from tesorotools._registry import (
25
+ Artist,
26
+ get_artist,
27
+ get_provider,
28
+ register_artist,
29
+ register_provider,
30
+ register_tag,
31
+ )
32
+ from tesorotools.artists import (
33
+ Format,
34
+ HorizontalBarChart,
35
+ Legend,
36
+ LinePlot,
37
+ StackedAreaPlot,
38
+ StackedBarPlot,
39
+ TypeCurve,
40
+ )
41
+ from tesorotools.providers.base import DataProvider
42
+ from tesorotools.render import (
43
+ Content,
44
+ Image,
45
+ Images,
46
+ Report,
47
+ Section,
48
+ Subtitle,
49
+ Table,
50
+ Text,
51
+ Title,
52
+ )
53
+
54
+
55
+ def _register_builtins() -> None:
56
+ register_artist("line_plot", LinePlot)
57
+ register_artist("stacked_area", StackedAreaPlot)
58
+ register_artist("stacked_bar", StackedBarPlot)
59
+ register_artist("barh", HorizontalBarChart)
60
+ register_artist("type_curve", TypeCurve)
61
+
62
+ register_tag("format", Format)
63
+ register_tag("legend", Legend)
64
+ register_tag("report", Report)
65
+ register_tag("section", Section)
66
+ register_tag("image", Image)
67
+ register_tag("images", Images)
68
+ register_tag("table", Table)
69
+ register_tag("text", Text)
70
+ register_tag("title", Title)
71
+ register_tag("subtitle", Subtitle)
72
+
73
+
74
+ _register_builtins()
75
+
76
+
77
+ __all__ = [
78
+ "Artist",
79
+ "BdeProvider",
80
+ "Content",
81
+ "DataProvider",
82
+ "EcbProvider",
83
+ "Format",
84
+ "HorizontalBarChart",
85
+ "Image",
86
+ "Images",
87
+ "Legend",
88
+ "LinePlot",
89
+ "Report",
90
+ "Section",
91
+ "StackedAreaPlot",
92
+ "StackedBarPlot",
93
+ "Subtitle",
94
+ "Table",
95
+ "Text",
96
+ "Title",
97
+ "TypeCurve",
98
+ "get_artist",
99
+ "get_provider",
100
+ "register_artist",
101
+ "register_provider",
102
+ "register_tag",
103
+ ]
104
+
105
+
106
+ def __getattr__(name: str) -> Any:
107
+ if name == "BdeProvider":
108
+ from tesorotools.providers.bde import BdeProvider
109
+
110
+ return BdeProvider
111
+ if name == "EcbProvider":
112
+ from tesorotools.providers.ecb import EcbProvider
113
+
114
+ return EcbProvider
115
+ raise AttributeError(f"module 'tesorotools' has no attribute {name!r}")
@@ -0,0 +1,117 @@
1
+ """Registries for artists, providers, and YAML tags.
2
+
3
+ Single source of truth for "name -> class" lookups used both
4
+ by the YAML loader (``TemplateLoader``) and by code that
5
+ dispatches by string name.
6
+
7
+ Three public entry points:
8
+
9
+ ``register_artist(name, cls)``
10
+ Adds ``cls`` to the artist registry **and** registers the
11
+ YAML tag ``!{name}`` via ``cls.from_yaml``. Use for
12
+ chart/figure classes (LinePlot, StackedAreaPlot, etc.).
13
+
14
+ ``register_tag(name, constructor)``
15
+ Registers a YAML tag ``!{name}`` only. Use for things
16
+ that exist as YAML constructors but are not artists or
17
+ providers (Format, Legend, Title, Subtitle, Section,
18
+ Image, Text, Table, Report).
19
+
20
+ ``register_provider(name, cls)``
21
+ Adds ``cls`` to the provider registry. Programmatic
22
+ only -- providers do not appear in YAML today.
23
+
24
+ Look up registered classes via ``get_artist`` / ``get_provider``;
25
+ both raise ``KeyError`` listing the available names.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from typing import Any, Callable, Protocol, cast
31
+
32
+ from yaml.nodes import MappingNode
33
+
34
+ from tesorotools.providers.base import DataProvider
35
+ from tesorotools.utils.template import TemplateLoader
36
+
37
+
38
+ class Artist(Protocol):
39
+ """Anything constructible from a YAML mapping node.
40
+
41
+ Concrete artists satisfy this Protocol structurally;
42
+ inheriting from it is not required.
43
+
44
+ The ``loader`` parameter is typed as ``Any`` because some
45
+ in-tree ``from_yaml`` classmethods are declared with
46
+ ``yaml.Loader`` (the base class) and others with
47
+ ``TemplateLoader``; both work at runtime since the loader
48
+ is always a ``TemplateLoader``.
49
+ """
50
+
51
+ @classmethod
52
+ def from_yaml(cls, loader: Any, node: MappingNode) -> Any: ...
53
+
54
+
55
+ YamlConstructor = Callable[[Any, MappingNode], Any]
56
+
57
+
58
+ _ARTIST_REGISTRY: dict[str, type[Artist]] = {}
59
+ _PROVIDER_REGISTRY: dict[str, type[DataProvider]] = {}
60
+
61
+
62
+ def register_artist(name: str, cls: type[Artist]) -> None:
63
+ """Register ``cls`` as the artist for ``name``.
64
+
65
+ Adds ``cls`` to the artist registry and binds the YAML
66
+ tag ``!{name}`` to ``cls.from_yaml``. Re-registering the
67
+ same name overrides both bindings.
68
+ """
69
+ _ARTIST_REGISTRY[name] = cls
70
+ TemplateLoader.add_constructor(
71
+ f"!{name}", cast(YamlConstructor, cls.from_yaml)
72
+ )
73
+
74
+
75
+ def get_artist(name: str) -> type[Artist]:
76
+ try:
77
+ return _ARTIST_REGISTRY[name]
78
+ except KeyError:
79
+ available = sorted(_ARTIST_REGISTRY)
80
+ raise KeyError(
81
+ f"No artist registered as {name!r}. Available: {available}"
82
+ ) from None
83
+
84
+
85
+ def register_provider(name: str, cls: type[DataProvider]) -> None:
86
+ """Register ``cls`` as the provider for ``name``."""
87
+ _PROVIDER_REGISTRY[name] = cls
88
+
89
+
90
+ def get_provider(name: str) -> type[DataProvider]:
91
+ try:
92
+ return _PROVIDER_REGISTRY[name]
93
+ except KeyError:
94
+ available = sorted(_PROVIDER_REGISTRY)
95
+ raise KeyError(
96
+ f"No provider registered as {name!r}. Available: {available}"
97
+ ) from None
98
+
99
+
100
+ def register_tag(
101
+ name: str,
102
+ constructor: type[Artist] | YamlConstructor,
103
+ ) -> None:
104
+ """Register a YAML tag ``!{name}``.
105
+
106
+ Accepts either a class with a ``from_yaml`` classmethod
107
+ or a bare callable matching the loader signature. Use
108
+ ``register_artist`` / ``register_provider`` instead when
109
+ the registered name should also be looked up by code.
110
+ """
111
+ if isinstance(constructor, type):
112
+ cls = cast(type[Artist], constructor)
113
+ TemplateLoader.add_constructor(
114
+ f"!{name}", cast(YamlConstructor, cls.from_yaml)
115
+ )
116
+ else:
117
+ TemplateLoader.add_constructor(f"!{name}", constructor)
@@ -0,0 +1,25 @@
1
+ """Public artist API.
2
+
3
+ Importing this module applies the matplotlib stylesheet
4
+ defined by ``tesorotools.utils.globals.STYLE_SHEET``.
5
+ """
6
+
7
+ import matplotlib.style
8
+
9
+ from tesorotools.artists.barh_plot import HorizontalBarChart
10
+ from tesorotools.artists.line_plot import Format, Legend, LinePlot
11
+ from tesorotools.artists.stacked import StackedAreaPlot, StackedBarPlot
12
+ from tesorotools.artists.type_curve import TypeCurve
13
+ from tesorotools.utils.globals import STYLE_SHEET
14
+
15
+ matplotlib.style.use(STYLE_SHEET)
16
+
17
+ __all__ = [
18
+ "Format",
19
+ "HorizontalBarChart",
20
+ "Legend",
21
+ "LinePlot",
22
+ "StackedAreaPlot",
23
+ "StackedBarPlot",
24
+ "TypeCurve",
25
+ ]
@@ -0,0 +1,229 @@
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 pandas as pd
8
+ from matplotlib.axes import Axes
9
+ from matplotlib.figure import Figure
10
+ from pandas import Timestamp
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.matplotlib import (
22
+ PLOT_CONFIG,
23
+ format_annotation,
24
+ load_fonts,
25
+ )
26
+ from tesorotools.utils.template import TemplateLoader
27
+
28
+ TYPE_CURVE_CONFIG: dict[str, Any] = PLOT_CONFIG["type_curve"]
29
+
30
+ load_fonts()
31
+
32
+
33
+ def _rotate_xticks(ax: Axes) -> None:
34
+ ax.set_xticks( # type: ignore[reportUnknownMemberType]
35
+ ax.get_xticks(),
36
+ ax.get_xticklabels(), # type: ignore[reportArgumentType]
37
+ rotation=45,
38
+ ha="right",
39
+ )
40
+
41
+
42
+ def _format_data(data: pd.DataFrame) -> dict[str, Any]:
43
+ """Reduce raw daily data to the five series the chart draws.
44
+
45
+ Returns max/min envelopes for the current and previous
46
+ years plus the snapshot of the most recent observation.
47
+ """
48
+ date_index: pd.DatetimeIndex = data.index # type: ignore[assignment]
49
+ current_date: Timestamp = date_index.max() # type: ignore[assignment]
50
+ current_year: int = current_date.year
51
+ last_year: int = (current_date - pd.DateOffset(years=1)).year
52
+
53
+ current_data: pd.Series[Any] = data.loc[current_date, :] # type: ignore[assignment]
54
+ current_data.name = "current_data"
55
+
56
+ current_year_data: pd.DataFrame = data.loc[
57
+ date_index.year == current_year, :
58
+ ]
59
+ current_year_max: pd.Series[Any] = current_year_data.max()
60
+ current_year_max.name = "current_year_max"
61
+ current_year_min: pd.Series[Any] = current_year_data.min()
62
+ current_year_min.name = "current_year_min"
63
+
64
+ last_year_data: pd.DataFrame = data.loc[date_index.year == last_year, :]
65
+ last_year_max: pd.Series[Any] = last_year_data.max()
66
+ last_year_max.name = "last_year_max"
67
+ last_year_min: pd.Series[Any] = last_year_data.min()
68
+ last_year_min.name = "last_year_min"
69
+
70
+ formatted_data: pd.DataFrame = pd.concat(
71
+ [
72
+ last_year_max,
73
+ last_year_min,
74
+ current_year_max,
75
+ current_year_min,
76
+ current_data,
77
+ ],
78
+ axis=1,
79
+ )
80
+
81
+ return {
82
+ "data": formatted_data,
83
+ "current_date": current_date.strftime("%d/%m/%Y"),
84
+ "current_year": current_year,
85
+ "last_year": last_year,
86
+ }
87
+
88
+
89
+ class TypeCurve:
90
+ """Type-curve chart with the tesorotools visual style.
91
+
92
+ Plots two ranges (last year and current year) as filled
93
+ bands plus the latest snapshot as a marked line, with
94
+ the due-period axis on x and the rate/yield on y.
95
+
96
+ Parameters mirror ``LinePlot`` / ``StackedAreaPlot``
97
+ where applicable; visual constants (band colors, line
98
+ width, marker) come from ``PLOT_CONFIG['type_curve']``.
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ out_path: Path,
104
+ data: pd.DataFrame,
105
+ series: dict[str, str],
106
+ *,
107
+ scale: float = 1,
108
+ fmt: Format | None = None,
109
+ legend: Legend | None = None,
110
+ figsize: tuple[float, float] | None = None,
111
+ points_to_mark: list[str] | None = None,
112
+ ) -> None:
113
+ if out_path.suffix != ".png":
114
+ raise ValueError(f"out_path must be .png: {out_path}")
115
+ if len(series) < 2:
116
+ raise ValueError(
117
+ "A type curve must have at least two due periods. "
118
+ f"Given: {list(series)}"
119
+ )
120
+ self.out_path = out_path
121
+ self.data = data
122
+ self.series = series
123
+ self.scale = scale
124
+ self.fmt = fmt or Format()
125
+ self.legend = legend
126
+ self.figsize = figsize
127
+ self.points_to_mark = points_to_mark or []
128
+
129
+ @classmethod
130
+ def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
131
+ cfg: dict[str, Any] = loader.construct_mapping( # type: ignore[assignment]
132
+ node, deep=True
133
+ )
134
+ cfg.pop("id")
135
+ cfg["out_path"] = Path(cfg["out_path"])
136
+ cfg["data"] = pd.read_feather(cfg.pop("data_path"))
137
+ return cls(**cfg)
138
+
139
+ def plot(self) -> Axes:
140
+ plot_data = self.data.loc[:, list(self.series.keys())]
141
+ plot_data = plot_data.rename(columns=self.series)
142
+ plot_data = plot_data * self.scale
143
+
144
+ formatted_assets = _format_data(plot_data)
145
+ formatted_data: pd.DataFrame = formatted_assets["data"]
146
+ due_index: pd.Index[Any] = formatted_data.index
147
+
148
+ fig_kw = dict(FIG_CONFIG)
149
+ if self.figsize is not None:
150
+ fig_kw["figsize"] = self.figsize
151
+ fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
152
+ **fig_kw
153
+ )
154
+ ax: Axes = fig.add_subplot()
155
+
156
+ last_config: dict[str, Any] = TYPE_CURVE_CONFIG["last"]
157
+ ax.fill_between( # type: ignore[reportUnknownMemberType]
158
+ due_index,
159
+ formatted_data["last_year_min"],
160
+ formatted_data["last_year_max"],
161
+ alpha=last_config["alpha"],
162
+ color=last_config["color"],
163
+ edgecolor=None,
164
+ label=f"Rango {formatted_assets['last_year']}",
165
+ )
166
+
167
+ current_config: dict[str, Any] = TYPE_CURVE_CONFIG["current"]
168
+ ax.fill_between( # type: ignore[reportUnknownMemberType]
169
+ due_index,
170
+ formatted_data["current_year_min"],
171
+ formatted_data["current_year_max"],
172
+ alpha=current_config["alpha"],
173
+ color=current_config["color"],
174
+ edgecolor=None,
175
+ label=f"Rango {formatted_assets['current_year']}",
176
+ )
177
+
178
+ line_config: dict[str, Any] = TYPE_CURVE_CONFIG["line"]
179
+ current = formatted_data["current_data"]
180
+ current.plot(
181
+ ax=ax,
182
+ color=line_config["color"],
183
+ linewidth=line_config["linewidth"],
184
+ label=formatted_assets["current_date"],
185
+ )
186
+ for code in self.points_to_mark:
187
+ if code not in self.series:
188
+ raise KeyError(
189
+ f"points_to_mark entry {code!r} not in series keys: "
190
+ f"{list(self.series)}"
191
+ )
192
+ label = self.series[code]
193
+ value: float = current.loc[label]
194
+ ax.plot( # type: ignore[reportUnknownMemberType]
195
+ label,
196
+ value,
197
+ marker=line_config["marker"],
198
+ color=line_config["color"],
199
+ )
200
+ ax.annotate( # type: ignore[reportUnknownMemberType]
201
+ format_annotation(value, self.fmt.decimals, self.fmt.units),
202
+ (label, value), # type: ignore[reportArgumentType]
203
+ textcoords="offset points",
204
+ xytext=(0, 10),
205
+ ha="center",
206
+ )
207
+
208
+ style_spines(
209
+ ax,
210
+ decimals=self.fmt.decimals,
211
+ units=self.fmt.units,
212
+ **AX_CONFIG["spines"],
213
+ )
214
+ _rotate_xticks(ax)
215
+ style_baseline(ax, 0, **AX_CONFIG["baseline"])
216
+
217
+ handles, labels = ax.get_legend_handles_labels()
218
+ ncol = self.legend.ncol if (self.legend and self.legend.ncol) else 3
219
+ fig.legend( # type: ignore[reportUnknownMemberType]
220
+ handles,
221
+ labels,
222
+ loc="outside lower center",
223
+ ncol=ncol,
224
+ )
225
+ fig.savefig( # type: ignore[reportUnknownMemberType]
226
+ self.out_path
227
+ )
228
+ plt.close(fig)
229
+ return ax
@@ -6,9 +6,9 @@ from typing import Any
6
6
  import pandas as pd
7
7
 
8
8
  from tesorotools.artists.barh_plot import plot_barh_charts_from_flash
9
- from tesorotools.artists.line_plot import plot_line_charts
9
+ from tesorotools.artists.line_plot import Format, plot_line_charts
10
10
  from tesorotools.artists.table import generate_tables_from_flash
11
- from tesorotools.artists.type_curve import plot_type_curves
11
+ from tesorotools.artists.type_curve import TypeCurve
12
12
  from tesorotools.dependencies.resolution import (
13
13
  compute_derivate_series,
14
14
  concat_derivate_series,
@@ -105,5 +105,19 @@ if __name__ == "__main__":
105
105
  trimmed_df: pd.DataFrame = trim(full_df) # type: ignore[assignment]
106
106
  plot_barh_charts_from_flash(Path("."), full_df, barh_config_dicts)
107
107
  plot_line_charts(Path("."), trimmed_df, line_config_dicts)
108
- plot_type_curves(Path("."), trimmed_df, type_config_dicts)
108
+ for name, curve_cfg in type_config_dicts.items():
109
+ if name.startswith("."):
110
+ continue
111
+ yaxis_cfg: dict[str, Any] = curve_cfg.get("yaxis", {})
112
+ line_cfg: dict[str, Any] = curve_cfg.get("line", {})
113
+ TypeCurve(
114
+ out_path=Path(".") / f"{name}.png",
115
+ data=trimmed_df,
116
+ series=curve_cfg["series"],
117
+ fmt=Format(
118
+ units=yaxis_cfg.get("units", ""),
119
+ decimals=yaxis_cfg.get("decimals", 0),
120
+ ),
121
+ points_to_mark=line_cfg.get("points_to_mark"),
122
+ ).plot()
109
123
  generate_tables_from_flash(Path("."), full_df, table_config_dicts)
@@ -0,0 +1,32 @@
1
+ """Public provider API.
2
+
3
+ ``BdeProvider`` and ``EcbProvider`` depend on the optional
4
+ ``[bde]`` / ``[ecb]`` extras (which install ``requests``)
5
+ and are imported lazily through ``__getattr__``; importing
6
+ ``tesorotools.providers`` itself does not require those
7
+ extras.
8
+ """
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from tesorotools.providers.base import DataProvider
13
+
14
+ if TYPE_CHECKING:
15
+ from tesorotools.providers.bde import BdeProvider
16
+ from tesorotools.providers.ecb import EcbProvider
17
+
18
+ __all__ = ["BdeProvider", "DataProvider", "EcbProvider"]
19
+
20
+
21
+ def __getattr__(name: str) -> Any:
22
+ if name == "BdeProvider":
23
+ from tesorotools.providers.bde import BdeProvider
24
+
25
+ return BdeProvider
26
+ if name == "EcbProvider":
27
+ from tesorotools.providers.ecb import EcbProvider
28
+
29
+ return EcbProvider
30
+ raise AttributeError(
31
+ f"module 'tesorotools.providers' has no attribute {name!r}"
32
+ )
@@ -0,0 +1,22 @@
1
+ """Public render API."""
2
+
3
+ from tesorotools.render.content.content import Content
4
+ from tesorotools.render.content.images import Image, Images
5
+ from tesorotools.render.content.section import Section
6
+ from tesorotools.render.content.subtitle import Subtitle
7
+ from tesorotools.render.content.table import Table
8
+ from tesorotools.render.content.text import Text
9
+ from tesorotools.render.content.title import Title
10
+ from tesorotools.render.report import Report
11
+
12
+ __all__ = [
13
+ "Content",
14
+ "Image",
15
+ "Images",
16
+ "Report",
17
+ "Section",
18
+ "Subtitle",
19
+ "Table",
20
+ "Text",
21
+ "Title",
22
+ ]
@@ -0,0 +1,38 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+
4
+ import yaml
5
+
6
+ from tesorotools.utils.template import TemplateLoader
7
+
8
+
9
+ def clean_config_dicts(
10
+ config_dicts: dict[str, Any],
11
+ ) -> dict[str, Any]:
12
+ return {k: v for k, v in config_dicts.items() if not k.startswith(".")}
13
+
14
+
15
+ def read_config(
16
+ config_file: Path,
17
+ loader: type[yaml.FullLoader] | None = None,
18
+ clean: bool = True,
19
+ ) -> Any:
20
+ actual_loader: type[yaml.FullLoader] = (
21
+ yaml.FullLoader if loader is None else TemplateLoader
22
+ )
23
+ with open(config_file, encoding="utf8") as file:
24
+ config_dict: Any = yaml.load(file, Loader=actual_loader)
25
+ if clean and isinstance(config_dict, dict):
26
+ config_dict = clean_config_dicts(config_dict) # type: ignore[reportUnknownArgumentType]
27
+ return config_dict
28
+
29
+
30
+ def merge(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
31
+ # a overrides
32
+ for key in b:
33
+ if key in a:
34
+ if isinstance(a[key], dict) and isinstance(b[key], dict):
35
+ merge(a[key], b[key])
36
+ else:
37
+ a[key] = b[key]
38
+ return a
@@ -1,5 +0,0 @@
1
- import matplotlib.style
2
-
3
- from ..utils.globals import STYLE_SHEET
4
-
5
- matplotlib.style.use(STYLE_SHEET)
@@ -1,201 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- from typing import Any
5
-
6
- import matplotlib.pyplot as plt
7
- import pandas as pd
8
- from matplotlib.axes import Axes
9
- from matplotlib.figure import Figure
10
- from pandas import Timestamp
11
-
12
- from tesorotools.artists.line_plot import (
13
- style_baseline,
14
- style_spines,
15
- )
16
- from tesorotools.utils.config import merge
17
- from tesorotools.utils.matplotlib import (
18
- PLOT_CONFIG,
19
- format_annotation,
20
- load_fonts,
21
- )
22
-
23
- TYPE_CURVE_CONFIG: dict[str, Any] = PLOT_CONFIG["type_curve"]
24
- AX_CONFIG: dict[str, Any] = PLOT_CONFIG["ax"]
25
- FIG_CONFIG: dict[str, Any] = PLOT_CONFIG["figure"]
26
-
27
- load_fonts()
28
-
29
-
30
- def _rotate_xticks(ax: Axes) -> None:
31
- ax.set_xticks( # type: ignore[reportUnknownMemberType]
32
- ax.get_xticks(),
33
- ax.get_xticklabels(), # type: ignore[reportArgumentType]
34
- rotation=45,
35
- ha="right",
36
- )
37
-
38
-
39
- def _format_data(data: pd.DataFrame) -> dict[str, Any]:
40
- # metadata
41
- date_index: pd.DatetimeIndex = data.index # type: ignore[assignment]
42
- current_date: Timestamp = date_index.max() # type: ignore[assignment]
43
- current_year: int = current_date.year
44
- last_year: int = (current_date - pd.DateOffset(years=1)).year
45
-
46
- # current data
47
- current_data: pd.Series[Any] = data.loc[current_date, :] # type: ignore[assignment]
48
- current_data.name = "current_data"
49
-
50
- # current year
51
- current_year_data: pd.DataFrame = data.loc[
52
- date_index.year == current_year, :
53
- ]
54
- current_year_max: pd.Series[Any] = current_year_data.max()
55
- current_year_max.name = "current_year_max"
56
- current_year_min: pd.Series[Any] = current_year_data.min()
57
- current_year_min.name = "current_year_min"
58
-
59
- # last year
60
- last_year_data: pd.DataFrame = data.loc[date_index.year == last_year, :]
61
- last_year_max: pd.Series[Any] = last_year_data.max()
62
- last_year_max.name = "last_year_max"
63
- last_year_min: pd.Series[Any] = last_year_data.min()
64
- last_year_min.name = "last_year_min"
65
-
66
- formatted_data: pd.DataFrame = pd.concat(
67
- [
68
- last_year_max,
69
- last_year_min,
70
- current_year_max,
71
- current_year_min,
72
- current_data,
73
- ],
74
- axis=1,
75
- )
76
-
77
- return {
78
- "data": formatted_data,
79
- "current_date": current_date.strftime("%d/%m/%Y"),
80
- "current_year": current_year,
81
- "last_year": last_year,
82
- }
83
-
84
-
85
- def _plot_current_data(
86
- ax: Axes,
87
- data: pd.Series[Any],
88
- date_fmt: str,
89
- *,
90
- linewidth: float,
91
- marker: str,
92
- points_to_mark: list[str],
93
- color: str,
94
- decimals: int,
95
- units: str,
96
- ) -> None:
97
- data.plot(
98
- ax=ax,
99
- color=color,
100
- linewidth=linewidth,
101
- label=date_fmt,
102
- )
103
- for point in points_to_mark:
104
- value: float = data.loc[point]
105
- ax.plot( # type: ignore[reportUnknownMemberType]
106
- point,
107
- value,
108
- marker=marker,
109
- color=color,
110
- )
111
- ax.annotate( # type: ignore[reportUnknownMemberType]
112
- format_annotation(value, decimals, units),
113
- (point, value), # type: ignore[reportArgumentType]
114
- textcoords="offset points",
115
- xytext=(0, 10),
116
- ha="center",
117
- )
118
-
119
-
120
- def plot_type_curve(
121
- data: pd.DataFrame,
122
- out_file: Path,
123
- **config: Any,
124
- ) -> None:
125
- merged_config: dict[str, Any] = merge(config, TYPE_CURVE_CONFIG)
126
-
127
- if merged_config["yaxis"]["units"] == "p.b.":
128
- data = data * 100
129
-
130
- formatted_assets: dict[str, Any] = _format_data(data)
131
- formatted_data: pd.DataFrame = formatted_assets["data"]
132
- due_index: pd.Index[Any] = formatted_data.index
133
-
134
- fig: Figure = plt.figure( # type: ignore[reportUnknownMemberType]
135
- **FIG_CONFIG
136
- )
137
- ax: Axes = fig.add_subplot()
138
-
139
- last_config: dict[str, Any] = merged_config["last"]
140
- ax.fill_between( # type: ignore[reportUnknownMemberType]
141
- due_index,
142
- formatted_data["last_year_min"],
143
- formatted_data["last_year_max"],
144
- alpha=last_config["alpha"],
145
- color=last_config["color"],
146
- edgecolor=None,
147
- label=f"Rango {formatted_assets['last_year']}",
148
- )
149
-
150
- current_config: dict[str, Any] = merged_config["current"]
151
- ax.fill_between( # type: ignore[reportUnknownMemberType]
152
- due_index,
153
- formatted_data["current_year_min"],
154
- formatted_data["current_year_max"],
155
- alpha=current_config["alpha"],
156
- color=current_config["color"],
157
- edgecolor=None,
158
- label=f"Rango {formatted_assets['current_year']}",
159
- )
160
-
161
- _plot_current_data(
162
- ax,
163
- formatted_data["current_data"],
164
- formatted_assets["current_date"],
165
- **merged_config["line"],
166
- )
167
- style_spines(ax, **merged_config["yaxis"], **AX_CONFIG["spines"])
168
- _rotate_xticks(ax)
169
- style_baseline(ax, 0, **AX_CONFIG["baseline"])
170
- handles, labels = ax.get_legend_handles_labels()
171
- fig.legend( # type: ignore[reportUnknownMemberType]
172
- handles,
173
- labels,
174
- loc="outside lower center",
175
- ncol=3,
176
- )
177
- fig.savefig( # type: ignore[reportUnknownMemberType]
178
- out_file
179
- )
180
-
181
-
182
- # data is expected to be a simple time series data, columns are series and rows represents dates
183
- def plot_type_curves(
184
- out_path: Path,
185
- data: pd.DataFrame,
186
- config_dicts: dict[str, Any],
187
- ) -> None:
188
- for name, config in config_dicts.items():
189
- if not name.startswith("."): # aux entries
190
- series: dict[str, str] = config["series"]
191
- if len(series) < 2:
192
- raise ValueError(
193
- f"In plot {name}: A type curve must have at least two due periods. Given periods: {series.keys()}"
194
- )
195
- trimmed_data: pd.DataFrame = data.loc[:, series.keys()]
196
- trimmed_data = trimmed_data.rename(columns=series)
197
- plot_type_curve(
198
- data=trimmed_data,
199
- out_file=out_path / f"{name}.png",
200
- **config,
201
- )
@@ -1,98 +0,0 @@
1
- from pathlib import Path
2
- from typing import Any
3
-
4
- import yaml
5
-
6
- from tesorotools.utils.template import TemplateLoader
7
-
8
- _tags_registered = False
9
-
10
-
11
- def _register_all_tags() -> None:
12
- """Register every YAML tag on TemplateLoader.
13
-
14
- Called once, lazily, the first time ``read_config`` is
15
- invoked with ``loader=TemplateLoader``. This removes
16
- the need for consumers to import ``tesorotools.render``
17
- or ``tesorotools.artists`` just for their side effects.
18
- """
19
- global _tags_registered # noqa: PLW0603
20
- if _tags_registered:
21
- return
22
- _tags_registered = True
23
-
24
- # -- artists tags --
25
- from tesorotools.artists.barh_plot import (
26
- HorizontalBarChart,
27
- )
28
- from tesorotools.artists.line_plot import (
29
- Format,
30
- Legend,
31
- LinePlot,
32
- )
33
- from tesorotools.artists.stacked import (
34
- StackedAreaPlot,
35
- StackedBarPlot,
36
- )
37
-
38
- TemplateLoader.add_constructor("!line_plot", LinePlot.from_yaml)
39
- TemplateLoader.add_constructor("!format", Format.from_yaml)
40
- TemplateLoader.add_constructor("!legend", Legend.from_yaml)
41
- TemplateLoader.add_constructor("!stacked_area", StackedAreaPlot.from_yaml)
42
- TemplateLoader.add_constructor("!stacked_bar", StackedBarPlot.from_yaml)
43
- TemplateLoader.add_constructor("!barh", HorizontalBarChart.from_yaml)
44
-
45
- # -- render tags --
46
- from tesorotools.render.content.images import (
47
- Image,
48
- Images,
49
- )
50
- from tesorotools.render.content.section import Section
51
- from tesorotools.render.content.subtitle import Subtitle
52
- from tesorotools.render.content.table import Table
53
- from tesorotools.render.content.text import Text
54
- from tesorotools.render.content.title import Title
55
- from tesorotools.render.report import Report
56
-
57
- TemplateLoader.add_constructor("!report", Report.from_yaml)
58
- TemplateLoader.add_constructor("!section", Section.from_yaml)
59
- TemplateLoader.add_constructor("!image", Image.from_yaml)
60
- TemplateLoader.add_constructor("!images", Images.from_yaml)
61
- TemplateLoader.add_constructor("!table", Table.from_yaml)
62
- TemplateLoader.add_constructor("!text", Text.from_yaml)
63
- TemplateLoader.add_constructor("!title", Title.from_yaml)
64
- TemplateLoader.add_constructor("!subtitle", Subtitle.from_yaml)
65
-
66
-
67
- def clean_config_dicts(
68
- config_dicts: dict[str, Any],
69
- ) -> dict[str, Any]:
70
- return {k: v for k, v in config_dicts.items() if not k.startswith(".")}
71
-
72
-
73
- def read_config(
74
- config_file: Path,
75
- loader: type[yaml.FullLoader] | None = None,
76
- clean: bool = True,
77
- ) -> Any:
78
- actual_loader: type[yaml.FullLoader] = (
79
- yaml.FullLoader if loader is None else TemplateLoader
80
- )
81
- if actual_loader is TemplateLoader:
82
- _register_all_tags()
83
- with open(config_file, encoding="utf8") as file:
84
- config_dict: Any = yaml.load(file, Loader=actual_loader)
85
- if clean and isinstance(config_dict, dict):
86
- config_dict = clean_config_dicts(config_dict) # type: ignore[reportUnknownArgumentType]
87
- return config_dict
88
-
89
-
90
- def merge(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
91
- # a overrides
92
- for key in b:
93
- if key in a:
94
- if isinstance(a[key], dict) and isinstance(b[key], dict):
95
- merge(a[key], b[key])
96
- else:
97
- a[key] = b[key]
98
- return a