This project has been archived by its maintainers, and is no longer receiving any updates.
ModuLink MVP Documentation Draft
A minimal, composable, and observable async function orchestration ecosystem. Concepts build progressively from simple examples to advanced patterns.
1. Quick Start Example
from modulink import Chain, Context
from modulink.middleware import Logging, Timing
async def validate_email(ctx: Context) -> Context:
if "email" not in ctx:
ctx["error"] = "Missing email"
return ctx
async def send_welcome(ctx: Context) -> Context:
print(f"Welcome sent to {ctx['email']}")
return ctx
# Build a Chain with two Links (auto-named and wired)
signup = Chain(validate_email, send_welcome)
# Attach middleware for observability
signup.use(Logging())
signup.use(Timing())
# Execute with context
result = await signup.run({"email": "alice@example.com"})
# Inspect structure
print(signup.inspect())
# {
# "nodes": ["validate_email", "send_welcome"],
# "edges": [
# {"source":"validate_email","target":"send_welcome","condition":true}
# ]
# }
Start simple: define pure async functions, chain them, add middleware, and run.
2. Core Concepts
Recent Implementation Updates:
Chain.runnow executes links sequentially, supports middleware hooks, and handles exceptions by storing them in the context.- Hybrid error handling: when a link raises an exception, the chain checks for connected error/timeout handlers and routes execution accordingly.
- Example middleware (
Logging,Timing) are implemented and can be attached to a chain. - Dynamic docstrings for
ChainandListenerinstances reflect current configuration and update on mutation. - VSCode extension roadmap and TODOs added for future developer tooling.
2.1. Link
A Link is a pure unit of work:
from typing import Protocol, Callable, Awaitable
from modulink import Context
class Link(Protocol):
name: str # inferred from function or class name
__call__: Callable[[Context], Awaitable[Context]]
- Automatic Naming from
func.__name__or class name. - Docstrings preserved in
link.__doc__. - Single Responsibility: transforms input
Contextto outputContext. - Pure: no side-effects, no branching, no error handling.
2.2. Chain
A Chain is a named graph of Links:
from modulink import Chain, Context
# Auto-named by assignment: 'signup'
signup: Chain = Chain(validate_email, send_welcome)
- Auto-wiring: adjacent Links connected with
condition=True. - API:
add_link(link)connect(source, target, condition)use(middleware)run(ctx) -> Contextinspect() -> dict
2.3. Condition & Connection
Define edges explicitly for branching:
from typing import Union, Callable
from modulink import Context
ConditionExpr = Union[bool, Callable[[Context], bool]]
True⇒ always take edgeFalse⇒ neverlambda ctx: bool⇒ custom
signup.connect(
source = validate_email,
target = handle_error,
condition = lambda ctx: "error" in ctx
)
2.4. Middleware
class Middleware(Protocol):
async def before(self, link: Link, ctx: Context) -> None: ...
async def after(self, link: Link, ctx: Context, result: Context) -> None: ...
- Read-only: inspect
Context, log/metrics, no mutation - Attach via
.use()
3. Hybrid Error Handling
| Layer | Handles Errors? | Mechanism |
|---|---|---|
| Link | Optional | try/except → ctx['error'] |
| Chain | Always | wraps Link calls → ctx['exception'] |
| Connect | Routing | condition=lambda ctx: 'error' in ctx |
Expected errors in Links, unexpected caught by Chain, all routed via `` predicates.
4. Integration with External Services
Turn external calls into Links:
- HTTP via
httpx - gRPC via
grpc.aio - Message Queues via
aiokafka,aio-pika - Databases via
asyncpg - WebSockets via
websockets
Each integration is just an async function Link in your Chain.
5. Listeners (Triggers)
First-class server bindings that implement async __call__(self, ctx: Context) -> Context:
5.1. HTTP Listener (FastAPI)
from modulink import Chain
from modulink.listeners.http import HttpListener
signup = Chain(validate_email, send_welcome)
signup_listener = HttpListener(
chain = signup,
path = "/signup",
methods = ["POST"]
)
signup_listener.serve(port=8000)
5.2. TCP Listener
from modulink.listeners.tcp import TcpListener
echo_listener = TcpListener(
chain = echo_chain,
port = 9000
)
Unit-testable by direct call; extensible by subclassing ``.
6. Best Practices
6.1. File Organization
project/
├── app.py # Links, Chains, Listeners (business logic)
├── server.py # Bootstraps FastAPI/TCP server, mounts listeners
├── listeners.py # Custom listeners (BaseListener subclasses)
├── links.py # Pure Link definitions
└── chains.py # Chain compositions (optional)
6.2. Chain Docstring Injection
The Chain class automatically keeps its docstring up to date with the current structure (links, connections, middleware) using its internal _update_doc() method. This method is called whenever the chain is mutated (adding links, connections, or middleware), so IDE hovers and documentation always reflect the latest state.
Example:
from modulink import Chain
def a(ctx): "A link"; return ctx
def b(ctx): "B link"; return ctx
chain = Chain(a, b)
print(chain.__doc__)
# Shows links, connections, and middleware
chain.add_link(lambda ctx: ctx)
print(chain.__doc__)
# Docstring updates automatically
No monkeypatching is needed; docstrings are always current thanks to _update_doc().
7. Advanced Connection Examples
Showcasing verbose, multi-branch connection setups for complex flows.
from modulink import Chain, Context
async def validate_email(ctx: Context) -> Context:
"""Ensure 'email' exists; simulate timeout or validation error."""
# simulate conditions
if ctx.get("simulate_timeout"):
ctx["timeout"] = True
return ctx
if "email" not in ctx:
ctx["error"] = "Missing email"
return ctx
async def send_welcome(ctx: Context) -> Context:
"""Send welcome email if validation passed."""
print(f"Welcome sent to {ctx['email']}")
return ctx
async def handle_error(ctx: Context) -> Context:
"""Handle validation errors."""
print("Validation error:", ctx.get("error"))
return ctx
async def handle_timeout(ctx: Context) -> Context:
"""Handle timeouts separately."""
print("Operation timed out for user", ctx.get("user_id"))
return ctx
# Compose chain with primary and fallback branches
signup = Chain(validate_email, send_welcome)
# Register additional Links
signup.add_link(handle_error)
signup.add_link(handle_timeout)
# Explicit branching with predicates
signup.connect(
source = validate_email,
target = handle_timeout,
condition = lambda ctx: ctx.get("timeout", False)
)
signup.connect(
source = validate_email,
target = handle_error,
condition = lambda ctx: "error" in ctx
)
# Default path when no error/timeout
signup.connect(
source = validate_email,
target = send_welcome,
condition = lambda ctx: not ("error" in ctx or ctx.get("timeout", False))
)
# Now, run with different contexts:
await signup.run({"user_id": 123, "simulate_timeout": True})
# routes to handle_timeout
await signup.run({})
# routes to handle_error
await signup.run({"email": "alice@example.com"})
# routes to send_welcome
8. Example: Running an Exported (Static) Chain
After exporting a chain to a static module (e.g., exported_chain.py), you can use it as a normal Python module.
The chain is immutable and safe for production use.
static_run_example.py
import asyncio from exported_chain import chain # 'chain' is the exported, static Chain object
async def main(): ctx = {} result = await chain.run(ctx) print(result)
if name == "main": asyncio.run(main())
This script will execute the static chain just like the original, but with all mutation methods disabled.
The exported file is pure Python and can be versioned, audited, and deployed as a static artifact.
🖥️ CLI Tools & Usage
ModuLink provides several CLI tools for visualization, documentation, and automation. These are available in the modulink/ folder:
cli_visualize.py: Visualize a chain as SVG/Graphviz.modulink-doc: Command-line documentation browser for ModuLink topics.
Visualize a Chain
python -m modulink.cli_visualize <path_to_chain_file>
- Generates a visual representation (SVG/Graphviz) of your chain.
Run the Documentation CLI
python -m modulink.modulink-doc <topic>
- Prints documentation for a specific topic (e.g.,
chain,middleware,examples).
Example: Run CLI Integration
python examples/cli_example.py --input-dir ./data --output-dir ./out
- Runs a CLI pipeline using Click (see
examples/cli_example.py).
Metadata
Release files for modulink-py 4.0.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| modulink_py-4.0.3.tar.gz | 23.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| modulink_py-4.0.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 45.6 kB
Release files / modulink_py-4.0.3.tar.gz
| Download URL | modulink_py-4.0.3.tar.gz |
|---|---|
| Size | 23.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d0c2fcc3fe24c11b23799954859c4ed1f7141fae8c8c85a6a6f1a4fba8a54552
|
|
BLAKE2b-256 checksum How to use checksums |
2fc37941666dc781735ff2af9395b608c0f600bbc79ac6d6b04ddc045d78a9e3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.12.9
|
Release files / modulink_py-4.0.3-py3-none-any.whl
| Download URL | modulink_py-4.0.3-py3-none-any.whl |
|---|---|
| Size | 22.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4f5ccdb214536a00b45ec998bd05f73d9efd0c8be796c283a6a3065c359934d8
|
|
BLAKE2b-256 checksum How to use checksums |
245410ac1ddc0b8d01036b351106a0c66d6f087526eb69aed55cb3c425ad6b97
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.12.9
|
Security for modulink-py 4.0.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.