wordhelpers 0.1.3__tar.gz → 0.1.5__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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: wordhelpers
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Helpers for working with python-docx
5
5
  Author: AJ Cruz
6
6
  Author-email: 15045766-a-cruz@users.noreply.gitlab.com
@@ -9,6 +9,7 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
12
13
  Requires-Dist: pydantic (>=2.12.5,<3.0.0)
13
14
  Requires-Dist: python-docx (>=1.2.0,<2.0.0)
14
15
  Description-Content-Type: text/markdown
@@ -64,6 +65,7 @@ The WordTablesModel class has a number of methods available to help you build th
64
65
  - ```model_dump()```
65
66
  - ```pretty_print()```
66
67
  - ```write()```
68
+ - ```sort_rows_by_columns(primary_col: int, secondary_col: int | None = None, *, ascending: bool = True,)```
67
69
 
68
70
  If you prefer to create the tables manually via Python dictionary, the dictionary must follow a strict schema that looks something like this:
69
71
  ```python
@@ -49,6 +49,7 @@ The WordTablesModel class has a number of methods available to help you build th
49
49
  - ```model_dump()```
50
50
  - ```pretty_print()```
51
51
  - ```write()```
52
+ - ```sort_rows_by_columns(primary_col: int, secondary_col: int | None = None, *, ascending: bool = True,)```
52
53
 
53
54
  If you prefer to create the tables manually via Python dictionary, the dictionary must follow a strict schema that looks something like this:
54
55
  ```python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wordhelpers"
3
- version = "0.1.3"
3
+ version = "0.1.5"
4
4
  description = "Helpers for working with python-docx"
