Skip to main content

cordis_py

Cordis 的 Python 实现:面向动态系统的时空可组合性(spatiotemporal composability)元框架。

pip install cordis-python

PyPI 发布名为 cordis-python(导入名 cordis_py),仓库:https://github.com/xiwen-haochi/cordis_py。


1. 为什么存在(三个痛点 → 三个承诺)

传统动态系统的痛点 cordis_py 的承诺
功能“装上去容易、拆下来难” 一切可逆:卸载插件 = 自动回收它注册的所有东西(服务、事件、路由、资源),无残留
组件初始化顺序写死,换顺序/加组件就是重构 依赖响应式:先装消费者后装提供者也能自动激活;装配顺序无关
能力都在主程序里,扩展 = 改核心代码 声明式组合:应用 = 配置(插件列表)+ 插件(代码);新增能力 = 加一行配置、装一个插件

设计基准:论文《A Programming Paradigm for Spatiotemporal Composability》与 Node.js 版 Cordis v4。

2. 核心概念(30 秒总览)

                ┌────────────────────────────────────────────┐
  宿主        │  Context(根容器)                          │
 (main.py)    │   ├── Fiber:插件运行实例 + 生命周期状态机    │
   │          │   ├── 服务:provide / get / inject(realm 隔离)│
   │          │   ├── 事件:on / emit / parallel / waterfall │
   │          │   └── 效果:一切注册都可逆(LIFO 回收)       │
   │ 注入/装配  └──────────────┬─────────────────────────────┘
   ▼                           │ 声明式装配
  Loader ── 配置文件(JSON/YAML/TOML) ──> 插件列表(url + config)
                     │
                     ▼
               每个插件 = 一段代码 + 一份配置:
               - 提供服务(供别的插件消费)
               - 监听事件 / 组装中间件链(waterfall)
               - 注册路由、资源等一切“装上去”的东西

2.1 概念详解

概念 是什么 为什么 怎么用
Context 依赖容器(根或子)。插件拿到的每个 ctx 都是根链上的一个节点 隔离与服务查找都发生在上下文链上(isolate / intercept / filtered 返回子上下文) root = Context();插件函数第一参数
Fiber 一个插件的运行实例,带生命周期状态机(PENDING → ACTIVE → UNLOADING → DISPOSED) 状态公开可诊断(fiber.state / fiber.unsatisfied),可等待(await fiber)、可重启(restart / update)、可卸载(dispose) ctx.plugin(plugin, config) 返回 Fiber
效果(Effect) 注册到一个 fiber 的可逆动作:disposer / 同步迭代 / 异步迭代 可逆副作用的账本;fiber 卸载按 LIFO 逐个执行 ctx.effect(lambda: undo);服务提供、事件监听、路由注册内部都登记为效果
服务与注入 provide(name, value) 注册;ctx.get(name) 读取;@inject("name") 声明消费 服务的可见性由“值的提供者是否 ACTIVE”决定——这是响应式依赖的载体 ctx.provide("db", db);@inject(["db","log"]) def plugin(ctx, config)
契约校验 provide(version=) + @require("svc", ">=1.0") / 接口谓词 依赖升级的护栏;约束不满足 = 软等待,提供方变化后自动重评 @require("tenants", ">=1.0")
事件 on / once / emit / parallel / serial / bail / waterfall 插件间解耦通信;waterfall 是中间件链(不调用 next() 即拦截) ctx.on("http/request", handler);监听器随插件 fiber 自动回收
配置 插件挂 Config 属性(callable 或 pydantic 模型,可选)校验/转换 配置错误在装载期报错,而不是运行期诡异行为 plugin.Config = validate;internal/config waterfall 可在激活前改写(租户派生)
Loader 从配置装配:reconcile 增量协调、disable/enable、JSON/YAML/TOML 应用状态与配置描述始终一致 Loader(root).include("app.yml")
HMR 开发期热替换:模块依赖图分类 + 事务式重载 + 失败回滚 改插件源码不重启进程(见场景 B) HMR(loader) + watch(["src"])
隔离 isolate(服务 realm)/ filtered + scope(事件可见性)/ 契约 多租户与不可信插件边界(协调式,非 OS 沙箱) 见场景 A 与扩展实战
Bridge 跨进程服务代理与事件贯通(JSON-lines 帧协议) 昂贵共享能力留主进程,worker 代理调用 见场景 C

3. 第一个插件(完整可运行:异步队列工作者)

展示四个核心行为:响应式注入(先装消费者)、服务、事件、后台任务随卸载可逆取消。 与场景 A/B/C 不重叠,且能直接跑:

