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.
- tesorotools/__init__.py +0 -0
- tesorotools/artists/__init__.py +5 -0
- tesorotools/artists/barh_plot.py +310 -0
- tesorotools/artists/line_plot.py +114 -0
- tesorotools/artists/table.py +199 -0
- tesorotools/artists/type_curve.py +216 -0
- tesorotools/convert.py +93 -0
- tesorotools/data_sources/__init__.py +0 -0
- tesorotools/data_sources/debug.py +26 -0
- tesorotools/data_sources/eikon.py +117 -0
- tesorotools/database/__init__.py +0 -0
- tesorotools/database/push.py +70 -0
- tesorotools/dependencies/__init__.py +0 -0
- tesorotools/dependencies/functions.py +11 -0
- tesorotools/dependencies/node.py +34 -0
- tesorotools/dependencies/resolution.py +118 -0
- tesorotools/main.py +37 -0
- tesorotools/offsets/__init__.py +0 -0
- tesorotools/offsets/offsets.py +439 -0
- tesorotools/offsets/outliers.py +15 -0
- tesorotools/render/__init__.py +11 -0
- tesorotools/render/content/__init__.py +0 -0
- tesorotools/render/content/content.py +17 -0
- tesorotools/render/content/images.py +147 -0
- tesorotools/render/content/section.py +53 -0
- tesorotools/render/content/table.py +283 -0
- tesorotools/render/headline.py +40 -0
- tesorotools/render/introduction.py +49 -0
- tesorotools/render/report.py +29 -0
- tesorotools/utils/__init__.py +0 -0
- tesorotools/utils/config.py +35 -0
- tesorotools/utils/globals.py +12 -0
- tesorotools/utils/matplotlib.py +38 -0
- tesorotools/utils/series.py +40 -0
- tesorotools/utils/template.py +126 -0
- tesorotools_python-0.0.0.dist-info/METADATA +13 -0
- tesorotools_python-0.0.0.dist-info/RECORD +38 -0
- 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
|