decx 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.
decx-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.3
2
+ Name: decx
3
+ Version: 0.1.0
4
+ Summary: Automated PowerPoint report generation from Excel data via COM
5
+ Keywords: powerpoint,excel,automation,com,windows,reporting
6
+ Author: Albert Li
7
+ License: MIT
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Win32 (MS Windows)
10
+ Classifier: Intended Audience :: End Users/Desktop
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Office/Business
15
+ Requires-Dist: pywin32>=311 ; sys_platform == 'win32'
16
+ Requires-Dist: pyyaml>=6.0.3
17
+ Requires-Python: >=3.11
18
+ Project-URL: Repository, https://github.com/albertxli/decx
19
+ Description-Content-Type: text/markdown
20
+
21
+ # decx
22
+
23
+ Automated PowerPoint report generation from Excel data via COM.
24
+
25
+ `decx` reads data from Excel workbooks and updates linked OLE objects, tables, delta indicators, color coding, and charts in PowerPoint presentations — all driven from the command line.
26
+
27
+ ## Requirements
28
+
29
+ - **Windows** (COM automation requires Windows)
30
+ - **Microsoft PowerPoint** (installed and licensed)
31
+ - **Microsoft Excel** (installed and licensed)
32
+ - **Python 3.11+**
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ uv add decx
38
+ ```
39
+
40
+ Or with pip:
41
+
42
+ ```bash
43
+ pip install decx
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ### Update presentations
49
+
50
+ ```bash
51
+ # Single presentation with one Excel file
52
+ decx update report.pptx --excel data.xlsx
53
+
54
+ # Batch mode with explicit pptx:xlsx pairs
55
+ decx update --pair "us.pptx:us_data.xlsx" --pair "mx.pptx:mx_data.xlsx"
56
+
57
+ # Skip specific steps
58
+ decx update report.pptx --excel data.xlsx --skip-links --skip-charts
59
+
60
+ # Use a custom config file
61
+ decx update report.pptx --excel data.xlsx --config my_config.yaml
62
+
63
+ # Verbose output for debugging
64
+ decx update report.pptx --excel data.xlsx --verbose
65
+ ```
66
+
67
+ ### Initialize config
68
+
69
+ Write the default `config.yaml` to the current directory:
70
+
71
+ ```bash
72
+ decx init
73
+ ```
74
+
75
+ ### Info
76
+
77
+ ```bash
78
+ decx info
79
+ ```
80
+
81
+ ### Version
82
+
83
+ ```bash
84
+ decx --version
85
+ ```
86
+
87
+ ## Configuration
88
+
89
+ `decx` ships with sensible defaults. Run `decx init` to generate a `config.yaml` you can customize:
90
+
91
+ ```yaml
92
+ heatmap:
93
+ color_minimum: '#F8696B'
94
+ color_midpoint: '#FFEB84'
95
+ color_maximum: '#63BE7B'
96
+ dark_font: '#000000'
97
+ light_font: '#FFFFFF'
98
+
99
+ ccst:
100
+ positive_color: '#33CC33'
101
+ negative_color: '#ED0590'
102
+ neutral_color: '#595959'
103
+ positive_prefix: '+'
104
+ symbol_removal: '%'
105
+
106
+ delta:
107
+ template_positive: tmpl_delta_pos
108
+ template_negative: tmpl_delta_neg
109
+ template_none: tmpl_delta_none
110
+ template_slide: 1
111
+
112
+ links:
113
+ set_manual: true
114
+ ```
115
+
116
+ ## Pipeline
117
+
118
+ 1. **Re-link OLE objects** — point linked Excel objects to a new data file
119
+ 2. **Populate tables** — read Excel ranges and write values into PowerPoint tables
120
+ 3. **Delta indicators** — swap arrow shapes based on positive/negative values
121
+ 4. **Color coding** — apply color rules to `_ccst` tables
122
+ 5. **Update charts** — refresh linked chart data sources
123
+
124
+ ## License
125
+
126
+ MIT
127
+
128
+ ## Repository
129
+
130
+ https://github.com/albertxli/decx
decx-0.1.0/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # decx
2
+
3
+ Automated PowerPoint report generation from Excel data via COM.
4
+
5
+ `decx` reads data from Excel workbooks and updates linked OLE objects, tables, delta indicators, color coding, and charts in PowerPoint presentations — all driven from the command line.
6
+
7
+ ## Requirements
8
+
9
+ - **Windows** (COM automation requires Windows)
10
+ - **Microsoft PowerPoint** (installed and licensed)
11
+ - **Microsoft Excel** (installed and licensed)
12
+ - **Python 3.11+**
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ uv add decx
18
+ ```
19
+
20
+ Or with pip:
21
+
22
+ ```bash
23
+ pip install decx
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Update presentations
29
+
30
+ ```bash
31
+ # Single presentation with one Excel file
32
+ decx update report.pptx --excel data.xlsx
33
+
34
+ # Batch mode with explicit pptx:xlsx pairs
35
+ decx update --pair "us.pptx:us_data.xlsx" --pair "mx.pptx:mx_data.xlsx"
36
+
37
+ # Skip specific steps
38
+ decx update report.pptx --excel data.xlsx --skip-links --skip-charts
39
+
40
+ # Use a custom config file
41
+ decx update report.pptx --excel data.xlsx --config my_config.yaml
42
+
43
+ # Verbose output for debugging
44
+ decx update report.pptx --excel data.xlsx --verbose
45
+ ```
46
+
47
+ ### Initialize config
48
+
49
+ Write the default `config.yaml` to the current directory:
50
+
51
+ ```bash
52
+ decx init
53
+ ```
54
+
55
+ ### Info
56
+
57
+ ```bash
58
+ decx info
59
+ ```
60
+
61
+ ### Version
62
+
63
+ ```bash
64
+ decx --version
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ `decx` ships with sensible defaults. Run `decx init` to generate a `config.yaml` you can customize:
70
+
71
+ ```yaml
72
+ heatmap:
73
+ color_minimum: '#F8696B'
74
+ color_midpoint: '#FFEB84'
75
+ color_maximum: '#63BE7B'
76
+ dark_font: '#000000'
77
+ light_font: '#FFFFFF'
78
+
79
+ ccst:
80
+ positive_color: '#33CC33'
81
+ negative_color: '#ED0590'
82
+ neutral_color: '#595959'
83
+ positive_prefix: '+'
84
+ symbol_removal: '%'
85
+
86
+ delta:
87
+ template_positive: tmpl_delta_pos
88
+ template_negative: tmpl_delta_neg
89
+ template_none: tmpl_delta_none
90
+ template_slide: 1
91
+
92
+ links:
93
+ set_manual: true
94
+ ```
95
+
96
+ ## Pipeline
97
+
98
+ 1. **Re-link OLE objects** — point linked Excel objects to a new data file
99
+ 2. **Populate tables** — read Excel ranges and write values into PowerPoint tables
100
+ 3. **Delta indicators** — swap arrow shapes based on positive/negative values
101
+ 4. **Color coding** — apply color rules to `_ccst` tables
102
+ 5. **Update charts** — refresh linked chart data sources
103
+
104
+ ## License
105
+
106
+ MIT
107
+
108
+ ## Repository
109
+
110
+ https://github.com/albertxli/decx
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "decx"
3
+ version = "0.1.0"
4
+ description = "Automated PowerPoint report generation from Excel data via COM"
5
+ readme = "README.md"
6
+ license = {text = "MIT"}
7
+ requires-python = ">=3.11"
8
+ authors = [{name = "Albert Li"}]
9
+ keywords = ["powerpoint", "excel", "automation", "com", "windows", "reporting"]
10
+ classifiers = [
11
+ "Development Status :: 4 - Beta",
12
+ "Environment :: Win32 (MS Windows)",
13
+ "Intended Audience :: End Users/Desktop",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: Microsoft :: Windows",
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Office/Business",
18
+ ]
19
+ dependencies = [
20
+ "pywin32>=311; sys_platform == 'win32'",
21
+ "pyyaml>=6.0.3",
22
+ ]
23
+
24
+ [project.scripts]
25
+ decx = "decx.cli:main"
26
+
27
+ [project.urls]
28
+ Repository = "https://github.com/albertxli/decx"
29
+
30
+ [dependency-groups]
31
+ dev = ["pytest>=9.0"]
32
+
33
+ [tool.pytest.ini_options]
34
+ markers = ["integration: tests that require COM (Windows + PowerPoint + Excel)"]
35
+
36
+ [build-system]
37
+ requires = ["uv_build"]
38
+ build-backend = "uv_build"
@@ -0,0 +1,2 @@
1
+ __version__ = "0.1.0"
2
+ from decx.config import load_config, DEFAULT_CONFIG
@@ -0,0 +1,44 @@
1
+ """Step 2: Update linked chart data sources."""
2
+
3
+ import logging
4
+
5
+ from decx.shape_finder import collect_linked_charts
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+ # ppUpdateOptionManual = 1 (verified via PowerPoint type library)
10
+ PP_UPDATE_OPTION_MANUAL = 1
11
+
12
+
13
+ def update_charts(session, excel_path: str, inventory=None) -> int:
14
+ """Re-link all embedded charts to the specified Excel file.
15
+
16
+ Sets chart links to manual update mode after updating.
17
+
18
+ When inventory is provided, uses pre-collected charts list
19
+ instead of scanning all slides.
20
+
21
+ Returns the count of updated charts.
22
+ """
23
+ if inventory is not None:
24
+ charts = inventory.charts
25
+ else:
26
+ charts = collect_linked_charts(session.presentation)
27
+
28
+ if not charts:
29
+ log.info("No linked charts found")
30
+ return 0
31
+
32
+ updated = 0
33
+ for chart_shape in charts:
34
+ try:
35
+ chart_shape.LinkFormat.SourceFullName = excel_path
36
+ chart_shape.LinkFormat.Update()
37
+ chart_shape.LinkFormat.AutoUpdate = PP_UPDATE_OPTION_MANUAL
38
+ updated += 1
39
+ log.debug("Updated chart: %s", chart_shape.Name)
40
+ except Exception as e:
41
+ log.warning("Failed to update chart '%s': %s", chart_shape.Name, e)
42
+
43
+ log.info("Updated %d chart(s)", updated)
44
+ return updated
@@ -0,0 +1,287 @@
1
+ """CLI entry point for decx — PowerPoint Excel report automation."""
2
+
3
+ import argparse
4
+ import glob
5
+ import logging
6
+ import os
7
+ import sys
8
+ import time
9
+
10
+ import yaml
11
+
12
+ from decx import __version__
13
+ from decx.config import load_config, DEFAULT_CONFIG
14
+ from decx.session import Session
15
+ from decx import linker, table_updater, delta_updater, color_coder, chart_updater
16
+ from decx.shape_finder import build_presentation_inventory
17
+
18
+
19
+ def resolve_paths(patterns: list[str]) -> list[str]:
20
+ """Resolve glob patterns to absolute file paths."""
21
+ paths = []
22
+ for pattern in patterns:
23
+ expanded = glob.glob(pattern)
24
+ if expanded:
25
+ paths.extend(os.path.abspath(p) for p in expanded)
26
+ else:
27
+ # Treat as literal path
28
+ paths.append(os.path.abspath(pattern))
29
+ return paths
30
+
31
+
32
+ def parse_pair(pair_str: str) -> tuple[str, str]:
33
+ """Parse a 'pptx:xlsx' pair string into (pptx_path, excel_path)."""
34
+ if ":" not in pair_str:
35
+ print(f"Invalid pair format: '{pair_str}'. Expected 'file.pptx:data.xlsx'")
36
+ sys.exit(1)
37
+ # Split on last ':' to handle Windows drive letters like C:\path
38
+ # Find the colon that separates pptx from xlsx (not a drive letter colon)
39
+ # Strategy: split on ':', rejoin if we accidentally split a drive letter
40
+ parts = pair_str.split(":")
41
+ if len(parts) == 3:
42
+ # e.g. C:\file.pptx:C:\data.xlsx -> impossible, both have drive letters
43
+ # More likely: file.pptx:C:\data.xlsx or C:\file.pptx:data.xlsx
44
+ # Try: first part is just a drive letter -> rejoin
45
+ if len(parts[0]) == 1 and parts[0].isalpha():
46
+ # "C:\file.pptx:data.xlsx" -> pptx="C:\file.pptx", excel="data.xlsx"
47
+ pptx = f"{parts[0]}:{parts[1]}"
48
+ excel = parts[2]
49
+ else:
50
+ # "file.pptx:C:\data.xlsx" -> pptx="file.pptx", excel="C:\data.xlsx"
51
+ pptx = parts[0]
52
+ excel = f"{parts[1]}:{parts[2]}"
53
+ elif len(parts) == 4:
54
+ # "C:\file.pptx:C:\data.xlsx"
55
+ pptx = f"{parts[0]}:{parts[1]}"
56
+ excel = f"{parts[2]}:{parts[3]}"
57
+ elif len(parts) == 2:
58
+ pptx, excel = parts
59
+ else:
60
+ print(f"Invalid pair format: '{pair_str}'")
61
+ sys.exit(1)
62
+ return os.path.abspath(pptx), os.path.abspath(excel)
63
+
64
+
65
+ def process_presentation(
66
+ pptx_path: str,
67
+ excel_path: str,
68
+ config: dict,
69
+ options: argparse.Namespace,
70
+ ) -> dict:
71
+ """Process a single presentation through the full pipeline.
72
+
73
+ Returns a dict with counts: links, tables, deltas, colors, charts.
74
+ """
75
+ results = {"links": 0, "tables": 0, "deltas": 0, "colors": 0, "charts": 0}
76
+
77
+ with Session(pptx_path, excel_path) as session:
78
+ # Build shape inventory ONCE — all steps use O(1) lookups from this
79
+ inventory = build_presentation_inventory(session.presentation)
80
+
81
+ if not options.skip_links:
82
+ results["links"] = linker.update_links(
83
+ session, excel_path, config, inventory=inventory
84
+ )
85
+
86
+ results["tables"] = table_updater.update_tables(
87
+ session, config, inventory=inventory
88
+ )
89
+
90
+ if not options.skip_deltas:
91
+ results["deltas"] = delta_updater.update_deltas(
92
+ session, config, inventory=inventory
93
+ )
94
+
95
+ if not options.skip_coloring:
96
+ results["colors"] = color_coder.apply_color_coding(
97
+ session, config, inventory=inventory
98
+ )
99
+
100
+ if not options.skip_charts:
101
+ results["charts"] = chart_updater.update_charts(
102
+ session, excel_path, inventory=inventory
103
+ )
104
+
105
+ session.save()
106
+
107
+ return results
108
+
109
+
110
+ def _run_pairs(pairs: list[tuple[str, str]], config: dict, args: argparse.Namespace):
111
+ """Run the pipeline for a list of (pptx_path, excel_path) pairs."""
112
+ grand_total = {"links": 0, "tables": 0, "deltas": 0, "colors": 0, "charts": 0}
113
+ t_start = time.perf_counter()
114
+ processed = 0
115
+
116
+ for pptx_path, excel_path in pairs:
117
+ if not os.path.exists(pptx_path):
118
+ print(f"PPT not found, skipping: {pptx_path}")
119
+ continue
120
+ if not os.path.exists(excel_path):
121
+ print(f"Excel not found, skipping: {excel_path}")
122
+ continue
123
+
124
+ print(f"Processing: {os.path.basename(pptx_path)} <- {os.path.basename(excel_path)}")
125
+ t_file = time.perf_counter()
126
+
127
+ results = process_presentation(pptx_path, excel_path, config, args)
128
+
129
+ elapsed = time.perf_counter() - t_file
130
+ print(
131
+ f" Done in {elapsed:.2f}s — "
132
+ f"{results['links']} links, "
133
+ f"{results['tables']} tables, "
134
+ f"{results['deltas']} deltas, "
135
+ f"{results['colors']} colored, "
136
+ f"{results['charts']} charts"
137
+ )
138
+
139
+ for key in grand_total:
140
+ grand_total[key] += results[key]
141
+ processed += 1
142
+
143
+ total_elapsed = time.perf_counter() - t_start
144
+ print(
145
+ f"\nAll done! {processed} file(s) in {total_elapsed:.2f}s\n"
146
+ f" {grand_total['links']} link(s) updated\n"
147
+ f" {grand_total['tables']} table(s) refreshed\n"
148
+ f" {grand_total['deltas']} delta(s) updated\n"
149
+ f" {grand_total['colors']} table(s) color-coded\n"
150
+ f" {grand_total['charts']} chart(s) updated"
151
+ )
152
+
153
+
154
+ def cmd_update(args: argparse.Namespace):
155
+ """Handle the 'update' subcommand — main pipeline."""
156
+ # Logging
157
+ level = logging.DEBUG if args.verbose else logging.INFO
158
+ logging.basicConfig(
159
+ level=level,
160
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
161
+ datefmt="%H:%M:%S",
162
+ )
163
+
164
+ # Config
165
+ config = load_config(args.config)
166
+
167
+ # --- Mode 1: --pair for explicit pptx:xlsx pairs ---
168
+ if args.pair:
169
+ pairs = [parse_pair(p) for p in args.pair]
170
+ _run_pairs(pairs, config, args)
171
+ return
172
+
173
+ # --- Mode 2: presentations + --excel (or file picker) ---
174
+ if not args.presentations:
175
+ print("Error: Provide presentation file(s) or use --pair for batch pairs.")
176
+ sys.exit(1)
177
+
178
+ excel_path = args.excel
179
+ if not excel_path:
180
+ from decx.file_picker import pick_excel_file
181
+ excel_path = pick_excel_file()
182
+ if not excel_path:
183
+ print("No Excel file selected. Exiting.")
184
+ sys.exit(1)
185
+ excel_path = os.path.abspath(excel_path)
186
+
187
+ if not os.path.exists(excel_path):
188
+ print(f"Excel file not found: {excel_path}")
189
+ sys.exit(1)
190
+
191
+ pptx_files = resolve_paths(args.presentations)
192
+ if not pptx_files:
193
+ print("No presentation files found.")
194
+ sys.exit(1)
195
+
196
+ pairs = [(p, excel_path) for p in pptx_files]
197
+ _run_pairs(pairs, config, args)
198
+
199
+
200
+ def cmd_info(args: argparse.Namespace):
201
+ """Handle the 'info' subcommand — placeholder."""
202
+ print("Coming soon")
203
+
204
+
205
+ def cmd_init(args: argparse.Namespace):
206
+ """Handle the 'init' subcommand — write default config.yaml to current directory."""
207
+ output_path = os.path.join(os.getcwd(), "config.yaml")
208
+ if os.path.exists(output_path):
209
+ print(f"config.yaml already exists at {output_path}")
210
+ sys.exit(1)
211
+ with open(output_path, "w", encoding="utf-8") as f:
212
+ yaml.dump(DEFAULT_CONFIG, f, default_flow_style=False, sort_keys=False)
213
+ print(f"Wrote default config to {output_path}")
214
+
215
+
216
+ def main():
217
+ parser = argparse.ArgumentParser(
218
+ prog="decx",
219
+ description="Automated PowerPoint report generation from Excel data via COM",
220
+ )
221
+ parser.add_argument(
222
+ "--version", action="version", version=f"%(prog)s {__version__}"
223
+ )
224
+
225
+ subparsers = parser.add_subparsers(dest="command")
226
+
227
+ # --- update subcommand ---
228
+ update_parser = subparsers.add_parser(
229
+ "update",
230
+ help="Run the main update pipeline on presentations",
231
+ epilog=(
232
+ "Examples:\n"
233
+ " decx update report.pptx --excel data.xlsx\n"
234
+ " decx update report.pptx (file picker opens)\n"
235
+ ' decx update --pair "us.pptx:us.xlsx" --pair "mx.pptx:mx.xlsx"\n'
236
+ ),
237
+ formatter_class=argparse.RawDescriptionHelpFormatter,
238
+ )
239
+ update_parser.add_argument(
240
+ "presentations",
241
+ nargs="*",
242
+ help="One or more .pptx file paths (supports glob patterns). Used with --excel.",
243
+ )
244
+ update_parser.add_argument(
245
+ "--excel", "-e",
246
+ default=None,
247
+ help="Path to the Excel data file. If omitted, a file dialog will open.",
248
+ )
249
+ update_parser.add_argument(
250
+ "--pair", "-p",
251
+ action="append",
252
+ default=None,
253
+ metavar="PPT:XLSX",
254
+ help="A pptx:xlsx pair. Can be repeated for batch processing multiple pairs.",
255
+ )
256
+ update_parser.add_argument(
257
+ "--config", "-c",
258
+ default=None,
259
+ help="Path to config.yaml (default: built-in defaults)",
260
+ )
261
+ update_parser.add_argument("--skip-links", action="store_true", help="Skip Step 1a (re-link OLE)")
262
+ update_parser.add_argument("--skip-deltas", action="store_true", help="Skip Step 1c (delta arrows)")
263
+ update_parser.add_argument("--skip-coloring", action="store_true", help="Skip Step 1d (_ccst coloring)")
264
+ update_parser.add_argument("--skip-charts", action="store_true", help="Skip Step 2 (chart links)")
265
+ update_parser.add_argument("--verbose", "-v", action="store_true", help="Enable debug logging")
266
+
267
+ # --- info subcommand ---
268
+ subparsers.add_parser("info", help="Show project information (coming soon)")
269
+
270
+ # --- init subcommand ---
271
+ subparsers.add_parser("init", help="Write default config.yaml to current directory")
272
+
273
+ args = parser.parse_args()
274
+
275
+ if args.command == "update":
276
+ cmd_update(args)
277
+ elif args.command == "info":
278
+ cmd_info(args)
279
+ elif args.command == "init":
280
+ cmd_init(args)
281
+ else:
282
+ parser.print_help()
283
+ sys.exit(0)
284
+
285
+
286
+ if __name__ == "__main__":
287
+ main()