vsdxkit 0.7.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.
- vsdxkit-0.7.0/LICENSE +30 -0
- vsdxkit-0.7.0/PKG-INFO +240 -0
- vsdxkit-0.7.0/README.md +209 -0
- vsdxkit-0.7.0/pyproject.toml +126 -0
- vsdxkit-0.7.0/setup.cfg +4 -0
- vsdxkit-0.7.0/tests/test_add_page_at_positions.py +55 -0
- vsdxkit-0.7.0/tests/test_connect_constructor.py +73 -0
- vsdxkit-0.7.0/tests/test_connector_atomicity.py +91 -0
- vsdxkit-0.7.0/tests/test_connector_engine.py +218 -0
- vsdxkit-0.7.0/tests/test_containers.py +95 -0
- vsdxkit-0.7.0/tests/test_create_shape.py +49 -0
- vsdxkit-0.7.0/tests/test_data_property_reads.py +177 -0
- vsdxkit-0.7.0/tests/test_geometry.py +555 -0
- vsdxkit-0.7.0/tests/test_imports.py +19 -0
- vsdxkit-0.7.0/tests/test_interop_libreoffice.py +58 -0
- vsdxkit-0.7.0/tests/test_jinja.py +261 -0
- vsdxkit-0.7.0/tests/test_logging.py +72 -0
- vsdxkit-0.7.0/tests/test_malformed_shape_values.py +65 -0
- vsdxkit-0.7.0/tests/test_master.py +300 -0
- vsdxkit-0.7.0/tests/test_media.py +36 -0
- vsdxkit-0.7.0/tests/test_media_reuse.py +104 -0
- vsdxkit-0.7.0/tests/test_namespaces.py +101 -0
- vsdxkit-0.7.0/tests/test_package_limits.py +200 -0
- vsdxkit-0.7.0/tests/test_page.py +755 -0
- vsdxkit-0.7.0/tests/test_page_dimensions.py +52 -0
- vsdxkit-0.7.0/tests/test_page_removal_opc.py +104 -0
- vsdxkit-0.7.0/tests/test_palette.py +28 -0
- vsdxkit-0.7.0/tests/test_reanchor.py +79 -0
- vsdxkit-0.7.0/tests/test_regression_bite.py +131 -0
- vsdxkit-0.7.0/tests/test_release_tooling.py +164 -0
- vsdxkit-0.7.0/tests/test_retarget_route_ordering.py +34 -0
- vsdxkit-0.7.0/tests/test_review_findings_fixes.py +223 -0
- vsdxkit-0.7.0/tests/test_save_destinations.py +217 -0
- vsdxkit-0.7.0/tests/test_shape.py +787 -0
- vsdxkit-0.7.0/tests/test_shape_coordinates.py +104 -0
- vsdxkit-0.7.0/tests/test_shape_removal_cascade.py +186 -0
- vsdxkit-0.7.0/tests/test_sheet_reference_remap.py +145 -0
- vsdxkit-0.7.0/tests/test_visiofile.py +626 -0
- vsdxkit-0.7.0/tests/test_visiofile_not_open.py +27 -0
- vsdxkit-0.7.0/tests/test_visiofilediff.py +77 -0
- vsdxkit-0.7.0/tests/test_vsdxdiff_safety.py +64 -0
- vsdxkit-0.7.0/vsdx/__init__.py +66 -0
- vsdxkit-0.7.0/vsdx/connectors.py +354 -0
- vsdxkit-0.7.0/vsdx/containers.py +211 -0
- vsdxkit-0.7.0/vsdx/formulae.py +89 -0
- vsdxkit-0.7.0/vsdx/geometry.py +343 -0
- vsdxkit-0.7.0/vsdx/logging_support.py +59 -0
- vsdxkit-0.7.0/vsdx/masters.py +197 -0
- vsdxkit-0.7.0/vsdx/media/media.vsdx +0 -0
- vsdxkit-0.7.0/vsdx/media/palette_extended.vsdx +0 -0
- vsdxkit-0.7.0/vsdx/media.py +78 -0
- vsdxkit-0.7.0/vsdx/pages.py +592 -0
- vsdxkit-0.7.0/vsdx/py.typed +0 -0
- vsdxkit-0.7.0/vsdx/shapes.py +1129 -0
- vsdxkit-0.7.0/vsdx/templating.py +208 -0
- vsdxkit-0.7.0/vsdx/vsdxdiff.py +166 -0
- vsdxkit-0.7.0/vsdx/vsdxfile.py +1472 -0
- vsdxkit-0.7.0/vsdx/xmlio.py +206 -0
- vsdxkit-0.7.0/vsdxkit.egg-info/PKG-INFO +240 -0
- vsdxkit-0.7.0/vsdxkit.egg-info/SOURCES.txt +61 -0
- vsdxkit-0.7.0/vsdxkit.egg-info/dependency_links.txt +1 -0
- vsdxkit-0.7.0/vsdxkit.egg-info/requires.txt +5 -0
- vsdxkit-0.7.0/vsdxkit.egg-info/top_level.txt +1 -0
vsdxkit-0.7.0/LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020, Dave Howard
|
|
4
|
+
Copyright (c) 2026, Firm Footing
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
|
8
|
+
modification, are permitted provided that the following conditions are met:
|
|
9
|
+
|
|
10
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
11
|
+
list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
and/or other materials provided with the distribution.
|
|
16
|
+
|
|
17
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
18
|
+
contributors may be used to endorse or promote products derived from
|
|
19
|
+
this software without specific prior written permission.
|
|
20
|
+
|
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
22
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
23
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
24
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
25
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
26
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
27
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
28
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
29
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
vsdxkit-0.7.0/PKG-INFO
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vsdxkit
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: vsdxkit - create, edit and analyse Microsoft Visio .vsdx files, headlessly
|
|
5
|
+
Author: Shaun Eccles
|
|
6
|
+
Author-email: Dave Howard <dave@codypy.com>
|
|
7
|
+
License-Expression: BSD-3-Clause
|
|
8
|
+
Project-URL: Homepage, https://github.com/firmfooting/vsdxkit
|
|
9
|
+
Project-URL: Documentation, https://firmfooting.github.io/vsdxkit/
|
|
10
|
+
Project-URL: Issues, https://github.com/firmfooting/vsdxkit/issues
|
|
11
|
+
Project-URL: Original project, https://github.com/dave-howard/vsdx
|
|
12
|
+
Keywords: vsdx,visio,diagram,flowchart
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: Jinja2>=3.1.6
|
|
28
|
+
Requires-Dist: deprecation>=2.1.0
|
|
29
|
+
Requires-Dist: typing-extensions>=4.4.0; python_version < "3.12"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# vsdxkit
|
|
33
|
+
|
|
34
|
+
[](https://github.com/firmfooting/vsdxkit/actions/workflows/ci.yml)
|
|
35
|
+
[](https://www.python.org/)
|
|
36
|
+
[](LICENSE)
|
|
37
|
+
[](https://firmfooting.github.io/vsdxkit/)
|
|
38
|
+
|
|
39
|
+
Create, edit and analyse Microsoft Visio `.vsdx` files with Python. Visio is not required at runtime.
|
|
40
|
+
|
|
41
|
+
> **0.x API notice.** 0.7 is the last release of the inherited API. 1.0 renames `VisioFile` to `Document` and `Container` to `SwimlaneDiagram`, splits `Connect` into an internal `ConnectionRecord` and a public `Connector`, and drops the context manager: opening closes the archive before it returns, and `save()` is the only write. The [1.0 design](https://github.com/firmfooting/vsdxkit/blob/main/.hermes/plans/2026-09-12_simplification-usability-refactor.md) lists every change, and a migration guide lands at `docs/migration-1.0.rst` with the first breaking release. Pin `vsdxkit<1` to stay on the 0.x names.
|
|
42
|
+
|
|
43
|
+
The distribution is named **`vsdxkit`**. The import remains **`vsdx`**, so existing code keeps working.
|
|
44
|
+
|
|
45
|
+
vsdxkit adds shape creation, Visio-faithful connectors, connector re-anchoring, cross-functional flowchart swimlanes, stricter package handling, current Python tooling and typed public APIs. It began as a fork of [`dave-howard/vsdx`](https://github.com/dave-howard/vsdx) and is now developed as its own project; see [Provenance and licence](#provenance-and-licence).
|
|
46
|
+
|
|
47
|
+
## What it does
|
|
48
|
+
|
|
49
|
+
- Opens, queries and edits existing `.vsdx` files without Microsoft Visio.
|
|
50
|
+
- Finds shapes by ID, text, regular expression or Shape Data.
|
|
51
|
+
- Creates common flowchart shapes from a bundled palette.
|
|
52
|
+
- Creates dynamic or connection-point glue with straight, right-angle or curved routing.
|
|
53
|
+
- Re-anchors either end of an existing connector.
|
|
54
|
+
- Reads and extends Visio cross-functional flowchart swimlanes.
|
|
55
|
+
- Copies shapes and pages while rewriting package-local IDs and importing masters.
|
|
56
|
+
- Renders data into Visio templates with Jinja.
|
|
57
|
+
- Saves to a new file or safely replaces the source file in place.
|
|
58
|
+
|
|
59
|
+
The implementation edits the XML parts inside the Open Packaging Convention archive. It does not drive the Visio user interface. Generated connector and swimlane files are nevertheless checked against Microsoft Visio through COM as a release gate.
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
`vsdxkit` is not yet published on PyPI. Install it from GitHub:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python -m pip install "vsdxkit @ git+https://github.com/firmfooting/vsdxkit.git"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For development:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
git clone https://github.com/firmfooting/vsdxkit.git
|
|
73
|
+
cd vsdxkit
|
|
74
|
+
uv sync --locked
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Python 3.10–3.14 is supported on Linux and Windows. Add `--group docs` to that
|
|
78
|
+
sync if you also want to build the documentation; Sphinx needs Python 3.12 or
|
|
79
|
+
later.
|
|
80
|
+
|
|
81
|
+
## Open, edit and save
|
|
82
|
+
|
|
83
|
+
Use the context manager to close the package cleanly. Saving is explicit.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from vsdx import VisioFile
|
|
87
|
+
|
|
88
|
+
with VisioFile("diagram.vsdx") as vis:
|
|
89
|
+
page = vis.pages[0]
|
|
90
|
+
shape = page.find_shape_by_text("Shape to remove")
|
|
91
|
+
|
|
92
|
+
if shape is not None:
|
|
93
|
+
shape.text = "Renamed shape"
|
|
94
|
+
|
|
95
|
+
vis.save_vsdx("edited.vsdx")
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Call `save_vsdx()` without a filename to replace the source file in place:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
with VisioFile("diagram.vsdx") as vis:
|
|
102
|
+
vis.pages[0].name = "Current state"
|
|
103
|
+
vis.save_vsdx()
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Create shapes and connectors
|
|
107
|
+
|
|
108
|
+
Shape coordinates are in Visio page units, normally inches. `x` and `y` identify the shape centre.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from vsdx import VisioFile
|
|
112
|
+
|
|
113
|
+
with VisioFile("diagram.vsdx") as vis:
|
|
114
|
+
page = vis.pages[0]
|
|
115
|
+
|
|
116
|
+
start = vis.create_shape(
|
|
117
|
+
page, "PALETTE_START_END", 2.0, 6.0, text="Start"
|
|
118
|
+
)
|
|
119
|
+
work = vis.create_shape(
|
|
120
|
+
page, "PALETTE_PROCESS", 6.0, 6.0, text="Do the thing"
|
|
121
|
+
)
|
|
122
|
+
decision = vis.create_shape(
|
|
123
|
+
page, "PALETTE_DECISION", 10.0, 6.0, text="OK?"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
page.connect_shapes(start, work)
|
|
127
|
+
page.connect_shapes(work, decision, route="rightangle")
|
|
128
|
+
vis.save_vsdx("flow.vsdx")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Bundled palette names are:
|
|
132
|
+
|
|
133
|
+
- `PALETTE_PROCESS`
|
|
134
|
+
- `PALETTE_DECISION`
|
|
135
|
+
- `PALETTE_START_END`
|
|
136
|
+
- `PALETTE_PARALLELOGRAM`
|
|
137
|
+
- `PALETTE_DATABASE`
|
|
138
|
+
|
|
139
|
+
Connector `route` combines glue and routing behaviour:
|
|
140
|
+
|
|
141
|
+
| Value | Meaning |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `dynamic` | Dynamic shape glue. This is the default. |
|
|
144
|
+
| `point` | Glue to zero-based connection points selected with `from_cp` and `to_cp`. |
|
|
145
|
+
| `straight` | Dynamic glue with straight routing. |
|
|
146
|
+
| `rightangle` | Dynamic glue with right-angle routing. |
|
|
147
|
+
| `curved` | Dynamic glue with curved routing. |
|
|
148
|
+
| `point|curved` | Connection-point glue with curved routing. |
|
|
149
|
+
|
|
150
|
+
## Re-anchor a connector
|
|
151
|
+
|
|
152
|
+
Pass only the end that should move. A `None` endpoint keeps the current shape.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
connector = page.find_shape_by_id("9")
|
|
156
|
+
new_target = page.find_shape_by_text("Store")
|
|
157
|
+
|
|
158
|
+
if connector is not None and new_target is not None:
|
|
159
|
+
page.reanchor_connector(connector, to_shape=new_target)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Deleting a shape through `page.delete_shape(shape)` also removes incident connectors and their `Connect` records.
|
|
163
|
+
|
|
164
|
+
## Work with swimlanes
|
|
165
|
+
|
|
166
|
+
Swimlane operations require an existing Visio cross-functional flowchart (CFF) page. `add_swimlane()` clones the current top lane and updates the CFF container geometry.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
with VisioFile("cross-functional-flow.vsdx") as vis:
|
|
170
|
+
page = vis.pages[0]
|
|
171
|
+
container = page.get_container()
|
|
172
|
+
|
|
173
|
+
if container is None:
|
|
174
|
+
raise ValueError("The page is not a Visio CFF diagram")
|
|
175
|
+
|
|
176
|
+
review_lane = page.add_swimlane("Review")
|
|
177
|
+
check = vis.create_shape(
|
|
178
|
+
page, "PALETTE_PROCESS", 6.0, 2.0, text="Check"
|
|
179
|
+
)
|
|
180
|
+
page.add_shape_to_lane(check, review_lane)
|
|
181
|
+
|
|
182
|
+
assert container.lane_of(check) is not None
|
|
183
|
+
vis.save_vsdx("with-review-lane.vsdx")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Visio CFF membership is geometric. Shapes are associated with the lane whose vertical band contains their centre; there is no separate membership field to write.
|
|
187
|
+
|
|
188
|
+
## Render a Jinja template
|
|
189
|
+
|
|
190
|
+
Jinja expressions can be stored in shape text and rendered into a new file:
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
with VisioFile("template.vsdx") as vis:
|
|
194
|
+
vis.jinja_render_vsdx(
|
|
195
|
+
context={"project": "Ward refurbishment", "owner": "Facilities"}
|
|
196
|
+
)
|
|
197
|
+
vis.save_vsdx("rendered.vsdx")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The package also supports its existing group-shape loop and `showif` conventions. See `docs/templating.rst` and the `tests/test_jinja*.py` cases for the exact template structure.
|
|
201
|
+
|
|
202
|
+
## Limits
|
|
203
|
+
|
|
204
|
+
- The library starts from an existing `.vsdx`; it does not create a complete Visio document package from nothing.
|
|
205
|
+
- `.vsdm` files can be read and saved, but only back to a `.vsdm` destination. The package kind is decided by the content type of `visio/document.xml`, not by the filename, so `save_vsdx()` refuses a `.vsdx` destination for a macro-enabled package and a `.vsdm` destination for one that is not — either would produce a file whose extension and `[Content_Types].xml` disagree, which Visio reports as corrupt. Stripping macros to convert a `.vsdm` into a `.vsdx` is not supported. A destination with no extension, or with an unrelated one, gets the matching Visio extension appended.
|
|
206
|
+
- Swimlane creation works on existing Visio CFF diagrams. It does not convert an ordinary page into a CFF diagram.
|
|
207
|
+
- Visio may recalculate layout when a generated file opens. The library writes the glue and route cells but does not reproduce Visio's entire layout engine.
|
|
208
|
+
- Loading enforces package expansion limits before any archive member is read: at most 512 members, 64 MiB per member, 256 MiB total uncompressed, and a 100:1 compression ratio, plus rejection of duplicate and path-unsafe member names. A hostile or accidental archive is refused with `vsdx.PackageLimitError` instead of exhausting process memory. The defaults suit documents from unknown sources; trusted callers can relax the caps with `VisioFile(filename, limits=PackageLimits(...))` or `limits_path="vsdx.limits.json"` (same keys, JSON object).
|
|
209
|
+
|
|
210
|
+
## Development and verification
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
uv run --no-sync python -m pytest tests -q
|
|
214
|
+
uv run --no-sync ruff check vsdx tests/test_imports.py tests/test_shape_coordinates.py
|
|
215
|
+
uv run --no-sync ruff format --check vsdx tests/test_imports.py tests/test_shape_coordinates.py
|
|
216
|
+
uv run --no-sync pyrefly check vsdx --min-severity warn --output-format min-text
|
|
217
|
+
uv run --no-sync sphinx-build -W --keep-going -b html docs docs/_build/html
|
|
218
|
+
uv run --no-sync python -m build
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The package is held at pyrefly's `strict` preset. CI tests Python 3.10–3.14 on Ubuntu and Windows. Connector and swimlane changes also run through `tools/visio_check.ps1`, which opens generated files in an invisible Microsoft Visio instance and fails on package repair or automation errors.
|
|
222
|
+
|
|
223
|
+
## Documentation
|
|
224
|
+
|
|
225
|
+
The Sphinx source is in [`docs/`](docs/). Build it locally with:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
uv sync --locked --group docs
|
|
229
|
+
uv run --no-sync python -m sphinx -W --keep-going -b html docs docs/_build/html
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Sphinx is pinned in the `docs` dependency group, which requires Python 3.12 or
|
|
233
|
+
later. The library itself still supports 3.10, so run the docs build on a 3.12+
|
|
234
|
+
interpreter.
|
|
235
|
+
|
|
236
|
+
## Provenance and licence
|
|
237
|
+
|
|
238
|
+
vsdxkit descends from [`dave-howard/vsdx`](https://github.com/dave-howard/vsdx), originally written by Dave Howard and released under the BSD 3-Clause licence. That work is the foundation this library is built on, and its copyright notice is retained in [`LICENSE`](LICENSE) alongside our own.
|
|
239
|
+
|
|
240
|
+
vsdxkit is now developed independently: it is not a downstream of that project and does not track it. The `vsdx` import namespace is kept so existing code continues to work, and the licence remains BSD 3-Clause.
|
vsdxkit-0.7.0/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# vsdxkit
|
|
2
|
+
|
|
3
|
+
[](https://github.com/firmfooting/vsdxkit/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.python.org/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://firmfooting.github.io/vsdxkit/)
|
|
7
|
+
|
|
8
|
+
Create, edit and analyse Microsoft Visio `.vsdx` files with Python. Visio is not required at runtime.
|
|
9
|
+
|
|
10
|
+
> **0.x API notice.** 0.7 is the last release of the inherited API. 1.0 renames `VisioFile` to `Document` and `Container` to `SwimlaneDiagram`, splits `Connect` into an internal `ConnectionRecord` and a public `Connector`, and drops the context manager: opening closes the archive before it returns, and `save()` is the only write. The [1.0 design](https://github.com/firmfooting/vsdxkit/blob/main/.hermes/plans/2026-09-12_simplification-usability-refactor.md) lists every change, and a migration guide lands at `docs/migration-1.0.rst` with the first breaking release. Pin `vsdxkit<1` to stay on the 0.x names.
|
|
11
|
+
|
|
12
|
+
The distribution is named **`vsdxkit`**. The import remains **`vsdx`**, so existing code keeps working.
|
|
13
|
+
|
|
14
|
+
vsdxkit adds shape creation, Visio-faithful connectors, connector re-anchoring, cross-functional flowchart swimlanes, stricter package handling, current Python tooling and typed public APIs. It began as a fork of [`dave-howard/vsdx`](https://github.com/dave-howard/vsdx) and is now developed as its own project; see [Provenance and licence](#provenance-and-licence).
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
- Opens, queries and edits existing `.vsdx` files without Microsoft Visio.
|
|
19
|
+
- Finds shapes by ID, text, regular expression or Shape Data.
|
|
20
|
+
- Creates common flowchart shapes from a bundled palette.
|
|
21
|
+
- Creates dynamic or connection-point glue with straight, right-angle or curved routing.
|
|
22
|
+
- Re-anchors either end of an existing connector.
|
|
23
|
+
- Reads and extends Visio cross-functional flowchart swimlanes.
|
|
24
|
+
- Copies shapes and pages while rewriting package-local IDs and importing masters.
|
|
25
|
+
- Renders data into Visio templates with Jinja.
|
|
26
|
+
- Saves to a new file or safely replaces the source file in place.
|
|
27
|
+
|
|
28
|
+
The implementation edits the XML parts inside the Open Packaging Convention archive. It does not drive the Visio user interface. Generated connector and swimlane files are nevertheless checked against Microsoft Visio through COM as a release gate.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
`vsdxkit` is not yet published on PyPI. Install it from GitHub:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
python -m pip install "vsdxkit @ git+https://github.com/firmfooting/vsdxkit.git"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For development:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/firmfooting/vsdxkit.git
|
|
42
|
+
cd vsdxkit
|
|
43
|
+
uv sync --locked
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Python 3.10–3.14 is supported on Linux and Windows. Add `--group docs` to that
|
|
47
|
+
sync if you also want to build the documentation; Sphinx needs Python 3.12 or
|
|
48
|
+
later.
|
|
49
|
+
|
|
50
|
+
## Open, edit and save
|
|
51
|
+
|
|
52
|
+
Use the context manager to close the package cleanly. Saving is explicit.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from vsdx import VisioFile
|
|
56
|
+
|
|
57
|
+
with VisioFile("diagram.vsdx") as vis:
|
|
58
|
+
page = vis.pages[0]
|
|
59
|
+
shape = page.find_shape_by_text("Shape to remove")
|
|
60
|
+
|
|
61
|
+
if shape is not None:
|
|
62
|
+
shape.text = "Renamed shape"
|
|
63
|
+
|
|
64
|
+
vis.save_vsdx("edited.vsdx")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Call `save_vsdx()` without a filename to replace the source file in place:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
with VisioFile("diagram.vsdx") as vis:
|
|
71
|
+
vis.pages[0].name = "Current state"
|
|
72
|
+
vis.save_vsdx()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Create shapes and connectors
|
|
76
|
+
|
|
77
|
+
Shape coordinates are in Visio page units, normally inches. `x` and `y` identify the shape centre.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from vsdx import VisioFile
|
|
81
|
+
|
|
82
|
+
with VisioFile("diagram.vsdx") as vis:
|
|
83
|
+
page = vis.pages[0]
|
|
84
|
+
|
|
85
|
+
start = vis.create_shape(
|
|
86
|
+
page, "PALETTE_START_END", 2.0, 6.0, text="Start"
|
|
87
|
+
)
|
|
88
|
+
work = vis.create_shape(
|
|
89
|
+
page, "PALETTE_PROCESS", 6.0, 6.0, text="Do the thing"
|
|
90
|
+
)
|
|
91
|
+
decision = vis.create_shape(
|
|
92
|
+
page, "PALETTE_DECISION", 10.0, 6.0, text="OK?"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
page.connect_shapes(start, work)
|
|
96
|
+
page.connect_shapes(work, decision, route="rightangle")
|
|
97
|
+
vis.save_vsdx("flow.vsdx")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Bundled palette names are:
|
|
101
|
+
|
|
102
|
+
- `PALETTE_PROCESS`
|
|
103
|
+
- `PALETTE_DECISION`
|
|
104
|
+
- `PALETTE_START_END`
|
|
105
|
+
- `PALETTE_PARALLELOGRAM`
|
|
106
|
+
- `PALETTE_DATABASE`
|
|
107
|
+
|
|
108
|
+
Connector `route` combines glue and routing behaviour:
|
|
109
|
+
|
|
110
|
+
| Value | Meaning |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `dynamic` | Dynamic shape glue. This is the default. |
|
|
113
|
+
| `point` | Glue to zero-based connection points selected with `from_cp` and `to_cp`. |
|
|
114
|
+
| `straight` | Dynamic glue with straight routing. |
|
|
115
|
+
| `rightangle` | Dynamic glue with right-angle routing. |
|
|
116
|
+
| `curved` | Dynamic glue with curved routing. |
|
|
117
|
+
| `point|curved` | Connection-point glue with curved routing. |
|
|
118
|
+
|
|
119
|
+
## Re-anchor a connector
|
|
120
|
+
|
|
121
|
+
Pass only the end that should move. A `None` endpoint keeps the current shape.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
connector = page.find_shape_by_id("9")
|
|
125
|
+
new_target = page.find_shape_by_text("Store")
|
|
126
|
+
|
|
127
|
+
if connector is not None and new_target is not None:
|
|
128
|
+
page.reanchor_connector(connector, to_shape=new_target)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Deleting a shape through `page.delete_shape(shape)` also removes incident connectors and their `Connect` records.
|
|
132
|
+
|
|
133
|
+
## Work with swimlanes
|
|
134
|
+
|
|
135
|
+
Swimlane operations require an existing Visio cross-functional flowchart (CFF) page. `add_swimlane()` clones the current top lane and updates the CFF container geometry.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
with VisioFile("cross-functional-flow.vsdx") as vis:
|
|
139
|
+
page = vis.pages[0]
|
|
140
|
+
container = page.get_container()
|
|
141
|
+
|
|
142
|
+
if container is None:
|
|
143
|
+
raise ValueError("The page is not a Visio CFF diagram")
|
|
144
|
+
|
|
145
|
+
review_lane = page.add_swimlane("Review")
|
|
146
|
+
check = vis.create_shape(
|
|
147
|
+
page, "PALETTE_PROCESS", 6.0, 2.0, text="Check"
|
|
148
|
+
)
|
|
149
|
+
page.add_shape_to_lane(check, review_lane)
|
|
150
|
+
|
|
151
|
+
assert container.lane_of(check) is not None
|
|
152
|
+
vis.save_vsdx("with-review-lane.vsdx")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Visio CFF membership is geometric. Shapes are associated with the lane whose vertical band contains their centre; there is no separate membership field to write.
|
|
156
|
+
|
|
157
|
+
## Render a Jinja template
|
|
158
|
+
|
|
159
|
+
Jinja expressions can be stored in shape text and rendered into a new file:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
with VisioFile("template.vsdx") as vis:
|
|
163
|
+
vis.jinja_render_vsdx(
|
|
164
|
+
context={"project": "Ward refurbishment", "owner": "Facilities"}
|
|
165
|
+
)
|
|
166
|
+
vis.save_vsdx("rendered.vsdx")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The package also supports its existing group-shape loop and `showif` conventions. See `docs/templating.rst` and the `tests/test_jinja*.py` cases for the exact template structure.
|
|
170
|
+
|
|
171
|
+
## Limits
|
|
172
|
+
|
|
173
|
+
- The library starts from an existing `.vsdx`; it does not create a complete Visio document package from nothing.
|
|
174
|
+
- `.vsdm` files can be read and saved, but only back to a `.vsdm` destination. The package kind is decided by the content type of `visio/document.xml`, not by the filename, so `save_vsdx()` refuses a `.vsdx` destination for a macro-enabled package and a `.vsdm` destination for one that is not — either would produce a file whose extension and `[Content_Types].xml` disagree, which Visio reports as corrupt. Stripping macros to convert a `.vsdm` into a `.vsdx` is not supported. A destination with no extension, or with an unrelated one, gets the matching Visio extension appended.
|
|
175
|
+
- Swimlane creation works on existing Visio CFF diagrams. It does not convert an ordinary page into a CFF diagram.
|
|
176
|
+
- Visio may recalculate layout when a generated file opens. The library writes the glue and route cells but does not reproduce Visio's entire layout engine.
|
|
177
|
+
- Loading enforces package expansion limits before any archive member is read: at most 512 members, 64 MiB per member, 256 MiB total uncompressed, and a 100:1 compression ratio, plus rejection of duplicate and path-unsafe member names. A hostile or accidental archive is refused with `vsdx.PackageLimitError` instead of exhausting process memory. The defaults suit documents from unknown sources; trusted callers can relax the caps with `VisioFile(filename, limits=PackageLimits(...))` or `limits_path="vsdx.limits.json"` (same keys, JSON object).
|
|
178
|
+
|
|
179
|
+
## Development and verification
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
uv run --no-sync python -m pytest tests -q
|
|
183
|
+
uv run --no-sync ruff check vsdx tests/test_imports.py tests/test_shape_coordinates.py
|
|
184
|
+
uv run --no-sync ruff format --check vsdx tests/test_imports.py tests/test_shape_coordinates.py
|
|
185
|
+
uv run --no-sync pyrefly check vsdx --min-severity warn --output-format min-text
|
|
186
|
+
uv run --no-sync sphinx-build -W --keep-going -b html docs docs/_build/html
|
|
187
|
+
uv run --no-sync python -m build
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The package is held at pyrefly's `strict` preset. CI tests Python 3.10–3.14 on Ubuntu and Windows. Connector and swimlane changes also run through `tools/visio_check.ps1`, which opens generated files in an invisible Microsoft Visio instance and fails on package repair or automation errors.
|
|
191
|
+
|
|
192
|
+
## Documentation
|
|
193
|
+
|
|
194
|
+
The Sphinx source is in [`docs/`](docs/). Build it locally with:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
uv sync --locked --group docs
|
|
198
|
+
uv run --no-sync python -m sphinx -W --keep-going -b html docs docs/_build/html
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Sphinx is pinned in the `docs` dependency group, which requires Python 3.12 or
|
|
202
|
+
later. The library itself still supports 3.10, so run the docs build on a 3.12+
|
|
203
|
+
interpreter.
|
|
204
|
+
|
|
205
|
+
## Provenance and licence
|
|
206
|
+
|
|
207
|
+
vsdxkit descends from [`dave-howard/vsdx`](https://github.com/dave-howard/vsdx), originally written by Dave Howard and released under the BSD 3-Clause licence. That work is the foundation this library is built on, and its copyright notice is retained in [`LICENSE`](LICENSE) alongside our own.
|
|
208
|
+
|
|
209
|
+
vsdxkit is now developed independently: it is not a downstream of that project and does not track it. The `vsdx` import namespace is kept so existing code continues to work, and the licence remains BSD 3-Clause.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vsdxkit"
|
|
7
|
+
description = "vsdxkit - create, edit and analyse Microsoft Visio .vsdx files, headlessly"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "BSD-3-Clause"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Dave Howard", email = "dave@codypy.com" },
|
|
13
|
+
{ name = "Shaun Eccles" },
|
|
14
|
+
]
|
|
15
|
+
requires-python = ">=3.10"
|
|
16
|
+
dependencies = [
|
|
17
|
+
"Jinja2>=3.1.6",
|
|
18
|
+
"deprecation>=2.1.0",
|
|
19
|
+
"typing-extensions>=4.4.0; python_version < '3.12'",
|
|
20
|
+
]
|
|
21
|
+
dynamic = ["version"]
|
|
22
|
+
keywords = ["vsdx", "visio", "diagram", "flowchart"]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Programming Language :: Python :: 3.14",
|
|
33
|
+
"Topic :: Multimedia :: Graphics",
|
|
34
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/firmfooting/vsdxkit"
|
|
39
|
+
Documentation = "https://firmfooting.github.io/vsdxkit/"
|
|
40
|
+
Issues = "https://github.com/firmfooting/vsdxkit/issues"
|
|
41
|
+
"Original project" = "https://github.com/dave-howard/vsdx"
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
docs = [
|
|
45
|
+
"sphinx==9.1.0",
|
|
46
|
+
"sphinx-rtd-theme==3.1.0",
|
|
47
|
+
]
|
|
48
|
+
test = [
|
|
49
|
+
"pytest>=8.4.0",
|
|
50
|
+
]
|
|
51
|
+
lint = [
|
|
52
|
+
"ruff==0.16.7",
|
|
53
|
+
"pyrefly==1.3.0",
|
|
54
|
+
"zizmor==1.30.1",
|
|
55
|
+
"mypy>=2.3.1",
|
|
56
|
+
]
|
|
57
|
+
build = [
|
|
58
|
+
"build==1.6.1",
|
|
59
|
+
]
|
|
60
|
+
coverage = [
|
|
61
|
+
"pytest-cov>=6.2.1",
|
|
62
|
+
]
|
|
63
|
+
dev = [
|
|
64
|
+
{ include-group = "test" },
|
|
65
|
+
{ include-group = "lint" },
|
|
66
|
+
{ include-group = "build" },
|
|
67
|
+
{ include-group = "coverage" },
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[tool.uv.dependency-groups]
|
|
71
|
+
# Sphinx drops old interpreters faster than this library does: 9.x requires
|
|
72
|
+
# 3.12 while vsdxkit itself still supports 3.10. Giving the docs group its own
|
|
73
|
+
# floor keeps the universal resolution satisfiable across the whole
|
|
74
|
+
# requires-python range, instead of dragging the library up to the
|
|
75
|
+
# documentation toolchain's floor. Raise this together with the Python version
|
|
76
|
+
# used by the CI docs build and by the Docs workflow that publishes to Pages.
|
|
77
|
+
docs = { requires-python = ">=3.12" }
|
|
78
|
+
|
|
79
|
+
[tool.setuptools]
|
|
80
|
+
packages = ["vsdx"]
|
|
81
|
+
include-package-data = false
|
|
82
|
+
|
|
83
|
+
[tool.setuptools.package-data]
|
|
84
|
+
vsdx = ["media/*.vsdx", "py.typed"]
|
|
85
|
+
|
|
86
|
+
[tool.setuptools.dynamic]
|
|
87
|
+
version = { attr = "vsdx.__version__" }
|
|
88
|
+
|
|
89
|
+
[tool.ruff]
|
|
90
|
+
line-length = 127
|
|
91
|
+
target-version = "py310"
|
|
92
|
+
|
|
93
|
+
[tool.ruff.lint]
|
|
94
|
+
select = [
|
|
95
|
+
"E", # pycodestyle errors
|
|
96
|
+
"W", # pycodestyle warnings
|
|
97
|
+
"F", # pyflakes
|
|
98
|
+
"I", # isort
|
|
99
|
+
"B", # flake8-bugbear
|
|
100
|
+
"UP", # pyupgrade
|
|
101
|
+
"SIM", # flake8-simplify
|
|
102
|
+
"RUF", # ruff-specific
|
|
103
|
+
]
|
|
104
|
+
# E501 (line length) handled by formatter; docstring rules not adopted yet
|
|
105
|
+
ignore = ["E501"]
|
|
106
|
+
|
|
107
|
+
[tool.ruff.lint.per-file-ignores]
|
|
108
|
+
# tests use broad fixtures and long parametrise lines
|
|
109
|
+
"tests/*" = ["SIM117"]
|
|
110
|
+
|
|
111
|
+
[tool.pyrefly]
|
|
112
|
+
project-includes = ["vsdx"]
|
|
113
|
+
# strict preset: the package type-checks clean at this level, so keep it there
|
|
114
|
+
preset = "strict"
|
|
115
|
+
|
|
116
|
+
[tool.coverage.report]
|
|
117
|
+
# fail_under compares the rounded total, so two decimal places: at the default
|
|
118
|
+
# precision of 0, a threshold of 90 would really mean 89.5.
|
|
119
|
+
precision = 2
|
|
120
|
+
# Ratchet: this number only goes up. It is the coverage the whole suite already
|
|
121
|
+
# has, so a full run below it means a change arrived untested. Raise it when
|
|
122
|
+
# coverage improves; never edit it down to make a red build green. Measured
|
|
123
|
+
# 2026-09-13 on CPython 3.12: 2884 statements, 283 missed (90.19%), floored to
|
|
124
|
+
# a whole percent. It applies to every local `--cov` run as well, so a
|
|
125
|
+
# single-file run trips it; pass `--cov-fail-under=0` there.
|
|
126
|
+
fail_under = 90
|
vsdxkit-0.7.0/setup.cfg
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Relative page positions must not silently append without a reference page."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import os
|
|
5
|
+
import xml.etree.ElementTree as ET
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from vsdx import PagePosition, VisioFile
|
|
10
|
+
|
|
11
|
+
basedir = os.path.dirname(os.path.realpath(__file__))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _pages_rels_root(visio_file):
|
|
15
|
+
"""The Relationship element root of the loaded package's pages.xml.rels."""
|
|
16
|
+
for key, content in visio_file.zip_file_contents.items():
|
|
17
|
+
if key.endswith("visio/pages/_rels/pages.xml.rels"):
|
|
18
|
+
return ET.parse(io.BytesIO(content.getvalue())).getroot()
|
|
19
|
+
raise AssertionError("pages.xml.rels not found in package")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.parametrize("position", [PagePosition.BEFORE, PagePosition.AFTER])
|
|
23
|
+
def test_add_page_at_rejects_relative_position_without_reference_page(vsdx_copy, tmp_path, position):
|
|
24
|
+
path = os.path.join(basedir, "test1.vsdx")
|
|
25
|
+
copy_path = vsdx_copy("test1.vsdx")
|
|
26
|
+
output = os.path.join(str(tmp_path), "out.vsdx")
|
|
27
|
+
with VisioFile(path) as reference:
|
|
28
|
+
names_before = [p.name for p in reference.pages]
|
|
29
|
+
reference_rels = [rel for rel in _pages_rels_root(reference) if "page" in rel.attrib.get("Target", "")]
|
|
30
|
+
with VisioFile(copy_path) as vis:
|
|
31
|
+
with pytest.raises(ValueError, match="requires a reference page"):
|
|
32
|
+
vis.add_page_at(position, "new")
|
|
33
|
+
assert [p.name for p in vis.pages] == names_before # nothing appended
|
|
34
|
+
vis.save_vsdx(output) # a rejected call must leave nothing to persist
|
|
35
|
+
|
|
36
|
+
with VisioFile(output) as reloaded:
|
|
37
|
+
assert [p.name for p in reloaded.pages] == names_before
|
|
38
|
+
page_rels = [rel for rel in _pages_rels_root(reloaded) if "page" in rel.attrib.get("Target", "")]
|
|
39
|
+
assert len(page_rels) == len(reference_rels) # no dangling relationship
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_add_page_at_integer_and_relative_positions_place_correctly(vsdx_copy):
|
|
43
|
+
path = vsdx_copy("test1.vsdx")
|
|
44
|
+
with VisioFile(path) as vis:
|
|
45
|
+
first = vis.add_page_at(PagePosition.FIRST, "first-added")
|
|
46
|
+
assert vis.pages.index(first) == 0
|
|
47
|
+
|
|
48
|
+
at_one = vis.add_page_at(1, "at-one")
|
|
49
|
+
assert vis.pages.index(at_one) == 1
|
|
50
|
+
|
|
51
|
+
last = vis.add_page_at(PagePosition.LAST, "last-added")
|
|
52
|
+
assert vis.pages.index(last) == len(vis.pages) - 1
|
|
53
|
+
|
|
54
|
+
appended = vis.add_page("appended")
|
|
55
|
+
assert vis.pages.index(appended) == len(vis.pages) - 1
|