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.
- trueppm_scheduler-0.1.0a1/.gitignore +54 -0
- trueppm_scheduler-0.1.0a1/CONTRIBUTING.md +144 -0
- trueppm_scheduler-0.1.0a1/PKG-INFO +80 -0
- trueppm_scheduler-0.1.0a1/README.md +52 -0
- trueppm_scheduler-0.1.0a1/notebooks/01-cpm-quickstart.ipynb +277 -0
- trueppm_scheduler-0.1.0a1/notebooks/02-monte-carlo.ipynb +287 -0
- trueppm_scheduler-0.1.0a1/notebooks/03-calendar-aware.ipynb +350 -0
- trueppm_scheduler-0.1.0a1/notebooks/04-incremental-scheduling.ipynb +286 -0
- trueppm_scheduler-0.1.0a1/pyproject.toml +69 -0
- trueppm_scheduler-0.1.0a1/src/trueppm_scheduler/__init__.py +39 -0
- trueppm_scheduler-0.1.0a1/src/trueppm_scheduler/cli.py +109 -0
- trueppm_scheduler-0.1.0a1/src/trueppm_scheduler/engine.py +780 -0
- trueppm_scheduler-0.1.0a1/src/trueppm_scheduler/models.py +231 -0
- trueppm_scheduler-0.1.0a1/src/trueppm_scheduler/py.typed +0 -0
- trueppm_scheduler-0.1.0a1/tests/__init__.py +0 -0
- trueppm_scheduler-0.1.0a1/tests/test_bench.py +71 -0
- trueppm_scheduler-0.1.0a1/tests/test_engine.py +652 -0
- trueppm_scheduler-0.1.0a1/tests/test_models.py +133 -0
- trueppm_scheduler-0.1.0a1/tests/test_summary_expansion.py +139 -0
- trueppm_scheduler-0.1.0a1/tests/test_version.py +5 -0
- trueppm_scheduler-0.1.0a1/tests/test_wasm_conformance.py +89 -0
|
@@ -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
|
+
[](https://pypi.org/project/trueppm-scheduler/)
|
|
32
|
+
[](https://pypi.org/project/trueppm-scheduler/)
|
|
33
|
+
[](https://gitlab.com/trueppm/trueppm/-/pipelines)
|
|
34
|
+
[](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
|
+
[](https://pypi.org/project/trueppm-scheduler/)
|
|
4
|
+
[](https://pypi.org/project/trueppm-scheduler/)
|
|
5
|
+
[](https://gitlab.com/trueppm/trueppm/-/pipelines)
|
|
6
|
+
[](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
|
+
}
|