tesorotools-python 0.0.0__py2.py3-none-any.whl

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 (38) hide show
  1. tesorotools/__init__.py +0 -0
  2. tesorotools/artists/__init__.py +5 -0
  3. tesorotools/artists/barh_plot.py +310 -0
  4. tesorotools/artists/line_plot.py +114 -0
  5. tesorotools/artists/table.py +199 -0
  6. tesorotools/artists/type_curve.py +216 -0
  7. tesorotools/convert.py +93 -0
  8. tesorotools/data_sources/__init__.py +0 -0
  9. tesorotools/data_sources/debug.py +26 -0
  10. tesorotools/data_sources/eikon.py +117 -0
  11. tesorotools/database/__init__.py +0 -0
  12. tesorotools/database/push.py +70 -0
  13. tesorotools/dependencies/__init__.py +0 -0
  14. tesorotools/dependencies/functions.py +11 -0
  15. tesorotools/dependencies/node.py +34 -0
  16. tesorotools/dependencies/resolution.py +118 -0
  17. tesorotools/main.py +37 -0
  18. tesorotools/offsets/__init__.py +0 -0
  19. tesorotools/offsets/offsets.py +439 -0
  20. tesorotools/offsets/outliers.py +15 -0
  21. tesorotools/render/__init__.py +11 -0
  22. tesorotools/render/content/__init__.py +0 -0
  23. tesorotools/render/content/content.py +17 -0
  24. tesorotools/render/content/images.py +147 -0
  25. tesorotools/render/content/section.py +53 -0
  26. tesorotools/render/content/table.py +283 -0
  27. tesorotools/render/headline.py +40 -0
  28. tesorotools/render/introduction.py +49 -0
  29. tesorotools/render/report.py +29 -0
  30. tesorotools/utils/__init__.py +0 -0
  31. tesorotools/utils/config.py +35 -0
  32. tesorotools/utils/globals.py +12 -0
  33. tesorotools/utils/matplotlib.py +38 -0
  34. tesorotools/utils/series.py +40 -0
  35. tesorotools/utils/template.py +126 -0
  36. tesorotools_python-0.0.0.dist-info/METADATA +13 -0
  37. tesorotools_python-0.0.0.dist-info/RECORD +38 -0
  38. tesorotools_python-0.0.0.dist-info/WHEEL +5 -0