5
5
  authors = [
6
6
  {name = "AJ Cruz",email = "15045766-a-cruz@users.noreply.gitlab.com"}
@@ -81,7 +81,7 @@ def replace_placeholder_with_table(
81
81
 
82
82
  def inject_table(
83
83
  doc_obj: _Document,
84
- table: dict,
84
+ table: dict | WordTableModel,
85
85
  placeholder: str,
86
86
  remove_leading_para: bool = True,
87
87
  remove_placeholder: bool = True,
@@ -93,21 +93,27 @@ def inject_table(
93
93
  After moving the Word table after the placeholder paragraph, delete the
94
94
  placeholder paragraph.
95
95
  """
96
+
96
97
  # Locate the paragraph from the supplied placeholder text
97
98
  paragraph: Paragraph = get_para_by_string(doc_obj, placeholder)
99
+ if not paragraph:
100
+ raise ValueError(f'WARNING: Could not locate placeholder "{placeholder}"')
98
101
 
99
102
  # Build the word table and add it to the end of the document
100
- table: Table = build_table(doc_obj, table, remove_leading_para=remove_leading_para)
103
+ table = (
104
+ build_table(doc_obj, table, remove_leading_para=remove_leading_para)
105
+ if isinstance(table, dict)
106
+ else build_table(
107
+ doc_obj, table.model_dump(), remove_leading_para=remove_leading_para
108
+ )
109
+ )
101
110
 
102
- if not paragraph:
103
- print(f'WARNING: Could not locate placeholder "{placeholder}"')
104
- else:
105
- # Move the Word table to a new paragraph immediately after the placeholder paragraph
106
- paragraph._p.addnext(table._tbl)
111
+ # Move the Word table to a new paragraph immediately after the placeholder paragraph
112
+ paragraph._p.addnext(table._tbl)
107
113
 
108
- if remove_placeholder:
109
- # Delete the placeholder paragraph
110
- delete_paragraph(paragraph)
114
+ if remove_placeholder:
115
+ # Delete the placeholder paragraph
116
+ delete_paragraph(paragraph)
111
117
 
112
118
 
113
119
  def build_table(
@@ -63,34 +63,38 @@ class WordTableModel(BaseModel):
63
63
  style: str | None = None
64
64
  rows: list[WordRowModel] = Field(default_factory=list)
65
65
 
66
+ def add_row(
67
+ self,
68
+ width: int,
69
+ text: list[str] = [],
70
+ merge_cols: list[int] = [],
71
+ background_color: str | None = None,
72
+ style: str | None = None,
73
+ alignment: AlignmentEnum | None = None,
74
+ ) -> None:
66
75
 
67
- def add_row(self,
68
- width: int,
69
- text: list[str] = [],
70
- merge_cols: list[int] = [],
71
- background_color: str | None = None,
72
- style: str | None = None,
73
- alignment: AlignmentEnum | None = None,
74
- ) -> None:
75
-
76
76
  # Make sure width is same as existing rows if any
77
77
  if self.rows:
78
78
  existing_width = len(self.rows[0].cells)
79
79
  if width != existing_width:
80
- raise ValueError(f"New row width {width} does not match existing row width {existing_width}")
81
-
80
+ raise ValueError(
81
+ f"New row width {width} does not match existing row width {existing_width}"
82
+ )
83
+
82
84
  # Make sure text length is less than the row width minus merged columns
83
85
  num_merge_cols = len(merge_cols)
84
86
  if len(text) > width - num_merge_cols:
85
- raise ValueError(f"Text length {len(text)} exceeded expected length {width - num_merge_cols} based on width and merge_cols")
86
-
87
+ raise ValueError(
88
+ f"Text length {len(text)} exceeded expected length {width - num_merge_cols} based on width and merge_cols"
89
+ )
90
+
87
91
  # Make sure merge_cols are valid
88
92
  for col in merge_cols:
89
93
  if not isinstance(col, int):
90
94
  raise ValueError(f"merge_cols must contain integers, got: {type(col)}")
91
95
  if col < 0 or col >= width:
92
96
  raise ValueError(f"merge_cols contains invalid column index: {col}")
93
-
97
+
94
98
  # Build the cells
95
99
  cells: list = []
96
100
  for i in range(width):
@@ -101,37 +105,68 @@ class WordTableModel(BaseModel):
101
105
  # Build a normal cell
102
106
  paragraphs: list = []
103
107
  if text:
104
- paragraphs = [WordParagraphModel(style=style, alignment=alignment, text=[text.pop(0)])]
105
- cells.append(WordCellModel(background_color=background_color, paragraphs=paragraphs))
108
+ paragraphs = [
109
+ WordParagraphModel(
110
+ style=style, alignment=alignment, text=[text.pop(0)]
111
+ )
112
+ ]
113
+ cells.append(
114
+ WordCellModel(
115
+ background_color=background_color, paragraphs=paragraphs
116
+ )
117
+ )
106
118
  # Re-build the rows
107
119
  rows: list = self.rows.copy()
108
120
  rows.append(WordRowModel(cells=cells))
109
121
  self.rows = rows
110
122
 
111
-
112
- def add_text_to_row(self, row_index: int, text: list[str], style: str | None = None, alignment: AlignmentEnum | None = None) -> None:
123
+ def add_text_to_row(
124
+ self,
125
+ row_index: int,
126
+ text: list[str],
127
+ style: str | None = None,
128
+ alignment: AlignmentEnum | None = None,
129
+ ) -> None:
113
130
  # Validate row_index
114
131
  if row_index < 0 or row_index >= len(self.rows):
115
132
  raise IndexError(f"Row index {row_index} out of range")
116
-
133
+
117
134
  # Make sure text length is less than the row width minus merged columns
118
- num_merge_cols = len([cell for cell in self.rows[row_index].cells if isinstance(cell, str) and cell == "merge"])
135
+ num_merge_cols = len(
136
+ [
137
+ cell
138
+ for cell in self.rows[row_index].cells
139
+ if isinstance(cell, str) and cell == "merge"
140
+ ]
141
+ )
119
142
  width = len(self.rows[row_index].cells)
120
143
  if len(text) > width - num_merge_cols:
121
- raise ValueError(f"Text length {len(text)} exceeded expected length {width - num_merge_cols} based on width and merge_cols")
122
-
144
+ raise ValueError(
145
+ f"Text length {len(text)} exceeded expected length {width - num_merge_cols} based on width and merge_cols"
146
+ )
147
+
123
148
  rows = self.rows.copy()
124
149
  row = rows[row_index]
125
150
  for cell in row.cells:
126
151
  if isinstance(cell, str) and cell == "merge":
127
152
  continue # Skip merge cells
128
153
  if text:
129
- cell.paragraphs.append(WordParagraphModel(style=style, alignment=alignment, text=[text.pop(0)]))
154
+ cell.paragraphs.append(
155
+ WordParagraphModel(
156
+ style=style, alignment=alignment, text=[text.pop(0)]
157
+ )
158
+ )
130
159
 
131
160
  self.rows = rows
132
161
 
133
-
134
- def add_text_to_cell(self, row_index: int, col_index: int, text: str, style: str | None = None, alignment: AlignmentEnum | None = None) -> None:
162
+ def add_text_to_cell(
163
+ self,
164
+ row_index: int,
165
+ col_index: int,
166
+ text: str,
167
+ style: str | None = None,
168
+ alignment: AlignmentEnum | None = None,
169
+ ) -> None:
135
170
  # Validate row_index and col_index
136
171
  if row_index < 0 or row_index >= len(self.rows):
137
172
  raise IndexError(f"Row index {row_index} out of range")
@@ -139,21 +174,22 @@ class WordTableModel(BaseModel):
139
174
  row = rows[row_index]
140
175
  if col_index < 0 or col_index >= len(row.cells):
141
176
  raise IndexError(f"Column index {col_index} out of range")
142
-
177
+
143
178
  cell = row.cells[col_index]
144
179
  if isinstance(cell, str) and cell == "merge":
145
180
  raise ValueError("Cannot add text to a merged cell")
146
-
147
- cell.paragraphs.append(WordParagraphModel(style=style, alignment=alignment, text=[text]))
148
181
 
149
- self.rows = rows
182
+ cell.paragraphs.append(
183
+ WordParagraphModel(style=style, alignment=alignment, text=[text])
184
+ )
150
185
 
186
+ self.rows = rows
151
187
 
152
188
  def style_row(self, row_index: int, text_style: str) -> None:
153
189
  # Validate row_index
154
190
  if row_index < 0 or row_index >= len(self.rows):
155
191
  raise IndexError(f"Row index {row_index} out of range")
156
-
192
+
157
193
  rows = self.rows.copy()
158
194
  row = rows[row_index]
159
195
 
@@ -165,7 +201,6 @@ class WordTableModel(BaseModel):
165
201
 
166
202
  self.rows = rows
167
203
 
168
-
169
204
  def style_cell(self, row_index: int, col_index: int, text_style: str) -> None:
170
205
  # Validate row_index and col_index
171
206
  if row_index < 0 or row_index >= len(self.rows):
@@ -174,22 +209,21 @@ class WordTableModel(BaseModel):
174
209
  row = rows[row_index]
175
210
  if col_index < 0 or col_index >= len(row.cells):
176
211
  raise IndexError(f"Column index {col_index} out of range")
177
-
212
+
178
213
  cell = row.cells[col_index]
179
214
  if isinstance(cell, str) and cell == "merge":
180
215
  raise ValueError("Cannot style a merged cell")
181
-
216
+
182
217
  for paragraph in cell.paragraphs:
183
218
  paragraph.style = text_style
184
219
 
185
220
  self.rows = rows
186
221
 
187
-
188
222
  def color_row(self, row_index: int, background_color: str) -> None:
189
223
  # Validate row_index
190
224
  if row_index < 0 or row_index >= len(self.rows):
191
225
  raise IndexError(f"Row index {row_index} out of range")
192
-
226
+
193
227
  rows = self.rows.copy()
194
228
  row = rows[row_index]
195
229
 
@@ -200,7 +234,6 @@ class WordTableModel(BaseModel):
200
234
 
201
235
  self.rows = rows
202
236
 
203
-
204
237
  def color_cell(self, row_index: int, col_index: int, background_color: str) -> None:
205
238
  # Validate row_index and col_index
206
239
  if row_index < 0 or row_index >= len(self.rows):
@@ -209,21 +242,20 @@ class WordTableModel(BaseModel):
209
242
  row = rows[row_index]
210
243
  if col_index < 0 or col_index >= len(row.cells):
211
244
  raise IndexError(f"Column index {col_index} out of range")
212
-
245
+
213
246
  cell = row.cells[col_index]
214
247
  if isinstance(cell, str) and cell == "merge":
215
248
  raise ValueError("Cannot color a merged cell")
216
-
249
+
217
250
  cell.background_color = background_color
218
251
 
219
252
  self.rows = rows
220
253
 
221
-
222
254
  def align_row(self, row_index: int, alignment: AlignmentEnum) -> None:
223
255
  # Validate row_index
224
256
  if row_index < 0 or row_index >= len(self.rows):
225
257
  raise IndexError(f"Row index {row_index} out of range")
226
-
258
+
227
259
  rows = self.rows.copy()
228
260
  row = rows[row_index]
229
261
 
@@ -235,8 +267,9 @@ class WordTableModel(BaseModel):
235
267
 
236
268
  self.rows = rows
237
269
 
238
-
239
- def align_cell(self, row_index: int, col_index: int, alignment: AlignmentEnum) -> None:
270
+ def align_cell(
271
+ self, row_index: int, col_index: int, alignment: AlignmentEnum
272
+ ) -> None:
240
273
  # Validate row_index and col_index
241
274
  if row_index < 0 or row_index >= len(self.rows):
242
275
  raise IndexError(f"Row index {row_index} out of range")
@@ -244,22 +277,19 @@ class WordTableModel(BaseModel):
244
277
  row = rows[row_index]
245
278
  if col_index < 0 or col_index >= len(row.cells):
246
279
  raise IndexError(f"Column index {col_index} out of range")
247
-
280
+
248
281
  cell = row.cells[col_index]
249
282
  if isinstance(cell, str) and cell == "merge":
250
283
  raise ValueError("Cannot align a merged cell")
251
-
284
+
252
285
  for paragraph in cell.paragraphs:
253
286
  paragraph.alignment = alignment
254
287
 
255
288
  self.rows = rows
256
289
 
257
-
258
- def add_table_to_cell(self,
259
- row_index: int,
260
- col_index: int,
261
- table: WordTableModel
262
- ) -> None:
290
+ def add_table_to_cell(
291
+ self, row_index: int, col_index: int, table: WordTableModel
292
+ ) -> None:
263
293
  # Validate row_index and col_index
264
294
  if row_index < 0 or row_index >= len(self.rows):
265
295
  raise IndexError(f"Row index {row_index} out of range")
@@ -271,12 +301,11 @@ class WordTableModel(BaseModel):
271
301
 
272
302
  if isinstance(cell, str) and cell == "merge":
273
303
  raise ValueError("Cannot add table to a merged cell")
274
-
304
+
275
305
  cell.table = table
276
306
 
277
307
  self.rows = rows
278
308
 
279
-
280
309
  def delete_row(self, row_index: int) -> None:
281
310
  if row_index < 0 or row_index >= len(self.rows):
282
311
  raise IndexError(f"Row index {row_index} out of range")
@@ -284,12 +313,62 @@ class WordTableModel(BaseModel):
284
313
  del rows[row_index]
285
314
  self.rows = rows
286
315
 
287
-
288
316
  def pretty_print(self) -> None:
289
317
  print(json.dumps(self.model_dump(), indent=4))
290
318
 
291
-
292
319
  def write(self, filepath: str) -> None:
293
- with open(filepath, 'w') as f:
320
+ with open(filepath, "w") as f:
294
321
  json.dump(self.model_dump(), f, indent=4)
295
322
 
323
+ def _row_text_at(self, row: WordRowModel, col_index: int) -> str:
324
+ if col_index >= len(row.cells):
325
+ return ""
326
+
327
+ cell = row.cells[col_index]
328
+
329
+ if isinstance(cell, str): # "merge"
330
+ return ""
331
+
332
+ if not cell.paragraphs:
333
+ return ""
334
+
335
+ paragraph = cell.paragraphs[0]
336
+ text = paragraph.text
337
+
338
+ if isinstance(text, list) and text:
339
+ return text[0].lower()
340
+
341
+ if isinstance(text, str):
342
+ return text.lower()
343
+
344
+ return ""
345
+
346
+ def sort_rows_by_columns(
347
+ self,
348
+ primary_col: int,
349
+ secondary_col: int | None = None,
350
+ *,
351
+ ascending: bool = True,
352
+ ) -> None:
353
+ if len(self.rows) <= 1:
354
+ return
355
+
356
+ header = self.rows[0]
357
+ body = self.rows[1:]
358
+
359
+ def sort_key(row: WordRowModel):
360
+ primary = self._row_text_at(row, primary_col)
361
+ secondary = (
362
+ self._row_text_at(row, secondary_col)
363
+ if secondary_col is not None
364
+ else ""
365
+ )
366
+ return (primary, secondary)
367
+
368
+ body_sorted = sorted(
369
+ body,
370
+ key=sort_key,
371
+ reverse=not ascending,
372
+ )
373
+
374
+ self.rows = [header] + body_sorted