trueppm-scheduler 0.1.0a1__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,54 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ *.egg
6
+ .eggs/
7
+ dist/
8
+ build/
9
+ .cache/pip/
10
+ .venv/
11
+ venv/
12
+ env/
13
+
14
+ # Mypy / Ruff / Pytest
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .pytest_cache/
18
+ .coverage
19
+ htmlcov/
20
+
21
+ # Node
22
+ node_modules/
23
+
24
+ # Coverage
25
+ coverage/
26
+ coverage.xml
27
+
28
+ # uv lockfile — trueppm-scheduler is a library; lockfile not committed
29
+ uv.lock
30
+
31
+ # Docker
32
+ *.override.yml
33
+
34
+ # OS
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Editor
39
+ .idea/
40
+ .vscode/
41
+ *.swp
42
+ *.swo
43
+
44
+ # Docusaurus build cache
45
+ .docusaurus/
46
+
47
+ # Claude Code runtime state
48
+ .claude/scheduled_tasks.lock
49
+ .claude/worktrees/
50
+
51
+ # Playwright test runtime output
52
+ test-results/
53
+ packages/web/test-results/
54
+ packages/web/playwright-report/
@@ -0,0 +1,144 @@
1
+ # Contributing to trueppm-scheduler
2
+
3
+ The scheduler is a pure Python library with no Django dependency.
4
+ It ships independently as `trueppm-scheduler` on PyPI and is also consumed
5
+ by the TruePPM Django API as a service-layer dependency.
6
+
7
+ ## Prerequisites
8
+
9
+ - Python 3.12+
10
+ - [uv](https://docs.astral.sh/uv/) (recommended) or `pip`
11
+
12
+ ```bash
13
+ # Install with dev dependencies using uv
14
+ cd packages/scheduler
15
+ uv sync --extra dev
16
+
17
+ # Or with pip
18
+ pip install -e ".[dev]"
19
+ ```
20
+
21
+ ## Running the tests
22
+
23
+ ```bash
24
+ cd packages/scheduler
25
+ pytest # all tests, short output
26
+ pytest -v # verbose
27
+ pytest tests/test_engine.py -x # stop on first failure
28
+ ```
29
+
30
+ ### Performance benchmarks
31
+
32
+ The bench suite asserts hard time budgets. Run it before submitting a MR
33
+ that touches the engine:
34
+
35
+ ```bash
36
+ pytest tests/test_bench.py -v
37
+ ```
38
+
39
+ Expected output:
40
+
41
+ ```
42
+ PASSED tests/test_bench.py::test_schedule_performance[100-tasks]
43
+ PASSED tests/test_bench.py::test_schedule_performance[500-tasks]
44
+ ```
45
+
46
+ If either test fails, profile before investigating — a warm-up run is included
47
+ to exclude import-time costs, so a failure indicates a genuine algorithmic
48
+ regression.
49
+
50
+ ## Linting and type checking
51
+
52
+ ```bash
53
+ ruff check src tests # lint
54
+ ruff format src tests # format
55
+ mypy src # strict type check
56
+ ```
57
+
58
+ CI runs all three on every MR. Fixes must address root causes — do not use
59
+ `# type: ignore` or `# noqa` to silence errors.
60
+
61
+ ## Interactive notebooks
62
+
63
+ The `notebooks/` directory contains Jupyter notebooks that serve as
64
+ executable documentation:
65
+
66
+ | Notebook | Topic |
67
+ |----------|-------|
68
+ | `01-cpm-quickstart.ipynb` | Project definition, CPM, float table, cycle detection |
69
+ | `02-monte-carlo.ipynb` | PERT estimates, Monte Carlo, P50/P80/P95 |
70
+ | `03-calendar-aware.ipynb` | Custom working weeks, holiday exceptions, lag |
71
+ | `04-incremental-scheduling.ipynb` | Incremental CPM, fallback behaviour, bench targets |
72
+
73
+ Run them locally:
74
+
75
+ ```bash
76
+ pip install -e ".[dev]" jupyter matplotlib
77
+ jupyter notebook notebooks/
78
+ ```
79
+
80
+ The `scheduler:notebooks` CI job executes all notebooks with `jupyter nbconvert
81
+ --execute` to catch regressions. A notebook cell that raises an exception fails
82
+ the job.
83
+
84
+ ## Adding a new notebook
85
+
86
+ 1. Create `notebooks/0N-topic.ipynb` with a markdown header cell explaining
87
+ the topic and install instructions.
88
+ 2. Keep each cell self-contained — cells are run top-to-bottom by CI.
89
+ 3. Avoid dependencies outside `trueppm-scheduler` and `matplotlib`
90
+ (already installed in the CI image). Use a `try/import` guard for
91
+ optional visualisation libraries.
92
+ 4. Add a row to the table in `docs/features/scheduler.md`.
93
+
94
+ ## Project structure
95
+
96
+ ```
97
+ packages/scheduler/
98
+ ├── src/
99
+ │ └── trueppm_scheduler/
100
+ │ ├── __init__.py # Public API exports
101
+ │ ├── models.py # Data classes: Task, Dependency, Project, Calendar
102
+ │ ├── engine.py # schedule(), monte_carlo(), CPM algorithms
103
+ │ └── cli.py # CLI entry point (trueppm-scheduler command)
104
+ ├── tests/
105
+ │ ├── test_engine.py # Unit tests for schedule() and monte_carlo()
106
+ │ ├── test_bench.py # Performance regression guard
107
+ │ └── conftest.py # Shared fixtures
108
+ ├── notebooks/ # Executable documentation (see above)
109
+ ├── pyproject.toml
110
+ └── CONTRIBUTING.md # This file
111
+ ```
112
+
113
+ ## Design constraints
114
+
115
+ 1. **Zero Django dependency.** The scheduler must import and run in any Python
116
+ environment without Django installed. Never import from `django` or
117
+ `trueppm_api` inside `src/trueppm_scheduler/`.
118
+
119
+ 2. **Pure data in, pure data out.** `schedule()` and `monte_carlo()` accept
120
+ `Project` dataclasses and return `ScheduleResult` / `MonteCarloResult`
121
+ dataclasses. No side effects, no I/O.
122
+
123
+ 3. **Deterministic at a given seed.** Monte Carlo results are reproducible when
124
+ `seed` is provided. Do not introduce sources of non-determinism outside the
125
+ PERT sampler.
126
+
127
+ 4. **Incremental results must match full recompute.** Any change to the
128
+ incremental path must pass `tests/test_incremental_equivalence.py`, which
129
+ asserts result equality across 1000 random scenarios.
130
+
131
+ ## Commit and MR conventions
132
+
133
+ See the [Contributing guide](https://docs.trueppm.com/contributing/guide/) for
134
+ branch naming, commit format, and changelog fragments. The short version:
135
+
136
+ ```bash
137
+ git checkout -b feat/scheduler-<topic> # or fix/, docs/, perf/
138
+ # make changes
139
+ git commit -m "feat(scheduler): <description>"
140
+ # create changelog fragment: changelog.d/<issue>.<type>.md
141
+ ```
142
+
143
+ All scheduler MRs require a green `scheduler:lint`, `scheduler:type-check`,
144
+ `scheduler:test`, and `scheduler:bench` pipeline.
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: trueppm-scheduler
3
+ Version: 0.1.0a1
4
+ Summary: Critical-path and resource-leveling scheduler for project management
5
+ Project-URL: Homepage, https://gitlab.com/trueppm/trueppm
6
+ Project-URL: Repository, https://gitlab.com/trueppm/trueppm
7
+ Project-URL: Issues, https://gitlab.com/trueppm/trueppm/-/issues
8
+ Author: Kelly Hair
9
+ License-Expression: Apache-2.0
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Office/Business :: Scheduling
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: networkx>=3.0
19
+ Requires-Dist: numpy>=1.26
20
+ Provides-Extra: dev
21
+ Requires-Dist: diff-cover>=9.0; extra == 'dev'
22
+ Requires-Dist: mypy>=1.0; extra == 'dev'
23
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
24
+ Requires-Dist: pytest>=7.0; extra == 'dev'
25
+ Requires-Dist: ruff>=0.8; extra == 'dev'
26
+ Requires-Dist: types-networkx>=3.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # trueppm-scheduler
30
+
31
+ [![PyPI version](https://img.shields.io/pypi/v/trueppm-scheduler.svg)](https://pypi.org/project/trueppm-scheduler/)
32
+ [![PyPI downloads](https://img.shields.io/pypi/dm/trueppm-scheduler.svg)](https://pypi.org/project/trueppm-scheduler/)
33
+ [![CI](https://gitlab.com/trueppm/trueppm/badges/main/pipeline.svg)](https://gitlab.com/trueppm/trueppm/-/pipelines)
34
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
35
+
36
+ Pure-Python CPM scheduling engine for TruePPM. Ships independently on PyPI — no Django dependency.
37
+
38
+ ## Features
39
+
40
+ - Forward/backward CPM pass with all four dependency types (FS, SS, FF, SF)
41
+ - Calendar-aware working-day arithmetic (weekend skip + holiday exceptions)
42
+ - Monte Carlo simulation via PERT-Beta distributions (numpy-vectorised, ~10k runs/sec)
43
+ - CLI: `trueppm-scheduler schedule` / `trueppm-scheduler monte-carlo`
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install trueppm-scheduler
49
+ ```
50
+
51
+ ## Quick start
52
+
53
+ ```python
54
+ from datetime import date, timedelta
55
+ from trueppm_scheduler import schedule, Calendar, Project, Task, Dependency, DependencyType
56
+
57
+ calendar = Calendar() # Mon–Fri, 8 h/day, no holidays
58
+ task_a = Task(id="t-1", name="Design", duration=timedelta(days=5))
59
+ task_b = Task(id="t-2", name="Build", duration=timedelta(days=10))
60
+ dep = Dependency(predecessor_id="t-1", successor_id="t-2", dep_type=DependencyType.FS)
61
+
62
+ project = Project(
63
+ id="p-1",
64
+ name="My Project",
65
+ start_date=date(2026, 1, 5),
66
+ tasks=[task_a, task_b],
67
+ dependencies=[dep],
68
+ calendar=calendar,
69
+ )
70
+
71
+ result = schedule(project)
72
+ build = next(t for t in result.tasks if t.id == "t-2")
73
+ print(build.early_finish) # 2026-01-21 (5 + 10 working days from 2026-01-05)
74
+ ```
75
+
76
+ See [the full documentation](https://docs.trueppm.com/features/scheduler) for CPM output fields, Monte Carlo usage, and CLI reference.
77
+
78
+ ## License
79
+
80
+ Apache 2.0
@@ -0,0 +1,52 @@
1
+ # trueppm-scheduler
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/trueppm-scheduler.svg)](https://pypi.org/project/trueppm-scheduler/)
4
+ [![PyPI downloads](https://img.shields.io/pypi/dm/trueppm-scheduler.svg)](https://pypi.org/project/trueppm-scheduler/)
5
+ [![CI](https://gitlab.com/trueppm/trueppm/badges/main/pipeline.svg)](https://gitlab.com/trueppm/trueppm/-/pipelines)
6
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
7
+
8
+ Pure-Python CPM scheduling engine for TruePPM. Ships independently on PyPI — no Django dependency.
9
+
10
+ ## Features
11
+
12
+ - Forward/backward CPM pass with all four dependency types (FS, SS, FF, SF)
13
+ - Calendar-aware working-day arithmetic (weekend skip + holiday exceptions)
14
+ - Monte Carlo simulation via PERT-Beta distributions (numpy-vectorised, ~10k runs/sec)
15
+ - CLI: `trueppm-scheduler schedule` / `trueppm-scheduler monte-carlo`
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install trueppm-scheduler
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```python
26
+ from datetime import date, timedelta
27
+ from trueppm_scheduler import schedule, Calendar, Project, Task, Dependency, DependencyType
28
+
29
+ calendar = Calendar() # Mon–Fri, 8 h/day, no holidays
30
+ task_a = Task(id="t-1", name="Design", duration=timedelta(days=5))
31
+ task_b = Task(id="t-2", name="Build", duration=timedelta(days=10))
32
+ dep = Dependency(predecessor_id="t-1", successor_id="t-2", dep_type=DependencyType.FS)
33
+
34
+ project = Project(
35
+ id="p-1",
36
+ name="My Project",
37
+ start_date=date(2026, 1, 5),
38
+ tasks=[task_a, task_b],
39
+ dependencies=[dep],
40
+ calendar=calendar,
41
+ )
42
+
43
+ result = schedule(project)
44
+ build = next(t for t in result.tasks if t.id == "t-2")
45
+ print(build.early_finish) # 2026-01-21 (5 + 10 working days from 2026-01-05)
46
+ ```
47
+
48
+ See [the full documentation](https://docs.trueppm.com/features/scheduler) for CPM output fields, Monte Carlo usage, and CLI reference.
49
+
50
+ ## License
51
+
52
+ Apache 2.0
@@ -0,0 +1,277 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# CPM Scheduling — Quick Start\n",
8
+ "\n",
9
+ "This notebook walks through the core `trueppm-scheduler` API:\n",
10
+ "building a project, running the Critical Path Method (CPM), and\n",
11
+ "reading the results.\n",
12
+ "\n",
13
+ "**Install**\n",
14
+ "```bash\n",
15
+ "pip install trueppm-scheduler\n",
16
+ "```\n",
17
+ "\n",
18
+ "Or from the monorepo:\n",
19
+ "```bash\n",
20
+ "pip install -e packages/scheduler\n",
21
+ "```"
22
+ ]
23
+ },
24
+ {
25
+ "cell_type": "code",
26
+ "execution_count": null,
27
+ "metadata": {},
28
+ "outputs": [],
29
+ "source": [
30
+ "from datetime import date, timedelta\n",
31
+ "from trueppm_scheduler import (\n",
32
+ " Calendar, DateRange, Dependency, DependencyType,\n",
33
+ " Project, Task, schedule, CyclicDependencyError,\n",
34
+ ")"
35
+ ]
36
+ },
37
+ {
38
+ "cell_type": "markdown",
39
+ "metadata": {},
40
+ "source": [
41
+ "## 1. Define the project\n",
42
+ "\n",
43
+ "We model a simple software release: Design → (Build + Test in parallel) → Deploy."
44
+ ]
45
+ },
46
+ {
47
+ "cell_type": "code",
48
+ "execution_count": null,
49
+ "metadata": {},
50
+ "outputs": [],
51
+ "source": [
52
+ "# Calendar: Mon–Fri, no holidays (default)\n",
53
+ "cal = Calendar() # working_days=0b0011111 (Mon=bit0 .. Sun=bit6)\n",
54
+ "\n",
55
+ "tasks = [\n",
56
+ " Task(id=\"design\", name=\"Design\", duration=timedelta(days=5)),\n",
57
+ " Task(id=\"build\", name=\"Build\", duration=timedelta(days=10)),\n",
58
+ " Task(id=\"test\", name=\"Test\", duration=timedelta(days=7)),\n",
59
+ " Task(id=\"deploy\", name=\"Deploy\", duration=timedelta(days=2)),\n",
60
+ "]\n",
61
+ "\n",
62
+ "dependencies = [\n",
63
+ " Dependency(predecessor_id=\"design\", successor_id=\"build\"), # FS (default)\n",
64
+ " Dependency(predecessor_id=\"design\", successor_id=\"test\"), # FS\n",
65
+ " Dependency(predecessor_id=\"build\", successor_id=\"deploy\"), # FS\n",
66
+ " Dependency(predecessor_id=\"test\", successor_id=\"deploy\"), # FS\n",
67
+ "]\n",
68
+ "\n",
69
+ "project = Project(\n",
70
+ " id=\"release-v1\",\n",
71
+ " name=\"Release v1.0\",\n",
72
+ " start_date=date(2026, 4, 1),\n",
73
+ " tasks=tasks,\n",
74
+ " dependencies=dependencies,\n",
75
+ " calendar=cal,\n",
76
+ ")"
77
+ ]
78
+ },
79
+ {
80
+ "cell_type": "markdown",
81
+ "metadata": {},
82
+ "source": [
83
+ "## 2. Run CPM"
84
+ ]
85
+ },
86
+ {
87
+ "cell_type": "code",
88
+ "execution_count": null,
89
+ "metadata": {},
90
+ "outputs": [],
91
+ "source": [
92
+ "result = schedule(project)\n",
93
+ "print(f\"Project start: {result.project_start}\")\n",
94
+ "print(f\"Project finish: {result.project_finish}\")\n",
95
+ "print(f\"Critical path: {' → '.join(result.critical_path)}\")"
96
+ ]
97
+ },
98
+ {
99
+ "cell_type": "markdown",
100
+ "metadata": {},
101
+ "source": [
102
+ "## 3. Inspect per-task results\n",
103
+ "\n",
104
+ "Each task in `result.tasks` has its ES/EF/LS/LF and float populated."
105
+ ]
106
+ },
107
+ {
108
+ "cell_type": "code",
109
+ "execution_count": null,
110
+ "metadata": {},
111
+ "outputs": [],
112
+ "source": [
113
+ "print(f\"{'Task':<10} {'ES':>12} {'EF':>12} {'LS':>12} {'LF':>12} {'Float':>6} {'Critical':>8}\")\n",
114
+ "print(\"-\" * 72)\n",
115
+ "for t in result.tasks:\n",
116
+ " float_days = t.total_float.days\n",
117
+ " print(\n",
118
+ " f\"{t.name:<10} {str(t.early_start):>12} {str(t.early_finish):>12}\"\n",
119
+ " f\" {str(t.late_start):>12} {str(t.late_finish):>12}\"\n",
120
+ " f\" {float_days:>6} {'✓' if t.is_critical else '':>8}\"\n",
121
+ " )"
122
+ ]
123
+ },
124
+ {
125
+ "cell_type": "markdown",
126
+ "metadata": {},
127
+ "source": [
128
+ "## 4. Custom calendar: add a holiday\n",
129
+ "\n",
130
+ "The Good Friday bank holiday shifts the schedule by one working day."
131
+ ]
132
+ },
133
+ {
134
+ "cell_type": "code",
135
+ "execution_count": null,
136
+ "metadata": {},
137
+ "outputs": [],
138
+ "source": [
139
+ "cal_with_holiday = Calendar(\n",
140
+ " exceptions=[\n",
141
+ " DateRange(start=date(2026, 4, 3), end=date(2026, 4, 3)), # Good Friday\n",
142
+ " ]\n",
143
+ ")\n",
144
+ "\n",
145
+ "project_with_holiday = Project(\n",
146
+ " id=\"release-v1-holiday\",\n",
147
+ " name=\"Release v1.0 (with holiday)\",\n",
148
+ " start_date=date(2026, 4, 1),\n",
149
+ " tasks=[\n",
150
+ " Task(id=t.id, name=t.name, duration=t.duration) for t in tasks\n",
151
+ " ],\n",
152
+ " dependencies=[\n",
153
+ " Dependency(predecessor_id=d.predecessor_id, successor_id=d.successor_id)\n",
154
+ " for d in dependencies\n",
155
+ " ],\n",
156
+ " calendar=cal_with_holiday,\n",
157
+ ")\n",
158
+ "\n",
159
+ "result_holiday = schedule(project_with_holiday)\n",
160
+ "print(f\"Without holiday: {result.project_finish}\")\n",
161
+ "print(f\"With holiday: {result_holiday.project_finish}\")"
162
+ ]
163
+ },
164
+ {
165
+ "cell_type": "markdown",
166
+ "metadata": {},
167
+ "source": [
168
+ "## 5. SS / FF / SF dependency types\n",
169
+ "\n",
170
+ "Non-FS dependencies model parallel work with constraints."
171
+ ]
172
+ },
173
+ {
174
+ "cell_type": "code",
175
+ "execution_count": null,
176
+ "metadata": {},
177
+ "outputs": [],
178
+ "source": [
179
+ "# SS with 2-day lag: testing can start 2 days after coding starts\n",
180
+ "coding = Task(id=\"code\", name=\"Code\", duration=timedelta(days=10))\n",
181
+ "testing = Task(id=\"test2\", name=\"Test\", duration=timedelta(days=8))\n",
182
+ "\n",
183
+ "p2 = Project(\n",
184
+ " id=\"p2\",\n",
185
+ " name=\"SS example\",\n",
186
+ " start_date=date(2026, 4, 1),\n",
187
+ " tasks=[coding, testing],\n",
188
+ " dependencies=[\n",
189
+ " Dependency(\n",
190
+ " predecessor_id=\"code\",\n",
191
+ " successor_id=\"test2\",\n",
192
+ " dep_type=DependencyType.SS,\n",
193
+ " lag=timedelta(days=2),\n",
194
+ " )\n",
195
+ " ],\n",
196
+ ")\n",
197
+ "\n",
198
+ "r2 = schedule(p2)\n",
199
+ "for t in r2.tasks:\n",
200
+ " print(f\"{t.name}: {t.early_start} → {t.early_finish}\")"
201
+ ]
202
+ },
203
+ {
204
+ "cell_type": "markdown",
205
+ "metadata": {},
206
+ "source": [
207
+ "## 6. Cycle detection"
208
+ ]
209
+ },
210
+ {
211
+ "cell_type": "code",
212
+ "execution_count": null,
213
+ "metadata": {},
214
+ "outputs": [],
215
+ "source": [
216
+ "t_a = Task(id=\"a\", name=\"A\", duration=timedelta(days=3))\n",
217
+ "t_b = Task(id=\"b\", name=\"B\", duration=timedelta(days=3))\n",
218
+ "\n",
219
+ "p_cycle = Project(\n",
220
+ " id=\"p-cycle\",\n",
221
+ " name=\"Cycle example\",\n",
222
+ " start_date=date(2026, 4, 1),\n",
223
+ " tasks=[t_a, t_b],\n",
224
+ " dependencies=[\n",
225
+ " Dependency(predecessor_id=\"a\", successor_id=\"b\"),\n",
226
+ " Dependency(predecessor_id=\"b\", successor_id=\"a\"), # cycle!\n",
227
+ " ],\n",
228
+ ")\n",
229
+ "\n",
230
+ "try:\n",
231
+ " schedule(p_cycle)\n",
232
+ "except CyclicDependencyError as e:\n",
233
+ " print(f\"Caught cycle: {' → '.join(e.cycle)}\")"
234
+ ]
235
+ },
236
+ {
237
+ "cell_type": "markdown",
238
+ "metadata": {},
239
+ "source": [
240
+ "## 7. Serialising a project to JSON\n",
241
+ "\n",
242
+ "`Project.to_json()` / `Project.from_json()` round-trip all fields."
243
+ ]
244
+ },
245
+ {
246
+ "cell_type": "code",
247
+ "execution_count": null,
248
+ "metadata": {},
249
+ "outputs": [],
250
+ "source": [
251
+ "import json\n",
252
+ "\n",
253
+ "json_str = project.to_json(indent=2)\n",
254
+ "print(json_str[:400], \"...\")\n",
255
+ "\n",
256
+ "# Round-trip\n",
257
+ "project_rt = Project.from_json(json_str)\n",
258
+ "result_rt = schedule(project_rt)\n",
259
+ "assert result_rt.project_finish == result.project_finish\n",
260
+ "print(\"\\nRound-trip OK:\", result_rt.project_finish)"
261
+ ]
262
+ }
263
+ ],
264
+ "metadata": {
265
+ "kernelspec": {
266
+ "display_name": "Python 3",
267
+ "language": "python",
268
+ "name": "python3"
269
+ },
270
+ "language_info": {
271
+ "name": "python",
272
+ "version": "3.11.0"
273
+ }
274
+ },
275
+ "nbformat": 4,
276
+ "nbformat_minor": 5
277
+ }