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 +130 -0
- decx-0.1.0/README.md +110 -0
- decx-0.1.0/pyproject.toml +38 -0
- decx-0.1.0/src/decx/__init__.py +2 -0
- decx-0.1.0/src/decx/chart_updater.py +44 -0
- decx-0.1.0/src/decx/cli.py +287 -0
- decx-0.1.0/src/decx/color_coder.py +106 -0
- decx-0.1.0/src/decx/config.py +34 -0
- decx-0.1.0/src/decx/delta_updater.py +206 -0
- decx-0.1.0/src/decx/file_picker.py +43 -0
- decx-0.1.0/src/decx/formatting.py +267 -0
- decx-0.1.0/src/decx/linker.py +67 -0
- decx-0.1.0/src/decx/session.py +180 -0
- decx-0.1.0/src/decx/shape_finder.py +268 -0
- decx-0.1.0/src/decx/table_updater.py +273 -0
- decx-0.1.0/src/decx/utils.py +75 -0
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,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()
|