tesorotools-python 0.0.40__tar.gz → 0.0.41__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 (68) hide show
  1. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/PKG-INFO +1 -1
  2. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/pyproject.toml +1 -1
  3. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/plots.yaml +3 -0
  4. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/render/content/table.py +145 -35
  5. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/.gitignore +0 -0
  6. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/__init__.py +0 -0
  7. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/_build_context.py +0 -0
  8. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/_registry.py +0 -0
  9. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/artists/__init__.py +0 -0
  10. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/artists/_common.py +0 -0
  11. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/artists/barh_plot.py +0 -0
  12. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/artists/line_plot.py +0 -0
  13. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/artists/stacked.py +0 -0
  14. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/artists/type_curve.py +0 -0
  15. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/README.md +0 -0
  16. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/fonts/CabinetGrotesk-Black.otf +0 -0
  17. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/fonts/CabinetGrotesk-Bold.otf +0 -0
  18. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/fonts/CabinetGrotesk-Extrabold.otf +0 -0
  19. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/fonts/CabinetGrotesk-Extralight.otf +0 -0
  20. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/fonts/CabinetGrotesk-Light.otf +0 -0
  21. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/fonts/CabinetGrotesk-Medium.otf +0 -0
  22. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/fonts/CabinetGrotesk-Regular.otf +0 -0
  23. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/fonts/CabinetGrotesk-Thin.otf +0 -0
  24. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/fonts/README.md +0 -0
  25. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/assets/tesoro.mplstyle +0 -0
  26. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/data_sources/__init__.py +0 -0
  27. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/data_sources/debug.py +0 -0
  28. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/database/__init__.py +0 -0
  29. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/database/local.py +0 -0
  30. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/database/push.py +0 -0
  31. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/database/shared.py +0 -0
  32. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/dependencies/__init__.py +0 -0
  33. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/dependencies/node.py +0 -0
  34. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/dependencies/resolution.py +0 -0
  35. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/driver.py +0 -0
  36. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/manifest.py +0 -0
  37. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/offsets/__init__.py +0 -0
  38. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/offsets/offsets.py +0 -0
  39. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/offsets/outliers.py +0 -0
  40. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/orchestration.py +0 -0
  41. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/pipeline/__init__.py +0 -0
  42. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/pipeline/diagnose.py +0 -0
  43. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/pipeline/engine.py +0 -0
  44. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/pipeline/rules.py +0 -0
  45. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/providers/__init__.py +0 -0
  46. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/providers/base.py +0 -0
  47. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/providers/bde.py +0 -0
  48. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/providers/ecb.py +0 -0
  49. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/py.typed +0 -0
  50. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/render/__init__.py +0 -0
  51. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/render/content/__init__.py +0 -0
  52. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/render/content/content.py +0 -0
  53. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/render/content/images.py +0 -0
  54. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/render/content/section.py +0 -0
  55. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/render/content/subtitle.py +0 -0
  56. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/render/content/text.py +0 -0
  57. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/render/content/title.py +0 -0
  58. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/render/report.py +0 -0
  59. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/testing/__init__.py +0 -0
  60. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/testing/compare.py +0 -0
  61. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/utils/__init__.py +0 -0
  62. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/utils/config.py +0 -0
  63. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/utils/format.py +0 -0
  64. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/utils/globals.py +0 -0
  65. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/utils/matplotlib.py +0 -0
  66. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/utils/series.py +0 -0
  67. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/src/tesorotools/utils/shortcuts.py +0 -0
  68. {tesorotools_python-0.0.40 → tesorotools_python-0.0.41}/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.40