import asyncio
from cordis_py import Context, inject


def queue_provider(ctx: Context, config: dict):
    """插件一:提供服务 —— 一个异步队列(演示用)。"""
    ctx.provide("queue", asyncio.Queue())


@inject("queue")                                # 声明依赖:queue 未就绪时本插件不激活
def worker_plugin(ctx: Context, config: dict):
    """插件二:消费队列 —— 后台任务 + 事件发布;卸载时可逆取消。"""
    queue = ctx.queue                           # 激活后才可读(依赖已满足)

    async def consume():                        # 后台任务:随插件激活启动
        while True:
            item = await queue.get()
            result = {"item": item, "processed": True}
            ctx.emit("job/done", result)        # 事件:旁观者解耦监听

    task = asyncio.create_task(consume())

    def undo() -> None:
        task.cancel()                           # 卸载动作:撤销后台任务

    ctx.effect(lambda: undo)                    # 登记为可逆效果(fiber dispose 时执行)
    return None


async def main():
    root = Context()
    heard = []
    root.on("job/done", lambda result: heard.append(result["item"]))

    # 先装消费者(worker),后装提供者(queue)——顺序无关,自动激活
    await root.plugin(worker_plugin)            # 依赖未满足:软等待
    await root.plugin(queue_provider)           # 提供者出现 → worker 激活并开始消费
    await asyncio.sleep(0.05)                   # 允许后台激活完成

    root.services["queue"].put_nowait("task-1") # 投递任务
    await asyncio.sleep(0.05)
    assert heard == ["task-1"]                  # 消费链:queue → worker → 事件 → 旁听者

    await root.fiber.dispose()                  # LIFO 回收:消费任务被取消(可逆)


asyncio.run(main())

关键点:

  • ctx.effect(lambda: undo) 的写法是刻意的:effect 会立即执行回调并收集返回值作为 disposer——所以必须返回undo 函数本身;写成 ctx.effect(lambda: task.cancel()) 会在注册时立即取消任务并因返回值非法而报错;
  • 服务/事件/任务全部登记为效果:dispose() 后应用无任何残留(打印、网络、定时器、监听器)。

没有事件循环时用同步 API:root.fiber.dispose_sync()(其余为 restart_sync / update_sync;遇到必须事件循环的操作会抛 AsyncRequiredError)。


4. 场景 A:插件化 HTTP 多租户任务 API(examples/task_api)

4.1 这个 example 为什么这么写(设计目的)

传统同规模 FastAPI 应用的主程序通常是:路由、中间件、依赖、配置、生命周期混在一个文件里。 example 的目的不是“演示功能”,而是展示组织方式:

  • 每个关注点是一个插件:HTTP 装配、认证、限流、审计、指标、业务、租户、健康检查是 8 个正交能力,各自一个文件 + 一份配置——增删能力不动其他文件;
  • 配置即架构图:app.yml 列出全部插件与其参数,读者不看 import 就能画出系统;
  • 装配顺序刻意“错误”:业务插件 tasks 排在 http / tenant 之前——证明依赖响应式(提供者后出现也能自动激活);
  • 宿主只做两件事:创建应用对象(注入 fastapi_app)与加载配置——核心包零 Web 依赖,换框架只换宿主层;
  • 一切可逆:路由注册登记为效果——插件卸载/热替换时自动清理,不会出现“旧路由还在”的僵尸状态。

4.2 目录结构

examples/task_api/
├── main.py                  # 宿主:约 40 行(创建 FastAPI、注入、装配、可选 HMR)
├── app.yml                  # 声明式装配:9 个插件
├── plugins/
│   ├── logger_plugin.py     # log 服务(日志工厂)
│   ├── http_service.py      # gate 中间件 + 瀑布链 + 路由注册表
│   ├── tenant.py            # 租户 realm 隔离 + TaskStore
│   ├── auth.py              # API Key 认证(契约:authenticate(request))
│   ├── jwt_auth.py          # ← JWT 认证(同契约替代实现,见 §7)
│   ├── quota.py             # 限流(瀑布链短路径)
│   ├── audit.py             # 审计日志(事件监听)
│   ├── metrics.py           # 指标计数 + /api/metrics
│   ├── tasks.py             # 业务 CRUD(emit 事件 + 指标埋点)
│   └── health.py            # 健康检查
└── tests/                   # 11 个集成/单元测试(含 JWT 契约)

4.3 宿主:应用与插件唯一的接缝(main.py)

