pymppwriter 0.3.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kevin McAleer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,259 @@
1
+ Metadata-Version: 2.4
2
+ Name: pymppwriter
3
+ Version: 0.3.0
4
+ Summary: Write Microsoft Project .mpp files from pure Python (template-based MPP14 writer)
5
+ Author: Kevin McAleer
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kevinmcaleer/pymppwriter
8
+ Project-URL: Source, https://github.com/kevinmcaleer/pymppwriter
9
+ Project-URL: Documentation, https://kevinmcaleer.github.io/pymppwriter/
10
+ Project-URL: Issues, https://github.com/kevinmcaleer/pymppwriter/issues
11
+ Keywords: mpp,microsoft-project,project-management,gantt,scheduling
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Office/Business :: Scheduling
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: olefile>=0.47
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest; extra == "dev"
31
+ Requires-Dist: mpxj; extra == "dev"
32
+ Requires-Dist: jpype1; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # pymppwriter
36
+
37
+ **Write Microsoft Project `.mpp` files from pure Python.** No Java, no .NET, no Microsoft Project
38
+ installation, no commercial library. MIT licensed.
39
+
40
+ `pymppwriter` produces native MPP14 files (the format used by Project 2010 through the current
41
+ Microsoft 365 desktop client). Files it writes open in Project by double-click — which is the
42
+ whole point: an `.mpp` download is associated with Project on every corporate PC, whereas the
43
+ MSPDI `.xml` export has to be opened manually from inside Project.
44
+
45
+ > **Status: alpha.** Verified to open correctly in Project M365: task names, hierarchy, dates,
46
+ > durations (values, display units, estimated flags, milestones and summary rollups),
47
+ > dependencies, resources and assignments, calendars (working weeks, holidays, extra base
48
+ > calendars, per-task calendars), the wider task fields (notes, WBS, constraints, deadlines,
49
+ > progress, priority, type, custom text/number/date/flag fields, manual scheduling) and project
50
+ > properties (document metadata, status date, currency). Resource rates and costs, baselines and
51
+ > timephased data are not yet written. Treat this as a working proof-of-concept, not a product.
52
+
53
+ ## How it works
54
+
55
+ The MPP format is an undocumented OLE2 compound document. Reading it selectively is a solved
56
+ problem (see [MPXJ](https://www.mpxj.org)); writing one from scratch means reproducing ~170 KB
57
+ of view definitions, Gantt bar styles, tables and filters that Project insists on.
58
+
59
+ `pymppwriter` sidesteps that with a **template-and-patch** approach:
60
+
61
+ 1. You save a near-empty project from your own copy of Project once (`templates/template.mpp`).
62
+ 2. The library keeps every stream it doesn't understand byte-for-byte.
63
+ 3. It rewrites only the task, dependency and project-property streams, cloning prototype
64
+ records from the template and patching the fields it controls.
65
+
66
+ The field-offset map is read from the template's own `Props` stream, so the writer adapts to
67
+ whatever Project version wrote the template. Details in [`docs/FORMAT_NOTES.md`](docs/FORMAT_NOTES.md).
68
+
69
+ ## Installation
70
+
71
+ ```bash
72
+ pip install git+https://github.com/kevinmcaleer/pymppwriter
73
+ ```
74
+
75
+ Only runtime dependency: [`olefile`](https://pypi.org/project/olefile/) (used to *read* the
76
+ template; the container writer is our own).
77
+
78
+ ## Make your template (one-time)
79
+
80
+ In Microsoft Project: **File → New → Blank Project**, then
81
+
82
+ | # | Task Name | Action |
83
+ |---|-----------|--------|
84
+ | 1 | Task 1 | leave as-is |
85
+ | 2 | Task 2 | **Indent** it under Task 1 (Task 1 becomes a summary) |
86
+ | 3 | Task 3 | select Tasks 2 and 3, **Link Tasks** (Finish-to-Start) |
87
+
88
+ Don't add resources, baselines or calendar changes. **Save As** `templates/template.mpp`.
89
+
90
+ Why you must make it yourself: the template is a file written by Project, so it must come from
91
+ a copy you're licensed to use, and it embeds your username. It is `.gitignore`d.
92
+
93
+ Save the template from the **same Project version that will open the generated files** — several
94
+ structures (calendar definitions in particular) are stored in version-specific dialects, and a
95
+ template written by your own copy guarantees the output speaks the dialect your Project reads.
96
+ Both current M365 and 2010-era templates are supported.
97
+
98
+ ## Usage
99
+
100
+ ### Command line
101
+
102
+ Describe the plan in JSON ([`examples/example_project.json`](examples/example_project.json)):
103
+
104
+ ```json
105
+ {
106
+ "title": "My plan",
107
+ "start": "2026-09-07T08:00",
108
+ "tasks": [
109
+ {"uid": 1, "name": "Phase 1", "start": "2026-09-07T08:00", "finish": "2026-09-09T17:00", "duration_days": 3, "outline_level": 1},
110
+ {"uid": 2, "name": "Do the thing", "start": "2026-09-07T08:00", "finish": "2026-09-08T17:00", "duration_days": 2, "outline_level": 2, "parent_uid": 1}
111
+ ],
112
+ "links": [ {"pred": 1, "succ": 2, "type": "FS", "lag_days": 0} ]
113
+ }
114
+ ```
115
+
116
+ ```bash
117
+ pymppwriter build examples/example_project.json --template templates/template.mpp --out plan.mpp
118
+ pymppwriter inspect plan.mpp # dump the OLE stream tree
119
+ ```
120
+
121
+ ### Python API
122
+
123
+ ```python
124
+ from datetime import datetime as D
125
+ from pymppwriter import MppWriter, Project, Task, Relation
126
+
127
+ project = Project(
128
+ title="Robot build plan",
129
+ start=D(2026, 10, 5, 8, 0),
130
+ tasks=[
131
+ Task(uid=1, name="Design", start=D(2026,10,5,8), finish=D(2026,10,9,17), duration_days=5),
132
+ Task(uid=2, name="Print parts", start=D(2026,10,12,8), finish=D(2026,10,14,17), duration_days=3),
133
+ Task(uid=3, name="Assemble", start=D(2026,10,15,8), finish=D(2026,10,16,17), duration_days=2,
134
+ outline_level=1, parent_uid=0),
135
+ ],
136
+ relations=[Relation(1, 2), Relation(2, 3, type="FS", lag_days=0)],
137
+ )
138
+
139
+ MppWriter("templates/template.mpp").write(project, "robot-build.mpp")
140
+ ```
141
+
142
+ **Model reference**
143
+
144
+ | Class | Field | Notes |
145
+ |-------|-------|-------|
146
+ | `Project` | `title`, `start`, `tasks`, `relations` | `start` sets the project start date |
147
+ | `Task` | `uid` | unique, > 0, stable across exports |
148
+ | | `name`, `start`, `finish` | `datetime`s |
149
+ | | `duration_days` | working days; 0 = milestone; ignored for summary tasks (rolled up from children in working time) |
150
+ | | `duration_units` | display units: `"m"`, `"h"`, `"d"` (default), `"w"`, `"mo"` |
151
+ | | `estimated` | `True` shows the duration with a trailing `?` |
152
+ | | `outline_level` | 1 = top level, 2 = child, … |
153
+ | | `parent_uid` | 0 = top level, else uid of the summary task |
154
+ | | `guid` | auto-generated; pass your own to keep GUIDs stable between exports |
155
+ | `Relation` | `pred_uid`, `succ_uid` | |
156
+ | | `type` | `"FS"` (default), `"SS"`, `"FF"`, `"SF"` |
157
+ | | `lag_days` | may be negative for lead |
158
+ | `Resource` | `uid` | unique, > 0 |
159
+ | | `name`, `initials`, `email` | strings; only `name` is required |
160
+ | | `max_units` | 1.0 = 100% (default) |
161
+ | | `guid` | auto-generated; pass your own to keep GUIDs stable |
162
+ | `Assignment` | `task_uid`, `resource_uid` | must reference existing tasks/resources |
163
+ | | `units` | 1.0 = 100% (default); work is computed from the task's duration |
164
+ | `Calendar` | `name` | `Project.calendar` edits Standard; `Project.calendars` adds base calendars |
165
+ | | `week` | `{weekday: ranges}`; weekday 0=Mon..6=Sun; ranges = `[(start_min, end_min), …]` or `None` for non-working; missing days keep defaults |
166
+ | | `exceptions` | list of `CalendarException(start, finish=None, name="")` — non-working dates |
167
+ | `CalendarException` | `start`, `finish` | `datetime.date`s; `finish` defaults to `start` |
168
+
169
+ Set `Task.calendar` to a calendar name to schedule that task on it, and
170
+ `Project.default_calendar` to change the project calendar. Summary/rollup durations are computed
171
+ in working time using `Project.calendar`'s week and holidays.
172
+
173
+ ```json
174
+ "calendar": {"week": {"wed": [["08:00", "12:00"]], "sat": null},
175
+ "holidays": ["2026-09-21", {"from": "2026-10-01", "to": "2026-10-02", "name": "Conf"}]},
176
+ "calendars": [ {"name": "Nights", "week": {"mon": [["18:00", "22:00"]]}} ]
177
+ ```
178
+
179
+ In JSON specs, resources and assignments look like:
180
+
181
+ ```json
182
+ "resources": [ {"uid": 1, "name": "Kevin", "initials": "K", "max_units": 1.0} ],
183
+ "assignments": [ {"task": 1, "resource": 1, "units": 0.5} ]
184
+ ```
185
+
186
+ Tasks are written in list order, which becomes the ID / row order in Project.
187
+
188
+ ### Validation
189
+
190
+ `MppWriter.write()` validates the plan first and refuses to write a file Project would reject or
191
+ silently repair: duplicate or non-positive task uids, parent references that contradict the outline
192
+ levels, finishes before starts, links to unknown tasks, self-links and dependency cycles all raise
193
+ `ValueError`. Two softer disagreements come back as `ScheduleWarning`s, because Project accepts the
194
+ file but changes it: a task whose declared start is *earlier* than its predecessors allow (Project
195
+ moves it on the next recalculation), and a task calendar that shares no working time with its
196
+ assigned resources' calendars (Project opens with "Not enough common working time").
197
+
198
+ Declared starts that Project's scheduler would not produce on its own are held in place with a
199
+ Start-No-Earlier-Than constraint, exactly as Project does for a typed-in date; tasks their
200
+ predecessors already place are left as-soon-as-possible so plans stay link-driven.
201
+
202
+ ### Reading a plan back
203
+
204
+ ```python
205
+ from pymppwriter import read_project
206
+
207
+ project = read_project("plan.mpp") # any MPP14 file, 2010 through M365
208
+ for task in project.tasks:
209
+ print(task.uid, task.name, task.duration_days, task.percent_complete)
210
+ ```
211
+
212
+ `read_project()` returns the same `Project` the writer takes, so a file can be
213
+ read, edited and written again. Every offset comes from the file's own `Props`
214
+ field maps and every flag from its meta bitmaps — there are no hard-coded record
215
+ layouts — so it reads files saved by any Project version of that era, not just
216
+ ones this library wrote. Tasks (names, dates, durations, outline, notes, WBS,
217
+ constraints, progress, manual scheduling), dependencies with types and lags,
218
+ resources and assignments come back; baselines, costs and timephased data do
219
+ not. A file that is not an MPP14 project raises `MppReadError`.
220
+
221
+ ## Verifying output without Project
222
+
223
+ If you have Java installed, `scripts/mpxj_oracle.py` reads any `.mpp` back through
224
+ [MPXJ](https://www.mpxj.org) (`pip install mpxj jpype1`) and prints tasks, links and
225
+ resources. `scripts/analyze_mpp.py` dumps the task records field-by-field — useful when
226
+ diffing against a file Project saved.
227
+
228
+ ## Development
229
+
230
+ ```bash
231
+ git clone https://github.com/kevinmcaleer/pymppwriter && cd pymppwriter
232
+ pip install -e ".[dev]"
233
+ pytest
234
+ ```
235
+
236
+ The end-to-end test is skipped unless `templates/template.mpp` exists.
237
+
238
+ ## Roadmap
239
+
240
+ Tracked in the [GitHub Project](../../projects). Headline epics:
241
+
242
+ 1. ~~**Durations honoured by Project**~~ — done (verified in Project M365)
243
+ 2. ~~**Resources & assignments**~~ — done (verified in Project M365)
244
+ 3. ~~**Calendars**~~ — project calendar, extra base calendars, per-task calendars — done
245
+ 4. ~~**Notes, custom fields, WBS, constraints, deadlines**~~ — done
246
+ 5. ~~**Project properties**~~ — document metadata, status date, currency — done
247
+ 6. ~~**Round-trip fidelity**~~ — `Save` from Project after opening produces the same schedule
248
+ 7. **NoodlePlanner integration** — markdown → `.mpp` export
249
+
250
+ ## Provenance & licensing
251
+
252
+ All format knowledge comes from the public [MS-CFB] specification, the observable read behaviour
253
+ of the LGPL MPXJ library, and byte-diffing files saved by Microsoft Project. No code was
254
+ derived from MPXJ or from any proprietary library, and no proprietary binaries were decompiled.
255
+ The repository ships **no** `.mpp` files: MPXJ's test fixtures are LGPL and were used only as
256
+ read-only references during development.
257
+
258
+ Microsoft Project is a trademark of Microsoft Corporation. This project is not affiliated with
259
+ Microsoft.
@@ -0,0 +1,225 @@
1
+ # pymppwriter
2
+
3
+ **Write Microsoft Project `.mpp` files from pure Python.** No Java, no .NET, no Microsoft Project
4
+ installation, no commercial library. MIT licensed.
5
+
6
+ `pymppwriter` produces native MPP14 files (the format used by Project 2010 through the current
7
+ Microsoft 365 desktop client). Files it writes open in Project by double-click — which is the
8
+ whole point: an `.mpp` download is associated with Project on every corporate PC, whereas the
9
+ MSPDI `.xml` export has to be opened manually from inside Project.
10
+
11
+ > **Status: alpha.** Verified to open correctly in Project M365: task names, hierarchy, dates,
12
+ > durations (values, display units, estimated flags, milestones and summary rollups),
13
+ > dependencies, resources and assignments, calendars (working weeks, holidays, extra base
14
+ > calendars, per-task calendars), the wider task fields (notes, WBS, constraints, deadlines,
15
+ > progress, priority, type, custom text/number/date/flag fields, manual scheduling) and project
16
+ > properties (document metadata, status date, currency). Resource rates and costs, baselines and
17
+ > timephased data are not yet written. Treat this as a working proof-of-concept, not a product.
18
+
19
+ ## How it works
20
+
21
+ The MPP format is an undocumented OLE2 compound document. Reading it selectively is a solved
22
+ problem (see [MPXJ](https://www.mpxj.org)); writing one from scratch means reproducing ~170 KB
23
+ of view definitions, Gantt bar styles, tables and filters that Project insists on.
24
+
25
+ `pymppwriter` sidesteps that with a **template-and-patch** approach:
26
+
27
+ 1. You save a near-empty project from your own copy of Project once (`templates/template.mpp`).
28
+ 2. The library keeps every stream it doesn't understand byte-for-byte.
29
+ 3. It rewrites only the task, dependency and project-property streams, cloning prototype
30
+ records from the template and patching the fields it controls.
31
+
32
+ The field-offset map is read from the template's own `Props` stream, so the writer adapts to
33
+ whatever Project version wrote the template. Details in [`docs/FORMAT_NOTES.md`](docs/FORMAT_NOTES.md).
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install git+https://github.com/kevinmcaleer/pymppwriter
39
+ ```
40
+
41
+ Only runtime dependency: [`olefile`](https://pypi.org/project/olefile/) (used to *read* the
42
+ template; the container writer is our own).
43
+
44
+ ## Make your template (one-time)
45
+
46
+ In Microsoft Project: **File → New → Blank Project**, then
47
+
48
+ | # | Task Name | Action |
49
+ |---|-----------|--------|
50
+ | 1 | Task 1 | leave as-is |
51
+ | 2 | Task 2 | **Indent** it under Task 1 (Task 1 becomes a summary) |
52
+ | 3 | Task 3 | select Tasks 2 and 3, **Link Tasks** (Finish-to-Start) |
53
+
54
+ Don't add resources, baselines or calendar changes. **Save As** `templates/template.mpp`.
55
+
56
+ Why you must make it yourself: the template is a file written by Project, so it must come from
57
+ a copy you're licensed to use, and it embeds your username. It is `.gitignore`d.
58
+
59
+ Save the template from the **same Project version that will open the generated files** — several
60
+ structures (calendar definitions in particular) are stored in version-specific dialects, and a
61
+ template written by your own copy guarantees the output speaks the dialect your Project reads.
62
+ Both current M365 and 2010-era templates are supported.
63
+
64
+ ## Usage
65
+
66
+ ### Command line
67
+
68
+ Describe the plan in JSON ([`examples/example_project.json`](examples/example_project.json)):
69
+
70
+ ```json
71
+ {
72
+ "title": "My plan",
73
+ "start": "2026-09-07T08:00",
74
+ "tasks": [
75
+ {"uid": 1, "name": "Phase 1", "start": "2026-09-07T08:00", "finish": "2026-09-09T17:00", "duration_days": 3, "outline_level": 1},
76
+ {"uid": 2, "name": "Do the thing", "start": "2026-09-07T08:00", "finish": "2026-09-08T17:00", "duration_days": 2, "outline_level": 2, "parent_uid": 1}
77
+ ],
78
+ "links": [ {"pred": 1, "succ": 2, "type": "FS", "lag_days": 0} ]
79
+ }
80
+ ```
81
+
82
+ ```bash
83
+ pymppwriter build examples/example_project.json --template templates/template.mpp --out plan.mpp
84
+ pymppwriter inspect plan.mpp # dump the OLE stream tree
85
+ ```
86
+
87
+ ### Python API
88
+
89
+ ```python
90
+ from datetime import datetime as D
91
+ from pymppwriter import MppWriter, Project, Task, Relation
92
+
93
+ project = Project(
94
+ title="Robot build plan",
95
+ start=D(2026, 10, 5, 8, 0),
96
+ tasks=[
97
+ Task(uid=1, name="Design", start=D(2026,10,5,8), finish=D(2026,10,9,17), duration_days=5),
98
+ Task(uid=2, name="Print parts", start=D(2026,10,12,8), finish=D(2026,10,14,17), duration_days=3),
99
+ Task(uid=3, name="Assemble", start=D(2026,10,15,8), finish=D(2026,10,16,17), duration_days=2,
100
+ outline_level=1, parent_uid=0),
101
+ ],
102
+ relations=[Relation(1, 2), Relation(2, 3, type="FS", lag_days=0)],
103
+ )
104
+
105
+ MppWriter("templates/template.mpp").write(project, "robot-build.mpp")
106
+ ```
107
+
108
+ **Model reference**
109
+
110
+ | Class | Field | Notes |
111
+ |-------|-------|-------|
112
+ | `Project` | `title`, `start`, `tasks`, `relations` | `start` sets the project start date |
113
+ | `Task` | `uid` | unique, > 0, stable across exports |
114
+ | | `name`, `start`, `finish` | `datetime`s |
115
+ | | `duration_days` | working days; 0 = milestone; ignored for summary tasks (rolled up from children in working time) |
116
+ | | `duration_units` | display units: `"m"`, `"h"`, `"d"` (default), `"w"`, `"mo"` |
117
+ | | `estimated` | `True` shows the duration with a trailing `?` |
118
+ | | `outline_level` | 1 = top level, 2 = child, … |
119
+ | | `parent_uid` | 0 = top level, else uid of the summary task |
120
+ | | `guid` | auto-generated; pass your own to keep GUIDs stable between exports |
121
+ | `Relation` | `pred_uid`, `succ_uid` | |
122
+ | | `type` | `"FS"` (default), `"SS"`, `"FF"`, `"SF"` |
123
+ | | `lag_days` | may be negative for lead |
124
+ | `Resource` | `uid` | unique, > 0 |
125
+ | | `name`, `initials`, `email` | strings; only `name` is required |
126
+ | | `max_units` | 1.0 = 100% (default) |
127
+ | | `guid` | auto-generated; pass your own to keep GUIDs stable |
128
+ | `Assignment` | `task_uid`, `resource_uid` | must reference existing tasks/resources |
129
+ | | `units` | 1.0 = 100% (default); work is computed from the task's duration |
130
+ | `Calendar` | `name` | `Project.calendar` edits Standard; `Project.calendars` adds base calendars |
131
+ | | `week` | `{weekday: ranges}`; weekday 0=Mon..6=Sun; ranges = `[(start_min, end_min), …]` or `None` for non-working; missing days keep defaults |
132
+ | | `exceptions` | list of `CalendarException(start, finish=None, name="")` — non-working dates |
133
+ | `CalendarException` | `start`, `finish` | `datetime.date`s; `finish` defaults to `start` |
134
+
135
+ Set `Task.calendar` to a calendar name to schedule that task on it, and
136
+ `Project.default_calendar` to change the project calendar. Summary/rollup durations are computed
137
+ in working time using `Project.calendar`'s week and holidays.
138
+
139
+ ```json
140
+ "calendar": {"week": {"wed": [["08:00", "12:00"]], "sat": null},
141
+ "holidays": ["2026-09-21", {"from": "2026-10-01", "to": "2026-10-02", "name": "Conf"}]},
142
+ "calendars": [ {"name": "Nights", "week": {"mon": [["18:00", "22:00"]]}} ]
143
+ ```
144
+
145
+ In JSON specs, resources and assignments look like:
146
+
147
+ ```json
148
+ "resources": [ {"uid": 1, "name": "Kevin", "initials": "K", "max_units": 1.0} ],
149
+ "assignments": [ {"task": 1, "resource": 1, "units": 0.5} ]
150
+ ```
151
+
152
+ Tasks are written in list order, which becomes the ID / row order in Project.
153
+
154
+ ### Validation
155
+
156
+ `MppWriter.write()` validates the plan first and refuses to write a file Project would reject or
157
+ silently repair: duplicate or non-positive task uids, parent references that contradict the outline
158
+ levels, finishes before starts, links to unknown tasks, self-links and dependency cycles all raise
159
+ `ValueError`. Two softer disagreements come back as `ScheduleWarning`s, because Project accepts the
160
+ file but changes it: a task whose declared start is *earlier* than its predecessors allow (Project
161
+ moves it on the next recalculation), and a task calendar that shares no working time with its
162
+ assigned resources' calendars (Project opens with "Not enough common working time").
163
+
164
+ Declared starts that Project's scheduler would not produce on its own are held in place with a
165
+ Start-No-Earlier-Than constraint, exactly as Project does for a typed-in date; tasks their
166
+ predecessors already place are left as-soon-as-possible so plans stay link-driven.
167
+
168
+ ### Reading a plan back
169
+
170
+ ```python
171
+ from pymppwriter import read_project
172
+
173
+ project = read_project("plan.mpp") # any MPP14 file, 2010 through M365
174
+ for task in project.tasks:
175
+ print(task.uid, task.name, task.duration_days, task.percent_complete)
176
+ ```
177
+
178
+ `read_project()` returns the same `Project` the writer takes, so a file can be
179
+ read, edited and written again. Every offset comes from the file's own `Props`
180
+ field maps and every flag from its meta bitmaps — there are no hard-coded record
181
+ layouts — so it reads files saved by any Project version of that era, not just
182
+ ones this library wrote. Tasks (names, dates, durations, outline, notes, WBS,
183
+ constraints, progress, manual scheduling), dependencies with types and lags,
184
+ resources and assignments come back; baselines, costs and timephased data do
185
+ not. A file that is not an MPP14 project raises `MppReadError`.
186
+
187
+ ## Verifying output without Project
188
+
189
+ If you have Java installed, `scripts/mpxj_oracle.py` reads any `.mpp` back through
190
+ [MPXJ](https://www.mpxj.org) (`pip install mpxj jpype1`) and prints tasks, links and
191
+ resources. `scripts/analyze_mpp.py` dumps the task records field-by-field — useful when
192
+ diffing against a file Project saved.
193
+
194
+ ## Development
195
+
196
+ ```bash
197
+ git clone https://github.com/kevinmcaleer/pymppwriter && cd pymppwriter
198
+ pip install -e ".[dev]"
199
+ pytest
200
+ ```
201
+
202
+ The end-to-end test is skipped unless `templates/template.mpp` exists.
203
+
204
+ ## Roadmap
205
+
206
+ Tracked in the [GitHub Project](../../projects). Headline epics:
207
+
208
+ 1. ~~**Durations honoured by Project**~~ — done (verified in Project M365)
209
+ 2. ~~**Resources & assignments**~~ — done (verified in Project M365)
210
+ 3. ~~**Calendars**~~ — project calendar, extra base calendars, per-task calendars — done
211
+ 4. ~~**Notes, custom fields, WBS, constraints, deadlines**~~ — done
212
+ 5. ~~**Project properties**~~ — document metadata, status date, currency — done
213
+ 6. ~~**Round-trip fidelity**~~ — `Save` from Project after opening produces the same schedule
214
+ 7. **NoodlePlanner integration** — markdown → `.mpp` export
215
+
216
+ ## Provenance & licensing
217
+
218
+ All format knowledge comes from the public [MS-CFB] specification, the observable read behaviour
219
+ of the LGPL MPXJ library, and byte-diffing files saved by Microsoft Project. No code was
220
+ derived from MPXJ or from any proprietary library, and no proprietary binaries were decompiled.
221
+ The repository ships **no** `.mpp` files: MPXJ's test fixtures are LGPL and were used only as
222
+ read-only references during development.
223
+
224
+ Microsoft Project is a trademark of Microsoft Corporation. This project is not affiliated with
225
+ Microsoft.
@@ -0,0 +1,12 @@
1
+ from .writer import (MppWriter, Project, Task, Relation, Resource, Assignment,
2
+ Calendar, CalendarException, ScheduleWarning, validate)
3
+ from .reader import read_project, MppReadError
4
+
5
+ try: # the installed distribution's version
6
+ from importlib.metadata import PackageNotFoundError, version as _version
7
+ __version__ = _version("pymppwriter")
8
+ except (ImportError, PackageNotFoundError): # running from a source tree
9
+ __version__ = "0.0.0.dev0"
10
+ __all__ = ["MppWriter", "Project", "Task", "Relation", "Resource", "Assignment",
11
+ "Calendar", "CalendarException", "ScheduleWarning", "validate",
12
+ "read_project", "MppReadError", "__version__"]