xmlppt 0.1.0__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.
xmlppt-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: xmlppt
3
+ Version: 0.1.0
4
+ Summary: PowerPoint editing via Open XML package manipulation
5
+ Author: Your Name
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jlondon626/powerpoint_editor
8
+ Project-URL: Repository, https://github.com/jlondon626/powerpoint_editor
9
+ Keywords: pptx,powerpoint,openxml,slides,excel
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Operating System :: Microsoft :: Windows
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: lxml>=4.0.0
18
+ Requires-Dist: openpyxl>=3.0.0
19
+ Requires-Dist: pywin32>=306; sys_platform == "win32"
20
+
21
+ # xmlppt
22
+
23
+ A small utility package for editing PowerPoint (.pptx) files by
24
+ manipulating the Open XML package directly. It supports duplicating
25
+ template slides, editing named textboxes, updating embedded Excel
26
+ workbooks for charts, and basic listing/debug utilities.
27
+
28
+ This repository contains a single package `xmlppt` with the main
29
+ implementation in `xmlppt/editor.py` and a minimal `main.py` example
30
+ entrypoint.
31
+
32
+ Requirements
33
+ - Python 3.10+
34
+ - lxml
35
+ - openpyxl
36
+ - pywin32 (only required for `refresh_chart()` on Windows)
37
+
38
+ Install
39
+
40
+ Create a virtual environment and install dependencies:
41
+
42
+ ```bash
43
+ python -m venv .venv
44
+ .venv\Scripts\Activate.ps1 # Windows PowerShell
45
+ pip install -r requirements.txt
46
+ ```
47
+
48
+ Install the package locally for development:
49
+
50
+ ```bash
51
+ pip install -e .
52
+ ```
53
+
54
+ Quick usage (programmatic)
55
+
56
+ ```python
57
+ from xmlppt import PowerPointEditor
58
+
59
+ editor = PowerPointEditor('input.pptx')
60
+ editor.duplicate_template_slide('RESERVE_WATERFALL')
61
+ editor.edit_textbox_html('AOM Text', '<b>Updated</b>')
62
+ editor.save('output.pptx')
63
+ ```
64
+
65
+ CLI example
66
+
67
+ Run the example entrypoint which shows basic diagnostics and attempts
68
+ to run a sample flow (expects a marker template slide):
69
+
70
+ ```bash
71
+ python main.py --input example.pptx --run-example
72
+ ```
73
+
74
+ Run tests
75
+
76
+ ```bash
77
+ pytest -q
78
+ ```
79
+
80
+ CI
81
+
82
+ A GitHub Actions workflow is included at `.github/workflows/python-package.yml`
83
+ that installs the package and runs the test suite on Windows.
84
+
85
+ Publishing
86
+
87
+ A release workflow is included at `.github/workflows/publish.yml`.
88
+ When you create a GitHub release and publish it, the workflow will build
89
+ and upload the package to PyPI using the `PYPI_API_TOKEN` secret.
90
+
91
+ To configure publishing:
92
+
93
+ 1. Create a PyPI API token at https://pypi.org/manage/account/token/
94
+ 2. Add it to your repository secrets as `PYPI_API_TOKEN`
95
+ 3. Create a release on GitHub
96
+
97
+ Notes
98
+ - The `refresh_chart()` function uses COM automation to refresh chart
99
+ visuals inside PowerPoint; this only works on Windows with PowerPoint
100
+ installed.
101
+ - The package manipulates the raw PPTX (zip) contents; always test on
102
+ copies of presentations before running on production files.
xmlppt-0.1.0/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # xmlppt
2
+
3
+ A small utility package for editing PowerPoint (.pptx) files by
4
+ manipulating the Open XML package directly. It supports duplicating
5
+ template slides, editing named textboxes, updating embedded Excel
6
+ workbooks for charts, and basic listing/debug utilities.
7
+
8
+ This repository contains a single package `xmlppt` with the main
9
+ implementation in `xmlppt/editor.py` and a minimal `main.py` example
10
+ entrypoint.
11
+
12
+ Requirements
13
+ - Python 3.10+
14
+ - lxml
15
+ - openpyxl
16
+ - pywin32 (only required for `refresh_chart()` on Windows)
17
+
18
+ Install
19
+
20
+ Create a virtual environment and install dependencies:
21
+
22
+ ```bash
23
+ python -m venv .venv
24
+ .venv\Scripts\Activate.ps1 # Windows PowerShell
25
+ pip install -r requirements.txt
26
+ ```
27
+
28
+ Install the package locally for development:
29
+
30
+ ```bash
31
+ pip install -e .
32
+ ```
33
+
34
+ Quick usage (programmatic)
35
+
36
+ ```python
37
+ from xmlppt import PowerPointEditor
38
+
39
+ editor = PowerPointEditor('input.pptx')
40
+ editor.duplicate_template_slide('RESERVE_WATERFALL')
41
+ editor.edit_textbox_html('AOM Text', '<b>Updated</b>')
42
+ editor.save('output.pptx')
43
+ ```
44
+
45
+ CLI example
46
+
47
+ Run the example entrypoint which shows basic diagnostics and attempts
48
+ to run a sample flow (expects a marker template slide):
49
+
50
+ ```bash
51
+ python main.py --input example.pptx --run-example
52
+ ```
53
+
54
+ Run tests
55
+
56
+ ```bash
57
+ pytest -q
58
+ ```
59
+
60
+ CI
61
+
62
+ A GitHub Actions workflow is included at `.github/workflows/python-package.yml`
63
+ that installs the package and runs the test suite on Windows.
64
+
65
+ Publishing
66
+
67
+ A release workflow is included at `.github/workflows/publish.yml`.
68
+ When you create a GitHub release and publish it, the workflow will build
69
+ and upload the package to PyPI using the `PYPI_API_TOKEN` secret.
70
+
71
+ To configure publishing:
72
+
73
+ 1. Create a PyPI API token at https://pypi.org/manage/account/token/
74
+ 2. Add it to your repository secrets as `PYPI_API_TOKEN`
75
+ 3. Create a release on GitHub
76
+
77
+ Notes
78
+ - The `refresh_chart()` function uses COM automation to refresh chart
79
+ visuals inside PowerPoint; this only works on Windows with PowerPoint
80
+ installed.
81
+ - The package manipulates the raw PPTX (zip) contents; always test on
82
+ copies of presentations before running on production files.
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xmlppt"
7
+ version = "0.1.0"
8
+ description = "PowerPoint editing via Open XML package manipulation"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Your Name" },
14
+ ]
15
+ urls = { "Homepage" = "https://github.com/jlondon626/powerpoint_editor", "Repository" = "https://github.com/jlondon626/powerpoint_editor" }
16
+ dependencies = [
17
+ "lxml>=4.0.0",
18
+ "openpyxl>=3.0.0",
19
+ "pywin32>=306; sys_platform == \"win32\"",
20
+ ]
21
+ keywords = ["pptx", "powerpoint", "openxml", "slides", "excel"]
22
+ classifiers = [
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Operating System :: Microsoft :: Windows",
28
+ ]
29
+
30
+ [tool.setuptools]
31
+ packages = ["xmlppt"]
xmlppt-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,274 @@
1
+ import tempfile
2
+ import zipfile
3
+ import os
4
+ from io import BytesIO
5
+ from openpyxl import Workbook, load_workbook
6
+ from xmlppt import editor as edmod
7
+ from xmlppt import PowerPointEditor
8
+ from lxml import etree
9
+
10
+
11
+ P_NS = edmod.P_NS
12
+ A_NS = edmod.A_NS
13
+ R_NS = edmod.R_NS
14
+
15
+
16
+ def _make_minimal_pptx(path: str) -> None:
17
+ """Create a minimal pptx zip with one slide containing a textbox and a table."""
18
+ # Content types
19
+ content_types = (
20
+ '<?xml version="1.0" encoding="UTF-8"?>'
21
+ '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
22
+ '<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>'
23
+ '</Types>'
24
+ ).encode("utf-8")
25
+
26
+ # presentation.xml
27
+ presentation = (
28
+ f'<?xml version="1.0" encoding="UTF-8"?>'
29
+ f'<p:presentation xmlns:p="{P_NS}" xmlns:r="{R_NS}">'
30
+ f'<p:sldIdLst>'
31
+ f'<p:sldId id="256" r:id="rId1"/>'
32
+ f'</p:sldIdLst>'
33
+ f'</p:presentation>'
34
+ ).encode("utf-8")
35
+
36
+ presentation_rels = (
37
+ '<?xml version="1.0" encoding="UTF-8"?>'
38
+ f'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
39
+ f'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>'
40
+ '</Relationships>'
41
+ ).encode("utf-8")
42
+
43
+ # slide1.xml with a textbox named TestBox and a table named MyTable
44
+ slide = (
45
+ f'<?xml version="1.0" encoding="UTF-8"?>'
46
+ f'<p:sld xmlns:p="{P_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">'
47
+ f'<p:cSld><p:spTree>'
48
+ # Textbox
49
+ f'<p:sp>'
50
+ f' <p:nvSpPr><p:cNvPr id="2" name="TestBox"/></p:nvSpPr>'
51
+ f' <p:txBody><a:bodyPr/><a:lstStyle/>'
52
+ f' <a:p><a:r><a:rPr/><a:t>Hello</a:t></a:r></a:p>'
53
+ f' </p:txBody>'
54
+ f'</p:sp>'
55
+ # Table in graphicFrame
56
+ f'<p:graphicFrame>'
57
+ f' <p:nvGraphicFramePr><p:cNvPr id="5" name="MyTable"/></p:nvGraphicFramePr>'
58
+ f' <a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table">'
59
+ f' <a:tbl>'
60
+ f' <a:tr>'
61
+ f' <a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>R1C1</a:t></a:r></a:p></a:txBody></a:tc>'
62
+ f' <a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>R1C2</a:t></a:r></a:p></a:txBody></a:tc>'
63
+ f' </a:tr>'
64
+ f' <a:tr>'
65
+ f' <a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>R2C1</a:t></a:r></a:p></a:txBody></a:tc>'
66
+ f' <a:tc><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>R2C2</a:t></a:r></a:p></a:txBody></a:tc>'
67
+ f' </a:tr>'
68
+ f' </a:tbl>'
69
+ f' </a:graphicData></a:graphic>'
70
+ f'</p:graphicFrame>'
71
+ f'</p:spTree></p:cSld></p:sld>'
72
+ ).encode("utf-8")
73
+
74
+ # create zip
75
+ with zipfile.ZipFile(path, 'w') as z:
76
+ z.writestr('[Content_Types].xml', content_types)
77
+ z.writestr('ppt/presentation.xml', presentation)
78
+ z.writestr('ppt/_rels/presentation.xml.rels', presentation_rels)
79
+ z.writestr('ppt/slides/slide1.xml', slide)
80
+
81
+
82
+ def _make_minimal_pptx_with_chart(path: str) -> None:
83
+ content_types = (
84
+ '<?xml version="1.0" encoding="UTF-8"?>'
85
+ '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
86
+ '<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>'
87
+ '<Override PartName="/ppt/charts/chart1.xml" ContentType="application/vnd.openxmlformats-officedocument.drawingml.chart+xml"/>'
88
+ '<Override PartName="/ppt/embeddings/Microsoft_Excel_Worksheet1.xlsx" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"/>'
89
+ '</Types>'
90
+ ).encode('utf-8')
91
+
92
+ presentation = (
93
+ f'<?xml version="1.0" encoding="UTF-8"?>'
94
+ f'<p:presentation xmlns:p="{P_NS}" xmlns:r="{R_NS}">'
95
+ f'<p:sldIdLst>'
96
+ f'<p:sldId id="256" r:id="rId1"/>'
97
+ f'</p:sldIdLst>'
98
+ f'</p:presentation>'
99
+ ).encode('utf-8')
100
+
101
+ presentation_rels = (
102
+ '<?xml version="1.0" encoding="UTF-8"?>'
103
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
104
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>'
105
+ '</Relationships>'
106
+ ).encode('utf-8')
107
+
108
+ slide = (
109
+ f'<?xml version="1.0" encoding="UTF-8"?>'
110
+ f'<p:sld xmlns:p="{P_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">'
111
+ f'<p:cSld><p:spTree>'
112
+ f'<p:graphicFrame>'
113
+ f' <p:nvGraphicFramePr><p:cNvPr id="5" name="TestChart"/></p:nvGraphicFramePr>'
114
+ f' <a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart">'
115
+ f' <c:chart xmlns:c="{edmod.C_NS}" xmlns:r="{R_NS}" r:id="rId1"/>'
116
+ f' </a:graphicData></a:graphic>'
117
+ f'</p:graphicFrame>'
118
+ f'</p:spTree></p:cSld></p:sld>'
119
+ ).encode('utf-8')
120
+
121
+ chart_xml = (
122
+ '<?xml version="1.0" encoding="UTF-8"?>'
123
+ f'<c:chartSpace xmlns:c="{edmod.C_NS}" xmlns:a="{A_NS}" xmlns:r="{R_NS}">'
124
+ f' <c:chart>'
125
+ f' <c:plotArea>'
126
+ f' <c:barChart>'
127
+ f' <c:ser>'
128
+ f' <c:cat>'
129
+ f' <c:strRef>'
130
+ f' <c:strCache>'
131
+ f' <c:ptCount val="2"/>'
132
+ f' <c:pt idx="0"><c:v>One</c:v></c:pt>'
133
+ f' <c:pt idx="1"><c:v>Two</c:v></c:pt>'
134
+ f' </c:strCache>'
135
+ f' </c:strRef>'
136
+ f' </c:cat>'
137
+ f' <c:val>'
138
+ f' <c:numRef>'
139
+ f' <c:numCache>'
140
+ f' <c:ptCount val="2"/>'
141
+ f' <c:pt idx="0"><c:v>10</c:v></c:pt>'
142
+ f' <c:pt idx="1"><c:v>20</c:v></c:pt>'
143
+ f' </c:numCache>'
144
+ f' </c:numRef>'
145
+ f' </c:val>'
146
+ f' </c:ser>'
147
+ f' </c:barChart>'
148
+ f' </c:plotArea>'
149
+ f' </c:chart>'
150
+ f'</c:chartSpace>'
151
+ ).encode('utf-8')
152
+
153
+ chart_rels = (
154
+ '<?xml version="1.0" encoding="UTF-8"?>'
155
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
156
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/package" Target="../embeddings/Microsoft_Excel_Worksheet1.xlsx"/>'
157
+ '</Relationships>'
158
+ ).encode('utf-8')
159
+
160
+ wb = Workbook()
161
+ ws = wb.active
162
+ ws.title = 'Sheet1'
163
+ ws['A1'] = 'Category'
164
+ ws['B1'] = 'Value'
165
+ ws['A2'] = 'One'
166
+ ws['B2'] = 10
167
+ out = BytesIO()
168
+ wb.save(out)
169
+ workbook_bytes = out.getvalue()
170
+
171
+ with zipfile.ZipFile(path, 'w') as z:
172
+ z.writestr('[Content_Types].xml', content_types)
173
+ z.writestr('ppt/presentation.xml', presentation)
174
+ z.writestr('ppt/_rels/presentation.xml.rels', presentation_rels)
175
+ z.writestr('ppt/slides/slide1.xml', slide)
176
+ z.writestr('ppt/slides/_rels/slide1.xml.rels', (
177
+ '<?xml version="1.0" encoding="UTF-8"?>'
178
+ '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
179
+ '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" Target="../charts/chart1.xml"/>'
180
+ '</Relationships>'
181
+ ).encode('utf-8'))
182
+ z.writestr('ppt/charts/chart1.xml', chart_xml)
183
+ z.writestr('ppt/charts/_rels/chart1.xml.rels', chart_rels)
184
+ z.writestr('ppt/embeddings/Microsoft_Excel_Worksheet1.xlsx', workbook_bytes)
185
+
186
+
187
+ def test_import():
188
+ import xmlppt
189
+
190
+ assert hasattr(xmlppt, 'PowerPointEditor')
191
+
192
+
193
+ def test_textbox_find_and_edit(tmp_path):
194
+ pptx = tmp_path / 'test.pptx'
195
+ _make_minimal_pptx(str(pptx))
196
+
197
+ editor = PowerPointEditor(str(pptx))
198
+
199
+ found = editor.find_textbox_anywhere('TestBox')
200
+ assert found['slide_number'] == 1
201
+
202
+ editor.edit_textbox_on_slide(1, 'TestBox', 'New\nLine')
203
+
204
+ slide_xml = editor.files['ppt/slides/slide1.xml'].decode('utf-8')
205
+ assert 'New' in slide_xml
206
+ assert 'Line' in slide_xml
207
+
208
+
209
+ def test_table_find_and_edit_cell(tmp_path):
210
+ pptx = tmp_path / 'test_table.pptx'
211
+ _make_minimal_pptx(str(pptx))
212
+
213
+ editor = PowerPointEditor(str(pptx))
214
+
215
+ found = editor.find_table_anywhere('MyTable')
216
+ assert found['slide_number'] == 1
217
+
218
+ editor.edit_table_cell_on_slide(1, 'MyTable', 0, 1, 'NEWVAL')
219
+
220
+ slide_xml = editor.files['ppt/slides/slide1.xml'].decode('utf-8')
221
+ assert 'NEWVAL' in slide_xml
222
+
223
+
224
+ def test_table_edit_range(tmp_path):
225
+ pptx = tmp_path / 'test_table2.pptx'
226
+ _make_minimal_pptx(str(pptx))
227
+
228
+ editor = PowerPointEditor(str(pptx))
229
+
230
+ data = [['A1', 'A2'], ['B1', 'B2']]
231
+ editor.edit_table_range_on_slide(1, 'MyTable', data)
232
+
233
+ slide_xml = editor.files['ppt/slides/slide1.xml'].decode('utf-8')
234
+ assert 'A1' in slide_xml and 'B2' in slide_xml
235
+
236
+
237
+ def test_waterfall_data_edit(tmp_path):
238
+ pptx = tmp_path / 'test_chart.pptx'
239
+ _make_minimal_pptx_with_chart(str(pptx))
240
+
241
+ editor = PowerPointEditor(str(pptx))
242
+ editor.edit_waterfall_data_on_slide(1, 'TestChart', ['NewOne', 'NewTwo'], [11, 22])
243
+
244
+ chart_xml = editor.files['ppt/charts/chart1.xml'].decode('utf-8')
245
+ assert 'NewOne' in chart_xml
246
+ assert '22' in chart_xml
247
+
248
+
249
+ def test_embedded_workbook_for_chart_on_slide(tmp_path):
250
+ pptx = tmp_path / 'test_embedded_chart.pptx'
251
+ _make_minimal_pptx_with_chart(str(pptx))
252
+
253
+ editor = PowerPointEditor(str(pptx))
254
+ editor.edit_embedded_workbook_for_chart_on_slide(
255
+ slide_number=1,
256
+ chart_name='TestChart',
257
+ categories=['CatA', 'CatB'],
258
+ values=[100, 200],
259
+ sheet_name='Sheet1',
260
+ )
261
+
262
+ chart_xml = editor.files['ppt/charts/chart1.xml'].decode('utf-8')
263
+ assert 'CatA' in chart_xml
264
+ assert '200' in chart_xml
265
+
266
+ rels = etree.fromstring(editor.files['ppt/charts/_rels/chart1.xml.rels'])
267
+ workbook_target = rels.xpath('./pr:Relationship', namespaces=edmod.NS)[0].get('Target')
268
+ workbook_path = PowerPointEditor._normalize_relationship_target('ppt/charts/chart1.xml', workbook_target)
269
+ workbook_data = editor.files[workbook_path]
270
+
271
+ loaded_wb = load_workbook(BytesIO(workbook_data))
272
+ sheet = loaded_wb['Sheet1']
273
+ assert sheet['A2'].value == 'CatA'
274
+ assert sheet['B3'].value == 200
@@ -0,0 +1,3 @@
1
+ from .editor import PowerPointEditor
2
+
3
+ __all__ = ["PowerPointEditor"]