@asynccontextmanager
async def lifespan(app: FastAPI):
    root = Context()
    root.provide("fastapi_app", app)        # ① 宿主注入运行时对象(不进配置文件)
    loader = Loader(root)
    await loader.include(BASE / "app.yml")  # ② 声明式装配(文件即架构)
    if getattr(app.state, "watch", False):  # ③ 开发期热替换(默认开启)
        hmr = HMR(loader)
        app.state.hmr_watcher = hmr.watch([str(BASE / "plugins")])
    yield
    await loader.dispose()                  # ④ 退出:reverse 顺序回收全部插件
    await root.fiber.dispose()

4.4 关键机制(三段代码)

瀑布中间件(http_service 插件):认证 → 插件链 → 真实处理。quota 插件不调用 next() 就返回 429——中间件是“可插拔的插件”,不进 FastAPI 栈。

async def gate(request, call_next):
    rejected = (ctx.get("auth") or NoopAuth()).authenticate(request)  # 认证是服务,可替换
    if rejected is not None:
        return rejected                                                # 401
    return await ctx.waterfall("http/request", tenant, request,
                               fallback=lambda *args: call_next(request))

租户数据隔离(tenant 插件):isolate("tasks", realm) 为每个租户建立独立服务作用域,全局只按 (realm, name) 查找——globex 的请求物理上无法读到 acme 的存储(而不是靠业务里 if tenant == ... 的约定)。

scoped = ctx.isolate("tasks", name)     # 子上下文:该租户 realm
scoped.provide("tasks", TaskStore(name))  # 键 = (name, "tasks")
...
store = self._scopes[name].get("tasks")   # 经该租户作用域解析

可逆路由(tasks 插件):路由注册登记为 fiber 效果——插件重载时旧路由自动移除、新插件重注册同名路由(整体替换,无僵尸路由)。

undos = [routes.add("GET", "/api/tasks", list_tasks), ...]
ctx.effect(lambda: undo_all)            # dispose 时逐一撤销

4.5 插件职责与配置参数

插件 (id) 职责 配置 说明
logger 提供 log 服务(logging 工厂) level(默认 INFO) 其他插件 ctx.get("log")("name")
http 装配:gate 中间件、routes 注册表、app 服务 title 宿主注入 fastapi_app
tenant 每租户 isolate + 专属存储;tenants 服务(version=1.0) tenants: [acme, globex] realm 服务查找
auth X-Tenant / X-API-Key → request.state.tenant keys: {tenant: key}、public_paths 失败 401;契约接口可被 jwt_auth 替换
quota 固定窗口限流(瀑布短路径 429) limit(默认 5)、window 秒(默认 30) 公开路径不限流
tasks 任务 CRUD + 事件 + 指标 page_size(默认 20) 装配顺序无关(响应式)
audit 任务事件审计日志 event_prefix 监听器随 fiber 回收
metrics 计数服务 + /api/metrics namespace(默认 cordis) 键如 taskapi.tasks.created
health 公开健康检查 — 返回状态与租户列表

4.6 运行与验证

cd examples/task_api
pip install -r requirements.txt     # fastapi / uvicorn / httpx / watchdog
python main.py --port 8000          # 启动即启用 HMR(默认开启)
pytest tests/ -q                    # 11 个测试
curl -s http://127.0.0.1:8000/api/health                                     # 公开健康检查
curl -s -X POST -H "X-Tenant: acme" -H "X-API-Key: key-acme" \
     -H "Content-Type: application/json" -d '{"title": "写文档", "priority": 2}' \
     http://127.0.0.1:8000/api/tasks                                          # acme 创建
curl -s -H "X-Tenant: globex" -H "X-API-Key: key-globex" \
     http://127.0.0.1:8000/api/tasks                                          # globex:空(隔离)
# 连续第 6 次请求 → 429 rate_limited;/api/metrics 查看计数

5. 场景 B:开发期热更新(HMR + 配置热更)

目的:改插件源码或装配配置不重启进程。依赖图分类保证“只重载受影响的条目”,事务回滚保证失败不破坏运行状态。配置热更走 Loader 增量协调(fiber.update()),与源码 HMR 并列。

from cordis_py import HMR

hmr = HMR(loader)                          # 安装模块依赖图追踪(运行时导入边 + 已加载模块 AST 补全)
watcher = hmr.watch(["src/plugins"])       # 源码热替换(需要 cordis-python[watch])
# watcher 参数:ignored(默认忽略 .*/__pycache__/node_modules/.venv/cache/data)、
#              debounce(秒,默认 0.1,合并连续保存)、recursive、on_error(path, exc)
config_watcher = hmr.watch_config(["app.yml"])  # 配置热更:改装配配置不重启
# watch_config 参数:targets(必须,配置文件路径)、debounce、backend、on_error

