Coverage for src/rtflite/input.py: 87%

228 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-02-03 15:40 +0000

1from collections.abc import MutableSequence, Sequence 

2from typing import Any, Text, Tuple 

3 

4import numpy as np 

5import pandas as pd 

6from pydantic import BaseModel, ConfigDict, Field 

7 

8from .row import Border, Cell, Row, TextContent 

9from .strwidth import get_string_width 

10 

11 

12class TextAttributes(BaseModel): 

13 """Base class for text-related attributes in RTF components""" 

14 

15 model_config = ConfigDict(arbitrary_types_allowed=True) 

16 

17 text_font: int | Sequence[int] | pd.DataFrame | Tuple | None = Field( 

18 default=None, description="Font number for text" 

19 ) 

20 text_format: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

21 default=None, description="Text formatting (e.g. 'bold', 'italic')" 

22 ) 

23 text_font_size: float | Sequence[float] | pd.DataFrame | Tuple | None = Field( 

24 default=None, description="Font size in points" 

25 ) 

26 text_color: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

27 default=None, description="Text color name or RGB value" 

28 ) 

29 text_background_color: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

30 default=None, description="Background color name or RGB value" 

31 ) 

32 text_justification: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

33 default=None, 

34 description="Text alignment ('l'=left, 'c'=center, 'r'=right, 'j'=justify)", 

35 ) 

36 text_indent_first: float | Sequence[float] | pd.DataFrame | Tuple | None = Field( 

37 default=None, description="First line indent in inches/twips" 

38 ) 

39 text_indent_left: float | Sequence[float] | pd.DataFrame | Tuple | None = Field( 

40 default=None, description="Left indent in inches/twips" 

41 ) 

42 text_indent_right: float | Sequence[float] | pd.DataFrame | Tuple | None = Field( 

43 default=None, description="Right indent in inches/twips" 

44 ) 

45 text_space: float | Sequence[float] | pd.DataFrame | Tuple | None = Field( 

46 default=None, description="Line spacing multiplier" 

47 ) 

48 text_space_before: float | Sequence[float] | pd.DataFrame | Tuple | None = Field( 

49 default=None, description="Space before paragraph in twips" 

50 ) 

51 text_space_after: float | Sequence[float] | pd.DataFrame | Tuple | None = Field( 

52 default=None, description="Space after paragraph in twips" 

53 ) 

54 text_hyphenation: bool | Sequence[bool] | pd.DataFrame | Tuple | None = Field( 

55 default=None, description="Enable automatic hyphenation" 

56 ) 

57 text_convert: bool | Sequence[bool] | pd.DataFrame | Tuple | None = Field( 

58 default=None, description="Convert special characters to RTF" 

59 ) 

60 

61 def _encode(self, text: Sequence[str], method: str) -> str: 

62 """Convert the RTF title into RTF syntax using the Text class.""" 

63 

64 dim = [len(text), 1] 

65 

66 text_components = [] 

67 for i in range(dim[0]): 

68 text_components.append( 

69 TextContent( 

70 text=str(text[i]), 

71 font=BroadcastValue(value=self.text_font, dimension=dim).iloc(i, 0), 

72 size=BroadcastValue(value=self.text_font_size, dimension=dim).iloc( 

73 i, 0 

74 ), 

75 format=BroadcastValue(value=self.text_format, dimension=dim).iloc( 

76 i, 0 

77 ), 

78 color=BroadcastValue(value=self.text_color, dimension=dim).iloc( 

79 i, 0 

80 ), 

81 background_color=BroadcastValue( 

82 value=self.text_background_color, dimension=dim 

83 ).iloc(i, 0), 

84 justification=BroadcastValue( 

85 value=self.text_justification, dimension=dim 

86 ).iloc(i, 0), 

87 indent_first=BroadcastValue( 

88 value=self.text_indent_first, dimension=dim 

89 ).iloc(i, 0), 

90 indent_left=BroadcastValue( 

91 value=self.text_indent_left, dimension=dim 

92 ).iloc(i, 0), 

93 indent_right=BroadcastValue( 

94 value=self.text_indent_right, dimension=dim 

95 ).iloc(i, 0), 

96 space=BroadcastValue(value=self.text_space, dimension=dim).iloc( 

97 i, 0 

98 ), 

99 space_before=BroadcastValue( 

100 value=self.text_space_before, dimension=dim 

101 ).iloc(i, 0), 

102 space_after=BroadcastValue( 

103 value=self.text_space_after, dimension=dim 

104 ).iloc(i, 0), 

105 convert=BroadcastValue(value=self.text_convert, dimension=dim).iloc( 

106 i, 0 

107 ), 

108 hyphenation=BroadcastValue( 

109 value=self.text_hyphenation, dimension=dim 

110 ).iloc(i, 0), 

111 ) 

112 ) 