3
+ Version: 0.0.41
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.40"
4
+ version = "0.0.41"
5
5
  dependencies = [
6
6
  # database and ORM
7
7
  "psycopg[binary]>=3.1",
@@ -39,3 +39,6 @@ line:
39
39
  table:
40
40
  style: Light Shading Accent 1
41
41
  autofit: False
42
+ block_separator:
43
+ fill: "BFBFBF" # hex color of the inter-block band
44
+ height_twips: 20 # half-band height; total band = 2 * height_twips
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  # pyright: reportPrivateUsage=false
4
4
  from dataclasses import dataclass
5
5
  from pathlib import Path
6
- from typing import Any, Self
6
+ from typing import Any, Self, TypedDict
7
7
 
8
8
  import numpy as np
9
9
  import pandas as pd
@@ -25,6 +25,35 @@ from tesorotools.utils.template import TemplateLoader
25
25
  RENDER_CONFIG: dict[str, Any] = read_config(PLOT_CONFIG_FILE)["table"]
26
26
 
27
27
 
28
+ class BlockSeparatorConfig(TypedDict, total=False):
29
+ """User-facing visual config for the inter-block separator band.
30
+
31
+ The band is rendered as **two adjacent rows** sharing the same
32
+ fill, so the pair shifts Word's automatic banding count by an
33
+ even number (preserving alternation in data rows below). Both
34
+ keys are optional; missing keys fall back to defaults.
35
+ """
36
+
37
+ fill: str
38
+ """Hex color of the band, without leading ``#``. Default ``BFBFBF``."""
39
+
40
+ height_twips: int
41
+ """Height of *each* of the two rows. Total band height is
42
+ ``2 * height_twips``. Default ``20`` (1pt per row, 2pt total)."""
43
+
44
+
45
+ class _ResolvedSeparator(TypedDict):
46
+ """Internal, fully-resolved variant of :class:`BlockSeparatorConfig`.
47
+
48
+ After defaults are merged in, every field is guaranteed present —
49
+ this lets call sites index without ``.get()`` and silences
50
+ pyright's NotRequired warnings.
51
+ """
52
+
53
+ fill: str
54
+ height_twips: int
55
+
56
+
28
57
  @dataclass(frozen=True)
29
58
  class RenderConfig:
30
59
  """Per-call rendering options for :func:`render_table`.
@@ -36,12 +65,18 @@ class RenderConfig:
36
65
 
37
66
  style: str | None = None
38
67
  autofit: bool = False
68
+ block_separator: BlockSeparatorConfig | None = None
39
69
 
40
70
  @classmethod
41
71
  def from_global(cls) -> RenderConfig:
72
+ sep_raw: Any = RENDER_CONFIG.get("block_separator")
73
+ block_separator: BlockSeparatorConfig | None = (
74
+ None if sep_raw is None else BlockSeparatorConfig(**sep_raw)
75
+ )
42
76
  return cls(
43
77
  style=RENDER_CONFIG.get("style"),
44
78
  autofit=bool(RENDER_CONFIG["autofit"]),
79
+ block_separator=block_separator,
45
80
  )
46
81
 
47
82
 
@@ -142,58 +177,125 @@ def _fill_index_names(
142
177
  _style_index_names(cell)
143
178
 
144
179
 
180
+ _DEFAULT_SEPARATOR: _ResolvedSeparator = {
181
+ "fill": "BFBFBF",
182
+ "height_twips": 20,
183
+ }
184
+
185
+
145
186
  # we only separate blocks in vertically stacked tables
146
187
  def _separate_blocks(
147
188
  index: pd.Index[Any] | pd.MultiIndex,
148
189
  table_docx: TableDocx,
190
+ *,
191
+ horizontal: bool,
192
+ config: BlockSeparatorConfig | None = None,
149
193
  ) -> None:
194
+ """Insert two adjacent separator rows after every block boundary.
195
+
196
+ Each separator is one merged cell (``<w:gridSpan>`` covering all
197
+ columns) with exact-height ``<w:trHeight>`` and a solid ``<w:shd>``
198
+ fill. Two adjacent rows with identical fill render as a single
199
+ seamless band; their pair shifts Word's automatic banding count by
200
+ an even number, preserving the alternation in the data rows below.
201
+
202
+ The cnfStyle approach this replaces was OOXML-valid but Word
203
+ treated ``<w:cnfStyle>`` as descriptive bookkeeping rather than as
204
+ an override directive — the conditional formatting was never
205
+ applied. See README "Block separators" for the postmortem.
206
+ """
150
207
  if not isinstance(index, pd.MultiIndex):
151
208
  raise ValueError(
152
209
  "block_sep=True requires the row index to be a MultiIndex; "
153
210
  "got a flat Index. Set block_sep=False or wrap rows in a "
154
211
  "MultiIndex with the block name as level 0."
155
212
  )
156
- _disable_implicit_last_row(table_docx)
213
+ cfg: _ResolvedSeparator = {**_DEFAULT_SEPARATOR, **(config or {})}
214
+ num_cols: int = len(table_docx.columns)
215
+ grid_total: int = _grid_total_twips(table_docx)
216
+ header_rows: int = 2 if horizontal else 1
157
217
  blocks: list[str] = list(index.get_level_values(level=0).unique())
158
218
  previous_rows: int = 0
219
+ inserted: int = 0
159
220
  for block in blocks[:-1]:
160
221
  block_size: int = len(index[index.get_level_values(level=0) == block])
161
- _separate_row(table_docx.rows[block_size + previous_rows])
222
+ anchor_idx: int = (
223
+ header_rows + previous_rows + block_size - 1 + inserted
224
+ )
225
+ anchor: TableRow = table_docx.rows[anchor_idx]
226
+ _insert_double_separator(anchor, num_cols, grid_total, cfg)
162
227
  previous_rows += block_size
228
+ inserted += 2
163
229
 
164
230
 
165
- def _separate_row(row: TableRow) -> None:
166
- """Mark *row* as ``lastRow`` via conditional-formatting reference.
167
-
168
- The visible separator border belongs to the active table style
169
- (``<w:tblStylePr w:type="lastRow">`` in ``styles.xml``). Emitting
170
- only ``<w:cnfStyle>`` on ``<w:trPr>`` keeps direct formatting out of
171
- ``<w:tcPr>``, which is the construct Word's co-authoring merge
172
- engine corrupts in shared OneDrive/SharePoint documents. The
173
- consumer's ``template.docx`` must define the ``lastRow`` conditional
174
- formatting on the table style referenced by ``plots.yaml``.
175
- """
176
- trPr: Any = row._tr.get_or_add_trPr() # type: ignore[reportUnknownMemberType]
177
- cnfStyle: Any = OxmlElement("w:cnfStyle") # type: ignore[reportUnknownVariableType]
178
- # 12-bit mask, ECMA-376 §17.4.7. Bit 1 (zero-indexed) = lastRow.
179
- cnfStyle.set(qn("w:val"), "010000000000") # type: ignore[reportUnknownMemberType]
180
- trPr.append(cnfStyle) # type: ignore[reportUnknownMemberType]
181
-
231
+ def _grid_total_twips(table_docx: TableDocx) -> int:
232
+ """Sum the widths of every ``<w:gridCol>`` under ``<w:tblGrid>``."""
233
+ tblGrid: Any = table_docx._element.find(qn("w:tblGrid")) # type: ignore[reportUnknownMemberType]
234
+ cols: Any = tblGrid.findall(qn("w:gridCol")) # type: ignore[reportUnknownMemberType]
235
+ return sum(int(gc.get(qn("w:w"))) for gc in cols) # type: ignore[reportUnknownArgumentType, reportUnknownVariableType]
182
236
 
183
- def _disable_implicit_last_row(table_docx: TableDocx) -> None:
184
- """Force ``<w:tblLook w:lastRow="0"/>`` so the real last row stays plain.
185
237
 
186
- With block_sep we mark *interior* rows as ``lastRow`` via
187
- ``cnfStyle``. The table's own last row must not pick up the same
188
- conditional formatting automatically, so we disable the implicit
189
- ``lastRow`` flag at the ``<w:tblLook>`` level.
190
- """
191
- tblPr: Any = table_docx._element.tblPr # type: ignore[reportUnknownMemberType]
192
- tblLook: Any = tblPr.find(qn("w:tblLook")) # type: ignore[reportUnknownMemberType]
193
- if tblLook is None:
194
- tblLook = OxmlElement("w:tblLook") # type: ignore[reportUnknownVariableType]
195
- tblPr.append(tblLook) # type: ignore[reportUnknownMemberType]
196
- tblLook.set(qn("w:lastRow"), "0") # type: ignore[reportUnknownMemberType]
238
+ def _insert_double_separator(
239
+ anchor_row: TableRow,
240
+ num_cols: int,
241
+ grid_total_twips: int,
242
+ cfg: _ResolvedSeparator,
243
+ ) -> None:
244
+ sep1: Any = _build_separator_row(num_cols, grid_total_twips, cfg)
245
+ sep2: Any = _build_separator_row(num_cols, grid_total_twips, cfg)
246
+ # Insert sep2 first, then sep1 in front of sep2 — both end up after
247
+ # anchor_row in document order (anchor → sep1 → sep2 → next data row).
248
+ anchor_row._tr.addnext(sep2) # type: ignore[reportUnknownMemberType]
249
+ anchor_row._tr.addnext(sep1) # type: ignore[reportUnknownMemberType]
250
+
251
+
252
+ def _build_separator_row(
253
+ num_cols: int, grid_total_twips: int, cfg: _ResolvedSeparator
254
+ ) -> Any:
255
+ fill: str = cfg["fill"]
256
+ height_twips: int = cfg["height_twips"]
257
+
258
+ tr: Any = OxmlElement("w:tr") # type: ignore[reportUnknownVariableType]
259
+
260
+ trPr: Any = OxmlElement("w:trPr") # type: ignore[reportUnknownVariableType]
261
+ trHeight: Any = OxmlElement("w:trHeight") # type: ignore[reportUnknownVariableType]
262
+ trHeight.set(qn("w:val"), str(height_twips)) # type: ignore[reportUnknownMemberType]
263
+ trHeight.set(qn("w:hRule"), "exact") # type: ignore[reportUnknownMemberType]
264
+ trPr.append(trHeight) # type: ignore[reportUnknownMemberType]
265
+ tr.append(trPr) # type: ignore[reportUnknownMemberType]
266
+
267
+ tc: Any = OxmlElement("w:tc") # type: ignore[reportUnknownVariableType]
268
+ tcPr: Any = OxmlElement("w:tcPr") # type: ignore[reportUnknownVariableType]
269
+ tcW: Any = OxmlElement("w:tcW") # type: ignore[reportUnknownVariableType]
270
+ tcW.set(qn("w:w"), str(grid_total_twips)) # type: ignore[reportUnknownMemberType]
271
+ tcW.set(qn("w:type"), "dxa") # type: ignore[reportUnknownMemberType]
272
+ tcPr.append(tcW) # type: ignore[reportUnknownMemberType]
273
+ gridSpan: Any = OxmlElement("w:gridSpan") # type: ignore[reportUnknownVariableType]
274
+ gridSpan.set(qn("w:val"), str(num_cols)) # type: ignore[reportUnknownMemberType]
275
+ tcPr.append(gridSpan) # type: ignore[reportUnknownMemberType]
276
+ shd: Any = OxmlElement("w:shd") # type: ignore[reportUnknownVariableType]
277
+ shd.set(qn("w:val"), "clear") # type: ignore[reportUnknownMemberType]
278
+ shd.set(qn("w:color"), "auto") # type: ignore[reportUnknownMemberType]
279
+ shd.set(qn("w:fill"), fill) # type: ignore[reportUnknownMemberType]
280
+ tcPr.append(shd) # type: ignore[reportUnknownMemberType]
281
+ tc.append(tcPr) # type: ignore[reportUnknownMemberType]
282
+
283
+ # OOXML requires <w:tc> to contain at least one block-level element.
284
+ # The inner paragraph carries exact line spacing so Word does not
285
+ # inflate the row past the configured trHeight on screen.
286
+ p: Any = OxmlElement("w:p") # type: ignore[reportUnknownVariableType]
287
+ pPr: Any = OxmlElement("w:pPr") # type: ignore[reportUnknownVariableType]
288
+ spacing: Any = OxmlElement("w:spacing") # type: ignore[reportUnknownVariableType]
289
+ spacing.set(qn("w:before"), "0") # type: ignore[reportUnknownMemberType]
290
+ spacing.set(qn("w:after"), "0") # type: ignore[reportUnknownMemberType]
291
+ spacing.set(qn("w:line"), str(height_twips)) # type: ignore[reportUnknownMemberType]
292
+ spacing.set(qn("w:lineRule"), "exact") # type: ignore[reportUnknownMemberType]
293
+ pPr.append(spacing) # type: ignore[reportUnknownMemberType]
294
+ p.append(pPr) # type: ignore[reportUnknownMemberType]
295
+ tc.append(p) # type: ignore[reportUnknownMemberType]
296
+
297
+ tr.append(tc) # type: ignore[reportUnknownMemberType]
298
+ return tr # type: ignore[reportUnknownVariableType]
197
299
 
198
300
 
199
301
  def _is_bright(hex_color: str) -> bool:
@@ -296,9 +398,17 @@ def render_table(
296
398
  horizontal=horizontal,
297
399
  **kwargs,
298
400
  )
299
- if block_sep:
300
- _separate_blocks(table.index, table_docx)
301
401
  _fill_content(table, color_table, shade_table, table_docx, horizontal)
402
+ # Block separators insert *new rows* into the table, so they must
403
+ # run after _fill_content — otherwise the (row, col) indexing in
404
+ # _fill_content would land on the inserted separator rows.
405
+ if block_sep:
406
+ _separate_blocks(
407
+ table.index,
408
+ table_docx,
409
+ horizontal=horizontal,
410
+ config=cfg.block_separator,
411
+ )
302
412
  table_docx.alignment = WD_TABLE_ALIGNMENT.CENTER
303
413
  return table_docx
304
414