await hmr.reload_file("src/plugins/worker.py")   # 手动触发源码重载:返回受影响条目 id 列表
await hmr.reload_module("myapp.helpers")         # 或按模块名
await hmr.reload_entry("worker")                 # 或按条目 id(连带重载其依赖者)
await hmr.reload_config("app.yml")               # 手动触发配置重读:返回 config 变化的条目 id
print(hmr.affected({"myapp.helpers"}))           # 只预测不执行

await watcher.stop()
await config_watcher.stop()
hmr.dispose()                                    # 卸载追踪器(幂等)

验证热替换:修改 src/plugins/worker.py 保存后 1.5 秒内新逻辑生效;限流窗口、计数器等插件内部状态归零 = 旧实例已被完整替换。支持编辑器原子保存(临时文件 + rename,watcher 视为新建)。

验证配置热更:修改 app.yml 里某插件的 config 保存后,仅该条目被 fiber.update()(撤销旧效果、按新配置重启),其余插件不动;config 无变化的条目是空操作。注意:配置在启动时被 Loader 读入一次并定格在内存 entry.config,因此“改代码默认值”不是配置热更的生效路径——要么改配置(reload_config),要么改代码(reload_file),两条路径互不替代。

机制:变更模块按依赖图分类 accepted/declined(对齐 Node analyzeChanges)→ 重载集 = 变更模块 + 受影响条目模块(拓扑序,依赖先于导入者)→ 快照插件与模块命名空间 → 失败恢复快照并用旧插件重新应用(回滚)。仅纯 Python 源码模块;__main__ / cordis_py 自身变更视为全量重启(不在范围)。


6. 场景 C:跨进程共享服务(Bridge)

目的:昂贵共享能力(模型、缓存客户端、连接池)留在主进程,worker 进程代理调用;服务与事件语义跨进程一致。

# 主进程:暴露服务
from cordis_py import Bridge, Context
server, addr = await Bridge.serve()                          # TCP host:port 或 unix=套接字路径
server.expose(root, "model", ModelService(), version="1.0")  # 可逆:fiber dispose 自动反注册

# worker:代理调用 + 事件
client = await Bridge.connect(addr)                          # "host:port" 或 Unix 路径
result = await client.proxy("model").predict(payload)        # await proxy.method(*args)
client.send_event("model/updated", "v2")                     # 事件贯通(fire-and-forget)
disposer = client.on_event("train/event", handler)           # 接收对端事件(随 disposer 移除)
await client.close()                                         # 幂等:bye → 未决调用失败

边界:hello 握手(protocol=1,5s 超时,不兼容关闭);仅 JSON 兼容值;远端异常重建 RemoteError(类型名/消息),断连后调用抛 RemoteClosed,协议损坏抛 ProtocolError;同步调用链抛 AsyncRequiredError;无认证/加密层,限受信内网。


7. 扩展实战:给应用加 JWT 认证

思路:认证在 cordis_py 里不是“内置功能”,而是一个名为 auth 的服务契约——authenticate(request) -> None | JSONResponse(成功写入 request.state.tenant 并返回 None,失败返回响应)。因此换认证方式 = 写一个实现同契约的插件,替换配置中的插件行。其余插件(http 的 gate、tasks、quota)零改动。

第一步:编写插件(examples/task_api/plugins/jwt_auth.py,标准库 HS256)

class JwtAuthService:
    def __init__(self, secret, issuer="cordis-task-api", audience="cordis-task-api", public_paths=()):
        self._secret = secret.encode()
        ...

    def issue(self, tenant, *, ttl=3600) -> str:      # 签发(管理端/测试用)
        header = {"alg": "HS256", "typ": "JWT"}
        payload = {"sub": tenant, "iss": ..., "aud": ..., "exp": time.time() + ttl}
        signing = b".".join([_b64(json.dumps(header)), _b64(json.dumps(payload))])
        signature = hmac.new(self._secret, signing, hashlib.sha256).digest()
        return (signing + b"." + _b64(signature)).decode()

    def authenticate(self, request) -> JSONResponse | None:   # 契约入口
        if request.url.path in self._public_paths: return None
        token = request.headers.get("authorization", "")[7:]  # "Bearer xxx"
        # 验段数 → hmac.compare_digest 验签 → 校验 iss/aud/exp → request.state.tenant = sub
        ...

def plugin(ctx: Context, config: dict) -> None:
    ctx.provide("auth", JwtAuthService(secret=config["secret"], ...))   # 同名服务即可替换

第二步:注册使用(app.yml 替换 auth 行——其余 8 个插件不动)