113 

114 if method == "paragraph": 

115 return [ 

116 text_component._as_rtf(method="paragraph") 

117 for text_component in text_components 

118 ] 

119 

120 if method == "line": 

121 line = "\\line".join( 

122 [ 

123 text_component._as_rtf(method="plain") 

124 for text_component in text_components 

125 ] 

126 ) 

127 

128 return TextContent( 

129 text=str(line), 

130 font=BroadcastValue(value=self.text_font, dimension=dim).iloc(i, 0), 

131 size=BroadcastValue(value=self.text_font_size, dimension=dim).iloc( 

132 i, 0 

133 ), 

134 format=BroadcastValue(value=self.text_format, dimension=dim).iloc(i, 0), 

135 color=BroadcastValue(value=self.text_color, dimension=dim).iloc(i, 0), 

136 background_color=BroadcastValue( 

137 value=self.text_background_color, dimension=dim 

138 ).iloc(i, 0), 

139 justification=BroadcastValue( 

140 value=self.text_justification, dimension=dim 

141 ).iloc(i, 0), 

142 indent_first=BroadcastValue( 

143 value=self.text_indent_first, dimension=dim 

144 ).iloc(i, 0), 

145 indent_left=BroadcastValue( 

146 value=self.text_indent_left, dimension=dim 

147 ).iloc(i, 0), 

148 indent_right=BroadcastValue( 

149 value=self.text_indent_right, dimension=dim 

150 ).iloc(i, 0), 

151 space=BroadcastValue(value=self.text_space, dimension=dim).iloc(i, 0), 

152 space_before=BroadcastValue( 

153 value=self.text_space_before, dimension=dim 

154 ).iloc(i, 0), 

155 space_after=BroadcastValue( 

156 value=self.text_space_after, dimension=dim 

157 ).iloc(i, 0), 

158 convert=BroadcastValue(value=self.text_convert, dimension=dim).iloc( 

159 i, 0 

160 ), 

161 hyphenation=BroadcastValue( 

162 value=self.text_hyphenation, dimension=dim 

163 ).iloc(i, 0), 

164 )._as_rtf(method="paragraph_format") 

165 

166 raise ValueError(f"Invalid method: {method}") 

167 

168 

169class TableAttributes(TextAttributes): 

170 """Base class for table-related attributes in RTF components""" 

171 

172 model_config = ConfigDict(arbitrary_types_allowed=True) 

173 

174 col_rel_width: float | Sequence[float] | None = Field( 

175 default=None, description="Relative widths of table columns" 

176 ) 

177 border_left: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

178 default=None, description="Left border style" 

179 ) 

180 border_right: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

181 default=None, description="Right border style" 

182 ) 

183 border_top: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

184 default=None, description="Top border style" 

185 ) 

186 border_bottom: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

187 default=None, description="Bottom border style" 

188 ) 

189 border_first: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

190 default=None, description="First row border style" 

191 ) 

192 border_last: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

193 default=None, description="Last row border style" 

194 ) 

195 border_color_left: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

196 default=None, description="Left border color" 

197 ) 

198 border_color_right: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

199 default=None, description="Right border color" 

200 ) 

201 border_color_top: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

