cpnx 0.1.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.
- cpnx-0.1.0/.agents/AGENTS.md +26 -0
- cpnx-0.1.0/.github/workflows/ci.yml +24 -0
- cpnx-0.1.0/.gitignore +37 -0
- cpnx-0.1.0/LICENSE +21 -0
- cpnx-0.1.0/Makefile +14 -0
- cpnx-0.1.0/PKG-INFO +327 -0
- cpnx-0.1.0/README.md +304 -0
- cpnx-0.1.0/examples/api_rate_limit.py +34 -0
- cpnx-0.1.0/examples/etl_pipeline.py +68 -0
- cpnx-0.1.0/examples/gpu_pipeline.py +31 -0
- cpnx-0.1.0/pyproject.toml +46 -0
- cpnx-0.1.0/src/cpnx/__init__.py +22 -0
- cpnx-0.1.0/src/cpnx/engine.py +806 -0
- cpnx-0.1.0/src/cpnx/places.py +473 -0
- cpnx-0.1.0/src/cpnx/py.typed +1 -0
- cpnx-0.1.0/src/cpnx/sandbox.py +177 -0
- cpnx-0.1.0/src/cpnx/tokens.py +129 -0
- cpnx-0.1.0/src/cpnx/transitions.py +143 -0
- cpnx-0.1.0/src/cpnx/visualization.py +74 -0
- cpnx-0.1.0/tests/test_backpressure.py +117 -0
- cpnx-0.1.0/tests/test_batch.py +117 -0
- cpnx-0.1.0/tests/test_chronology.py +140 -0
- cpnx-0.1.0/tests/test_color_sets.py +139 -0
- cpnx-0.1.0/tests/test_concurrent.py +156 -0
- cpnx-0.1.0/tests/test_deep_review_fixes.py +390 -0
- cpnx-0.1.0/tests/test_encapsulation.py +225 -0
- cpnx-0.1.0/tests/test_engine.py +268 -0
- cpnx-0.1.0/tests/test_error.py +282 -0
- cpnx-0.1.0/tests/test_guards.py +152 -0
- cpnx-0.1.0/tests/test_input_arc_expression.py +141 -0
- cpnx-0.1.0/tests/test_optimizations.py +108 -0
- cpnx-0.1.0/tests/test_places.py +130 -0
- cpnx-0.1.0/tests/test_priority.py +157 -0
- cpnx-0.1.0/tests/test_public_api.py +69 -0
- cpnx-0.1.0/tests/test_pure_evaluation.py +161 -0
- cpnx-0.1.0/tests/test_resource.py +117 -0
- cpnx-0.1.0/tests/test_routing.py +125 -0
- cpnx-0.1.0/tests/test_threshold.py +142 -0
- cpnx-0.1.0/tests/test_tokens.py +59 -0
- cpnx-0.1.0/tests/test_visualization.py +121 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Petriq Agent Instructions & Rules
|
|
2
|
+
|
|
3
|
+
These project-scoped guidelines apply to all AI agents working on the `cpnx` codebase.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Concurrency & Thread Safety Rules
|
|
8
|
+
* **No Locks During Callbacks**: Never invoke user-supplied callbacks (such as `on_transition_fired`, `on_error`, or `on_token_deposited`) while holding the internal engine lock (`self._lock`). Doing so poses high risks of re-entrant deadlocks.
|
|
9
|
+
* **Encapsulation Invariant**: Avoid direct accesses to private fields of places (e.g., `place._tokens` or `place._lock`) inside the engine code. Always delegate to thread-safe public interfaces like `len(place)` or explicit query methods.
|
|
10
|
+
* **Lock Re-entrancy Safety**: The place and engine locks are standard `threading.Lock` instances (non-reentrant). Subclasses (like `PacedResourcePlace`) overriding retrieval/deposit methods must manipulate inner properties directly under their own lock instead of using nested `super()` calls that would attempt to acquire the lock a second time and deadlock.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 2. Resource & Memory Safety Rules
|
|
15
|
+
* **No Token Leaks**:
|
|
16
|
+
* Always wrap `self._executor.submit()` calls in try-except blocks to catch executor failures (e.g., pool shutdown) and restore consumed input tokens back to their source places.
|
|
17
|
+
* Surplus resource tokens remaining in transition queues must be returned to their original source places upon successful transition execution.
|
|
18
|
+
* **Prune Cooldowns**: Ensure custom resource places (e.g., `PacedResourcePlace`) clean up internal cooldown mapping dictionaries when tokens are retrieved.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 3. Formatting, Linting & Python Styling
|
|
23
|
+
* **Compliance Checks**: Before proposing any change, always run `make format` and `make lint` to ensure compliance with Ruff formatting rules.
|
|
24
|
+
* **Testing Requirement**: Always run `make test` and ensure all unit tests pass before concluding a task. Add new tests for any modified or new behavior.
|
|
25
|
+
* **Python Target**: Code should align with Python 3.10+ conventions (Union typing `A | B` rather than `Union[A, B]`).
|
|
26
|
+
* **Docstring Guidelines**: Follow PEP 257 docstring conventions for all public classes, methods, and functions.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
strategy:
|
|
9
|
+
fail-fast: false
|
|
10
|
+
matrix:
|
|
11
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
12
|
+
include:
|
|
13
|
+
- python-version: "3.14"
|
|
14
|
+
experimental: true
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
allow-prereleases: true
|
|
21
|
+
- run: pip install -e ".[dev]"
|
|
22
|
+
- run: ruff check src/ tests/
|
|
23
|
+
- run: pytest tests/ -v
|
|
24
|
+
continue-on-error: ${{ matrix.experimental == true }}
|
cpnx-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*.pyo
|
|
6
|
+
*.pyd
|
|
7
|
+
*.egg-info/
|
|
8
|
+
*.egg
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.eggs/
|
|
12
|
+
*.so
|
|
13
|
+
|
|
14
|
+
# Test / lint caches
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
.hypothesis/
|
|
19
|
+
htmlcov/
|
|
20
|
+
.coverage
|
|
21
|
+
coverage.xml
|
|
22
|
+
*.cover
|
|
23
|
+
|
|
24
|
+
# macOS
|
|
25
|
+
.DS_Store
|
|
26
|
+
.AppleDouble
|
|
27
|
+
.LSOverride
|
|
28
|
+
|
|
29
|
+
# Editors
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
*.swp
|
|
33
|
+
*.swo
|
|
34
|
+
*~
|
|
35
|
+
|
|
36
|
+
# Env vars
|
|
37
|
+
.env
|
cpnx-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 philgresh
|
|
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.
|
cpnx-0.1.0/Makefile
ADDED
cpnx-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cpnx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Coloured Petri Net executor for concurrent Python pipelines
|
|
5
|
+
Project-URL: Repository, https://github.com/philgresh/cpnx
|
|
6
|
+
Project-URL: Documentation, https://github.com/philgresh/cpnx#readme
|
|
7
|
+
Author-email: Phil Gresham <phil@gresham.dev>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: colored-petri-net,coloured-petri-net,concurrency,cpn,orchestration,petri-net,pipeline,workflow
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# cpnx
|
|
25
|
+
|
|
26
|
+
[](https://pypi.org/project/cpnx/)
|
|
27
|
+
[](https://pypi.org/project/cpnx/)
|
|
28
|
+
[](https://github.com/philgresh/cpnx/actions)
|
|
29
|
+
[](https://opensource.org/licenses/MIT)
|
|
30
|
+
|
|
31
|
+
**cpnx** is a Coloured Petri Net (CPN) executor for concurrent Python pipelines — zero dependencies, stdlib-only threading.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Motivation
|
|
36
|
+
|
|
37
|
+
Python has excellent Petri net modeling libraries (like [SNAKES](https://snakes.ibisc.univ-evry.fr/) for formal analysis and [pm4py](https://pm4py.fit.fraunhofer.de/) for process mining) but lacks a lightweight concurrent runtime executor. Developers managing resource-constrained workflows (GPU slots, API rate limits, database connection pools) often stitch together `threading.Semaphore`, `ThreadPoolExecutor`, and `queue.Queue` by hand — ad-hoc wiring that is hard to visualise and impossible to formally reason about.
|
|
38
|
+
|
|
39
|
+
**cpnx** fills this gap: it models your concurrent pipeline as a Coloured Petri Net where transitions execute real work on thread pools, resource tokens are returned atomically on failure, and the net's structure makes resource contention a mathematical property rather than scattered locking code.
|
|
40
|
+
|
|
41
|
+
The execution model is aligned with Jensen's CPN formalism (see [Theoretical Foundation](#theoretical-foundation)), so the net you write is also amenable to formal analysis with standard CPN tools.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install cpnx
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Quickstart
|
|
54
|
+
|
|
55
|
+
A pool of 2 GPU slots shared across 10 concurrent training jobs:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
"""examples/gpu_pipeline.py — GPU slot management with cpnx."""
|
|
59
|
+
|
|
60
|
+
import time
|
|
61
|
+
from cpnx import InputArc, OutputArc, PetriNet, Place, ResourcePlace, Token, Transition
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def train_model(tokens: list[Token]) -> list[Token]:
|
|
65
|
+
data = tokens[0]
|
|
66
|
+
time.sleep(0.5) # simulate GPU work
|
|
67
|
+
# Tokens are immutable — produce a new one with updated payload
|
|
68
|
+
return [data.evolve(payload_updates={"trained": True})]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
net = PetriNet(max_workers=4)
|
|
72
|
+
|
|
73
|
+
net.add_place(Place("raw_data"))
|
|
74
|
+
net.add_place(Place("trained_models"))
|
|
75
|
+
net.add_place(ResourcePlace("gpu_slots", capacity=2))
|
|
76
|
+
|
|
77
|
+
net.add_transition(Transition(
|
|
78
|
+
name="train",
|
|
79
|
+
inputs=[InputArc("raw_data"), InputArc("gpu_slots")],
|
|
80
|
+
outputs=[OutputArc("trained_models"), OutputArc("gpu_slots")],
|
|
81
|
+
action=train_model,
|
|
82
|
+
))
|
|
83
|
+
|
|
84
|
+
for i in range(10):
|
|
85
|
+
net.deposit("raw_data", Token(payload={"model_id": i}))
|
|
86
|
+
|
|
87
|
+
net.run(deadline=time.monotonic() + 30)
|
|
88
|
+
|
|
89
|
+
print(f"Trained: {len(net.places['trained_models'].tokens)}")
|
|
90
|
+
print(f"GPU slots returned: {len(net.places['gpu_slots'].tokens)}")
|
|
91
|
+
# Trained: 10
|
|
92
|
+
# GPU slots returned: 2
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Core Concepts
|
|
98
|
+
|
|
99
|
+
A CPN consists of **places** (token containers), **transitions** (processing steps), and **arcs** (directed connections). Tokens carry a **colour** that determines which places they may occupy and which transitions may consume them.
|
|
100
|
+
|
|
101
|
+
```mermaid
|
|
102
|
+
graph LR
|
|
103
|
+
raw_data(("Place: raw_data")) --> train[Transition: train]
|
|
104
|
+
gpu_slots(("ResourcePlace: gpu_slots\n(capacity=2)")) --> train
|
|
105
|
+
train --> trained_models(("Place: trained_models"))
|
|
106
|
+
train --> gpu_slots
|
|
107
|
+
|
|
108
|
+
style raw_data fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
|
|
109
|
+
style trained_models fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
|
|
110
|
+
style gpu_slots fill:#efebe9,stroke:#5d4037,stroke-width:2px
|
|
111
|
+
style train fill:#fffde7,stroke:#fbc02d,stroke-width:2px
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Tokens
|
|
115
|
+
|
|
116
|
+
Tokens are **immutable**. Their `payload` is a [`FrozenDict`](#frozendict) — a hashable, recursively-immutable mapping. To produce a token with updated data, use `token.evolve()`:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
result = token.evolve(payload_updates={"score": 0.92})
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Each token carries a `color: str | None` field — the CPN colour. `None` means an uncoloured data token; `"resource"` is the built-in colour for permit tokens. You can define your own colours for domain-typed nets.
|
|
123
|
+
|
|
124
|
+
### Places
|
|
125
|
+
|
|
126
|
+
All places are thread-safe.
|
|
127
|
+
|
|
128
|
+
| Type | Behaviour |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `Place` | Unbounded FIFO queue for data/work items |
|
|
131
|
+
| `ResourcePlace(capacity)` | Pre-filled bounded pool of `"resource"` permit tokens; returned on transition completion or failure |
|
|
132
|
+
| `PacedResourcePlace(capacity, pacing_secs)` | Like `ResourcePlace`, but returned tokens cool down for `pacing_secs` before becoming reusable (rate-limiting) |
|
|
133
|
+
| `ThresholdPlace(threshold)` | Tokens only consumable once the queue depth reaches `threshold` (batch accumulation) |
|
|
134
|
+
|
|
135
|
+
### Transitions
|
|
136
|
+
|
|
137
|
+
A transition is **enabled** when all input places contain sufficient tokens and any guard expression evaluates to `True`. When fired, it consumes input tokens, executes the action on a thread pool, and deposits output tokens.
|
|
138
|
+
|
|
139
|
+
**Resource Return Invariant:** if a transition action raises, the engine catches the exception, routes the data token to the `error_place` (default: `"failed"`), and atomically returns all resource tokens to their source places. Deadlocks from failed actions are structurally impossible.
|
|
140
|
+
|
|
141
|
+
### Arc Expressions
|
|
142
|
+
|
|
143
|
+
Both `InputArc` and `OutputArc` accept an `expression` — a callable that filters or orders token consumption (input) or gates token deposit (output):
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# Consume the highest-priority lead first
|
|
147
|
+
InputArc("leads", count=1,
|
|
148
|
+
expression=lambda tokens: sorted(tokens, key=lambda t: -t.payload.get("score", 0)))
|
|
149
|
+
|
|
150
|
+
# Only deposit to the output place if processing succeeded
|
|
151
|
+
OutputArc("results", expression=lambda tokens: bool(tokens))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Marking
|
|
155
|
+
|
|
156
|
+
The **marking** is the complete distribution of tokens across all places at a given moment — the formal CPN state:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
m = net.marking # dict[str, list[Token]]
|
|
160
|
+
dead = net.is_dead() # True if no transition can fire in this marking
|
|
161
|
+
quiet = net.is_quiescent() # True if dead AND no in-flight transitions
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## API Reference
|
|
167
|
+
|
|
168
|
+
### `Token`
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
@dataclass(frozen=True)
|
|
172
|
+
class Token:
|
|
173
|
+
id: str # 16-char hex, auto-generated
|
|
174
|
+
payload: FrozenDict # immutable enrichment data; use .evolve() to update
|
|
175
|
+
created_at: float # monotonic creation timestamp
|
|
176
|
+
color: str | None # CPN colour; None = uncoloured, "resource" = permit token
|
|
177
|
+
available_at: float # timed CPN: earliest time this token may be consumed
|
|
178
|
+
|
|
179
|
+
def evolve(self, payload_updates: dict | None = None, **field_updates) -> Token: ...
|
|
180
|
+
@property
|
|
181
|
+
def is_resource(self) -> bool: ... # shorthand for color == "resource"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `FrozenDict`
|
|
185
|
+
|
|
186
|
+
An immutable, hashable mapping. Nested dicts and lists are frozen recursively at construction time.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
fd = FrozenDict({"x": 1, "tags": ["a", "b"]})
|
|
190
|
+
fd["x"] # 1
|
|
191
|
+
fd.as_dict() # {"x": 1, "tags": ["a", "b"]} — plain dict, JSON-serialisable
|
|
192
|
+
fd.set("y", 2) # returns a new FrozenDict — fd is unchanged
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Places
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
Place(name: str, bound: int | None = None, color_set: set[str] | None = None,
|
|
199
|
+
initial_marking: list[Token] | None = None)
|
|
200
|
+
|
|
201
|
+
ResourcePlace(name: str, capacity: int)
|
|
202
|
+
PacedResourcePlace(name: str, capacity: int, pacing_secs: float)
|
|
203
|
+
ThresholdPlace(name: str, threshold: int)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- `bound` — k-bounded place; raises if a deposit would exceed capacity (standard CPN)
|
|
207
|
+
- `color_set` — if set, `deposit()` rejects tokens whose `color` is not in the set
|
|
208
|
+
- `initial_marking` — tokens deposited at construction time
|
|
209
|
+
|
|
210
|
+
### Arcs
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
InputArc(place: str, count: int = 1, consume_all: bool = False,
|
|
214
|
+
settle_secs: float = 0.0,
|
|
215
|
+
expression: Callable[[list[Token]], list[Token]] | None = None)
|
|
216
|
+
|
|
217
|
+
OutputArc(place: str, count: int = 1,
|
|
218
|
+
expression: Callable[[list[Token]], bool] | None = None)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### `Transition`
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
@dataclass
|
|
225
|
+
class Transition:
|
|
226
|
+
name: str
|
|
227
|
+
inputs: list[InputArc]
|
|
228
|
+
outputs: list[OutputArc]
|
|
229
|
+
action: Callable[[list[Token]], list[Token]]
|
|
230
|
+
guard: Callable[[], bool] | str | None = None # transition guard (CPN standard)
|
|
231
|
+
priority: int = 10 # lower fires first among equally-enabled transitions
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### `PetriNet`
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
class PetriNet:
|
|
238
|
+
def __init__(self, max_workers: int = 4, timeout_secs: float = 30.0,
|
|
239
|
+
expr_timeout_secs: float = 0.1, error_place: str = "failed",
|
|
240
|
+
places: list[Place] | None = None,
|
|
241
|
+
transitions: list[Transition] | None = None): ...
|
|
242
|
+
|
|
243
|
+
def add_place(self, place: Place) -> None: ...
|
|
244
|
+
def add_transition(self, transition: Transition) -> None: ...
|
|
245
|
+
def deposit(self, place_name: str, token: Token) -> None: ...
|
|
246
|
+
|
|
247
|
+
def step(self) -> bool: ... # fire one enabled transition; False if none
|
|
248
|
+
def run(self, deadline: float) -> None: ... # loop until quiescent or deadline
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def marking(self) -> dict[str, list[Token]]: ... # current CPN marking
|
|
252
|
+
def is_dead(self) -> bool: ... # no transition enabled in current marking
|
|
253
|
+
def is_quiescent(self) -> bool: ... # dead AND no in-flight transitions
|
|
254
|
+
def advance_time(self, t: float) -> None: ... # advance timed CPN model clock
|
|
255
|
+
def snapshot(self) -> dict: ... # JSON-serialisable marking snapshot
|
|
256
|
+
def to_dot(self) -> str: ... # Graphviz DOT representation
|
|
257
|
+
|
|
258
|
+
# Callback hooks
|
|
259
|
+
on_transition_fired: Callable[[str, float], None] | None # (name, duration_secs)
|
|
260
|
+
on_token_deposited: Callable[[str, Token], None] | None # (place_name, token)
|
|
261
|
+
on_error: Callable[[str, Exception, Token | None], None] | None # (name, exc, token)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Examples
|
|
267
|
+
|
|
268
|
+
- [examples/gpu_pipeline.py](examples/gpu_pipeline.py) — GPU slot pool; shows concurrent throttling
|
|
269
|
+
- [examples/api_rate_limit.py](examples/api_rate_limit.py) — paced resource tokens enforce external API rate limits
|
|
270
|
+
- [examples/etl_pipeline.py](examples/etl_pipeline.py) — multi-stage ETL using `ThresholdPlace` for batch accumulation
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Sandboxing & Pure Evaluation
|
|
275
|
+
|
|
276
|
+
cpnx supports two forms of guard and arc expressions:
|
|
277
|
+
|
|
278
|
+
1. **String expressions** — evaluated by `SandboxEvaluator` via static AST analysis against a strict allowlist of mathematical and comparison operations. Fully hermetic.
|
|
279
|
+
|
|
280
|
+
2. **Callable expressions** — Python functions or lambdas. Executed in a separate thread pool (`cpnx-expr`) bounded by `expr_timeout_secs` (default 100 ms). Not I/O-isolated, but `verify_callable_purity` performs AST analysis at construction time to block common I/O calls (`open`, `print`, `time.sleep`, `os.system`, etc.). Full hermetic isolation requires string expressions.
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## FAQ
|
|
285
|
+
|
|
286
|
+
### Why not Airflow or Celery?
|
|
287
|
+
|
|
288
|
+
Airflow and Celery are excellent for distributed, long-running DAGs. They require external brokers (Redis, Postgres) and add deployment complexity. cpnx is an in-process threading library for fine-grained resource control within a single Python process — no infrastructure required.
|
|
289
|
+
|
|
290
|
+
### Why not asyncio?
|
|
291
|
+
|
|
292
|
+
ML/AI pipelines, CPU-bound parsing, and legacy database integrations use synchronous libraries. Thread pools let synchronous code run concurrently without rewriting blocking calls to async.
|
|
293
|
+
|
|
294
|
+
### Can it prevent deadlocks?
|
|
295
|
+
|
|
296
|
+
Structurally, yes — as long as resource tokens are always returned (which the Resource Return Invariant enforces). Beyond that, CPNs are amenable to formal reachability analysis: expressing constraints as explicit token structures rather than scattered locks makes deadlock-freedom properties checkable with standard CPN tools.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Theoretical Foundation
|
|
301
|
+
|
|
302
|
+
cpnx's execution model is aligned with **Coloured Petri Nets (CPNs)** as formalised by Kurt Jensen's group at Aarhus University. The key CPN concepts — colour sets, arc expressions, transition guards, formal markings, and k-bounded places — map directly onto cpnx's API.
|
|
303
|
+
|
|
304
|
+
**References:**
|
|
305
|
+
|
|
306
|
+
- Jensen, K. et al. — *CPN Group at Aarhus University* — https://cs.au.dk/cpnets
|
|
307
|
+
The canonical reference for CPN theory, tools (CPN Tools), and formalism.
|
|
308
|
+
|
|
309
|
+
- Winkler, T. et al. — *CPN-Py: A Python Framework for Coloured Petri Nets* (2025) — https://arxiv.org/html/2506.12238v1
|
|
310
|
+
The closest Python CPN library; cpnx differs by targeting concurrent **execution** rather than sequential **simulation** and formal state-space analysis.
|
|
311
|
+
|
|
312
|
+
**Where cpnx intentionally diverges from standard CPN theory:**
|
|
313
|
+
|
|
314
|
+
| cpnx feature | Status |
|
|
315
|
+
|---|---|
|
|
316
|
+
| `PacedResourcePlace`, `settle_secs` | Pragmatic concurrency extensions; no CPN equivalent |
|
|
317
|
+
| `expr_timeout_secs`, `verify_callable_purity` | Pragmatic sandboxing; no CPN equivalent |
|
|
318
|
+
| `is_quiescent()` | Dead marking AND no in-flight threads; no single CPN term |
|
|
319
|
+
| `ResourcePlace`, `ThresholdPlace` | CPN patterns expressed as typed place shorthands |
|
|
320
|
+
| `Place.bound` | Standard CPN: k-bounded place |
|
|
321
|
+
| `Token.color`, `Place.color_set` | Standard CPN: colours and colour sets |
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## License
|
|
326
|
+
|
|
327
|
+
MIT — see [LICENSE](LICENSE).
|