planaco 0.2.2__tar.gz → 0.2.3__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.
- {planaco-0.2.2/src/planaco.egg-info → planaco-0.2.3}/PKG-INFO +31 -26
- {planaco-0.2.2 → planaco-0.2.3}/README.md +30 -25
- {planaco-0.2.2 → planaco-0.2.3}/pyproject.toml +4 -1
- {planaco-0.2.2 → planaco-0.2.3}/src/planaco/__init__.py +17 -20
- {planaco-0.2.2 → planaco-0.2.3}/src/planaco/config.py +1 -1
- {planaco-0.2.2 → planaco-0.2.3}/src/planaco/project.py +4 -5
- {planaco-0.2.2 → planaco-0.2.3}/src/planaco/task.py +4 -6
- {planaco-0.2.2 → planaco-0.2.3/src/planaco.egg-info}/PKG-INFO +31 -26
- {planaco-0.2.2 → planaco-0.2.3}/src/planaco.egg-info/SOURCES.txt +1 -0
- {planaco-0.2.2 → planaco-0.2.3}/tests/test_cli.py +2 -1
- {planaco-0.2.2 → planaco-0.2.3}/tests/test_distributions.py +6 -2
- planaco-0.2.3/tests/test_readme_examples.py +69 -0
- {planaco-0.2.2 → planaco-0.2.3}/tests/test_refactoring.py +3 -1
- {planaco-0.2.2 → planaco-0.2.3}/LICENSE.md +0 -0
- {planaco-0.2.2 → planaco-0.2.3}/setup.cfg +0 -0
- {planaco-0.2.2 → planaco-0.2.3}/src/planaco/cli.py +0 -0
- {planaco-0.2.2 → planaco-0.2.3}/src/planaco/distributions.py +0 -0
- {planaco-0.2.2 → planaco-0.2.3}/src/planaco.egg-info/dependency_links.txt +0 -0
- {planaco-0.2.2 → planaco-0.2.3}/src/planaco.egg-info/entry_points.txt +0 -0
- {planaco-0.2.2 → planaco-0.2.3}/src/planaco.egg-info/requires.txt +0 -0
- {planaco-0.2.2 → planaco-0.2.3}/src/planaco.egg-info/top_level.txt +0 -0
- {planaco-0.2.2 → planaco-0.2.3}/tests/test_config.py +0 -0
- {planaco-0.2.2 → planaco-0.2.3}/tests/test_project.py +0 -0
- {planaco-0.2.2 → planaco-0.2.3}/tests/test_task.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: planaco
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Task and project estimation using Monte Carlo simulation
|
|
5
5
|
Author: Martijn Wieriks
|
|
6
6
|
License: MIT
|
|
@@ -39,15 +39,20 @@ Dynamic: license-file
|
|
|
39
39
|
|
|
40
40
|
<div align="center">
|
|
41
41
|
<picture>
|
|
42
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
43
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
42
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/brand/planaco-lockup-dark.svg">
|
|
43
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/brand/planaco-lockup-light.svg" alt="planaco" width="320">
|
|
44
44
|
</picture>
|
|
45
45
|
|
|
46
46
|
**Probabilistic Project Planning with Monte Carlo Simulation**
|
|
47
47
|
|
|
48
|
-
[](https://github.com/sepam/planaco/actions/workflows/ci.yml)
|
|
49
|
+
[](https://pypi.org/project/planaco/)
|
|
50
|
+
[](https://pypi.org/project/planaco/)
|
|
49
51
|
[](LICENSE.md)
|
|
50
52
|

|
|
53
|
+
[](https://github.com/psf/black)
|
|
54
|
+
[](https://github.com/astral-sh/ruff)
|
|
55
|
+
[](https://mypy-lang.org/)
|
|
51
56
|
|
|
52
57
|
<p align="center">
|
|
53
58
|
<a href="#why-planaco">Why Planaco?</a> •
|
|
@@ -69,7 +74,7 @@ Estimating the time it takes to complete a task or project is hard. Traditional
|
|
|
69
74
|
|
|
70
75
|
**Planaco helps you make better estimates by modeling tasks as random processes**, accounting for uncertainty and task dependencies through Monte Carlo simulation.
|
|
71
76
|
|
|
72
|
-
> **Status:** Planaco is in **alpha** (v0.2.
|
|
77
|
+
> **Status:** Planaco is in **alpha** (v0.2.3). The public API may still change between releases.
|
|
73
78
|
|
|
74
79
|
## Features
|
|
75
80
|
|
|
@@ -118,7 +123,7 @@ task = Task(
|
|
|
118
123
|
```
|
|
119
124
|
|
|
120
125
|
<div align="center">
|
|
121
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
126
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/task_definition.png" alt="Task Definition" width="600"/>
|
|
122
127
|
</div>
|
|
123
128
|
|
|
124
129
|
### Build Projects with Dependencies
|
|
@@ -147,7 +152,7 @@ project.add_task(deploy, depends_on=[testing])
|
|
|
147
152
|
```
|
|
148
153
|
|
|
149
154
|
<div align="center">
|
|
150
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
155
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/project_estimation.png" alt="Project Estimation" width="700"/>
|
|
151
156
|
</div>
|
|
152
157
|
|
|
153
158
|
---
|
|
@@ -191,7 +196,7 @@ project.plot(n=10000, hist=True)
|
|
|
191
196
|
```
|
|
192
197
|
|
|
193
198
|
<div align="center">
|
|
194
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
199
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/monte_carlo_estimation.png" alt="Monte Carlo Histogram" width="550"/>
|
|
195
200
|
</div>
|
|
196
201
|
|
|
197
202
|
### Cumulative Distribution
|
|
@@ -204,7 +209,7 @@ project.plot(n=10000, hist=False)
|
|
|
204
209
|
```
|
|
205
210
|
|
|
206
211
|
<div align="center">
|
|
207
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
212
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/monte_carlo_cumulative.png" alt="Cumulative Distribution" width="550"/>
|
|
208
213
|
</div>
|
|
209
214
|
|
|
210
215
|
The cumulative distribution shows the probability of completing the project within a given timeframe, accounting for both parallel and sequential task execution.
|
|
@@ -217,10 +222,10 @@ The cumulative distribution shows the probability of completing the project with
|
|
|
217
222
|
|
|
218
223
|
```python
|
|
219
224
|
# Export to JSON
|
|
220
|
-
project.export_results(n=10000, format='json',
|
|
225
|
+
project.export_results(n=10000, format='json', output='results.json')
|
|
221
226
|
|
|
222
227
|
# Export to CSV
|
|
223
|
-
project.export_results(n=10000, format='csv',
|
|
228
|
+
project.export_results(n=10000, format='csv', output='results.csv')
|
|
224
229
|
```
|
|
225
230
|
|
|
226
231
|
### Probability Distributions
|
|
@@ -366,20 +371,20 @@ project:
|
|
|
366
371
|
seed: 42 # optional, for reproducible runs
|
|
367
372
|
|
|
368
373
|
tasks:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
374
|
+
design:
|
|
375
|
+
name: Design
|
|
376
|
+
estimator: pert
|
|
377
|
+
min_duration: 5
|
|
378
|
+
mode_duration: 7
|
|
379
|
+
max_duration: 14
|
|
380
|
+
|
|
381
|
+
development:
|
|
382
|
+
name: Development
|
|
383
|
+
estimator: triangular
|
|
384
|
+
min_duration: 10
|
|
385
|
+
mode_duration: 15
|
|
386
|
+
max_duration: 25
|
|
387
|
+
depends_on: [design]
|
|
383
388
|
```
|
|
384
389
|
|
|
385
390
|
Run `planaco --help` or `planaco <command> --help` to see all available options, including `--seed` for reproducible results and `-p/--percentile` markers for plots.
|
|
@@ -421,7 +426,7 @@ Please open an issue to discuss larger changes before starting work.
|
|
|
421
426
|
|
|
422
427
|
## License
|
|
423
428
|
|
|
424
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
429
|
+
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
|
|
425
430
|
|
|
426
431
|
---
|
|
427
432
|
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
4
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/brand/planaco-lockup-dark.svg">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/brand/planaco-lockup-light.svg" alt="planaco" width="320">
|
|
5
5
|
</picture>
|
|
6
6
|
|
|
7
7
|
**Probabilistic Project Planning with Monte Carlo Simulation**
|
|
8
8
|
|
|
9
|
-
[](https://github.com/sepam/planaco/actions/workflows/ci.yml)
|
|
10
|
+
[](https://pypi.org/project/planaco/)
|
|
11
|
+
[](https://pypi.org/project/planaco/)
|
|
10
12
|
[](LICENSE.md)
|
|
11
13
|

|
|
14
|
+
[](https://github.com/psf/black)
|
|
15
|
+
[](https://github.com/astral-sh/ruff)
|
|
16
|
+
[](https://mypy-lang.org/)
|
|
12
17
|
|
|
13
18
|
<p align="center">
|
|
14
19
|
<a href="#why-planaco">Why Planaco?</a> •
|
|
@@ -30,7 +35,7 @@ Estimating the time it takes to complete a task or project is hard. Traditional
|
|
|
30
35
|
|
|
31
36
|
**Planaco helps you make better estimates by modeling tasks as random processes**, accounting for uncertainty and task dependencies through Monte Carlo simulation.
|
|
32
37
|
|
|
33
|
-
> **Status:** Planaco is in **alpha** (v0.2.
|
|
38
|
+
> **Status:** Planaco is in **alpha** (v0.2.3). The public API may still change between releases.
|
|
34
39
|
|
|
35
40
|
## Features
|
|
36
41
|
|
|
@@ -79,7 +84,7 @@ task = Task(
|
|
|
79
84
|
```
|
|
80
85
|
|
|
81
86
|
<div align="center">
|
|
82
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
87
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/task_definition.png" alt="Task Definition" width="600"/>
|
|
83
88
|
</div>
|
|
84
89
|
|
|
85
90
|
### Build Projects with Dependencies
|
|
@@ -108,7 +113,7 @@ project.add_task(deploy, depends_on=[testing])
|
|
|
108
113
|
```
|
|
109
114
|
|
|
110
115
|
<div align="center">
|
|
111
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
116
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/project_estimation.png" alt="Project Estimation" width="700"/>
|
|
112
117
|
</div>
|
|
113
118
|
|
|
114
119
|
---
|
|
@@ -152,7 +157,7 @@ project.plot(n=10000, hist=True)
|
|
|
152
157
|
```
|
|
153
158
|
|
|
154
159
|
<div align="center">
|
|
155
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
160
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/monte_carlo_estimation.png" alt="Monte Carlo Histogram" width="550"/>
|
|
156
161
|
</div>
|
|
157
162
|
|
|
158
163
|
### Cumulative Distribution
|
|
@@ -165,7 +170,7 @@ project.plot(n=10000, hist=False)
|
|
|
165
170
|
```
|
|
166
171
|
|
|
167
172
|
<div align="center">
|
|
168
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
173
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/monte_carlo_cumulative.png" alt="Cumulative Distribution" width="550"/>
|
|
169
174
|
</div>
|
|
170
175
|
|
|
171
176
|
The cumulative distribution shows the probability of completing the project within a given timeframe, accounting for both parallel and sequential task execution.
|
|
@@ -178,10 +183,10 @@ The cumulative distribution shows the probability of completing the project with
|
|
|
178
183
|
|
|
179
184
|
```python
|
|
180
185
|
# Export to JSON
|
|
181
|
-
project.export_results(n=10000, format='json',
|
|
186
|
+
project.export_results(n=10000, format='json', output='results.json')
|
|
182
187
|
|
|
183
188
|
# Export to CSV
|
|
184
|
-
project.export_results(n=10000, format='csv',
|
|
189
|
+
project.export_results(n=10000, format='csv', output='results.csv')
|
|
185
190
|
```
|
|
186
191
|
|
|
187
192
|
### Probability Distributions
|
|
@@ -327,20 +332,20 @@ project:
|
|
|
327
332
|
seed: 42 # optional, for reproducible runs
|
|
328
333
|
|
|
329
334
|
tasks:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
335
|
+
design:
|
|
336
|
+
name: Design
|
|
337
|
+
estimator: pert
|
|
338
|
+
min_duration: 5
|
|
339
|
+
mode_duration: 7
|
|
340
|
+
max_duration: 14
|
|
341
|
+
|
|
342
|
+
development:
|
|
343
|
+
name: Development
|
|
344
|
+
estimator: triangular
|
|
345
|
+
min_duration: 10
|
|
346
|
+
mode_duration: 15
|
|
347
|
+
max_duration: 25
|
|
348
|
+
depends_on: [design]
|
|
344
349
|
```
|
|
345
350
|
|
|
346
351
|
Run `planaco --help` or `planaco <command> --help` to see all available options, including `--seed` for reproducible results and `-p/--percentile` markers for plots.
|
|
@@ -382,7 +387,7 @@ Please open an issue to discuss larger changes before starting work.
|
|
|
382
387
|
|
|
383
388
|
## License
|
|
384
389
|
|
|
385
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
390
|
+
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
|
|
386
391
|
|
|
387
392
|
---
|
|
388
393
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "planaco"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"]
|
|
8
8
|
description = "Task and project estimation using Monte Carlo simulation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -63,6 +63,9 @@ Repository = "https://github.com/sepam/planaco"
|
|
|
63
63
|
[tool.setuptools.packages.find]
|
|
64
64
|
where = ["src"]
|
|
65
65
|
|
|
66
|
+
[tool.setuptools.dynamic]
|
|
67
|
+
version = {attr = "planaco.__version__"}
|
|
68
|
+
|
|
66
69
|
[tool.mypy]
|
|
67
70
|
python_version = "3.9"
|
|
68
71
|
warn_return_any = true
|
|
@@ -72,7 +72,7 @@ Using Distribution Objects
|
|
|
72
72
|
>>> from planaco import Task, PERTDistribution
|
|
73
73
|
>>>
|
|
74
74
|
>>> # Use PERT distribution for smoother estimates
|
|
75
|
-
>>> dist = PERTDistribution(
|
|
75
|
+
>>> dist = PERTDistribution(min_value=5, mode_value=7, max_value=14)
|
|
76
76
|
>>> task = Task(name="Design", distribution=dist)
|
|
77
77
|
|
|
78
78
|
Configuration Files
|
|
@@ -87,20 +87,21 @@ Example YAML::
|
|
|
87
87
|
unit: "days"
|
|
88
88
|
|
|
89
89
|
tasks:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
90
|
+
design:
|
|
91
|
+
name: "Design"
|
|
92
|
+
estimator: "pert"
|
|
93
|
+
min_duration: 5
|
|
94
|
+
mode_duration: 7
|
|
95
|
+
max_duration: 14
|
|
96
|
+
|
|
97
|
+
development:
|
|
98
|
+
name: "Development"
|
|
99
|
+
estimator: "triangular"
|
|
100
|
+
min_duration: 10
|
|
101
|
+
mode_duration: 15
|
|
102
|
+
max_duration: 25
|
|
103
|
+
depends_on:
|
|
104
|
+
- design
|
|
104
105
|
|
|
105
106
|
Command Line Interface
|
|
106
107
|
----------------------
|
|
@@ -119,10 +120,6 @@ planaco.task : Task class for individual tasks.
|
|
|
119
120
|
planaco.project : Project class for task collections.
|
|
120
121
|
planaco.config : YAML configuration loading.
|
|
121
122
|
planaco.cli : Command-line interface.
|
|
122
|
-
|
|
123
|
-
Version
|
|
124
|
-
-------
|
|
125
|
-
0.2.2
|
|
126
123
|
"""
|
|
127
124
|
|
|
128
125
|
# Import order matters: distributions -> task -> project to avoid circular imports
|
|
@@ -138,7 +135,7 @@ from planaco.distributions import (
|
|
|
138
135
|
from planaco.task import Task
|
|
139
136
|
from planaco.project import Project
|
|
140
137
|
|
|
141
|
-
__version__ = "0.2.
|
|
138
|
+
__version__ = "0.2.3"
|
|
142
139
|
|
|
143
140
|
__all__ = [
|
|
144
141
|
# Core classes
|
|
@@ -104,7 +104,7 @@ def load_config(config_path: str) -> Dict[str, Any]:
|
|
|
104
104
|
raise FileNotFoundError(f"Configuration file not found: {config_path}")
|
|
105
105
|
|
|
106
106
|
with open(path) as f:
|
|
107
|
-
config = yaml.safe_load(f)
|
|
107
|
+
config: Dict[str, Any] = yaml.safe_load(f)
|
|
108
108
|
|
|
109
109
|
_validate_config(config)
|
|
110
110
|
return config
|
|
@@ -74,7 +74,6 @@ import seaborn as sns
|
|
|
74
74
|
from planaco.task import Task
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
|
|
78
77
|
class Project:
|
|
79
78
|
"""A collection of tasks with optional dependencies for Monte Carlo simulation.
|
|
80
79
|
|
|
@@ -115,7 +114,6 @@ class Project:
|
|
|
115
114
|
>>> print(f"P85: {stats['percentiles']['p85']:.1f} {stats['unit']}")
|
|
116
115
|
"""
|
|
117
116
|
|
|
118
|
-
|
|
119
117
|
def __init__(self, name: Optional[str] = None, unit: str = "days") -> None:
|
|
120
118
|
"""Initialize a new Project.
|
|
121
119
|
|
|
@@ -843,7 +841,7 @@ class Project:
|
|
|
843
841
|
if show_percentiles:
|
|
844
842
|
colors = ["red", "orange", "purple", "green", "blue"]
|
|
845
843
|
for i, p in enumerate(percentiles):
|
|
846
|
-
p_value = np.percentile(sims, p)
|
|
844
|
+
p_value = float(np.percentile(sims, p))
|
|
847
845
|
color = colors[i % len(colors)]
|
|
848
846
|
ax.axvline(
|
|
849
847
|
x=p_value,
|
|
@@ -861,8 +859,9 @@ class Project:
|
|
|
861
859
|
)
|
|
862
860
|
else:
|
|
863
861
|
# Default: show only median
|
|
864
|
-
|
|
865
|
-
ax.
|
|
862
|
+
median = float(np.median(sims))
|
|
863
|
+
ax.axvline(x=median, color="red", label="50%")
|
|
864
|
+
ax.text(median - 0.5, ax.get_ylim()[0], "50%", color="red")
|
|
866
865
|
|
|
867
866
|
ax.legend()
|
|
868
867
|
|
|
@@ -45,7 +45,7 @@ Example
|
|
|
45
45
|
|
|
46
46
|
>>> # Modern style: use a Distribution object
|
|
47
47
|
>>> from planaco import PERTDistribution
|
|
48
|
-
>>> dist = PERTDistribution(
|
|
48
|
+
>>> dist = PERTDistribution(min_value=5, mode_value=8, max_value=15)
|
|
49
49
|
>>> task = Task(name="Development", distribution=dist)
|
|
50
50
|
|
|
51
51
|
See Also
|
|
@@ -78,8 +78,6 @@ class Task:
|
|
|
78
78
|
----------
|
|
79
79
|
task_id : str
|
|
80
80
|
Unique identifier (UUID) for the task.
|
|
81
|
-
cdate : datetime
|
|
82
|
-
Creation timestamp.
|
|
83
81
|
name : str or None
|
|
84
82
|
Human-readable name for the task.
|
|
85
83
|
|
|
@@ -233,7 +231,7 @@ class Task:
|
|
|
233
231
|
"""
|
|
234
232
|
if self._distribution is not None:
|
|
235
233
|
if hasattr(self._distribution, "min_value"):
|
|
236
|
-
return self._distribution.min_value
|
|
234
|
+
return float(self._distribution.min_value)
|
|
237
235
|
return self._min_duration
|
|
238
236
|
|
|
239
237
|
@property
|
|
@@ -254,7 +252,7 @@ class Task:
|
|
|
254
252
|
"""
|
|
255
253
|
if self._distribution is not None:
|
|
256
254
|
if hasattr(self._distribution, "mode_value"):
|
|
257
|
-
return self._distribution.mode_value
|
|
255
|
+
return float(self._distribution.mode_value)
|
|
258
256
|
return self._mode_duration
|
|
259
257
|
|
|
260
258
|
@property
|
|
@@ -275,7 +273,7 @@ class Task:
|
|
|
275
273
|
"""
|
|
276
274
|
if self._distribution is not None:
|
|
277
275
|
if hasattr(self._distribution, "max_value"):
|
|
278
|
-
return self._distribution.max_value
|
|
276
|
+
return float(self._distribution.max_value)
|
|
279
277
|
return self._max_duration
|
|
280
278
|
|
|
281
279
|
def estimate(self) -> float:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: planaco
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Task and project estimation using Monte Carlo simulation
|
|
5
5
|
Author: Martijn Wieriks
|
|
6
6
|
License: MIT
|
|
@@ -39,15 +39,20 @@ Dynamic: license-file
|
|
|
39
39
|
|
|
40
40
|
<div align="center">
|
|
41
41
|
<picture>
|
|
42
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
43
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
42
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/brand/planaco-lockup-dark.svg">
|
|
43
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/brand/planaco-lockup-light.svg" alt="planaco" width="320">
|
|
44
44
|
</picture>
|
|
45
45
|
|
|
46
46
|
**Probabilistic Project Planning with Monte Carlo Simulation**
|
|
47
47
|
|
|
48
|
-
[](https://github.com/sepam/planaco/actions/workflows/ci.yml)
|
|
49
|
+
[](https://pypi.org/project/planaco/)
|
|
50
|
+
[](https://pypi.org/project/planaco/)
|
|
49
51
|
[](LICENSE.md)
|
|
50
52
|

|
|
53
|
+
[](https://github.com/psf/black)
|
|
54
|
+
[](https://github.com/astral-sh/ruff)
|
|
55
|
+
[](https://mypy-lang.org/)
|
|
51
56
|
|
|
52
57
|
<p align="center">
|
|
53
58
|
<a href="#why-planaco">Why Planaco?</a> •
|
|
@@ -69,7 +74,7 @@ Estimating the time it takes to complete a task or project is hard. Traditional
|
|
|
69
74
|
|
|
70
75
|
**Planaco helps you make better estimates by modeling tasks as random processes**, accounting for uncertainty and task dependencies through Monte Carlo simulation.
|
|
71
76
|
|
|
72
|
-
> **Status:** Planaco is in **alpha** (v0.2.
|
|
77
|
+
> **Status:** Planaco is in **alpha** (v0.2.3). The public API may still change between releases.
|
|
73
78
|
|
|
74
79
|
## Features
|
|
75
80
|
|
|
@@ -118,7 +123,7 @@ task = Task(
|
|
|
118
123
|
```
|
|
119
124
|
|
|
120
125
|
<div align="center">
|
|
121
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
126
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/task_definition.png" alt="Task Definition" width="600"/>
|
|
122
127
|
</div>
|
|
123
128
|
|
|
124
129
|
### Build Projects with Dependencies
|
|
@@ -147,7 +152,7 @@ project.add_task(deploy, depends_on=[testing])
|
|
|
147
152
|
```
|
|
148
153
|
|
|
149
154
|
<div align="center">
|
|
150
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
155
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/project_estimation.png" alt="Project Estimation" width="700"/>
|
|
151
156
|
</div>
|
|
152
157
|
|
|
153
158
|
---
|
|
@@ -191,7 +196,7 @@ project.plot(n=10000, hist=True)
|
|
|
191
196
|
```
|
|
192
197
|
|
|
193
198
|
<div align="center">
|
|
194
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
199
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/monte_carlo_estimation.png" alt="Monte Carlo Histogram" width="550"/>
|
|
195
200
|
</div>
|
|
196
201
|
|
|
197
202
|
### Cumulative Distribution
|
|
@@ -204,7 +209,7 @@ project.plot(n=10000, hist=False)
|
|
|
204
209
|
```
|
|
205
210
|
|
|
206
211
|
<div align="center">
|
|
207
|
-
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.
|
|
212
|
+
<img src="https://raw.githubusercontent.com/sepam/planaco/v0.2.3/example/monte_carlo_cumulative.png" alt="Cumulative Distribution" width="550"/>
|
|
208
213
|
</div>
|
|
209
214
|
|
|
210
215
|
The cumulative distribution shows the probability of completing the project within a given timeframe, accounting for both parallel and sequential task execution.
|
|
@@ -217,10 +222,10 @@ The cumulative distribution shows the probability of completing the project with
|
|
|
217
222
|
|
|
218
223
|
```python
|
|
219
224
|
# Export to JSON
|
|
220
|
-
project.export_results(n=10000, format='json',
|
|
225
|
+
project.export_results(n=10000, format='json', output='results.json')
|
|
221
226
|
|
|
222
227
|
# Export to CSV
|
|
223
|
-
project.export_results(n=10000, format='csv',
|
|
228
|
+
project.export_results(n=10000, format='csv', output='results.csv')
|
|
224
229
|
```
|
|
225
230
|
|
|
226
231
|
### Probability Distributions
|
|
@@ -366,20 +371,20 @@ project:
|
|
|
366
371
|
seed: 42 # optional, for reproducible runs
|
|
367
372
|
|
|
368
373
|
tasks:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
374
|
+
design:
|
|
375
|
+
name: Design
|
|
376
|
+
estimator: pert
|
|
377
|
+
min_duration: 5
|
|
378
|
+
mode_duration: 7
|
|
379
|
+
max_duration: 14
|
|
380
|
+
|
|
381
|
+
development:
|
|
382
|
+
name: Development
|
|
383
|
+
estimator: triangular
|
|
384
|
+
min_duration: 10
|
|
385
|
+
mode_duration: 15
|
|
386
|
+
max_duration: 25
|
|
387
|
+
depends_on: [design]
|
|
383
388
|
```
|
|
384
389
|
|
|
385
390
|
Run `planaco --help` or `planaco <command> --help` to see all available options, including `--seed` for reproducible results and `-p/--percentile` markers for plots.
|
|
@@ -421,7 +426,7 @@ Please open an issue to discuss larger changes before starting work.
|
|
|
421
426
|
|
|
422
427
|
## License
|
|
423
428
|
|
|
424
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
429
|
+
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
|
|
425
430
|
|
|
426
431
|
---
|
|
427
432
|
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
import pytest
|
|
7
7
|
from click.testing import CliRunner
|
|
8
8
|
|
|
9
|
+
from planaco import __version__
|
|
9
10
|
from planaco.cli import main
|
|
10
11
|
|
|
11
12
|
# Get path to fixtures directory
|
|
@@ -43,7 +44,7 @@ class TestMainGroup:
|
|
|
43
44
|
"""Test --version flag."""
|
|
44
45
|
result = runner.invoke(main, ["--version"])
|
|
45
46
|
assert result.exit_code == 0
|
|
46
|
-
assert
|
|
47
|
+
assert __version__ in result.output
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
class TestInitCommand:
|
|
@@ -726,7 +726,9 @@ class TestDistributionRoundTrip:
|
|
|
726
726
|
|
|
727
727
|
def test_normal_round_trip_with_bounds(self):
|
|
728
728
|
"""Normal distribution with explicit bounds survives round-trip."""
|
|
729
|
-
original = NormalDistribution(
|
|
729
|
+
original = NormalDistribution(
|
|
730
|
+
mean=7.0, std_dev=2.0, min_value=3.0, max_value=11.0
|
|
731
|
+
)
|
|
730
732
|
d = original.to_dict()
|
|
731
733
|
restored = create_distribution(estimator=d.pop("type"), **d)
|
|
732
734
|
assert isinstance(restored, NormalDistribution)
|
|
@@ -772,7 +774,9 @@ class TestDistributionRoundTrip:
|
|
|
772
774
|
|
|
773
775
|
def test_lognormal_round_trip_with_bounds(self):
|
|
774
776
|
"""LogNormal with explicit bounds survives round-trip."""
|
|
775
|
-
original = LogNormalDistribution(
|
|
777
|
+
original = LogNormalDistribution(
|
|
778
|
+
mean=8.0, std_dev=3.0, min_value=2.0, max_value=20.0
|
|
779
|
+
)
|
|
776
780
|
d = original.to_dict()
|
|
777
781
|
restored = create_distribution(estimator=d.pop("type"), **d)
|
|
778
782
|
assert isinstance(restored, LogNormalDistribution)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Ensure code examples in README.md stay in sync with the actual API.
|
|
2
|
+
|
|
3
|
+
Extracts fenced ``python`` and ``yaml`` code blocks from README.md and
|
|
4
|
+
executes them, so a README example that no longer matches the library
|
|
5
|
+
fails the test suite instead of failing for a new user.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import matplotlib
|
|
12
|
+
|
|
13
|
+
matplotlib.use("Agg") # Non-interactive backend for testing
|
|
14
|
+
|
|
15
|
+
import matplotlib.pyplot as plt # noqa: E402
|
|
16
|
+
import pytest # noqa: E402
|
|
17
|
+
|
|
18
|
+
from planaco.config import build_project_from_config, load_config # noqa: E402
|
|
19
|
+
|
|
20
|
+
README_PATH = Path(__file__).resolve().parent.parent / "README.md"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _extract_code_blocks(language):
|
|
24
|
+
"""Return the contents of all fenced code blocks for a language."""
|
|
25
|
+
text = README_PATH.read_text(encoding="utf-8")
|
|
26
|
+
pattern = re.compile(rf"```{language}\n(.*?)```", re.DOTALL)
|
|
27
|
+
return pattern.findall(text)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_readme_python_examples_run(tmp_path, monkeypatch):
|
|
31
|
+
"""All Python examples must run in order without errors.
|
|
32
|
+
|
|
33
|
+
Blocks are executed cumulatively in a shared namespace because later
|
|
34
|
+
examples build on objects defined in earlier ones (e.g. ``project``).
|
|
35
|
+
"""
|
|
36
|
+
monkeypatch.chdir(tmp_path) # exported files land in a temp dir
|
|
37
|
+
blocks = _extract_code_blocks("python")
|
|
38
|
+
assert blocks, "No Python code blocks found in README.md"
|
|
39
|
+
|
|
40
|
+
namespace = {}
|
|
41
|
+
for i, block in enumerate(blocks, start=1):
|
|
42
|
+
try:
|
|
43
|
+
exec(compile(block, f"README.md python block {i}", "exec"), namespace)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
pytest.fail(
|
|
46
|
+
f"README.md python block {i} raised {type(e).__name__}: {e}\n"
|
|
47
|
+
f"---\n{block}---"
|
|
48
|
+
)
|
|
49
|
+
finally:
|
|
50
|
+
plt.close("all")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_readme_yaml_examples_load(tmp_path):
|
|
54
|
+
"""All YAML project examples must pass config validation and simulate."""
|
|
55
|
+
blocks = [b for b in _extract_code_blocks("yaml") if "project:" in b]
|
|
56
|
+
assert blocks, "No YAML project config blocks found in README.md"
|
|
57
|
+
|
|
58
|
+
for i, block in enumerate(blocks, start=1):
|
|
59
|
+
config_file = tmp_path / f"readme_example_{i}.yaml"
|
|
60
|
+
config_file.write_text(block, encoding="utf-8")
|
|
61
|
+
try:
|
|
62
|
+
config = load_config(str(config_file))
|
|
63
|
+
project = build_project_from_config(config)
|
|
64
|
+
project.statistics(n=100)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
pytest.fail(
|
|
67
|
+
f"README.md yaml block {i} raised {type(e).__name__}: {e}\n"
|
|
68
|
+
f"---\n{block}---"
|
|
69
|
+
)
|
|
@@ -254,7 +254,9 @@ class TestExportResultsEfficiency:
|
|
|
254
254
|
call_count += 1
|
|
255
255
|
return original_estimate()
|
|
256
256
|
|
|
257
|
-
with tempfile.NamedTemporaryFile(
|
|
257
|
+
with tempfile.NamedTemporaryFile( # noqa: SIM117
|
|
258
|
+
suffix=".json", delete=True
|
|
259
|
+
) as tmp:
|
|
258
260
|
with patch.object(p, "estimate", side_effect=counting_estimate):
|
|
259
261
|
p.export_results(n=n, format="json", output=tmp.name)
|
|
260
262
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|