202 default=None, description="Top border color" 

203 ) 

204 border_color_bottom: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

205 default=None, description="Bottom border color" 

206 ) 

207 border_color_first: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

208 default=None, description="First row border color" 

209 ) 

210 border_color_last: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

211 default=None, description="Last row border color" 

212 ) 

213 border_width: int | Sequence[int] | pd.DataFrame | Tuple | None = Field( 

214 default=None, description="Border width in twips" 

215 ) 

216 cell_height: float | Sequence[float] | pd.DataFrame | Tuple | None = Field( 

217 default=None, description="Cell height in inches" 

218 ) 

219 cell_justification: str | Sequence[str] | pd.DataFrame | Tuple | None = Field( 

220 default=None, 

221 description="Cell horizontal alignment ('l'=left, 'c'=center, 'r'=right, 'j'=justify)", 

222 ) 

223 cell_vertical_justification: str | Sequence[str] | pd.DataFrame | Tuple | None = ( 

224 Field( 

225 default=None, 

226 description="Cell vertical alignment ('top', 'center', 'bottom')", 

227 ) 

228 ) 

229 cell_nrow: int | Sequence[int] | pd.DataFrame | Tuple | None = Field( 

230 default=None, description="Number of rows per cell" 

231 ) 

232 

233 def _get_section_attributes(self, indices) -> dict: 

234 """Helper method to collect all attributes for a section""" 

235 text_attrs = { 

236 "text_font": self.text_font, 

237 "text_format": self.text_format, 

238 "text_font_size": self.text_font_size, 

239 "text_color": self.text_color, 

240 "text_background_color": self.text_background_color, 

241 "text_justification": self.text_justification, 

242 "text_indent_first": self.text_indent_first, 

243 "text_indent_left": self.text_indent_left, 

244 "text_indent_right": self.text_indent_right, 

245 "text_space": self.text_space, 

246 "text_space_before": self.text_space_before, 

247 "text_space_after": self.text_space_after, 

248 "text_hyphenation": self.text_hyphenation, 

249 "text_convert": self.text_convert, 

250 "col_rel_width": self.col_rel_width, 

251 "border_left": self.border_left, 

252 "border_right": self.border_right, 

253 "border_top": self.border_top, 

254 "border_bottom": self.border_bottom, 

255 "border_first": self.border_first, 

256 "border_last": self.border_last, 

257 "border_color_left": self.border_color_left, 

258 "border_color_right": self.border_color_right, 

259 "border_color_top": self.border_color_top, 

260 "border_color_bottom": self.border_color_bottom, 

261 "border_color_first": self.border_color_first, 

262 "border_color_last": self.border_color_last, 

263 "border_width": self.border_width, 

264 "cell_height": self.cell_height, 

265 "cell_justification": self.cell_justification, 

266 "cell_vertical_justification": self.cell_vertical_justification, 

267 "cell_nrow": self.cell_nrow, 

268 } 

269 

270 # Broadcast attributes to section indices 

271 return { 

272 attr: [BroadcastValue(value=val).iloc(row, col) for row, col in indices] 

273 for attr, val in text_attrs.items() 

274 } 

275 

276 def _encode( 

277 self, df: pd.DataFrame, col_widths: Sequence[float] 

278 ) -> MutableSequence[str]: 

279 dim = df.shape 

280 

281 if self.cell_nrow is None: 

282 self.cell_nrow = np.zeros(dim) 

283 

284 for i in range(dim[0]): 

285 for j in range(dim[1]): 

286 text = str(BroadcastValue(value=df, dimension=dim).iloc(i, j)) 

287 font_size = BroadcastValue( 

288 value=self.text_font_size, dimension=dim 

289 ).iloc(i, j) 

290 col_width = BroadcastValue(value=col_widths, dimension=dim).iloc( 

291 i, j 

292 ) 

293 cell_text_width = get_string_width(text=text, font_size=font_size) 

294 self.cell_nrow[i, j] = np.ceil(cell_text_width / col_width) 

295 

296 rows: MutableSequence[str] = [] 