@@ -0,0 +1,53 @@
1
+ from typing import Any, Self
2
+
3
+ from docx.document import Document
4
+ from yaml import Loader, MappingNode
5
+
6
+ from tesorotools.render.content.content import Content
7
+
8
+
9
+ class Section:
10
+ def __init__(
11
+ self,
12
+ id: str,
13
+ title: str | None = None,
14
+ contents: dict[str, Content] | None = None,
15
+ ) -> None:
16
+ self._id: str = id
17
+ self._title: str = title if title is not None else ""
18
+ self._contents: dict[str, Content] = (
19
+ contents if contents is not None else {}
20
+ )
21
+ self._level = 1
22
+
23
+ @property
24
+ def level(self) -> int:
25
+ return self._level
26
+
27
+ @level.setter
28
+ def level(self, level: int) -> None:
29
+ self._level = level
30
+
31
+ @classmethod
32
+ def from_yaml(cls, loader: Loader, node: MappingNode) -> Self:
33
+ values: dict[str, Any] = loader.construct_mapping(node, deep=True)
34
+ id: str = values.pop("id")
35
+ title: str = values.pop("title", None)
36
+ contents: dict[str, Content] = values
37
+ section: Self = cls(id=id, title=title, contents=contents)
38
+ section.nest()
39
+ return section
40
+
41
+ def render(self, document: Document) -> Document:
42
+ # Use the "Heading `level`" style from the base document
43
+ document.add_heading(self._title, level=self._level)
44
+ for _, content in self._contents.items():
45
+ document.add_paragraph()
46
+ document = content.render(document)
47
+ return document
48
+
49
+ def nest(self):
50
+ for _, content in self._contents.items():
51
+ if isinstance(content, Section):
52
+ content.level += 1
53
+ content.nest()
@@ -0,0 +1,283 @@
1
+ from pathlib import Path
2
+ from typing import Any, Self
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from docx.document import Document
7
+ from docx.enum.table import WD_ALIGN_VERTICAL
8
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
9
+ from docx.oxml import OxmlElement, parse_xml
10
+ from docx.oxml.ns import nsdecls, qn
11
+ from docx.shared import Inches, Pt, RGBColor
12
+ from docx.table import Table
13
+ from docx.table import _Cell as TableCell
14
+ from yaml import MappingNode
15
+
16
+ from tesorotools.utils.config import read_config
17
+ from tesorotools.utils.globals import EXAMPLES
18
+ from tesorotools.utils.template import TemplateLoader
19
+
20
+ RENDER_CONFIG: dict[str, Any] = read_config(EXAMPLES / "plots.yaml")["table"]
21
+
22
+ TEXTO_TABLAS = 9
23
+
24
+ CENTER = WD_ALIGN_PARAGRAPH.CENTER
25
+
26
+
27
+ def _set_cell_border(cell: TableCell, **kwargs):
28
+ """
29
+ Set cell`s border
30
+ Usage:
31
+
32
+ set_cell_border(
33
+ cell,
34
+ top={"sz": 12, "val": "single", "color": "#FF0000", "space": "0"},
35
+ bottom={"sz": 12, "color": "#00FF00", "val": "single"},
36
+ start={"sz": 24, "val": "dashed", "shadow": "true"},
37
+ end={"sz": 12, "val": "dashed"},
38
+ )
39
+ """
40
+ tc = cell._tc
41
+ tcPr = tc.get_or_add_tcPr()
42
+
43
+ # check for tag existence, if none found, create one
44
+ tcBorders = tcPr.first_child_found_in("w:tcBorders")
45
+ if tcBorders is None:
46
+ tcBorders = OxmlElement("w:tcBorders")
47
+ tcPr.append(tcBorders)
48
+
49
+ # list over all available tags
50
+ for edge in ("start", "top", "end", "bottom", "insideH", "insideV"):
51
+ edge_data = kwargs.get(edge)
52
+ if edge_data:
53
+ tag = "w:{}".format(edge)
54
+
55
+ # check for tag existence, if none found, then create one
56
+ element = tcBorders.find(qn(tag))
57
+ if element is None:
58
+ element = OxmlElement(tag)
59
+ tcBorders.append(element)
60
+
61
+ # looks like order of attributes is important
62
+ for key in ["sz", "val", "color", "space", "shadow"]:
63
+ if key in edge_data:
64
+ element.set(qn("w:{}".format(key)), str(edge_data[key]))
65
+
66
+
67
+ def _style_horizontal_blocks_header(cell: TableCell):
68
+ cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
69
+ cell.paragraphs[0].runs[0].font.size = Pt(12)
70
+
71
+
72
+ def _horizontal_blocks_header(columns: pd.MultiIndex, table_docx: Table):
73
+ column_counter: int = 1
74
+ blocks: list[str] = list(columns.get_level_values(level=0).unique())
75
+ for block in blocks:
76
+ cell: TableCell = table_docx.cell(0, column_counter)
77
+ columns_to_merge: int = len(
78
+ columns[columns.get_level_values(level=0) == block]
79
+ )
80
+ for _ in range(columns_to_merge - 1):
81
+ column_counter = column_counter + 1
82
+ cell.merge(table_docx.cell(0, column_counter))
83
+ column_counter = column_counter + 1
84
+ cell.text = block
85
+ _style_horizontal_blocks_header(cell)
86
+
87
+
88
+ def _style_column_names(cell: TableCell):
89
+ cell.paragraphs[0].runs[0].font.size = Pt(10)
90
+ cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
91
+ cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
92
+
93
+
94
+ def _fill_column_names(
95
+ table: pd.DataFrame, table_docx: Table, horizontal: bool
96
+ ):
97
+ if horizontal:
98
+ start_row: int = 1
99
+ _horizontal_blocks_header(table.columns, table_docx)
100
+ columns: np.ndarray = table.columns.get_level_values(level=1).values
101
+ else:
102
+ start_row: int = 0
103
+ columns: np.ndarray = table.columns.values
104
+
105
+ for idx, column_name in enumerate(columns, start=1):
106
+ cell: TableCell = table_docx.cell(start_row, idx)
107
+ cell.text = column_name
108
+ _style_column_names(cell)
109
+
110
+
111
+ def _style_index_names(cell: TableCell):
112
+ cell.paragraphs[0].runs[0].font.size = Pt(TEXTO_TABLAS)
113
+ cell.paragraphs[0].runs[0].font.bold = True
114
+ cell.width = Inches(1)
115
+
116
+
117
+ def _fill_index_names(
118
+ index: pd.Index | pd.MultiIndex, table_docx: Table, horizontal: bool
119
+ ):
120
+ start_row: int = 2 if horizontal else 1
121
+
122
+ index_names: pd.Index = (
123
+ index if horizontal else index.get_level_values(level=1)
124
+ )
125
+
126
+ for idx, name in enumerate(index_names, start=start_row):
127
+ cell: TableCell = table_docx.cell(idx, 0)
128
+ cell.text = name
129
+ _style_index_names(cell)
130
+
131
+
132
+ # we only separate blocks in vertically stacked tables
133
+ def _separate_blocks(index: pd.MultiIndex, table_docx: Table):
134
+ blocks: list[str] = list(index.get_level_values(level=0).unique())
135
+ previous_rows = 0
136
+ for block in blocks[:-1]:
137
+ block_size: int = len(index[index.get_level_values(level=0) == block])
138
+ for cell in table_docx.rows[block_size + previous_rows].cells:
139
+ _separate_cell(cell)
140
+ previous_rows += block_size
141
+
142
+
143
+ def _separate_cell(cell: TableCell):
144
+ _set_cell_border(
145
+ cell,
146
+ bottom={
147
+ "sz": 1,
148
+ "val": "double",
149
+ "color": "#000000",
150
+ "space": 2,
151
+ },
152
+ )
153
+
154
+
155
+ def _is_bright(hex_color):
156
+ red = int(hex_color[:2], 16)
157
+ green = int(hex_color[2:4], 16)
158
+ blue = int(hex_color[4:], 16)
159
+ luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue
160
+ return luminance > 180
161
+
162
+
163
+ def _shade_cell(cell: TableCell, hex_color: str):
164
+ bright = _is_bright(hex_color)
165
+ shading_element = parse_xml(
166
+ r'<w:shd {} w:fill="{hex_color}"/>'.format(
167
+ nsdecls("w"), hex_color=hex_color
168
+ )
169
+ )
170
+ cell._tc.get_or_add_tcPr().append(shading_element)
171
+ if bright:
172
+ cell.paragraphs[0].runs[0].font.color.rgb = RGBColor(0, 0, 0)
173
+ else:
174
+ cell.paragraphs[0].runs[0].font.color.rgb = RGBColor(255, 255, 255)
175
+
176
+
177
+ def _style_content(cell: TableCell):
178
+ cell.paragraphs[0].runs[0].font.size = Pt(TEXTO_TABLAS)
179
+ cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.RIGHT
180
+ cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
181
+
182
+
183
+ def _fill_content(
184
+ table: pd.DataFrame,
185
+ color_table: pd.DataFrame,
186
+ shade_table: pd.DataFrame,
187
+ table_docx: Table,
188
+ horizontal: bool,
189
+ ):
190
+ start_row: int = 2 if horizontal else 1
191
+ values = table.values
192
+ for (x, y), value in np.ndenumerate(values):
193
+ cell: TableCell = table_docx.cell(x + start_row, y + 1)
194
+ cell.text = value if value is not None else ""
195
+ color: str | None = color_table.values[x, y]
196
+ shade: str | None = shade_table.values[x, y]
197
+ if color is not None:
198
+ cell.paragraphs[0].runs[0].font.color.rgb = RGBColor.from_string(
199
+ color
200
+ )
201
+ if shade is not None:
202
+ _shade_cell(cell, shade)
203
+ _style_content(cell)
204
+
205
+
206
+ def _style_table(table_docx: Table):
207
+ table_docx.style = RENDER_CONFIG.get("style", None)
208
+ table_docx.autofit = RENDER_CONFIG["autofit"]
209
+
210
+
211
+ def render_table(
212
+ table: pd.DataFrame,
213
+ color_table: pd.DataFrame,
214
+ shade_table: pd.DataFrame,
215
+ document: Document,
216
+ block_sep: bool,
217
+ ) -> Table:
218
+
219
+ horizontal: bool = isinstance(table.columns, pd.MultiIndex)
220
+ table_docx: Table = document.add_table(
221
+ rows=len(table.index) + table.columns.nlevels,
222
+ cols=len(table.columns) + 1,
223
+ )
224
+
225
+ _style_table(table_docx)
226
+ _fill_column_names(table, table_docx, horizontal)
227
+ _fill_index_names(
228
+ index=table.index, table_docx=table_docx, horizontal=horizontal
229
+ )
230
+ if block_sep:
231
+ _separate_blocks(table.index, table_docx)
232
+ _fill_content(table, color_table, shade_table, table_docx, horizontal)
233
+ return document
234
+
235
+
236
+ class Table:
237
+ """A rendered table in the document"""
238
+
239
+ def __init__(
240
+ self,
241
+ data_file: Path | None,
242
+ color_file: Path | None,
243
+ shade_file: Path | None,
244
+ block_sep: bool = False,
245
+ title: str | None = None,
246
+ ):
247
+ if (
248
+ (data_file is None)
249
+ and (color_file is None)
250
+ and (shade_file is None)
251
+ ):
252
+ raise ValueError("At least a piece of data should be given")
253
+ self._data: pd.DataFrame | None = (
254
+ pd.read_feather(data_file) if data_file is not None else None
255
+ )
256
+ self._color: pd.DataFrame | None = (
257
+ pd.read_feather(color_file) if color_file is not None else None
258
+ )
259
+ self._shade: pd.DataFrame | None = (
260
+ pd.read_feather(shade_file) if shade_file is not None else None
261
+ )
262
+ self._title: str | None = title
263
+ self._block_sep: bool = block_sep
264
+
265
+ @classmethod
266
+ def from_yaml(cls, loader: TemplateLoader, node: MappingNode) -> Self:
267
+ table_cfg: dict[str, Any] = loader.construct_mapping(node, deep=True)
268
+ root_path: Path = loader.imports["table"]
269
+ file_prefix: str = table_cfg.pop("id")
270
+ data_file: Path = root_path / f"{file_prefix}_data.feather"
271
+ color_file: Path = root_path / f"{file_prefix}_color.feather"
272
+ shade_file: Path = root_path / f"{file_prefix}_shade.feather"
273
+ return cls(data_file, color_file, shade_file, **table_cfg)
274
+
275
+ def render(self, document: Document) -> Document:
276
+ document = render_table(
277
+ self._data,
278
+ self._color,
279
+ self._shade,
280
+ document,
281
+ block_sep=self._block_sep,
282
+ )
283
+ return document
@@ -0,0 +1,40 @@
1
+ from typing import Any, Self
2
+
3
+ from docx.document import Document
4
+
5
+
6
+ class HeadLine:
7
+ def __init__(
8
+ self, title: str | None = None, comment: str | None = None
9
+ ) -> None:
10
+ self._title: str = "" if title is None else title
11
+ self._comment: str = "" if comment is None else comment
12
+
13
+ @classmethod
14
+ def from_dict(cls, headline_cfg: dict[str, Any]) -> Self:
15
+ title: str | None = headline_cfg.get("title", None)
16
+ comment: str | None = headline_cfg.get("comment", None)
17
+ return cls(title, comment)
18
+
19
+ def render(self, document: Document) -> Document:
20
+ if (self.title == "") and (self.comment == ""):
21
+ title_text: str = ""
22
+ elif (self.title == "") and (self.comment != ""):
23
+ title_text: str = self.comment
24
+ else:
25
+ title_text: str = f"{self.title}: {self.comment}"
26
+ # Use the "Title" style in the word template
27
+ document.add_heading(text=title_text, level=0)
28
+ return document
29
+
30
+ @property
31
+ def title(self) -> str:
32
+ return self._title
33
+
34
+ @title.setter
35
+ def title(self, title) -> None:
36
+ self._title = title
37
+
38
+ @property
39
+ def comment(self) -> str:
40
+ return self._comment
@@ -0,0 +1,49 @@
1
+ from datetime import date, datetime, time
2
+ from typing import Any, Self
3
+
4
+ from babel.dates import format_datetime
5
+ from docx.document import Document
6
+
7
+
8
+ class Introduction:
9
+ def __init__(self, date_time: datetime | None = None) -> None:
10
+ if date_time is None:
11
+ date_time: datetime = datetime.now()
12
+ self._date_time: datetime = date_time
13
+
14
+ @classmethod
15
+ def from_dict(cls, introduction_cfg: dict[str, Any]) -> Self:
16
+ # parse date
17
+ date_cfg: str | date | None = introduction_cfg.get("date", None)
18
+ if date_cfg is None:
19
+ date_time: datetime = datetime.now()
20
+ elif isinstance(date_cfg, date):
21
+ date_time: datetime = datetime.combine(date_cfg, time.min)
22
+ elif isinstance(date_cfg, str):
23
+ date_time: datetime = datetime.strptime(date_cfg, "%Y-%m-%d")
24
+
25
+ # parse hour
26
+ hour_cfg: str | None = introduction_cfg.get("hour", None)
27
+ if hour_cfg is not None:
28
+ hour_str, minute_str = hour_cfg.split(sep=":")
29
+ date_time = date_time.replace(
30
+ hour=int(hour_str), minute=int(minute_str)
31
+ )
32
+
33
+ return cls(date_time)
34
+
35
+ def set_time(self, hour: int, minute: int) -> None:
36
+ self._date_time = self._date_time.replace(hour=hour, minute=minute)
37
+
38
+ def _format_datetime(self) -> str:
39
+ date_fmt: str = format_datetime(
40
+ self._date_time, "EEEE, dd 'de' MMMM 'de' yyyy, HH:mm", locale="es"
41
+ )
42
+ date_fmt = date_fmt.capitalize()
43
+ return date_fmt
44
+
45
+ def render(self, document: Document) -> Document:
46
+ date_fmt = self._format_datetime()
47
+ document.add_paragraph(date_fmt, style="Subtitle")
48
+ document.add_paragraph()
49
+ return document
@@ -0,0 +1,29 @@
1
+ from typing import Any, Self
2
+
3
+ from docx.document import Document
4
+ from yaml import Loader, MappingNode
5
+
6
+ from tesorotools.render.content.content import Content
7
+
8
+
9
+ class Report:
10
+ def __init__(
11
+ self,
12
+ title: str,
13
+ contents: dict[str, Content],
14
+ ):
15
+ self.title: str = title
16
+ self.contents: dict[str, Content] = contents
17
+
18
+ @classmethod
19
+ def from_yaml(cls, loader: Loader, node: MappingNode) -> Self:
20
+ report_cfg: dict[str, Any] = loader.construct_mapping(node, deep=True)
21
+ title: str = report_cfg.pop("id")
22
+ return cls(title=title, contents=report_cfg)
23
+
24
+ def render(self, document: Document) -> Document:
25
+ document._body.clear_content()
26
+ for _, content in self.contents.items():
27
+ content.render(document)
28
+ document.add_paragraph()
29
+ return document
File without changes
@@ -0,0 +1,35 @@
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(config_dicts: dict[str, Any]):
10
+ if config_dicts is None:
11
+ return None
12
+ return {k: v for k, v in config_dicts.items() if not k.startswith(".")}
13
+
14
+
15
+ # maybe a dedicated function for templates would be nice
16
+ def read_config(
17
+ config_file: Path, loader: yaml.FullLoader = None, clean: bool = True
18
+ ) -> Any | dict:
19
+ loader = yaml.FullLoader if loader is None else TemplateLoader
20
+ with open(config_file, encoding="utf8") as file:
21
+ config_dict = yaml.load(file, Loader=loader)
22
+ if clean and isinstance(config_dict, dict):
23
+ config_dict = clean_config_dicts(config_dict)
24
+ return config_dict
25
+
26
+
27
+ def merge(a: dict, b: dict):
28
+ # a overrides
29
+ for key in b:
30
+ if key in a:
31
+ if isinstance(a[key], dict) and isinstance(b[key], dict):
32
+ merge(a[key], b[key])
33
+ else:
34
+ a[key] = b[key]
35
+ return a
@@ -0,0 +1,12 @@
1
+ from pathlib import Path
2
+
3
+ DEBUG: Path = Path("debug")
4
+ CONFIG: Path = Path("config")
5
+ EXAMPLES: Path = Path("examples")
6
+
7
+ ASSETS: Path = Path("assets")
8
+ FONTS: Path = ASSETS / "fonts"
9
+
10
+ STYLE_SHEET: Path = Path("tesoro.mplstyle")
11
+
12
+ PLOT_CONFIG_FILE: Path = EXAMPLES / "plots.yaml"
@@ -0,0 +1,38 @@
1
+ from typing import Any
2
+
3
+ import matplotlib
4
+ import matplotlib.font_manager
5
+
6
+ from .config import read_config
7
+ from .globals import FONTS, PLOT_CONFIG_FILE
8
+
9
+ # this should only be done once
10
+ PLOT_CONFIG: dict[str, Any] = read_config(PLOT_CONFIG_FILE)
11
+
12
+
13
+ def load_fonts() -> None:
14
+ for font in FONTS.iterdir():
15
+ if font.suffix == ".otf":
16
+ matplotlib.font_manager.fontManager.addfont(font)
17
+ matplotlib.rcParams["font.family"] = PLOT_CONFIG["style"]["font"]
18
+
19
+
20
+ # this is not really matplotlib specific, so it should be elsewhere
21
+ def format_annotation(value: float, decimals: int, units: str) -> str:
22
+ decimal_formatted: str = f"{value:_.{decimals}f}".replace(".", ",").replace(
23
+ "_", "."
24
+ )
25
+ decimal_formatted = (
26
+ decimal_formatted[1:]
27
+ if (decimal_formatted.startswith("-0") and decimals == 0)
28
+ else decimal_formatted
29
+ )
30
+ return f"{decimal_formatted}{units}"
31
+
32
+
33
+ def is_zero(value: float, decimals: int) -> bool:
34
+ formatted = format_annotation(value, decimals, units="")
35
+ unique = "".join(set(formatted.replace(",", "").replace(".", "")))
36
+ if unique == "0":
37
+ return True
38
+ return False
@@ -0,0 +1,40 @@
1
+ from pathlib import Path
2
+ from typing import Any
3
+
4
+ import yaml
5
+
6
+ from tesorotools.dependencies.node import Node
7
+ from tesorotools.dependencies.resolution import resolve_series
8
+
9
+
10
+ def compile_series(
11
+ config_dicts: list[dict[str, Any]],
12
+ dependencies_cfg: dict[str, Any],
13
+ out: Path,
14
+ ) -> dict[str, list[str]]:
15
+ """Generates a document with all the series used in the document. Useful when you want to download them all at once."""
16
+
17
+ resolved_dict = resolve_series(
18
+ config_dicts,
19
+ dependencies_cfg,
20
+ )
21
+
22
+ dependent_series: list[Node] = resolved_dict["dependent"]
23
+ dependent_series_id_list: list[str] = [
24
+ node.name for node in dependent_series
25
+ ]
26
+ independent_series_id_list: list[str] = list(resolved_dict["independent"])
27
+ result = {
28
+ "dependent": dependent_series_id_list,
29
+ "independent": independent_series_id_list,
30
+ }
31
+
32
+ with open(out, "w", encoding="utf-8") as stream:
33
+ comment: str = "# Autogenerated document, do not touch!\n\n"
34
+ stream.write(comment)
35
+ yaml.dump(
36
+ result,
37
+ stream,
38
+ )
39
+
40
+ return result