petritype 0.1.1__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.
- petritype-0.1.1/LICENSE-MIT +22 -0
- petritype-0.1.1/PKG-INFO +353 -0
- petritype-0.1.1/README.md +315 -0
- petritype-0.1.1/petritype/__init__.py +5 -0
- petritype-0.1.1/petritype/_optional.py +23 -0
- petritype-0.1.1/petritype/core/ast_extraction.py +232 -0
- petritype-0.1.1/petritype/core/data_structures.py +100 -0
- petritype-0.1.1/petritype/core/descriptions.py +39 -0
- petritype-0.1.1/petritype/core/executable_graph_components.py +1421 -0
- petritype-0.1.1/petritype/core/parse_modules.py +116 -0
- petritype-0.1.1/petritype/core/relationship_graph_components.py +104 -0
- petritype-0.1.1/petritype/core/type_comparisons.py +157 -0
- petritype-0.1.1/petritype/decorator.py +82 -0
- petritype-0.1.1/petritype/helpers/io/io_helper.py +76 -0
- petritype-0.1.1/petritype/helpers/structures.py +39 -0
- petritype-0.1.1/petritype/marimo_controls.py +64 -0
- petritype-0.1.1/petritype/plotting/rustworkx_graph.py +193 -0
- petritype-0.1.1/petritype/plotting/rustworkx_to_graphviz.py +256 -0
- petritype-0.1.1/petritype/plotting/simple_graphviz.py +106 -0
- petritype-0.1.1/petritype/runtime.py +759 -0
- petritype-0.1.1/pyproject.toml +68 -0
- petritype-0.1.1/pyproject.toml.orig +83 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Oleg Lenive
|
|
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.
|
|
22
|
+
|
petritype-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: petritype
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Typed, executable, visual Petri nets in Python.
|
|
5
|
+
Keywords: petri-net,workflow,pipeline,dataflow,concurrency,type-checking,visualisation
|
|
6
|
+
Author: Oleg Lenive
|
|
7
|
+
Author-email: Oleg Lenive <oleg@manifold-exp.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE-MIT
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Dist: pydantic>=2.12.5
|
|
20
|
+
Requires-Dist: pytest>=9.0.2 ; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-asyncio>=1.3.0 ; extra == 'dev'
|
|
22
|
+
Requires-Dist: petritype[viz] ; extra == 'examples'
|
|
23
|
+
Requires-Dist: petritype[marimo] ; extra == 'examples'
|
|
24
|
+
Requires-Dist: numpy>=1.25 ; extra == 'examples'
|
|
25
|
+
Requires-Dist: marimo>=0.23.9 ; extra == 'marimo'
|
|
26
|
+
Requires-Dist: matplotlib>=3.11.0 ; extra == 'viz'
|
|
27
|
+
Requires-Dist: pillow>=9 ; extra == 'viz'
|
|
28
|
+
Requires-Dist: rustworkx>=0.18.0 ; extra == 'viz'
|
|
29
|
+
Requires-Python: >=3.12
|
|
30
|
+
Project-URL: Homepage, https://github.com/olenive/petritype
|
|
31
|
+
Project-URL: Repository, https://github.com/olenive/petritype
|
|
32
|
+
Project-URL: Issues, https://github.com/olenive/petritype/issues
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Provides-Extra: examples
|
|
35
|
+
Provides-Extra: marimo
|
|
36
|
+
Provides-Extra: viz
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# Petritype
|
|
40
|
+
|
|
41
|
+
Typed, executable, visual Petri nets in Python.
|
|
42
|
+
|
|
43
|
+
Petritype turns [Petri nets](https://en.wikipedia.org/wiki/Petri_net) into a practical tool for building and visualising data processing pipelines. Places are typed containers, transitions are real Python functions, and tokens are your actual data — Pydantic models, dataclasses, primitives, whatever you need. Types are enforced at runtime, so wiring errors surface immediately rather than silently propagating.
|
|
44
|
+
|
|
45
|
+
*Early stage — contributions and feedback welcome.*
|
|
46
|
+
|
|
47
|
+
<p align="center"><img src="https://raw.githubusercontent.com/olenive/petritype/v0.1.1/images/docs/illustrations/readme_example/animation.gif" alt="Pipeline animation" width="380"></p>
|
|
48
|
+
|
|
49
|
+
## Core Idea
|
|
50
|
+
|
|
51
|
+
A Petri net has two kinds of nodes connected by directed edges:
|
|
52
|
+
|
|
53
|
+
- **Places** (blue ovals) — typed containers that hold tokens
|
|
54
|
+
- **Transitions** (green rectangles) — functions that consume tokens from input places and produce tokens into output places
|
|
55
|
+
|
|
56
|
+
A transition can only fire when all of its input places have tokens available. When it fires, it pops tokens from input places, calls its function, and routes the result to output places based on type matching. The current set of tokens across all places represents the live state of the system.
|
|
57
|
+
|
|
58
|
+
Petritype adds a type system on top of this: each place declares the Python type it accepts, and every token is checked against that type. This means the graph itself encodes the shape of your data pipeline — what types flow where, what each function expects, and where different outcomes end up.
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import asyncio
|
|
64
|
+
from petritype.core.executable_graph_components import (
|
|
65
|
+
ExecutableGraphOperations,
|
|
66
|
+
FunctionTransitionNode,
|
|
67
|
+
ListPlaceNode,
|
|
68
|
+
ArgumentEdgeToTransition,
|
|
69
|
+
ReturnedEdgeFromTransition,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# 1. Define your types and functions
|
|
73
|
+
def double(x: int) -> int:
|
|
74
|
+
return x * 2
|
|
75
|
+
|
|
76
|
+
# 2. Build the graph
|
|
77
|
+
graph = ExecutableGraphOperations.construct_graph([
|
|
78
|
+
ListPlaceNode('Input', int, [1, 2, 3]),
|
|
79
|
+
ArgumentEdgeToTransition('Input', 'Double', 'x'),
|
|
80
|
+
FunctionTransitionNode('Double', double),
|
|
81
|
+
ReturnedEdgeFromTransition('Double', 'Output'),
|
|
82
|
+
ListPlaceNode('Output', int),
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
# 3. Execute
|
|
86
|
+
graph, fired = asyncio.run(
|
|
87
|
+
ExecutableGraphOperations.execute_graph(graph, stop_after_n_firings=3)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
print(graph.place_named('Output').tokens) # [2, 4, 6]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`construct_graph` takes a flat list of places, transitions, and edges in any order — it sorts them out. Execution runs an async loop: find enabled transitions, select one, fire it, repeat.
|
|
94
|
+
|
|
95
|
+
## Key Features
|
|
96
|
+
|
|
97
|
+
### Runtime type checking
|
|
98
|
+
|
|
99
|
+
Places declare types. Tokens are validated on entry. If a transition returns a `str` but the output place expects `int`, you get an immediate error — not a silent downstream failure.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
ListPlaceNode('Scores', float, [0.95, 0.87]) # only accepts float tokens
|
|
103
|
+
ListPlaceNode('Labels', str, ['cat', 'dog']) # only accepts str tokens
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Type matching also governs output routing: when a transition has multiple output places, the result is sent to the place whose type matches.
|
|
107
|
+
|
|
108
|
+
### Static structure checks
|
|
109
|
+
|
|
110
|
+
For pipeline-shaped nets you can prove termination before running anything: `construct_graph(..., expect_acyclic=True)` (or `ExecutableGraphCheck.assert_acyclic(graph)`) rejects token cycles at build time, naming the offending path (`P1 → T1 → P2 → T2 → P1`). Acyclicity alone is not termination — a *source* transition that consumes no tokens fires forever in a perfectly acyclic net — so pair it with `ExecutableGraphCheck.assert_no_source_transitions(graph)`; a net that passes both provably quiesces. Nets that are cyclic or source-fed on purpose get run-time bounds instead (`error_after_n_firings`).
|
|
111
|
+
|
|
112
|
+
### Failure semantics
|
|
113
|
+
|
|
114
|
+
If a transition body raises, the firing fails with `TransitionFailedError`. The tokens it consumed are **not** put back: the body may have mutated them before failing, and a place should never silently hold corrupted tokens — a visible loss beats invisible corruption. The consumed tokens ride along on the error:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
try:
|
|
118
|
+
await ExecutableGraphOperations.execute_graph(graph)
|
|
119
|
+
except TransitionFailedError as e:
|
|
120
|
+
e.transition_name # 'Charge Card'
|
|
121
|
+
e.consumed # {'order': <the consumed token>}
|
|
122
|
+
e.__cause__ # the original exception
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
*Expected* failures are the body's job: catch them and return an error token, and type-based routing (e.g. `-> Receipt | FailedOrder`) delivers it to an error-handling place. If your tokens are immutable or bodies don't mutate them before failing, `restore_tokens_on_failure=True` (on `construct_graph` or per `execute_graph` call) opts into putting consumed tokens back.
|
|
126
|
+
|
|
127
|
+
### Async execution
|
|
128
|
+
|
|
129
|
+
Transition functions can be `async`. The execution loop handles both sync and async functions transparently.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
async def fetch(url: str) -> str:
|
|
133
|
+
async with aiohttp.ClientSession() as session:
|
|
134
|
+
async with session.get(url) as resp:
|
|
135
|
+
return await resp.text()
|
|
136
|
+
|
|
137
|
+
FunctionTransitionNode('Fetch', fetch)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Guards, priorities, and transition selectors
|
|
141
|
+
|
|
142
|
+
Each transition can carry two optional callables. Both receive the live graph, so they can read the whole marking:
|
|
143
|
+
|
|
144
|
+
- **`guard`** — an enabling condition the engine enforces in every execution mode: a transition whose guard returns `False` is not enabled, regardless of available tokens. Guards run on every enabled-discovery sweep, so keep them cheap, side-effect-free predicates over the marking.
|
|
145
|
+
- **`priority`** — a selection hint: the default selector fires the highest-priority enabled transition. Unset scores `0.0`; ties fall back to definition order.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# Guard: BatchProcess is not enabled until the pool is full
|
|
149
|
+
def batch_ready(graph) -> bool:
|
|
150
|
+
return len(graph.place_named('Pool').tokens) >= 10
|
|
151
|
+
|
|
152
|
+
# Priority: drain the longest queue first
|
|
153
|
+
def queue_pressure(graph) -> float:
|
|
154
|
+
return len(graph.place_named('Queue').tokens)
|
|
155
|
+
|
|
156
|
+
FunctionTransitionNode('BatchProcess', batch_process, guard=batch_ready)
|
|
157
|
+
FunctionTransitionNode('Drain', drain, priority=queue_pressure)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
For full control over what fires next, replace the selector itself — a function from the enabled transitions to the one to fire:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
graph.transition_selector = my_selector # (graph, enabled) -> transition or None
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The older `activation_function` field is deprecated: the engine never consulted it, so use `guard` or `priority` instead. It remains visible to custom selectors until removed.
|
|
167
|
+
|
|
168
|
+
A selector is just a callable over the enabled transitions, so round-robin, bottleneck-aware and other policies are a few lines each.
|
|
169
|
+
|
|
170
|
+
### Visualisation
|
|
171
|
+
|
|
172
|
+
> Needs the `viz` extra: `pip install 'petritype[viz]'`, plus the Graphviz `dot`
|
|
173
|
+
> binary on your PATH (`brew install graphviz` / `apt install graphviz`).
|
|
174
|
+
|
|
175
|
+
Built-in Graphviz rendering shows the graph structure, types, and current token state — including read arcs (dashed) and, in concurrent runs, in-flight transitions highlighted while their bodies run. The example notebooks (marimo) step through and animate execution live.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from petritype.plotting.rustworkx_graph import RustworkxGraph
|
|
179
|
+
from petritype.plotting.simple_graphviz import SimpleGraphvizVisualization
|
|
180
|
+
|
|
181
|
+
# The renderer draws a rustworkx view of the net, built once from the graph
|
|
182
|
+
pydigraph = RustworkxGraph.from_executable_graph(graph)
|
|
183
|
+
|
|
184
|
+
# Static graph image
|
|
185
|
+
SimpleGraphvizVisualization.graph(pydigraph)
|
|
186
|
+
|
|
187
|
+
# Step-by-step animation, inside a notebook (`display` comes from marimo/IPython)
|
|
188
|
+
async for step, diagram, fired in SimpleGraphvizVisualization.animate_execution_generator(
|
|
189
|
+
graph, pydigraph
|
|
190
|
+
):
|
|
191
|
+
display(diagram)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Output distribution
|
|
195
|
+
|
|
196
|
+
When a transition has multiple output places, tokens are routed by type matching. For custom routing logic, provide an output distribution function:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
def route_result(result) -> dict[str, Any]:
|
|
200
|
+
if result.score > 0.9:
|
|
201
|
+
return {'Approved': result}
|
|
202
|
+
else:
|
|
203
|
+
return {'NeedsReview': result}
|
|
204
|
+
|
|
205
|
+
FunctionTransitionNode(
|
|
206
|
+
'Classify', classify,
|
|
207
|
+
output_distribution_function=route_result,
|
|
208
|
+
)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Token copying
|
|
212
|
+
|
|
213
|
+
When a transition produces a token that matches multiple output places by type, Petritype raises an error by default — this prevents accidental duplication. If you want the same token to be sent to multiple output places (via `deepcopy`), enable token copying when constructing the graph:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
graph = ExecutableGraphOperations.construct_graph([...], allow_token_copying=True)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
This is useful when the same piece of data needs to flow down multiple independent paths — for example, a configuration token consumed by both a planning stage and a data-fetching stage.
|
|
220
|
+
|
|
221
|
+
### List-mode transitions
|
|
222
|
+
|
|
223
|
+
If a transition argument is typed as `list[T]` and the input place holds tokens of type `T`, all tokens are passed as a list in a single call — useful for batch operations.
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
def summarise(items: list[str]) -> str:
|
|
227
|
+
return f"Processed {len(items)} items"
|
|
228
|
+
|
|
229
|
+
# All str tokens from 'Items' are passed at once
|
|
230
|
+
ArgumentEdgeToTransition('Items', 'Summarise', 'items')
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Read arcs — non-consuming reads
|
|
234
|
+
|
|
235
|
+
A transition can *read* a place without consuming its tokens — useful for parameters, toggles, or guard state, kept as visible, pokable nodes. Both require a token to be present to enable the transition, but firing never consumes it:
|
|
236
|
+
|
|
237
|
+
- **`SnapshotEdge`** — the transition receives a deep-copy; the place is untouched.
|
|
238
|
+
- **`MutateEdge`** — the transition receives the live tokens and may modify them in place.
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from petritype.core.executable_graph_components import SnapshotEdge, MutateEdge
|
|
242
|
+
|
|
243
|
+
SnapshotEdge('Multiplier', 'Scale', 'factor') # Scale reads Multiplier, never consumes it
|
|
244
|
+
MutateEdge('Counter', 'Scale', 'tally') # Scale increments Counter in place
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Read arcs render dashed, distinct from the solid consuming / producing arrows.
|
|
248
|
+
|
|
249
|
+
### Decorator for registration
|
|
250
|
+
|
|
251
|
+
Mark functions as Petri net factories with execution mode metadata, useful for discovery and orchestration tooling.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from petritype import petri_net
|
|
255
|
+
|
|
256
|
+
@petri_net(name="data-pipeline", mode="batch")
|
|
257
|
+
def data_pipeline() -> ExecutableGraph:
|
|
258
|
+
return ExecutableGraphOperations.construct_graph([...])
|
|
259
|
+
|
|
260
|
+
@petri_net(name="health-check", mode="cron", schedule="*/5 * * * *")
|
|
261
|
+
def health_check() -> ExecutableGraph:
|
|
262
|
+
return ExecutableGraphOperations.construct_graph([...])
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Modes: `manual` (default), `24/7` (continuous), `batch` (run once), `cron` (scheduled).
|
|
266
|
+
|
|
267
|
+
## Runtime — observable, interactive nets
|
|
268
|
+
|
|
269
|
+
Beyond running a net to completion, `petritype.runtime` turns a net into a **live object you can watch and poke while it runs** — for monitoring, real-time simulations, or interactive tools. The graph is the single source of truth; a `Runner` (a set of functions, no objects to construct) drives it via a passive `RunContext` the caller owns.
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
from petritype.runtime import Runner, RunContext, ExecutionMode, Extend
|
|
273
|
+
|
|
274
|
+
ctx = RunContext(graph=graph, mode=ExecutionMode.CONCURRENT, observers=(render,))
|
|
275
|
+
await Runner.run_to_completion(ctx) # or Runner.step(ctx) / Runner.run_indefinitely(ctx, tick=0.1)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
- **One definition, two execution modes** — `SEQUENTIAL` fires one transition fully before the next; `CONCURRENT` runs independent transitions' bodies as overlapping tasks (wall-clock ≈ max instead of sum). Selected by `RunContext.mode` — the only line that changes.
|
|
279
|
+
- **Observation** — `observers` are plain callables handed the *live graph* after each state change, so any renderer (a marimo notebook, a web frontend) can redraw at its own pace. In-flight transitions are exposed via `graph.in_flight`, so a renderer could for example highlight them by changing their colour while their bodies run. To know *what fired since you last looked*, snapshot `graph.fired_counts` and diff it at the next notification with `fired_since(previous, current)` — lossless in every mode, unlike `graph.last_fired`, which names only one completion per concurrent batch.
|
|
280
|
+
- **Interactive input** — write to a running net by putting typed commands on `RunContext.inbox`, drained between steps: `Extend` / `SetTokens` (places), `SetParam` / `Enable` / `Disable` (transitions). A UI only ever *produces commands* — the runner is the only thing that mutates the net.
|
|
281
|
+
- **Real-time** — `run_indefinitely(ctx, tick=...)` drives the net on an internal clock until `ctx.stop`, surviving idle ticks, so you can inject input live.
|
|
282
|
+
- **Limits** — `Runner.run(ctx, stop_after_n_firings=N)` paces: it always returns a `RunSummary` whose `quiesced` flag tells "stopped by the limit, call again to continue" from "nothing left to fire". `RunContext.error_after_n_firings` is a run-wide fuse for nets that should quiesce quickly: `TooManyFiringsError` is raised *before* the net fires past it (a net that fires exactly n and quiesces is fine).
|
|
283
|
+
- **Offload** — mark a blocking / CPU-bound body `FunctionTransitionNode(..., execution="thread")` and it runs in a thread pool, so it never freezes the loop (and parallelises in concurrent mode).
|
|
284
|
+
- **Control-map** — bind UI widgets to nodes declaratively with `{name: ControlSpec}`, kept off the net; `petritype.marimo_controls` renders them and feeds their values to the inbox (needs the `marimo` extra: `pip install 'petritype[marimo]'`).
|
|
285
|
+
|
|
286
|
+
Runnable examples: `examples/execution_modes/` (sequential vs concurrent, animated) and `examples/interactive/` (live parcel sorters, a read-arc scaler) — open with `uv run --extra examples marimo edit <notebook>`.
|
|
287
|
+
|
|
288
|
+
## When to Use This
|
|
289
|
+
|
|
290
|
+
Petritype is useful when you have stateful data processing where:
|
|
291
|
+
|
|
292
|
+
1. **Data flows through multiple stages with different representations** — the typed places make the shape of each stage explicit.
|
|
293
|
+
2. **Outcomes are not easily predictable** — different result types route to different places, making branching logic visible in the graph rather than hidden in conditionals.
|
|
294
|
+
3. **You need to reason about complex processes** — the graph is both the implementation and the documentation.
|
|
295
|
+
|
|
296
|
+
It has been particularly useful for processes involving many calls to stateful external data sources, where the data flow paths depend on responses and not all paths can be known in advance.
|
|
297
|
+
|
|
298
|
+
## When Not to Use This
|
|
299
|
+
|
|
300
|
+
1. **Simple pipelines** — if your processing is a straightforward chain of pure functions, the Petri net overhead adds complexity without benefit.
|
|
301
|
+
2. **Order-sensitive processing with shared state** — each transition firing mutates the graph in place. If the order matters and is hard to control, this can be a source of bugs.
|
|
302
|
+
3. **Complex net dynamics** — cycles, deadlocks, and infinite loops are all possible in Petri nets and can be difficult to debug. Use the formalism with care.
|
|
303
|
+
|
|
304
|
+
## Background: What is a Petri Net?
|
|
305
|
+
|
|
306
|
+
A [Petri net](https://en.wikipedia.org/wiki/Petri_net) is a bipartite directed graph used to model concurrent processes. It was originally developed by Carl Adam Petri in 1962 and has been applied across chemistry, logistics, manufacturing, protocol verification, and many other fields.
|
|
307
|
+
|
|
308
|
+
The graph has two types of nodes — **places** and **transitions** — connected by directed edges (arcs). Places hold **tokens** representing the state of the system. A transition is enabled when all its input places contain at least one token. When a transition fires, it removes tokens from its inputs and adds tokens to its outputs.
|
|
309
|
+
|
|
310
|
+
What makes Petri nets powerful is that they can model concurrency, synchronisation, and resource contention in a way that is both formally analysable and visually intuitive.
|
|
311
|
+
|
|
312
|
+
Petritype builds on this foundation by making places typed (so tokens must match a declared Python type) and making transitions executable (so they call real functions). The result is a system where the Petri net is not just a model of your process — it *is* your process.
|
|
313
|
+
|
|
314
|
+
## Installation
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
pip install petritype
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
uv add petritype
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Requires Python 3.12+. The base install is deliberately light — `pydantic` only.
|
|
327
|
+
Optional features live behind extras:
|
|
328
|
+
|
|
329
|
+
| Extra | Provides | Install |
|
|
330
|
+
| --- | --- | --- |
|
|
331
|
+
| `viz` | Graphviz rendering (`petritype.plotting`) | `pip install 'petritype[viz]'` |
|
|
332
|
+
| `marimo` | Interactive notebook controls (`petritype.marimo_controls`) | `pip install 'petritype[marimo]'` |
|
|
333
|
+
| `examples` | Dependencies for running the example notebooks (see [Examples](#examples)) | `pip install 'petritype[examples]'` |
|
|
334
|
+
|
|
335
|
+
The `viz` extra also needs the Graphviz `dot` binary on your PATH
|
|
336
|
+
(`brew install graphviz` / `apt install graphviz`).
|
|
337
|
+
|
|
338
|
+
## Examples
|
|
339
|
+
|
|
340
|
+
See the [`examples/`](https://github.com/olenive/petritype/tree/v0.1.1/examples) directory:
|
|
341
|
+
|
|
342
|
+
- **Caching** — database retrieval with cache fallback, demonstrating typed routing for cache hits vs misses
|
|
343
|
+
- **ML Training** — multi-step model training pipeline with evaluation and retraining loops
|
|
344
|
+
- **Time Series** — statistical processing of time series data
|
|
345
|
+
|
|
346
|
+
The notebooks live in the repository — they are not part of the pip package — so
|
|
347
|
+
run them from a clone:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
git clone https://github.com/olenive/petritype
|
|
351
|
+
cd petritype
|
|
352
|
+
uv run --extra examples marimo edit examples/toy/parcel_distribution.py
|
|
353
|
+
```
|