297 for i in range(dim[0]): 

298 row = df.iloc[i] 

299 cells = [] 

300 

301 for j in range(dim[1]): 

302 col = df.columns[j] 

303 

304 if j == dim[1] - 1: 

305 border_right = Border( 

306 style=BroadcastValue( 

307 value=self.border_right, dimension=dim 

308 ).iloc(i, j) 

309 ) 

310 else: 

311 border_right = None 

312 

313 cell = Cell( 

314 text=TextContent( 

315 text=str(row[col]), 

316 font=BroadcastValue(value=self.text_font, dimension=dim).iloc( 

317 i, j 

318 ), 

319 size=BroadcastValue( 

320 value=self.text_font_size, dimension=dim 

321 ).iloc(i, j), 

322 format=BroadcastValue( 

323 value=self.text_format, dimension=dim 

324 ).iloc(i, j), 

325 color=BroadcastValue(value=self.text_color, dimension=dim).iloc( 

326 i, j 

327 ), 

328 background_color=BroadcastValue( 

329 value=self.text_background_color, dimension=dim 

330 ).iloc(i, j), 

331 justification=BroadcastValue( 

332 value=self.text_justification, dimension=dim 

333 ).iloc(i, j), 

334 indent_first=BroadcastValue( 

335 value=self.text_indent_first, dimension=dim 

336 ).iloc(i, j), 

337 indent_left=BroadcastValue( 

338 value=self.text_indent_left, dimension=dim 

339 ).iloc(i, j), 

340 indent_right=BroadcastValue( 

341 value=self.text_indent_right, dimension=dim 

342 ).iloc(i, j), 

343 space=BroadcastValue(value=self.text_space, dimension=dim).iloc( 

344 i, j 

345 ), 

346 space_before=BroadcastValue( 

347 value=self.text_space_before, dimension=dim 

348 ).iloc(i, j), 

349 space_after=BroadcastValue( 

350 value=self.text_space_after, dimension=dim 

351 ).iloc(i, j), 

352 convert=BroadcastValue( 

353 value=self.text_convert, dimension=dim 

354 ).iloc(i, j), 

355 hyphenation=BroadcastValue( 

356 value=self.text_hyphenation, dimension=dim 

357 ).iloc(i, j), 

358 ), 

359 width=col_widths[j], 

360 border_left=Border( 

361 style=BroadcastValue( 

362 value=self.border_left, dimension=dim 

363 ).iloc(i, j) 

364 ), 

365 border_right=border_right, 

366 border_top=Border( 

367 style=BroadcastValue(value=self.border_top, dimension=dim).iloc( 

368 i, j 

369 ) 

370 ), 

371 border_bottom=Border( 

372 style=BroadcastValue( 

373 value=self.border_bottom, dimension=dim 

374 ).iloc(i, j) 

375 ), 

376 vertical_justification=BroadcastValue( 

377 value=self.cell_vertical_justification, dimension=dim 

378 ).iloc(i, j), 

379 ) 

380 cells.append(cell) 

381 rtf_row = Row( 

382 row_cells=cells, 

383 justification=BroadcastValue( 

384 value=self.cell_justification, dimension=dim 

385 ).iloc(i, 0), 

386 height=BroadcastValue(value=self.cell_height, dimension=dim).iloc(i, 0), 

387 ) 

388 rows.extend(rtf_row._as_rtf()) 

389 

390 return rows 

391 

392 

393class BroadcastValue(BaseModel): 

394 model_config = ConfigDict(arbitrary_types_allowed=True) 

395 

396 value: int | float | str | Tuple | Sequence[Any] | pd.DataFrame | None = Field( 

397 ..., 

398 description="The value of the table, can be various types including DataFrame.", 

399 ) 

400 

401 dimension: Tuple[int, int] | None = Field( 

402 None, description="Dimensions of the table (rows, columns)" 

403 ) 

404 

405 def iloc(self, row_index: int | slice, column_index: int | slice) -> Any: 