- id: auth
  url: plugins.jwt_auth:plugin # 原 plugins.auth:plugin
  config:
    secret: dev-secret-change-me # 生产用环境变量/密钥管理注入
    issuer: task-api
    audience: task-api
    public_paths: [/api/health] # 公开路径白名单

第三步:验证

from plugins.jwt_auth import JwtAuthService
service = JwtAuthService(secret="dev-secret-change-me", issuer="task-api", audience="task-api")
token = service.issue("acme")
# curl -H "Authorization: Bearer <token>" http://127.0.0.1:8000/api/tasks → 200
pytest examples/task_api/tests/test_jwt_auth.py -q   # 成功/缺失/篡改/过期/公开路径 5 项测试

为什么能无缝替换:http 的 gate 只认 ctx.get("auth") 的接口(而不是 import 某个类);worker 侧(tasks/quota/audit)不关心认证实现——这就是“服务契约 + 声明式装配”的扩展力:改配置即可换实现,插件间只认接口不认类。

同样的模式可以继续扩展:把 TaskStore 换 SQLite/PG(存储插件)、把 audit 换 OpenTelemetry(观测插件)、新增“签名校验”瀑布插件(限流同款)——都是“写一个插件 + 配置加一行”。


8. 核心 API 速查

想做什么 用什么 一句话说明
能力可插拔、可拆除 ctx.provide() / ctx.effect() / Fiber.dispose() 可逆副作用是全部语义的底座
声明式装配应用 Loader.include("app.yml") → reconcile / disable / enable 配置文件即架构图(JSON/YAML/TOML)
依赖升级护栏 Service.version + @require("svc", ">=1.0") 版本/接口契约,不满足=软等待
中间件 / 拦截链 ctx.on(event, handler) + waterfall(..., fallback=...) 不调用 next() 即拦截(限流/鉴权)
多租户 ctx.isolate(name, realm) + internal/config overlay realm 服务隔离 + 租户配置派生
不可信插件边界 Context.filtered() / create_scope / scope_target 事件协调式可见性隔离(非安全沙箱)
跨进程服务 Bridge.serve/connect + expose/proxy 远程服务代理 + 事件贯通
开发期热替换 HMR(loader) + watch([...]) 依赖图分类 + 事务式重载,失败回滚
插件自动发现 discover() / load_entry_points() Python 包入口点(插件市场形态)

9. 质量与边界

  • 验证:111 个核心单元/集成测试 + 案例 11 个测试(含 JWT 契约、watcher 原子保存、配置热更、Bridge 协议、HMR 依赖图);ruff 全绿;py.typed 随包发布。
  • 可选依赖:PyYAML / watch / pydantic 均为 extra,核心零第三方运行时依赖(仅 packaging 用于版本约束)。
  • 诚实边界:作用域隔离是协调式(事件/服务可见性),不是恶意代码的安全边界(OS 级沙箱属宿主职责);Bridge 无认证/加密层,限受信内网;跨进程调用仅 JSON 兼容值;HMR 仅纯 Python 源码模块。
  • 版本:0.9.2;0.x 阶段 API 按语义化演进,1.0 前事项:对外契约类型化(mypy)、CI、并发不变量压测。

参考

  • 论文:A Programming Paradigm for Spatiotemporal Composability
  • Node.js 版:Cordis v4 / DeepSeek Harness vendor(本实现完全以其语义为基准,不参考其他 Python Cordis 实现)

Metadata

Release files for cordis-python 0.9.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cordis-python 0.9.2
File Size Uploaded
cordis_python-0.9.2.tar.gz 65.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cordis-python 0.9.2
File Interpreter ABI Platform
cordis_python-0.9.2-py3-none-any.whl Python 3 none any Details

Total release size: 124.1 kB

Release files / cordis_python-0.9.2.tar.gz

Download URL cordis_python-0.9.2.tar.gz
Size 65.7 kB
Tags Source
SHA-256 checksum
How to use checksums
a98e107216f1e543545161c0ddda694e3e779169e126cc4b2242eccf8f3afaa4
BLAKE2b-256 checksum
How to use checksums
f6ac028f241a71c0361055e03efad363fd0b5e1e87041eac3a2e7115dd6247ce
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / cordis_python-0.9.2-py3-none-any.whl

Download URL cordis_python-0.9.2-py3-none-any.whl
Size 58.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1e13a9a1043311083aa7366f1878d67d4561d850dca779ddc3932356b13186bd
BLAKE2b-256 checksum
How to use checksums
69bb2ea4027c826999fa9a3b53c4a60553c45644d2dc4443373d64b64c21b85f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.9.2 This release

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page