Coverage for src/rtflite/input.py: 57%
237 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-07 05:03 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-07 05:03 +0000
1from collections.abc import Sequence
3from pydantic import BaseModel, ConfigDict, Field, field_validator
5from rtflite.attributes import TextAttributes, TableAttributes, BroadcastValue
6from rtflite.row import BORDER_CODES
9class RTFPage(BaseModel):
10 """RTF page configuration"""
12 orientation: str | None = Field(
13 default="portrait", description="Page orientation ('portrait' or 'landscape')"
14 )
16 @field_validator("orientation")
17 def validate_orientation(cls, v):
18 if v not in ["portrait", "landscape"]:
19 raise ValueError(
20 f"Invalid orientation. Must be 'portrait' or 'landscape'. Given: {v}"
21 )
22 return v
24 width: float | None = Field(default=None, description="Page width in inches")
25 height: float | None = Field(default=None, description="Page height in inches")
26 margin: Sequence[float] | None = Field(
27 default=None,
28 description="Page margins [left, right, top, bottom, header, footer] in inches",
29 )
31 @field_validator("margin")
32 def validate_margin(cls, v):
33 if v is not None and len(v) != 6:
34 raise ValueError("Margin must be a sequence of 6 values.")
35 return v
37 nrow: int | None = Field(default=None, description="Number of rows per page")
39 border_first: str | None = Field(
40 default="double", description="First row border style"
41 )
42 border_last: str | None = Field(
43 default="double", description="Last row border style"
44 )
45 col_width: float | None = Field(
46 default=None, description="Total width of table columns in inches"
47 )
48 use_color: bool | None = Field(
49 default=False, description="Whether to use color in the document"
50 )
52 @field_validator("border_first", "border_last")
53 def validate_border(cls, v):
54 if v not in BORDER_CODES:
55 raise ValueError(
56 f"{cls.__field_name__.capitalize()} with invalid border style: {v}"
57 )
58 return v
60 @field_validator("width", "height", "nrow", "col_width")
61 def validate_width_height(cls, v):
62 if v is not None and v <= 0:
63 raise ValueError(
64 f"{cls.__field_name__.capitalize()} must be greater than 0."
65 )
66 return v
68 def __init__(self, **data):
69 super().__init__(**data)
70 self._set_default()
72 def _set_default(self):
73 if self.orientation == "portrait":
74 self.width = self.width or 8.5
75 self.height = self.height or 11
76 self.margin = self.margin or [1.25, 1, 1.75, 1.25, 1.75, 1.00625]
77 self.col_width = self.col_width or self.width - 2.25
78 self.nrow = self.nrow or 40
80 if self.orientation == "landscape":
81 self.width = self.width or 11
82 self.height = self.height or 8.5
83 self.margin = self.margin or [1.0, 1.0, 2, 1.25, 1.25, 1.25]
84 self.col_width = self.col_width or self.width - 2.5
85 self.nrow = self.nrow or 24
87 if len(self.margin) != 6:
88 raise ValueError("Margin length must be 6.")
90 return self
93class RTFPageHeader(TextAttributes):
94 text: Sequence[str] | None = Field(
95 default="Page \\pagenumber of \\pagefield",
96 description="Page header text content",
97 )
99 @field_validator("text", mode="before")
100 def convert_text(cls, v):
101 if v is not None:
102 if isinstance(v, str):
103 return [v]
104 return v
106 text_indent_reference: str | None = Field(
107 default="page",
108 description="Reference point for indentation ('page' or 'table')",
109 )
111 def __init__(self, **data):
112 defaults = {
113 "text_font": [1],
114 "text_font_size": [9],
115 "text_justification": ["r"],
116 "text_indent_first": [0],
117 "text_indent_left": [0],
118 "text_indent_right": [0],
119 "text_space": [1.0],
120 "text_space_before": [15.0],
121 "text_space_after": [15.0],
122 "text_hyphenation": [True],
123 "text_convert": [True],
124 }
126 # Update defaults with any provided values
127 defaults.update(data)
128 super().__init__(**defaults)
129 self._set_default()
131 def _set_default(self):
132 for attr, value in self.__dict__.items():
133 if isinstance(value, (str, int, float, bool)):
134 setattr(self, attr, [value])
135 if isinstance(value, list):
136 setattr(self, attr, tuple(value))
137 return self
140class RTFPageFooter(TextAttributes):
141 text: Sequence[str] | None = Field(
142 default=None, description="Page footer text content"
143 )
144 text_indent_reference: str | None = Field(
145 default="page",
146 description="Reference point for indentation ('page' or 'table')",
147 )
149 @field_validator("text", mode="before")
150 def convert_text(cls, v):
151 if v is not None:
152 if isinstance(v, str):
153 return [v]
154 return v
156 def __init__(self, **data):
157 defaults = {
158 "text_font": [1],
159 "text_font_size": [9],
160 "text_justification": ["c"],
161 "text_indent_first": [0],
162 "text_indent_left": [0],
163 "text_indent_right": [0],
164 "text_space": [1.0],
165 "text_space_before": [15.0],
166 "text_space_after": [15.0],
167 "text_hyphenation": [True],
168 "text_convert": [True],
169 }
171 # Update defaults with any provided values
172 defaults.update(data)
173 super().__init__(**defaults)
174 self._set_default()
176 def _set_default(self):
177 for attr, value in self.__dict__.items():
178 if isinstance(value, (str, int, float, bool)):
179 setattr(self, attr, [value])
180 if isinstance(value, list):
181 setattr(self, attr, tuple(value))
182 return self
185class RTFSubline(TextAttributes):
186 text: Sequence[str] | None = Field(
187 default=None, description="Page footer text content"
188 )
189 text_indent_reference: str | None = Field(
190 default="table",
191 description="Reference point for indentation ('page' or 'table')",
192 )
194 @field_validator("text", mode="before")
195 def convert_text(cls, v):
196 if v is not None:
197 if isinstance(v, str):
198 return [v]
199 return v
201 def __init__(self, **data):
202 defaults = {
203 "text_font": [1],
204 "text_font_size": [9],
205 "text_justification": ["l"],
206 "text_indent_first": [0],
207 "text_indent_left": [0],
208 "text_indent_right": [0],
209 "text_space": [1.0],
210 "text_space_before": [15.0],
211 "text_space_after": [15.0],
212 "text_hyphenation": [True],
213 "text_convert": [True],
214 }
216 # Update defaults with any provided values
217 defaults.update(data)
218 super().__init__(**defaults)
219 self._set_default()
221 def _set_default(self):
222 for attr, value in self.__dict__.items():
223 if isinstance(value, (str, int, float, bool)):
224 setattr(self, attr, [value])
225 if isinstance(value, list):
226 setattr(self, attr, tuple(value))
227 return self
230class RTFFootnote(TableAttributes):
231 """Class for RTF footnote settings"""
233 model_config = ConfigDict(arbitrary_types_allowed=True)
235 text: Sequence[str] | None = Field(default=None, description="Footnote table")
237 @field_validator("text", mode="before")
238 def convert_text(cls, v):
239 if v is not None:
240 if isinstance(v, str):
241 return [v]
242 return v
244 def __init__(self, **data):
245 defaults = {
246 "col_rel_width": [1],
247 "border_left": ["single"],
248 "border_right": ["single"],
249 "border_top": ["single"],
250 "border_bottom": [""],
251 "border_width": [15],
252 "cell_height": [0.15],
253 "cell_justification": ["c"],
254 "cell_vertical_justification": ["top"],
255 "text_font": [1],
256 "text_format": [""],
257 "text_font_size": [9],
258 "text_justification": ["l"],
259 "text_indent_first": [0],
260 "text_indent_left": [0],
261 "text_indent_right": [0],
262 "text_space": [1],
263 "text_space_before": [15],
264 "text_space_after": [15],
265 "text_hyphenation": [False],
266 "text_convert": [True],
267 }
269 # Update defaults with any provided values
270 defaults.update(data)
271 super().__init__(**defaults)
272 self._set_default()
273 # Convert text to DataFrame during initialization
274 if self.text is not None:
275 if isinstance(self.text, Sequence):
276 self.text = "\\line ".join(self.text)
278 def _set_default(self):
279 for attr, value in self.__dict__.items():
280 if isinstance(value, (str, int, float, bool)):
281 setattr(self, attr, [value])
283 return self
286class RTFSource(TableAttributes):
287 """Class for RTF data source settings"""
289 model_config = ConfigDict(arbitrary_types_allowed=True)
291 text: Sequence[str] | None = Field(default=None, description="Data source table")
293 @field_validator("text", mode="before")
294 def convert_text(cls, v):
295 if v is not None:
296 if isinstance(v, str):
297 return [v]
298 return v
300 def __init__(self, **data):
301 defaults = {
302 "col_rel_width": [1],
303 "border_left": [""],
304 "border_right": [""],
305 "border_top": [""],
306 "border_bottom": [""],
307 "border_width": [15],
308 "cell_height": [0.15],
309 "cell_justification": ["c"],
310 "cell_vertical_justification": ["top"],
311 "text_font": [1],
312 "text_format": [""],
313 "text_font_size": [9],
314 "text_justification": ["c"],
315 "text_indent_first": [0],
316 "text_indent_left": [0],
317 "text_indent_right": [0],
318 "text_space": [1],
319 "text_space_before": [15],
320 "text_space_after": [15],
321 "text_hyphenation": [False],
322 "text_convert": [True],
323 }
325 # Update defaults with any provided values
326 defaults.update(data)
327 super().__init__(**defaults)
328 self._set_default()
330 # Convert text to DataFrame during initialization
331 if self.text is not None:
332 if isinstance(self.text, Sequence):
333 self.text = "\\line ".join(self.text)
335 def _set_default(self):
336 for attr, value in self.__dict__.items():
337 if isinstance(value, (str, int, float, bool)):
338 setattr(self, attr, [value])
340 return self
343class RTFTitle(TextAttributes):
344 text: Sequence[str] | None = Field(default=None, description="Title text content")
345 text_indent_reference: str | None = Field(
346 default="table",
347 description="Reference point for indentation ('page' or 'table')",
348 )
350 @field_validator("text", mode="before")
351 def convert_text(cls, v):
352 if v is not None:
353 if isinstance(v, str):
354 return [v]
355 return v
357 def __init__(self, **data):
358 defaults = {
359 "text_font": [1],
360 "text_font_size": [12],
361 "text_justification": ["c"],
362 "text_indent_first": [0],
363 "text_indent_left": [0],
364 "text_indent_right": [0],
365 "text_space": [1.0],
366 "text_space_before": [180.0],
367 "text_space_after": [180.0],
368 "text_hyphenation": [True],
369 "text_convert": [True],
370 }
372 # Update defaults with any provided values
373 defaults.update(data)
374 super().__init__(**defaults)
375 self._set_default()
377 def _set_default(self):
378 for attr, value in self.__dict__.items():
379 if isinstance(value, (str, int, float, bool)):
380 setattr(self, attr, [value])
381 if isinstance(value, list):
382 setattr(self, attr, tuple(value))
383 return self
386class RTFColumnHeader(TableAttributes):
387 """Class for RTF column header settings"""
389 model_config = ConfigDict(arbitrary_types_allowed=True)
391 text: Sequence[str] | None = Field(default=None, description="Column header table")
393 @field_validator("text", mode="before")
394 def convert_text(cls, v):
395 if v is not None:
396 if isinstance(v, str):
397 return [v]
398 return v
400 def __init__(self, **data):
401 defaults = {
402 "border_left": ["single"],
403 "border_right": ["single"],
404 "border_top": ["single"],
405 "border_bottom": [""],
406 "border_width": [15],
407 "cell_height": [0.15],
408 "cell_justification": ["c"],
409 "cell_vertical_justification": ["bottom"],
410 "text_font": [1],
411 "text_format": [""],
412 "text_font_size": [9],
413 "text_justification": ["c"],
414 "text_indent_first": [0],
415 "text_indent_left": [0],
416 "text_indent_right": [0],
417 "text_space": [1],
418 "text_space_before": [15],
419 "text_space_after": [15],
420 "text_hyphenation": [False],
421 "text_convert": [True],
422 }
424 # Update defaults with any provided values
425 defaults.update(data)
426 super().__init__(**defaults)
427 self._set_default()
429 def _set_default(self):
430 for attr, value in self.__dict__.items():
431 if isinstance(value, (str, int, float, bool)):
432 setattr(self, attr, [value])
434 return self
437class RTFBody(TableAttributes):
438 """Class for RTF document body settings"""
440 model_config = ConfigDict(arbitrary_types_allowed=True)
442 as_colheader: bool = Field(
443 default=True, description="Whether to display column headers"
444 )
445 group_by: Sequence[str] | None = Field(
446 default=None, description="Column name to group rows by"
447 )
448 page_by: Sequence[str] | None = Field(
449 default=None, description="Column name to create page breaks by"
450 )
451 new_page: bool = Field(default=False, description="Force new page before table")
452 pageby_header: bool = Field(default=True, description="Repeat headers on new pages")
453 pageby_row: str = Field(
454 default="column",
455 description="Page break handling for rows ('column' or 'value')",
456 )
457 subline_by: Sequence[str] | None = Field(
458 default=None, description="Column name to create sublines by"
459 )
460 last_row: bool = Field(
461 default=True,
462 description="Whether the table contains the last row of the final table",
463 )
465 @field_validator("group_by", "page_by", "subline_by", mode="before")
466 def convert_text(cls, v):
467 if v is not None:
468 if isinstance(v, str):
469 return [v]
470 return v
472 def __init__(self, **data):
473 defaults = {
474 "border_left": ["single"],
475 "border_right": ["single"],
476 "border_first": ["single"],
477 "border_last": ["single"],
478 "border_width": [15],
479 "cell_height": [0.15],
480 "cell_justification": ["c"],
481 "cell_vertical_justification": ["top"],
482 "text_font": [1],
483 "text_font_size": [9],
484 "text_indent_first": [0],
485 "text_indent_left": [0],
486 "text_indent_right": [0],
487 "text_space": [1],
488 "text_space_before": [15],
489 "text_space_after": [15],
490 "text_hyphenation": [False],
491 "text_convert": [True],
492 }
494 # Update defaults with any provided values
495 defaults.update(data)
496 super().__init__(**defaults)
497 self._set_default()
499 def _set_default(self):
500 for attr, value in self.__dict__.items():
501 if isinstance(value, (str, int, float, bool)) and attr not in [
502 "as_colheader",
503 "page_by",
504 "new_page",
505 "pageby_header",
506 "pageby_row",
507 "subline_by",
508 "last_row",
509 ]:
510 setattr(self, attr, [value])
512 self.border_top = self.border_top or [""]
513 self.border_bottom = self.border_bottom or [""]
514 self.border_left = self.border_left or ["single"]
515 self.border_right = self.border_right or ["single"]
516 self.border_first = self.border_first or ["single"]
517 self.border_last = self.border_last or ["single"]
518 self.cell_vertical_justification = self.cell_vertical_justification or [
519 "center"
520 ]
521 self.text_justification = self.text_justification or ["c"]
523 if self.page_by is None:
524 if self.new_page:
525 raise ValueError(
526 "`new_page` must be `False` if `page_by` is not specified"
527 )
529 return self