406 """ 

407 Access a value using row and column indices, based on the data type. 

408 

409 Parameters: 

410 - row_index: The row index or slice (for lists and DataFrames). 

411 - column_index: The column index or slice (for DataFrames and dictionaries). Optional for lists. 

412 

413 Returns: 

414 - The accessed value or an appropriate error message. 

415 """ 

416 if self.value is None: 

417 return None 

418 

419 if isinstance(self.value, pd.DataFrame): 

420 # Handle DataFrame as is 

421 try: 

422 return self.value.iloc[ 

423 row_index % self.value.shape[0], column_index % self.value.shape[1] 

424 ] 

425 except IndexError as e: 

426 raise ValueError(f"Invalid DataFrame index or slice: {e}") 

427 

428 if isinstance(self.value, list): 

429 # Handle list as column based broadcast data frame 

430 return self.value[column_index % len(self.value)] 

431 

432 if isinstance(self.value, tuple): 

433 # Handle Tuple as row based broadcast data frame 

434 values = list(self.value) 

435 return values[row_index % len(values)] 

436 

437 if isinstance(self.value, (int, float, str)): 

438 return self.value 

439 

440 def to_dataframe(self) -> pd.DataFrame: 

441 """ 

442 Convert the value to a pandas DataFrame based on the dimension variable if it is not None. 

443 

444 Returns: 

445 - A pandas DataFrame representation of the value. 

446 

447 Raises: 

448 - ValueError: If the dimension is None or if the value cannot be converted to a DataFrame. 

449 """ 

450 if self.dimension is None: 

451 if isinstance(self.value, pd.DataFrame): 

452 self.dimension = self.value.shape 

453 elif isinstance(self.value, list): 

454 self.dimension = (1, len(self.value)) 

455 elif isinstance(self.value, tuple): 

456 self.dimension = (len(self.value), 1) 

457 elif isinstance(self.value, (int, float, str)): 

458 self.dimension = (1, 1) 

459 else: 

460 raise ValueError("Dimension must be specified to convert to DataFrame.") 

461 

462 if isinstance(self.value, pd.DataFrame): 

463 # Ensure the DataFrame can be recycled to match the specified dimensions 

464 row_count, col_count = self.value.shape 

