wordhelpers 0.1.2__tar.gz → 0.1.3__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.
@@ -0,0 +1,267 @@
1
+ Metadata-Version: 2.3
2
+ Name: wordhelpers
3
+ Version: 0.1.3
4
+ Summary: Helpers for working with python-docx
5
+ Author: AJ Cruz
6
+ Author-email: 15045766-a-cruz@users.noreply.gitlab.com
7
+ Requires-Python: >=3.11
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Requires-Dist: pydantic (>=2.12.5,<3.0.0)
13
+ Requires-Dist: python-docx (>=1.2.0,<2.0.0)
14
+ Description-Content-Type: text/markdown
15
+
16
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wordhelpers.svg)](https://img.shields.io/pypi/pyversions/wordhelpers)
17
+ [![PyPI](https://img.shields.io/pypi/v/wordhelpers.svg)](https://pypi.python.org/pypi/wordhelpers)
18
+ [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
19
+
20
+ # wordhelpers
21
+ Helper functions for [python-docx](https://python-docx.readthedocs.io/en/latest/). I found myself re-learning docx every time I wanted to use it in a project, so this provides and abstraction. You represent Word tables as a properly-formatted Python dictionary or with the provided WordTableModel class and the helper function converts it to a docx table.
22
+
23
+ # Installation
24
+ wordhelpers can be installed via poetry with: ```poetry add wordhelpers```
25
+ or via pip with: ```pip install wordhelpers```
26
+
27
+ # Usage
28
+ For detailed documentation of the python-docx library see [python-docx](https://python-docx.readthedocs.io/en/latest/)
29
+
30
+ 1. Import the python-docx library into your script with:
31
+ ```python
32
+ from docx import Document
33
+ ```
34
+ 1. Import the helpers from this project with:
35
+ ```python
36
+ from wordhelpers import WordTableModel, inject_table
37
+ ```
38
+ 1. Create the docx Word document object with something like:
39
+ ```python
40
+ doc_obj = Document("a_word_template.docx")
41
+ ```
42
+ 1. Add tables to the document object as required (see the next section of this README for info on how to do that)
43
+ 1. When all changes to your document object are complete, write them with the docx `save()` method:
44
+ ```python
45
+ doc_obj.save("output_file.docx")
46
+ ```
47
+ # Adding tables to the document object
48
+ There are two methods available for creating tables for addition to a word document:
49
+ 1. The provided `WordTablesModel` class
50
+ 1. A properly-formatted python dictionary
51
+
52
+ The WordTablesModel class has a number of methods available to help you build the table:
53
+ - ```add_row(width: int, text: list[str] = [], merge_cols: list[int] = [], background_color: str | None = None, style: str | None = None, alignment: AlignmentEnum | None = None)```
54
+ - ```add_text_to_row(row_index: int, text: list[str], style: str | None = None, alignment: AlignmentEnum | None = None)```
55
+ - ```add_text_to_cell(row_index: int, col_index: int, text: str, style: str | None = None, alignment: AlignmentEnum | None = None)```
56
+ - ```style_row(row_index: int, text_style: str)```
57
+ - ```style_cell(row_index: int, col_index: int, text_style: str)```
58
+ - ```color_row(row_index: int, background_color: str)```
59
+ - ```color_cell(row_index: int, col_index: int, background_color: str)```
60
+ - ```align_row(row_index: int, alignment: AlignmentEnum)```
61
+ - ```align_cell(row_index: int, col_index: int, alignment: AlignmentEnum)```
62
+ - ```add_table_to_cell(row_index: int, col_index: int, table: WordTableModel)```
63
+ - ```delete_row(row_index: int)```
64
+ - ```model_dump()```
65
+ - ```pretty_print()```
66
+ - ```write()```
67
+
68
+ If you prefer to create the tables manually via Python dictionary, the dictionary must follow a strict schema that looks something like this:
69
+ ```python
70
+ {
71
+ "style": None,
72
+ "rows": [
73
+ {
74
+ "cells": [
75
+ {
76
+ "width": None,
77
+ "background": None,
78
+ "paragraphs": [{"style":None,"alignment": "center", "text":"Some Text"}],
79
+ "table": {optional child table}
80
+ },
81
+ {
82
+ "merge": None
83
+ },
84
+ ]
85
+ }
86
+ ]
87
+ }
88
+ ```
89
+ The cell **background** attribute is optional. If supplied with a hexidecimal color code, the cell will be shaded that color.
90
+
91
+ The cell **width** attribute is optional. If supplied with a decimal number (inches), it will hard-code that column's width to the supplied value.
92
+
93
+ The cell **table** attribute is optional. It can be used to nest tables within table cells. If "table" is provided, no other keys are required (background, paragraphs, etc).
94
+
95
+ The paragraph **style** attribute is optional. If set to anything besides None it will use the Word style referenced. The style must already exist in the source/template Word document.
96
+
97
+ The paragraph **alignment** attribute is optional. If set to ```"center"``` it will center-align the text within a cell, if set to ```"right"``` it will right-align the text within a cell
98
+
99
+ The **merge** key is optional. If used the cell will be merged with the cell above (from a dictionary view, to the left from a table view). Multiple merges can be used in a row to merge multiple cells.
100
+
101
+ By default a paragraph's **text** property will create a single-line (but wrapped) entry in the cell if the value is a string. If you would like to create a multi-line cell entry, supply the value as a list instead of a string. This will instruct the module to add a line break after each list item.
102
+
103
+ Schema enforcement of the dictionary is done through Pydantic v2 validations.
104
+
105
+ Injection of the table model (either via the class or a raw dictionary) is done via the provided ```inject_table()``` function.
106
+ The function has the following parameters:
107
+ - doc_obj: _Document
108
+ - table: dict
109
+ - placeholder: str
110
+ - remove_leading_para: bool = True
111
+ - remove_placeholder: bool = True
112
+
113
+ Notice the table parameter must be a python dictionary. So if you've created the table via the provided ```WordTableModel``` class you pass it to ```inject_table()``` with: ```my_table.model_dump()```
114
+
115
+ - **<remove_leading_para>** - This is an optional argument. If not set it will default to True. MS Word tables when created automatically have an empty paragraph at the top/beginning of the table cell. This can create unwanted spacing at the top of the table. By default (value set to "True") the paragraph will be deleted. If you want to keep the paragraph (to add text to it), set this to "False".
116
+ - **<remove_placeholder>** - This is an optional argument. You can leave the placeholder (```remove_placeholder=False```) if you need to keep injecting tables below the placeholder before final deletion
117
+
118
+ ### EXAMPLE
119
+ We start with a Microsoft Word template named "source-template.docx" that looks like this:
120
+
121
+ ![Word Template](artwork/word_template.jpg)
122
+
123
+ Our sample Python script looks like this:
124
+ ```python
125
+ from docx import Document
126
+ from wordhelpers import inject_table
127
+
128
+ doc_obj = Document("source-template.docx")
129
+
130
+ my_dictionary = {
131
+ "style": "plain",
132
+ "rows": [
133
+ {
134
+ "cells": [
135
+ {
136
+ "background": "#506279",
137
+ "paragraphs":[{"style": "regularbold", "text": "Header 1:"}]
138
+ },
139
+ {
140
+ "background": "#506279",
141
+ "paragraphs":[{"style": "regularbold", "text": "Header 2:"}]
142
+ },
143
+ {
144
+ "background": "#506279",
145
+ "paragraphs":[{"style": "regularbold", "text": "Header 3:"}]
146
+ }
147
+ ]
148
+ },
149
+ {
150
+ "cells": [
151
+ {
152
+ "background": "#D5DCE4",
153
+ "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 1:"}]
154
+ },
155
+ {
156
+ "background": "#D5DCE4",
157
+ "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 2:"}]
158
+ },
159
+ {
160
+ "background": "#D5DCE4",
161
+ "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 3:"}]
162
+ }
163
+ ]
164
+ },
165
+ {
166
+ "cells": [
167
+ {
168
+ "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 1:"}]
169
+ },
170
+ {
171
+ "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 2:"}]
172
+ },
173
+ {
174
+ "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 3:"}]
175
+ }
176
+ ]
177
+ },
178
+ {
179
+ "cells": [
180
+ {
181
+ "background": "#D5DCE4",
182
+ "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 1:"}]
183
+ },
184
+ {
185
+ "background": "#D5DCE4",
186
+ "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 2:"}]
187
+ },
188
+ {
189
+ "background": "#D5DCE4",
190
+ "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 3:"}]
191
+ }
192
+ ]
193
+ }
194
+ ]
195
+ }
196
+
197
+ inject_table(doc_obj, my_dictionary, "\[py_placeholder1\]")
198
+ doc_obj.save("output_word_doc.docx")
199
+ ```
200
+
201
+ Using the provided ```WordTableModel``` class instead of the raw dictionary, the python code would look like this:
202
+ ```python
203
+ from docx import Document
204
+ from wordhelpers import WordTableModel, inject_table
205
+
206
+ doc_obj = Document("source-template.docx")
207
+
208
+ my_table = WordTableModel()
209
+ my_table.style = "plain"
210
+ my_table.add_row(
211
+ 3,
212
+ text=[
213
+ "Header 1:",
214
+ "Header 2:",
215
+ "Header 3:",
216
+ ],
217
+ background_color="#506279",
218
+ style="regularbold",
219
+ )
220
+ my_table.add_row(
221
+ 3,
222
+ text=[
223
+ "Row 1 Data 1:",
224
+ "Row 1 Data 2:",
225
+ "Row 1 Data 3:",
226
+ ],
227
+ background_color="#D5DCE4",
228
+ style="No Spacing",
229
+ )
230
+ my_table.add_row(
231
+ 3,
232
+ text=[
233
+ "Row 2 Data 1:",
234
+ "Row 2 Data 2:",
235
+ "Row 2 Data 3:",
236
+ ],
237
+ style="No Spacing",
238
+ )
239
+ my_table.add_row(
240
+ 3,
241
+ text=[
242
+ "Row 3 Data 1:",
243
+ "Row 3 Data 2:",
244
+ "Row 3 Data 3:",
245
+ ],
246
+ background_color="#D5DCE4",
247
+ style="No Spacing",
248
+ )
249
+
250
+ inject_table(doc_obj, my_table.model_dump(), "\[py_placeholder1\]")
251
+ doc_obj.save("output_word_doc.docx")
252
+ ```
253
+
254
+ We run the Python script and it produces a new Word document named "output_word_doc.docx" that looks like this:
255
+
256
+ ![Word Template](artwork/word_output.jpg)
257
+
258
+
259
+ The project provides some additional docx functions that may be useful to your project:
260
+ - ```get_para_by_string(doc_obj: _Document, search: str)```: Searches for a keyword in the docx object and returns there paragraph where the keyword is found
261
+ - ```insert_paragraph_after(paragraph: Paragraph, text: str = None, style: str = None)```: Searches for a keyword in the docx object and inserts a new paragraph immediately after it with the supplied text
262
+ - ```delete_paragraph(paragraph: Paragraph)```: Deletes a given paragraph (after you've inserted text after it for example)
263
+
264
+ As well as the following helper functions for building raw dictionary table models:
265
+ - ```insert_text_into_row(cell_text: list)```: Builds a row (dictionary) from a list of text where each list item is a column in the row. Supports "merge"
266
+ - ```insert_text_by_table_coords(table: dict, row: int, col: int, text: str)```: Inserts text into a table dictionary given the row & column numbers.
267
+ - ```generate_table(num_rows: int, num_cols: int, header_row: list, style: str = None)```: Generates a basic table dictionary and populates the headers from a list of text (strings).
@@ -0,0 +1,252 @@
1
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wordhelpers.svg)](https://img.shields.io/pypi/pyversions/wordhelpers)
2
+ [![PyPI](https://img.shields.io/pypi/v/wordhelpers.svg)](https://pypi.python.org/pypi/wordhelpers)
3
+ [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
4
+
5
+ # wordhelpers
6
+ Helper functions for [python-docx](https://python-docx.readthedocs.io/en/latest/). I found myself re-learning docx every time I wanted to use it in a project, so this provides and abstraction. You represent Word tables as a properly-formatted Python dictionary or with the provided WordTableModel class and the helper function converts it to a docx table.
7
+
8
+ # Installation
9
+ wordhelpers can be installed via poetry with: ```poetry add wordhelpers```
10
+ or via pip with: ```pip install wordhelpers```
11
+
12
+ # Usage
13
+ For detailed documentation of the python-docx library see [python-docx](https://python-docx.readthedocs.io/en/latest/)
14
+
15
+ 1. Import the python-docx library into your script with:
16
+ ```python
17
+ from docx import Document
18
+ ```
19
+ 1. Import the helpers from this project with:
20
+ ```python
21
+ from wordhelpers import WordTableModel, inject_table
22
+ ```
23
+ 1. Create the docx Word document object with something like:
24
+ ```python
25
+ doc_obj = Document("a_word_template.docx")
26
+ ```
27
+ 1. Add tables to the document object as required (see the next section of this README for info on how to do that)
28
+ 1. When all changes to your document object are complete, write them with the docx `save()` method:
29
+ ```python
30
+ doc_obj.save("output_file.docx")
31
+ ```
32
+ # Adding tables to the document object
33
+ There are two methods available for creating tables for addition to a word document:
34
+ 1. The provided `WordTablesModel` class
35
+ 1. A properly-formatted python dictionary
36
+
37
+ The WordTablesModel class has a number of methods available to help you build the table:
38
+ - ```add_row(width: int, text: list[str] = [], merge_cols: list[int] = [], background_color: str | None = None, style: str | None = None, alignment: AlignmentEnum | None = None)```
39
+ - ```add_text_to_row(row_index: int, text: list[str], style: str | None = None, alignment: AlignmentEnum | None = None)```
40
+ - ```add_text_to_cell(row_index: int, col_index: int, text: str, style: str | None = None, alignment: AlignmentEnum | None = None)```
41
+ - ```style_row(row_index: int, text_style: str)```
42
+ - ```style_cell(row_index: int, col_index: int, text_style: str)```
43
+ - ```color_row(row_index: int, background_color: str)```
44
+ - ```color_cell(row_index: int, col_index: int, background_color: str)```
45
+ - ```align_row(row_index: int, alignment: AlignmentEnum)```
46
+ - ```align_cell(row_index: int, col_index: int, alignment: AlignmentEnum)```
47
+ - ```add_table_to_cell(row_index: int, col_index: int, table: WordTableModel)```
48
+ - ```delete_row(row_index: int)```
49
+ - ```model_dump()```
50
+ - ```pretty_print()```
51
+ - ```write()```
52
+
53
+ If you prefer to create the tables manually via Python dictionary, the dictionary must follow a strict schema that looks something like this:
54
+ ```python
55
+ {
56
+ "style": None,
57
+ "rows": [
58
+ {
59
+ "cells": [
60
+ {
61
+ "width": None,
62
+ "background": None,
63
+ "paragraphs": [{"style":None,"alignment": "center", "text":"Some Text"}],
64
+ "table": {optional child table}
65
+ },
66
+ {
67
+ "merge": None
68
+ },
69
+ ]
70
+ }
71
+ ]
72
+ }
73
+ ```
74
+ The cell **background** attribute is optional. If supplied with a hexidecimal color code, the cell will be shaded that color.
75
+
76
+ The cell **width** attribute is optional. If supplied with a decimal number (inches), it will hard-code that column's width to the supplied value.
77
+
78
+ The cell **table** attribute is optional. It can be used to nest tables within table cells. If "table" is provided, no other keys are required (background, paragraphs, etc).
79
+
80
+ The paragraph **style** attribute is optional. If set to anything besides None it will use the Word style referenced. The style must already exist in the source/template Word document.
81
+
82
+ The paragraph **alignment** attribute is optional. If set to ```"center"``` it will center-align the text within a cell, if set to ```"right"``` it will right-align the text within a cell
83
+
84
+ The **merge** key is optional. If used the cell will be merged with the cell above (from a dictionary view, to the left from a table view). Multiple merges can be used in a row to merge multiple cells.
85
+
86
+ By default a paragraph's **text** property will create a single-line (but wrapped) entry in the cell if the value is a string. If you would like to create a multi-line cell entry, supply the value as a list instead of a string. This will instruct the module to add a line break after each list item.
87
+
88
+ Schema enforcement of the dictionary is done through Pydantic v2 validations.
89
+
90
+ Injection of the table model (either via the class or a raw dictionary) is done via the provided ```inject_table()``` function.
91
+ The function has the following parameters:
92
+ - doc_obj: _Document
93
+ - table: dict
94
+ - placeholder: str
95
+ - remove_leading_para: bool = True
96
+ - remove_placeholder: bool = True
97
+
98
+ Notice the table parameter must be a python dictionary. So if you've created the table via the provided ```WordTableModel``` class you pass it to ```inject_table()``` with: ```my_table.model_dump()```
99
+
100
+ - **<remove_leading_para>** - This is an optional argument. If not set it will default to True. MS Word tables when created automatically have an empty paragraph at the top/beginning of the table cell. This can create unwanted spacing at the top of the table. By default (value set to "True") the paragraph will be deleted. If you want to keep the paragraph (to add text to it), set this to "False".
101
+ - **<remove_placeholder>** - This is an optional argument. You can leave the placeholder (```remove_placeholder=False```) if you need to keep injecting tables below the placeholder before final deletion
102
+
103
+ ### EXAMPLE
104
+ We start with a Microsoft Word template named "source-template.docx" that looks like this:
105
+
106
+ ![Word Template](artwork/word_template.jpg)
107
+
108
+ Our sample Python script looks like this:
109
+ ```python
110
+ from docx import Document
111
+ from wordhelpers import inject_table
112
+
113
+ doc_obj = Document("source-template.docx")
114
+
115
+ my_dictionary = {
116
+ "style": "plain",
117
+ "rows": [
118
+ {
119
+ "cells": [
120
+ {
121
+ "background": "#506279",
122
+ "paragraphs":[{"style": "regularbold", "text": "Header 1:"}]
123
+ },
124
+ {
125
+ "background": "#506279",
126
+ "paragraphs":[{"style": "regularbold", "text": "Header 2:"}]
127
+ },
128
+ {
129
+ "background": "#506279",
130
+ "paragraphs":[{"style": "regularbold", "text": "Header 3:"}]
131
+ }
132
+ ]
133
+ },
134
+ {
135
+ "cells": [
136
+ {
137
+ "background": "#D5DCE4",
138
+ "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 1:"}]
139
+ },
140
+ {
141
+ "background": "#D5DCE4",
142
+ "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 2:"}]
143
+ },
144
+ {
145
+ "background": "#D5DCE4",
146
+ "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 3:"}]
147
+ }
148
+ ]
149
+ },
150
+ {
151
+ "cells": [
152
+ {
153
+ "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 1:"}]
154
+ },
155
+ {
156
+ "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 2:"}]
157
+ },
158
+ {
159
+ "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 3:"}]
160
+ }
161
+ ]
162
+ },
163
+ {
164
+ "cells": [
165
+ {
166
+ "background": "#D5DCE4",
167
+ "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 1:"}]
168
+ },
169
+ {
170
+ "background": "#D5DCE4",
171
+ "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 2:"}]
172
+ },
173
+ {
174
+ "background": "#D5DCE4",
175
+ "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 3:"}]
176
+ }
177
+ ]
178
+ }
179
+ ]
180
+ }
181
+
182
+ inject_table(doc_obj, my_dictionary, "\[py_placeholder1\]")
183
+ doc_obj.save("output_word_doc.docx")
184
+ ```
185
+
186
+ Using the provided ```WordTableModel``` class instead of the raw dictionary, the python code would look like this:
187
+ ```python
188
+ from docx import Document
189
+ from wordhelpers import WordTableModel, inject_table
190
+
191
+ doc_obj = Document("source-template.docx")
192
+
193
+ my_table = WordTableModel()
194
+ my_table.style = "plain"
195
+ my_table.add_row(
196
+ 3,
197
+ text=[
198
+ "Header 1:",
199
+ "Header 2:",
200
+ "Header 3:",
201
+ ],
202
+ background_color="#506279",
203
+ style="regularbold",
204
+ )
205
+ my_table.add_row(
206
+ 3,
207
+ text=[
208
+ "Row 1 Data 1:",
209
+ "Row 1 Data 2:",
210
+ "Row 1 Data 3:",
211
+ ],
212
+ background_color="#D5DCE4",
213
+ style="No Spacing",
214
+ )
215
+ my_table.add_row(
216
+ 3,
217
+ text=[
218
+ "Row 2 Data 1:",
219
+ "Row 2 Data 2:",
220
+ "Row 2 Data 3:",
221
+ ],
222
+ style="No Spacing",
223
+ )
224
+ my_table.add_row(
225
+ 3,
226
+ text=[
227
+ "Row 3 Data 1:",
228
+ "Row 3 Data 2:",
229
+ "Row 3 Data 3:",
230
+ ],
231
+ background_color="#D5DCE4",
232
+ style="No Spacing",
233
+ )
234
+
235
+ inject_table(doc_obj, my_table.model_dump(), "\[py_placeholder1\]")
236
+ doc_obj.save("output_word_doc.docx")
237
+ ```
238
+
239
+ We run the Python script and it produces a new Word document named "output_word_doc.docx" that looks like this:
240
+
241
+ ![Word Template](artwork/word_output.jpg)
242
+
243
+
244
+ The project provides some additional docx functions that may be useful to your project:
245
+ - ```get_para_by_string(doc_obj: _Document, search: str)```: Searches for a keyword in the docx object and returns there paragraph where the keyword is found
246
+ - ```insert_paragraph_after(paragraph: Paragraph, text: str = None, style: str = None)```: Searches for a keyword in the docx object and inserts a new paragraph immediately after it with the supplied text
247
+ - ```delete_paragraph(paragraph: Paragraph)```: Deletes a given paragraph (after you've inserted text after it for example)
248
+
249
+ As well as the following helper functions for building raw dictionary table models:
250
+ - ```insert_text_into_row(cell_text: list)```: Builds a row (dictionary) from a list of text where each list item is a column in the row. Supports "merge"
251
+ - ```insert_text_by_table_coords(table: dict, row: int, col: int, text: str)```: Inserts text into a table dictionary given the row & column numbers.
252
+ - ```generate_table(num_rows: int, num_cols: int, header_row: list, style: str = None)```: Generates a basic table dictionary and populates the headers from a list of text (strings).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "wordhelpers"
3
- version = "0.1.2"
3
+ version = "0.1.3"
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"}
@@ -79,6 +79,37 @@ def replace_placeholder_with_table(
79
79
  delete_paragraph(paragraph)
80
80
 
81
81
 
82
+ def inject_table(
83
+ doc_obj: _Document,
84
+ table: dict,
85
+ placeholder: str,
86
+ remove_leading_para: bool = True,
87
+ remove_placeholder: bool = True,
88
+ ) -> None:
89
+ """
90
+ Function to relocate a Word table object to immediately follow a given
91
+ reference paragraph identified by the placeholder. Receives as input the
92
+ placeholder string and the Word table object (using docx module).
93
+ After moving the Word table after the placeholder paragraph, delete the
94
+ placeholder paragraph.
95
+ """
96
+ # Locate the paragraph from the supplied placeholder text
97
+ paragraph: Paragraph = get_para_by_string(doc_obj, placeholder)
98
+
99
+ # 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)
101
+
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)
107
+
108
+ if remove_placeholder:
109
+ # Delete the placeholder paragraph
110
+ delete_paragraph(paragraph)
111
+
112
+
82
113
  def build_table(
83
114
  docx_obj: _Document | Table, table_dict: dict, remove_leading_para: bool = True
84
115
  ) -> Table:
@@ -0,0 +1,295 @@
1
+ # Built-In Imports
2
+ from __future__ import annotations
3
+ import re
4
+ from enum import Enum
5
+ from typing import Optional
6
+ import json
7
+
8
+ # Third-Party Imports
9
+ from pydantic import BaseModel, field_validator, Field, ConfigDict
10
+
11
+
12
+ class AlignmentEnum(str, Enum):
13
+ left = "left"
14
+ center = "center"
15
+ right = "right"
16
+ justify = "justify"
17
+ distribute = "distribute"
18
+
19
+
20
+ class WordParagraphModel(BaseModel):
21
+ model_config = ConfigDict(extra="forbid", validate_assignment=True)
22
+ style: str | None = None
23
+ alignment: AlignmentEnum | None = None
24
+ text: list[str] | str = Field(default_factory=list)
25
+
26
+
27
+ class WordCellModel(BaseModel):
28
+ model_config = ConfigDict(extra="forbid", validate_assignment=True)
29
+ width: int | None = None
30
+ background_color: str | None = None
31
+ paragraphs: list[WordParagraphModel] = Field(default_factory=list)
32
+ table: Optional["WordTableModel"] | None = None # forward reference
33
+
34
+ @field_validator("background_color")
35
+ @classmethod
36
+ def validate_hex_color(cls, v):
37
+ if v is None:
38
+ return v # allow None
39
+ if not isinstance(v, str):
40
+ raise ValueError("background_color must be a string")
41
+ # regex: # followed by 3 or 6 hex digits
42
+ if not re.fullmatch(r"#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})", v):
43
+ raise ValueError(f"'{v}' is not a valid hex color")
44
+ return v
45
+
46
+
47
+ class WordRowModel(BaseModel):
48
+ model_config = ConfigDict(extra="forbid", validate_assignment=True)
49
+ cells: list[WordCellModel | str] = Field(default_factory=list)
50
+
51
+ @field_validator("cells")
52
+ @classmethod
53
+ def validate_cells(cls, v):
54
+ # The only valid string value is "merge"
55
+ if isinstance(v, str):
56
+ if not v.strip().lower() == "merge":
57
+ raise ValueError("If a cell is a string, it must be 'merge'")
58
+ return v
59
+
60
+
61
+ class WordTableModel(BaseModel):
62
+ model_config = ConfigDict(extra="forbid", validate_assignment=True, frozen=False)
63
+ style: str | None = None
64
+ rows: list[WordRowModel] = Field(default_factory=list)
65
+
66
+
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
+ # Make sure width is same as existing rows if any
77
+ if self.rows:
78
+ existing_width = len(self.rows[0].cells)
79
+ if width != existing_width:
80
+ raise ValueError(f"New row width {width} does not match existing row width {existing_width}")
81
+
82
+ # Make sure text length is less than the row width minus merged columns
83
+ num_merge_cols = len(merge_cols)
84
+ 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
+ # Make sure merge_cols are valid
88
+ for col in merge_cols:
89
+ if not isinstance(col, int):
90
+ raise ValueError(f"merge_cols must contain integers, got: {type(col)}")
91
+ if col < 0 or col >= width:
92
+ raise ValueError(f"merge_cols contains invalid column index: {col}")
93
+
94
+ # Build the cells
95
+ cells: list = []
96
+ for i in range(width):
97
+ if i in merge_cols:
98
+ # Insert a merge placeholder
99
+ cells.append("merge")
100
+ else:
101
+ # Build a normal cell
102
+ paragraphs: list = []
103
+ if text:
104
+ paragraphs = [WordParagraphModel(style=style, alignment=alignment, text=[text.pop(0)])]
105
+ cells.append(WordCellModel(background_color=background_color, paragraphs=paragraphs))
106
+ # Re-build the rows
107
+ rows: list = self.rows.copy()
108
+ rows.append(WordRowModel(cells=cells))
109
+ self.rows = rows
110
+
111
+
112
+ def add_text_to_row(self, row_index: int, text: list[str], style: str | None = None, alignment: AlignmentEnum | None = None) -> None:
113
+ # Validate row_index
114
+ if row_index < 0 or row_index >= len(self.rows):
115
+ raise IndexError(f"Row index {row_index} out of range")
116
+
117
+ # 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"])
119
+ width = len(self.rows[row_index].cells)
120
+ 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
+
123
+ rows = self.rows.copy()
124
+ row = rows[row_index]
125
+ for cell in row.cells:
126
+ if isinstance(cell, str) and cell == "merge":
127
+ continue # Skip merge cells
128
+ if text:
129
+ cell.paragraphs.append(WordParagraphModel(style=style, alignment=alignment, text=[text.pop(0)]))
130
+
131
+ self.rows = rows
132
+
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:
135
+ # Validate row_index and col_index
136
+ if row_index < 0 or row_index >= len(self.rows):
137
+ raise IndexError(f"Row index {row_index} out of range")
138
+ rows = self.rows.copy()
139
+ row = rows[row_index]
140
+ if col_index < 0 or col_index >= len(row.cells):
141
+ raise IndexError(f"Column index {col_index} out of range")
142
+
143
+ cell = row.cells[col_index]
144
+ if isinstance(cell, str) and cell == "merge":
145
+ raise ValueError("Cannot add text to a merged cell")
146
+
147
+ cell.paragraphs.append(WordParagraphModel(style=style, alignment=alignment, text=[text]))
148
+
149
+ self.rows = rows
150
+
151
+
152
+ def style_row(self, row_index: int, text_style: str) -> None:
153
+ # Validate row_index
154
+ if row_index < 0 or row_index >= len(self.rows):
155
+ raise IndexError(f"Row index {row_index} out of range")
156
+
157
+ rows = self.rows.copy()
158
+ row = rows[row_index]
159
+
160
+ for cell in row.cells:
161
+ if isinstance(cell, str) and cell == "merge":
162
+ continue # Skip merge cells
163
+ for paragraph in cell.paragraphs:
164
+ paragraph.style = text_style
165
+
166
+ self.rows = rows
167
+
168
+
169
+ def style_cell(self, row_index: int, col_index: int, text_style: str) -> None:
170
+ # Validate row_index and col_index
171
+ if row_index < 0 or row_index >= len(self.rows):
172
+ raise IndexError(f"Row index {row_index} out of range")
173
+ rows = self.rows.copy()
174
+ row = rows[row_index]
175
+ if col_index < 0 or col_index >= len(row.cells):
176
+ raise IndexError(f"Column index {col_index} out of range")
177
+
178
+ cell = row.cells[col_index]
179
+ if isinstance(cell, str) and cell == "merge":
180
+ raise ValueError("Cannot style a merged cell")
181
+
182
+ for paragraph in cell.paragraphs:
183
+ paragraph.style = text_style
184
+
185
+ self.rows = rows
186
+
187
+
188
+ def color_row(self, row_index: int, background_color: str) -> None:
189
+ # Validate row_index
190
+ if row_index < 0 or row_index >= len(self.rows):
191
+ raise IndexError(f"Row index {row_index} out of range")
192
+
193
+ rows = self.rows.copy()
194
+ row = rows[row_index]
195
+
196
+ for cell in row.cells:
197
+ if isinstance(cell, str) and cell == "merge":
198
+ continue # Skip merge cells
199
+ cell.background_color = background_color
200
+
201
+ self.rows = rows
202
+
203
+
204
+ def color_cell(self, row_index: int, col_index: int, background_color: str) -> None:
205
+ # Validate row_index and col_index
206
+ if row_index < 0 or row_index >= len(self.rows):
207
+ raise IndexError(f"Row index {row_index} out of range")
208
+ rows = self.rows.copy()
209
+ row = rows[row_index]
210
+ if col_index < 0 or col_index >= len(row.cells):
211
+ raise IndexError(f"Column index {col_index} out of range")
212
+
213
+ cell = row.cells[col_index]
214
+ if isinstance(cell, str) and cell == "merge":
215
+ raise ValueError("Cannot color a merged cell")
216
+
217
+ cell.background_color = background_color
218
+
219
+ self.rows = rows
220
+
221
+
222
+ def align_row(self, row_index: int, alignment: AlignmentEnum) -> None:
223
+ # Validate row_index
224
+ if row_index < 0 or row_index >= len(self.rows):
225
+ raise IndexError(f"Row index {row_index} out of range")
226
+
227
+ rows = self.rows.copy()
228
+ row = rows[row_index]
229
+
230
+ for cell in row.cells:
231
+ if isinstance(cell, str) and cell == "merge":
232
+ continue # Skip merge cells
233
+ for paragraph in cell.paragraphs:
234
+ paragraph.alignment = alignment
235
+
236
+ self.rows = rows
237
+
238
+
239
+ def align_cell(self, row_index: int, col_index: int, alignment: AlignmentEnum) -> None:
240
+ # Validate row_index and col_index
241
+ if row_index < 0 or row_index >= len(self.rows):
242
+ raise IndexError(f"Row index {row_index} out of range")
243
+ rows = self.rows.copy()
244
+ row = rows[row_index]
245
+ if col_index < 0 or col_index >= len(row.cells):
246
+ raise IndexError(f"Column index {col_index} out of range")
247
+
248
+ cell = row.cells[col_index]
249
+ if isinstance(cell, str) and cell == "merge":
250
+ raise ValueError("Cannot align a merged cell")
251
+
252
+ for paragraph in cell.paragraphs:
253
+ paragraph.alignment = alignment
254
+
255
+ self.rows = rows
256
+
257
+
258
+ def add_table_to_cell(self,
259
+ row_index: int,
260
+ col_index: int,
261
+ table: WordTableModel
262
+ ) -> None:
263
+ # Validate row_index and col_index
264
+ if row_index < 0 or row_index >= len(self.rows):
265
+ raise IndexError(f"Row index {row_index} out of range")
266
+ rows = self.rows.copy()
267
+ row = rows[row_index]
268
+ if col_index < 0 or col_index >= len(row.cells):
269
+ raise IndexError(f"Column index {col_index} out of range")
270
+ cell = row.cells[col_index]
271
+
272
+ if isinstance(cell, str) and cell == "merge":
273
+ raise ValueError("Cannot add table to a merged cell")
274
+
275
+ cell.table = table
276
+
277
+ self.rows = rows
278
+
279
+
280
+ def delete_row(self, row_index: int) -> None:
281
+ if row_index < 0 or row_index >= len(self.rows):
282
+ raise IndexError(f"Row index {row_index} out of range")
283
+ rows = self.rows.copy()
284
+ del rows[row_index]
285
+ self.rows = rows
286
+
287
+
288
+ def pretty_print(self) -> None:
289
+ print(json.dumps(self.model_dump(), indent=4))
290
+
291
+
292
+ def write(self, filepath: str) -> None:
293
+ with open(filepath, 'w') as f:
294
+ json.dump(self.model_dump(), f, indent=4)
295
+
@@ -1,217 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: wordhelpers
3
- Version: 0.1.2
4
- Summary: Helpers for working with python-docx
5
- Author: AJ Cruz
6
- Author-email: 15045766-a-cruz@users.noreply.gitlab.com
7
- Requires-Python: >=3.11
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.11
10
- Classifier: Programming Language :: Python :: 3.12
11
- Classifier: Programming Language :: Python :: 3.13
12
- Requires-Dist: pydantic (>=2.12.5,<3.0.0)
13
- Requires-Dist: python-docx (>=1.2.0,<2.0.0)
14
- Description-Content-Type: text/markdown
15
-
16
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wordhelpers.svg)](https://img.shields.io/pypi/pyversions/wordhelpers)
17
- [![PyPI](https://img.shields.io/pypi/v/wordhelpers.svg)](https://pypi.python.org/pypi/wordhelpers)
18
- [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
19
-
20
- # wordhelpers
21
- =============
22
- Helper functions for [python-docx](https://python-docx.readthedocs.io/en/latest/). I found myself re-learning docx every time I wanted to use it in a project, so this provides and abstraction. You represent Word tables as a properly-formatted Python dictionary and the helper function converts it to a docx table.
23
-
24
- # Installation
25
- wordhelpers can be installed via poetry with: ```poetry add wordhelpers```
26
- or via pip with: ```pip install wordhelpers```
27
-
28
- # Usage
29
- For detailed documentation of the python-docx library see [python-docx](https://python-docx.readthedocs.io/en/latest/)
30
-
31
- 1. Import the python-docx library into your script with:
32
- ```python
33
- from docx import Document
34
- ```
35
- 1. Import the helpers from this project with:
36
- ```python
37
- from wordhelpers import build_table, replace_placeholder_with_table
38
- ```
39
- 1. Create the docx Word document object with something like:
40
- ```python
41
- doc_obj = Document("a_word_template.docx")
42
- ```
43
- 1. Manipulate the document object as required (see the next section of this README for info on how to do that)
44
- 1. When all changes to your document object are complete, write them with:
45
- ```python
46
- doc_obj.save("output_file.docx")
47
- ```
48
- # Manipulating the document object
49
- wordhelpers provides two main functions available to your scripts:
50
- 1. build_table(<doc_obj>, <table_dict>, <remove_leading_para>)
51
- 1. replace_placeholder_with_table(<doc_obj>, <search_string>, <table_obj>)
52
-
53
- ### build_table(<doc_obj>, <table_dict>, <remove_leading_para>)
54
- The purpose of this function is to allow the script author to model Word tables using Python dictionaries. If formatted properly, the module will translate the Python dictionary to the appropriate python-docx syntax and create the Word table object.
55
-
56
- The build_table function has the following arguments:
57
- - **<doc_obj>** - The python-docx Word document object created in step 3 of the "Usage" section above.
58
- - **<table_dict>** - The Word table model (Python dictionary). The expected Python dictionary format to model a Word table is:
59
- ```python
60
- {
61
- "style": None,
62
- "rows": [
63
- {
64
- "cells": [
65
- {
66
- "width": None,
67
- "background": None,
68
- "paragraphs": [{"style":None,"alignment": "center", "text":"Some Text"}],
69
- "table": {optional child table}
70
- },
71
- {
72
- "merge": None
73
- },
74
- ]
75
- }
76
- ]
77
- }
78
- ```
79
- The cell **background** attribute is optional. If supplied with a hexidecimal color code, the cell will be shaded that color.
80
-
81
- The cell **width** attribute is optional. If supplied with a decimal number (inches), it will hard-code that column's width to the supplied value.
82
-
83
- The cell **table** attribute is optional. It can be used to nest tables within table cells. If "table" is provided, no other keys are required (background, paragraphs, etc).
84
-
85
- The paragraph **style** attribute is optional. If set to anything besides None it will use the Word style referenced. The style must already exist in the source/template Word document.
86
-
87
- The paragraph **alignment** attribute is optional. If set to ```"center"``` it will center-align the text within a cell, if set to ```"right"``` it will right-align the text within a cell
88
-
89
- The **merge** key is optional. If used the cell will be merged with the cell above (from a dictionary view, to the left from a table view). Multiple merges can be used in a row to merge multiple cells.
90
-
91
- By default a paragraph's **text** property will create a single-line (but wrapped) entry in the cell if the value is a string. If you would like to create a multi-line cell entry, supply the value as a list instead of a string. This will instruct the module to add a line break after each list item.
92
- - **<remove_leading_para>** - This is an optional argument. If not set it will default to True. MS Word tables when created automatically have an empty paragraph at the top/beginning of the table cell. This can create unwanted spacing at the top of the table. By default (value set to "True") the paragraph will be deleted. If you want to keep the paragraph (to add text to it), set this to "False".
93
-
94
- **IMPORTANT NOTE:** This adds the table object to very end of your Word file. If you want to relocate it, use the provided `replace_placeholder_with_table()` function (see below).
95
-
96
- ### replace_placeholder_with_table(<doc_obj>, <search_string>, <table_obj>)
97
- The purpose of this function is to search a Word file for a given string (the placeholder) and replace the string with a Word table object.
98
-
99
- The replace_placeholder_with_table function has the following arguments:
100
- - **<doc_obj>** - The python-docx Word document object created in step 2 of the "USING PYTHON-DOCX LIBRARY" section above.
101
- - **<search_string>** - The string to search for in the document object (doc_obj)
102
- - **<table_obj>** - The python-docx Word Table object that will replace the <search_string> in the document object (odc_obj)
103
-
104
- It will relocate the table to the placeholder and remove the placeholder.-
105
-
106
- ### EXAMPLE
107
- We start with a Microsoft Word template named "source-template.docx" that looks like this:
108
-
109
- ![Word Template](artwork/word_template.jpg)
110
-
111
- Our sample Python script looks like this:
112
- ```python
113
- from docx import Document
114
- from dcnet_msofficetools.docx_extensions import build_table, replace_placeholder_with_table
115
-
116
- doc_obj = Document("source-template.docx")
117
-
118
- my_dictionary = {
119
- "style": None,
120
- "rows": [
121
- {
122
- "cells": [
123
- {
124
- "paragraphs": [],
125
- "table": {
126
- "style": "plain",
127
- "rows": [
128
- {
129
- "cells": [
130
- {
131
- "background": "#506279",
132
- "paragraphs":[{"style": "regularbold", "text": "Header 1:"}]
133
- },
134
- {
135
- "background": "#506279",
136
- "paragraphs":[{"style": "regularbold", "text": "Header 2:"}]
137
- },
138
- {
139
- "background": "#506279",
140
- "paragraphs":[{"style": "regularbold", "text": "Header 3:"}]
141
- }
142
- ]
143
- },
144
- {
145
- "cells": [
146
- {
147
- "background": "#D5DCE4",
148
- "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 1:"}]
149
- },
150
- {
151
- "background": "#D5DCE4",
152
- "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 2:"}]
153
- },
154
- {
155
- "background": "#D5DCE4",
156
- "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 3:"}]
157
- }
158
- ]
159
- },
160
- {
161
- "cells": [
162
- {
163
- "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 1:"}]
164
- },
165
- {
166
- "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 2:"}]
167
- },
168
- {
169
- "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 3:"}]
170
- }
171
- ]
172
- },
173
- {
174
- "cells": [
175
- {
176
- "background": "#D5DCE4",
177
- "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 1:"}]
178
- },
179
- {
180
- "background": "#D5DCE4",
181
- "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 2:"}]
182
- },
183
- {
184
- "background": "#D5DCE4",
185
- "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 3:"}]
186
- }
187
- ]
188
- }
189
- ]
190
- }
191
- }
192
- ]
193
- }
194
- ]
195
- }
196
-
197
- my_table = build_table(doc_obj, my_dictionary)
198
-
199
- replace_placeholder_with_table(doc_obj, '\[py_placeholder1\]', my_table)
200
-
201
- doc_obj.save("output_word_doc.docx")
202
- ```
203
-
204
- We run the Python script and it produces a new Word document named "output_word_doc.docx" that looks like this:
205
-
206
- ![Word Template](artwork/word_output.jpg)
207
-
208
-
209
- The project provides some additional docx functions that may be useful to your project:
210
- - ```get_para_by_string(doc_obj: _Document, search: str)```: Searches for a keyword in the docx object and returns there paragraph where the keyword is found
211
- - ```insert_paragraph_after(paragraph: Paragraph, text: str = None, style: str = None)```: Searches for a keyword in the docx object and inserts a new paragraph immediately after it with the supplied text
212
- - ```delete_paragraph(paragraph: Paragraph)```: Deletes a given paragraph (after you've inserted text after it for example)
213
-
214
- As well as the following helper functions for the dictionary table models:
215
- - ```insert_text_into_row(cell_text: list)```: Builds a row (dictionary) from a list of text where each list item is a column in the row. Supports "merge"
216
- -```insert_text_by_table_coords(table: dict, row: int, col: int, text: str)```: Inserts text into a table dictionary given the row & column numbers.
217
- - ```generate_table(num_rows: int, num_cols: int, header_row: list, style: str = None)```: Generates a basic table dictionary and populates the headers from a list of text (strings).
@@ -1,202 +0,0 @@
1
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/wordhelpers.svg)](https://img.shields.io/pypi/pyversions/wordhelpers)
2
- [![PyPI](https://img.shields.io/pypi/v/wordhelpers.svg)](https://pypi.python.org/pypi/wordhelpers)
3
- [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
4
-
5
- # wordhelpers
6
- =============
7
- Helper functions for [python-docx](https://python-docx.readthedocs.io/en/latest/). I found myself re-learning docx every time I wanted to use it in a project, so this provides and abstraction. You represent Word tables as a properly-formatted Python dictionary and the helper function converts it to a docx table.
8
-
9
- # Installation
10
- wordhelpers can be installed via poetry with: ```poetry add wordhelpers```
11
- or via pip with: ```pip install wordhelpers```
12
-
13
- # Usage
14
- For detailed documentation of the python-docx library see [python-docx](https://python-docx.readthedocs.io/en/latest/)
15
-
16
- 1. Import the python-docx library into your script with:
17
- ```python
18
- from docx import Document
19
- ```
20
- 1. Import the helpers from this project with:
21
- ```python
22
- from wordhelpers import build_table, replace_placeholder_with_table
23
- ```
24
- 1. Create the docx Word document object with something like:
25
- ```python
26
- doc_obj = Document("a_word_template.docx")
27
- ```
28
- 1. Manipulate the document object as required (see the next section of this README for info on how to do that)
29
- 1. When all changes to your document object are complete, write them with:
30
- ```python
31
- doc_obj.save("output_file.docx")
32
- ```
33
- # Manipulating the document object
34
- wordhelpers provides two main functions available to your scripts:
35
- 1. build_table(<doc_obj>, <table_dict>, <remove_leading_para>)
36
- 1. replace_placeholder_with_table(<doc_obj>, <search_string>, <table_obj>)
37
-
38
- ### build_table(<doc_obj>, <table_dict>, <remove_leading_para>)
39
- The purpose of this function is to allow the script author to model Word tables using Python dictionaries. If formatted properly, the module will translate the Python dictionary to the appropriate python-docx syntax and create the Word table object.
40
-
41
- The build_table function has the following arguments:
42
- - **<doc_obj>** - The python-docx Word document object created in step 3 of the "Usage" section above.
43
- - **<table_dict>** - The Word table model (Python dictionary). The expected Python dictionary format to model a Word table is:
44
- ```python
45
- {
46
- "style": None,
47
- "rows": [
48
- {
49
- "cells": [
50
- {
51
- "width": None,
52
- "background": None,
53
- "paragraphs": [{"style":None,"alignment": "center", "text":"Some Text"}],
54
- "table": {optional child table}
55
- },
56
- {
57
- "merge": None
58
- },
59
- ]
60
- }
61
- ]
62
- }
63
- ```
64
- The cell **background** attribute is optional. If supplied with a hexidecimal color code, the cell will be shaded that color.
65
-
66
- The cell **width** attribute is optional. If supplied with a decimal number (inches), it will hard-code that column's width to the supplied value.
67
-
68
- The cell **table** attribute is optional. It can be used to nest tables within table cells. If "table" is provided, no other keys are required (background, paragraphs, etc).
69
-
70
- The paragraph **style** attribute is optional. If set to anything besides None it will use the Word style referenced. The style must already exist in the source/template Word document.
71
-
72
- The paragraph **alignment** attribute is optional. If set to ```"center"``` it will center-align the text within a cell, if set to ```"right"``` it will right-align the text within a cell
73
-
74
- The **merge** key is optional. If used the cell will be merged with the cell above (from a dictionary view, to the left from a table view). Multiple merges can be used in a row to merge multiple cells.
75
-
76
- By default a paragraph's **text** property will create a single-line (but wrapped) entry in the cell if the value is a string. If you would like to create a multi-line cell entry, supply the value as a list instead of a string. This will instruct the module to add a line break after each list item.
77
- - **<remove_leading_para>** - This is an optional argument. If not set it will default to True. MS Word tables when created automatically have an empty paragraph at the top/beginning of the table cell. This can create unwanted spacing at the top of the table. By default (value set to "True") the paragraph will be deleted. If you want to keep the paragraph (to add text to it), set this to "False".
78
-
79
- **IMPORTANT NOTE:** This adds the table object to very end of your Word file. If you want to relocate it, use the provided `replace_placeholder_with_table()` function (see below).
80
-
81
- ### replace_placeholder_with_table(<doc_obj>, <search_string>, <table_obj>)
82
- The purpose of this function is to search a Word file for a given string (the placeholder) and replace the string with a Word table object.
83
-
84
- The replace_placeholder_with_table function has the following arguments:
85
- - **<doc_obj>** - The python-docx Word document object created in step 2 of the "USING PYTHON-DOCX LIBRARY" section above.
86
- - **<search_string>** - The string to search for in the document object (doc_obj)
87
- - **<table_obj>** - The python-docx Word Table object that will replace the <search_string> in the document object (odc_obj)
88
-
89
- It will relocate the table to the placeholder and remove the placeholder.-
90
-
91
- ### EXAMPLE
92
- We start with a Microsoft Word template named "source-template.docx" that looks like this:
93
-
94
- ![Word Template](artwork/word_template.jpg)
95
-
96
- Our sample Python script looks like this:
97
- ```python
98
- from docx import Document
99
- from dcnet_msofficetools.docx_extensions import build_table, replace_placeholder_with_table
100
-
101
- doc_obj = Document("source-template.docx")
102
-
103
- my_dictionary = {
104
- "style": None,
105
- "rows": [
106
- {
107
- "cells": [
108
- {
109
- "paragraphs": [],
110
- "table": {
111
- "style": "plain",
112
- "rows": [
113
- {
114
- "cells": [
115
- {
116
- "background": "#506279",
117
- "paragraphs":[{"style": "regularbold", "text": "Header 1:"}]
118
- },
119
- {
120
- "background": "#506279",
121
- "paragraphs":[{"style": "regularbold", "text": "Header 2:"}]
122
- },
123
- {
124
- "background": "#506279",
125
- "paragraphs":[{"style": "regularbold", "text": "Header 3:"}]
126
- }
127
- ]
128
- },
129
- {
130
- "cells": [
131
- {
132
- "background": "#D5DCE4",
133
- "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 1:"}]
134
- },
135
- {
136
- "background": "#D5DCE4",
137
- "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 2:"}]
138
- },
139
- {
140
- "background": "#D5DCE4",
141
- "paragraphs":[{"style": "No Spacing", "text": "Row 1 Data 3:"}]
142
- }
143
- ]
144
- },
145
- {
146
- "cells": [
147
- {
148
- "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 1:"}]
149
- },
150
- {
151
- "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 2:"}]
152
- },
153
- {
154
- "paragraphs":[{"style": "No Spacing", "text": "Row 2 Data 3:"}]
155
- }
156
- ]
157
- },
158
- {
159
- "cells": [
160
- {
161
- "background": "#D5DCE4",
162
- "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 1:"}]
163
- },
164
- {
165
- "background": "#D5DCE4",
166
- "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 2:"}]
167
- },
168
- {
169
- "background": "#D5DCE4",
170
- "paragraphs":[{"style": "No Spacing", "text": "Row 3 Data 3:"}]
171
- }
172
- ]
173
- }
174
- ]
175
- }
176
- }
177
- ]
178
- }
179
- ]
180
- }
181
-
182
- my_table = build_table(doc_obj, my_dictionary)
183
-
184
- replace_placeholder_with_table(doc_obj, '\[py_placeholder1\]', my_table)
185
-
186
- doc_obj.save("output_word_doc.docx")
187
- ```
188
-
189
- We run the Python script and it produces a new Word document named "output_word_doc.docx" that looks like this:
190
-
191
- ![Word Template](artwork/word_output.jpg)
192
-
193
-
194
- The project provides some additional docx functions that may be useful to your project:
195
- - ```get_para_by_string(doc_obj: _Document, search: str)```: Searches for a keyword in the docx object and returns there paragraph where the keyword is found
196
- - ```insert_paragraph_after(paragraph: Paragraph, text: str = None, style: str = None)```: Searches for a keyword in the docx object and inserts a new paragraph immediately after it with the supplied text
197
- - ```delete_paragraph(paragraph: Paragraph)```: Deletes a given paragraph (after you've inserted text after it for example)
198
-
199
- As well as the following helper functions for the dictionary table models:
200
- - ```insert_text_into_row(cell_text: list)```: Builds a row (dictionary) from a list of text where each list item is a column in the row. Supports "merge"
201
- -```insert_text_by_table_coords(table: dict, row: int, col: int, text: str)```: Inserts text into a table dictionary given the row & column numbers.
202
- - ```generate_table(num_rows: int, num_cols: int, header_row: list, style: str = None)```: Generates a basic table dictionary and populates the headers from a list of text (strings).
@@ -1,57 +0,0 @@
1
- # Built-In Imports
2
- import re
3
- from enum import Enum
4
- from typing import Optional
5
-
6
- # Third-Party Imports
7
- from pydantic import BaseModel, field_validator, Field
8
-
9
-
10
- class AlignmentEnum(str, Enum):
11
- left = "left"
12
- center = "center"
13
- right = "right"
14
- justify = "justify"
15
- distribute = "distribute"
16
-
17
-
18
- class WordParagraphModel(BaseModel):
19
- style: str | None = None
20
- alignment: AlignmentEnum | None = None
21
- text: list[str] = Field(default_factory=list)
22
-
23
-
24
- class WordCellModel(BaseModel):
25
- width: int | None = None
26
- background_color: str | None = None
27
- paragraphs: list[WordParagraphModel] = Field(default_factory=list)
28
- table: Optional["WordTableModel"] | None = None # forward reference
29
-
30
- @field_validator("background_color")
31
- @classmethod
32
- def validate_hex_color(cls, v):
33
- if v is None:
34
- return v # allow None
35
- if not isinstance(v, str):
36
- raise ValueError("background_color must be a string")
37
- # regex: # followed by 3 or 6 hex digits
38
- if not re.fullmatch(r"#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})", v):
39
- raise ValueError(f"'{v}' is not a valid hex color")
40
- return v
41
-
42
-
43
- class WordRowModel(BaseModel):
44
- cells: list[WordCellModel | str] = Field(default_factory=list)
45
-
46
- @field_validator("cells")
47
- @classmethod
48
- def validate_cells(cls, v):
49
- if isinstance(v, str):
50
- if not v.strip().lower() == "merge":
51
- raise ValueError("If a cell is a string, it must be 'merge'")
52
- return v
53
-
54
-
55
- class WordTableModel(BaseModel):
56
- style: str | None = None
57
- rows: list[WordRowModel] = Field(default_factory=list)