465 row_repeats = max(1, (self.dimension[0] + row_count - 1) // row_count) 

466 recycled_rows = pd.concat( 

467 [self.value] * row_repeats, ignore_index=True 

468 ).head(self.dimension[0]) 

469 

470 col_repeats = max(1, (self.dimension[1] + col_count - 1) // col_count) 

471 recycled_df = pd.concat([recycled_rows] * col_repeats, axis=1).iloc[ 

472 :, : self.dimension[1] 

473 ] 

474 

475 return recycled_df.reset_index(drop=True) 

476 

477 if isinstance(self.value, (list, MutableSequence)): 

478 recycled_values = self.value * (self.dimension[1] // len(self.value) + 1) 

479 return pd.DataFrame( 

480 [ 

481 [ 

482 recycled_values[i % len(recycled_values)] 

483 for i in range(self.dimension[1]) 

484 ] 

485 ] 

486 * self.dimension[0] 

487 ) 

488 

489 if isinstance(self.value, tuple): 

490 values = list(self.value) 

491 return pd.DataFrame( 

492 [ 

493 [values[i % len(values)]] * self.dimension[1] 

494 for i in range(self.dimension[0]) 

495 ] 

496 ) 

497 

498 if isinstance(self.value, (int, float, str)): 

499 return pd.DataFrame([[self.value] * self.dimension[1]] * self.dimension[0]) 

500 

501 raise ValueError("Unsupported value type for DataFrame conversion.") 

502 

503 def update_row(self, row_index: int, row_value: list): 

504 value = self.to_dataframe() 

505 value.iloc[row_index] = row_value 

506 return value 

507 

508 def update_column(self, column_index: int, column_value: list): 

509 value = self.to_dataframe() 

510 value.iloc[:, column_index] = column_value 

511 return value 

512 

513 def update_cell(self, row_index: int, column_index: int, cell_value: Any): 

514 value = self.to_dataframe() 

515 value.iloc[row_index, column_index] = cell_value 

516 return value 

517 

518 

519class RTFPage(BaseModel): 

520 """RTF page configuration""" 

521 

522 width: float | None = Field(default=None, description="Page width in inches") 

523 height: float | None = Field(default=None, description="Page height in inches") 

524 margin: Sequence[float] | None = Field( 

525 default=None, 

526 description="Page margins [left, right, top, bottom, header, footer] in inches", 

527 ) 

528 orientation: str | None = Field( 

529 default="portrait", description="Page orientation ('portrait' or 'landscape')" 

530 ) 

531 col_width: float | None = Field( 

532 default=None, description="Total width of table columns in inches" 

533 ) 

534 nrow: int | None = Field(default=None, description="Number of rows per page") 

535 use_color: bool | None = Field( 

536 default=False, description="Whether to use color in the document" 

537 ) 

538 page_title: str | None = Field(default="all", description="Title display location") 

539 page_footnote: str | None = Field( 

540 default="last", description="Footnote display location" 

541 ) 

542 page_source: str | None = Field( 

543 default="last", description="Source display location" 

544 ) 

545 border_first: str | None = Field( 

546 default="double", description="First row border style" 

547 ) 

548 border_last: str | None = Field( 

549 default="double", description="Last row border style" 

550 ) 

551 

552 def _set_default(self): 

553 if self.orientation == "portrait": 

554 self.width = self.width or 8.5 

555 self.height = self.height or 11 

556 self.margin = self.margin or [1.25, 1, 1.75, 1.25, 1.75, 1.00625] 

557 self.col_width = self.col_width or self.width - 2.25 

558 self.nrow = self.nrow or 40 

559 

560 if self.orientation == "landscape": 

561 self.width = self.width or 11 

562 self.height = self.height or 8.5 

563 self.margin = self.margin or [1.0, 1.0, 2, 1.25, 1.25, 1.25] 

564 self.col_width = self.col_width or self.width - 2.5 

565 self.nrow = self.nrow or 24 

566 

567 if len(self.margin) != 6: 

568 raise ValueError("Margin length must be 6.") 

569 

570 return self 

571 

572 

573class RTFTitle(TextAttributes): 

574 text: str | Sequence[str] | None = Field( 

575 default=None, description="Title text content" 

576 ) 

577 text_indent_reference: str | Sequence[str] | None = Field( 

578 default="table", 

579 description="Reference point for indentation ('page' or 'table')", 

580 ) 

581 

582 def __init__(self, **data): 

583 defaults = { 

584 "text_font": 1, 

585 "text_font_size": 12, 

586 "text_justification": "c", 

587 "text_indent_first": 0, 

588 "text_indent_left": 0, 

589 "text_indent_right": 0, 

590 "text_space": 1.0, 

591 "text_space_before": 180.0, 

592 "text_space_after": 180.0, 

593 "text_hyphenation": True, 

594 "text_convert": True, 

595 } 

596 

597 # Update defaults with any provided values 

598 defaults.update(data) 

599 super().__init__(**defaults) 

600 

601 def _set_default(self): 

602 for attr, value in self.__dict__.items(): 

603 if isinstance(value, (str, int, float, bool)): 

604 setattr(self, attr, [value]) 

605 if isinstance(value, list): 

606 setattr(self, attr, tuple(value)) 

607 return self 

608 

609 

610class RTFColumnHeader(TableAttributes): 

611 """Class for RTF column header settings""" 

612 

613 model_config = ConfigDict(arbitrary_types_allowed=True) 

614 

615 df: str | Sequence[str] | pd.DataFrame | None = Field( 

616 default=None, description="Column header table" 

617 ) 

618 

619 def __init__(self, **data): 

620 defaults = { 

621 "border_left": "single", 

622 "border_right": "single", 

623 "border_top": "single", 

624 "border_bottom": "", 

625 "border_width": 15, 

626 "cell_height": 0.15, 

627 "cell_justification": "c", 

628 "cell_vertical_justification": "bottom", 

629 "text_font": 1, 

630 "text_format": "", 

631 "text_font_size": 9, 

632 "text_justification": "c", 

633 "text_indent_first": 0, 

634 "text_indent_left": 0, 

635 "text_indent_right": 0, 

636 "text_space": 1, 

637 "text_space_before": 15, 

638 "text_space_after": 15, 

639 "text_hyphenation": False, 

640 "text_convert": True, 

641 } 

642 

643 # Update defaults with any provided values 

644 defaults.update(data) 

645 super().__init__(**defaults) 

646 

647 # Convert df to DataFrame during initialization 

648 if self.df is not None: 

649 self.df = BroadcastValue(value=self.df).to_dataframe() 

650 

651 def _set_default(self): 

652 for attr, value in self.__dict__.items(): 

653 if isinstance(value, (str, int, float, bool)): 

654 setattr(self, attr, [value]) 

655 

656 return self 

657 

658 

659class RTFBody(TableAttributes): 

660 """Class for RTF document body settings""" 

661 

662 model_config = ConfigDict(arbitrary_types_allowed=True) 

663 df: pd.DataFrame | None = Field(default=None, description="Table data") 

664 

665 as_colheader: bool = Field( 

666 default=True, description="Whether to display column headers" 

667 ) 

668 group_by: Sequence[str] | None = Field( 

669 default=None, description="Column name to group rows by" 

670 ) 

671 page_by: Sequence[str] | None = Field( 

672 default=None, description="Column name to create page breaks by" 

673 ) 

674 new_page: bool = Field(default=False, description="Force new page before table") 

675 pageby_header: bool = Field(default=True, description="Repeat headers on new pages") 

676 pageby_row: str = Field( 

677 default="column", 

678 description="Page break handling for rows ('column' or 'value')", 

679 ) 

680 subline_by: Sequence[str] | None = Field( 

681 default=None, description="Column name to create sublines by" 

682 ) 

683 last_row: bool = Field( 

684 default=True, 

685 description="Whether the table contains the last row of the final table", 

686 ) 

687 

688 def __init__(self, **data): 

689 defaults = { 

690 "border_left": "single", 

691 "border_right": "single", 

692 "border_first": "single", 

693 "border_last": "single", 

694 "border_width": 15, 

695 "cell_height": 0.15, 

696 "cell_justification": "c", 

697 "cell_vertical_justification": "top", 

698 "text_font": 1, 

699 "text_font_size": 9, 

700 "text_indent_first": 0, 

701 "text_indent_left": 0, 

702 "text_indent_right": 0, 

703 "text_space": 1, 

704 "text_space_before": 15, 

705 "text_space_after": 15, 

706 "text_hyphenation": False, 

707 "text_convert": True, 

708 } 

709 

710 # Update defaults with any provided values 

711 defaults.update(data) 

712 super().__init__(**defaults) 

713 

714 def _set_default(self): 

715 for attr, value in self.__dict__.items(): 

716 if isinstance(value, (str, int, float, bool)) and attr not in [ 

717 "as_colheader", 

718 "page_by", 

719 "new_page", 

720 "pageby_header", 

721 "pageby_row", 

722 "subline_by", 

723 "last_row", 

724 ]: 

725 setattr(self, attr, [value]) 

726 

727 self.border_top = self.border_top or "" 

728 self.border_bottom = self.border_bottom or "" 

729 self.border_left = self.border_left or "single" 

730 self.border_right = self.border_right or "single" 

731 self.border_first = self.border_first or "single" 

732 self.border_last = self.border_last or "single" 

733 self.cell_vertical_justification = self.cell_vertical_justification or "c" 

734 self.text_justification = self.text_justification or "c" 

735 

736 if self.page_by is None: 

737 if self.new_page: 

738 raise ValueError( 

739 "`new_page` must be `False` if `page_by` is not specified" 

740 ) 

741 

742 return self