Skip to main content

pycordis

Distribution pycordis-sdk, import name pycordis.

Write Cordis plugins in Python. Python plugins run inside the original Node Cordis runtime: every Python plugin is a real Cordis fiber with its own effects, listeners, services and disposal, and Cordis HMR hot-reloads Python modules.

uv pip install pycordis-sdk
import asyncio, pycordis

def greeter(ctx, config):
    ctx.on('greet', lambda name: print(f"{config['greeting']}, {name}"))
    return lambda: print('greeter disposed')

async def main():
    async with pycordis.App() as app:
        await app.ctx.plugin(greeter, {'greeting': 'Hello'})
        app.ctx.emit('greet', 'Cordis')

asyncio.run(main())

Requirements: Python ≥ 3.12 and Node.js ≥ 22.15 on PATH (or PYCORDIS_NODE=/path/to/node). Linux and macOS. pycordis has no Python dependencies. The wheel bundles the Cordis runtime used by DeepSeek Harness: cordis 4.1, the Loader, Include, Group, Timer, HMR and logger-console plugins, with their dependencies and license notices. To use a DeepSeek Harness checkout's built vendor/ packages instead, pass App(vendor_dir=...) or set PYCORDIS_VENDOR_DIR.

Model

App spawns a Node process that hosts one root Cordis Context. The two runtimes exchange newline-delimited JSON frames over a pair of pipes.

  • Synchronous, re-entrant calls.
    • Every cross-runtime call blocks its caller until the other side answers. While waiting, each side serves the other side's nested calls.
    • This keeps Cordis's synchronous APIs exact: emit, bail, waterfall, accessors, Service.check and Standard Schema validation.
    • Node serves every request while it waits. Python serves only requests nested in its own pending call. Other Node-originated calls, such as timers or plugin activation, run once Python's event loop is idle. Python code therefore keeps JS-like run-to-completion between awaits.
  • Async values. A Python async def called from JS starts eagerly, like a JS async function, and returns a Promise. A JS Promise becomes an awaitable.
  • Reference semantics.
    • Only immutable values are copied: None/null, bools, numbers, strings, bytes and symbols.
    • Every mutable value crosses as a live proxy of the object in its home runtime. A Python dict provided as a service, an event payload mutated by a waterfall listener, or a JS config object is therefore one shared object.
    • Sending a proxy back to its home runtime returns the original object, so is holds.
    • pycordis.js.copy(value) and pycordis.to_py(js_value) make explicit snapshots.
  • Threading. Bridge calls must run on the App event-loop thread. From other threads, use app.call_soon_threadsafe.
  • Lifetime. A proxy releases its remote object when it is garbage-collected. Reference cycles that cross the bridge are not collected.

API

Python names mirror the Cordis API. Methods use snake_case, with camelCase aliases where the JS name differs.

Cordis (JS) pycordis
ctx.extend/isolate/intercept, ctx.root, ctx.baseUrl same; also ctx.base_url
Context.effect/filter/isolate/intercept, Context.is class attributes (symbols); Context.is_(value)
ctx.get/set/provide/accessor/mixin same; accessor(name, get=fn(ctx, receiver, error), set=fn(ctx, value, receiver, error))
ctx.foo (service access) ctx.foo, ctx['foo'], ctx.foo = value; Cordis inject rules apply
ctx.effect(execute, label) same; execute may return a disposer, None, a coroutine, a generator or an async generator of disposers; returns a Disposer whose call result is always awaitable
ctx.plugin/inject same; a plugin is a function, a class, an object with apply, or a module with apply; metadata comes from attributes (name, Config, inject, provide, intercept) or @pycordis.plugin(...)
ctx.emit/parallel/serial/bail/waterfall, ctx.on/once same; parallel and serial return awaitables; options are True or {'prepend': …, 'global': …}
Fiber Fiber with FiberState (IntEnum), await fiber, fiber.await_(), raw_config, effective_config, resolve_config, update, restart, and get_effects() returning EffectMeta
Service (init, check, invoke, extend, tracker, resolveConfig) pycordis.Service; members __cordis_init__, __cordis_check__, __cordis_invoke__, …, or decorators such as @Service.init
Config (Standard Schema) pycordis.schema builders, or JS Schemastery schemas
CordisError, ValidationError, AggregateError pycordis.CordisError, ValidationError, JsAggregateError (an ExceptionGroup); other JS errors raise JsError
any JS module await app.js.import_(spec), where spec is a bundled package name, a node: builtin, a path or a data URL
from pycordis import Service, schema

class Counter(Service):
    Config = schema.object({'start': schema.number().default(0)})

    def __init__(self, ctx, config):
        super().__init__(ctx, 'counter')
        self.value = config['start']

    def increment(self):
        self.value += 1
        return self.value

def consumer(ctx):
    print(ctx.counter.increment())
consumer.inject = ['counter']

Services read through a context are traced, as in Cordis. Inside a method of a Service reached as ctx.svc, self.ctx is the caller's context, so effects the service registers belong to the caller's fiber.

Reading a missing JS property raises AttributeError. Use getattr(obj, key, None) when the property may be absent.

Configuration files and HMR

async with pycordis.App() as app:
    await app.load_config('cordis.yml')             # Cordis Loader + Include
    await app.enable_hmr(root=['.'], debounce=100)  # Cordis HMR
# cordis.yml
- id: counter
  name: ./plugins/counter.py
  config: { start: 10 }
- id: consumer
  name: python:my_package.consumer     # a module on sys.path
  inject: [counter]

!!js expressions in config can read Python services.

Each Python module joins Node's module graph as a synthetic ES module. Its static imports mirror the module's local Python imports. The Cordis HMR therefore handles Python plugins unchanged:

  • watching and debounce
  • accept/decline analysis
  • plugin swaps that keep the raw config
  • rollback with a code frame on syntax or execution errors
  • Include refresh
  • hmr/change and hmr/reload

A changed Python module that the host imported directly, not through Loader, calls ctx.loader.exit(). This is the counterpart of HMR externals.

Reload semantics are those of Cordis HMR: a plugin whose provided service still has live dependents cannot be re-provided by a swap. Pure-JS plugins behave the same way.

License

MIT. The bundled Cordis runtime and its dependencies are MIT or ISC; their notices ship in pycordis/_node/runtime/THIRD_PARTY_LICENSES.txt.

Metadata

Release files for pycordis-sdk 1.0.0

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

Source distribution (sdist)

Source distribution for pycordis-sdk 1.0.0
File Size Uploaded Provenance Actions
pycordis_sdk-1.0.0.tar.gz 151.3 kB Without provenance Details for pycordis_sdk-1.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pycordis-sdk 1.0.0
File Interpreter ABI Platform Actions
pycordis_sdk-1.0.0-py3-none-any.whl Python 3 none any Details for pycordis_sdk-1.0.0-py3-none-any.whl

Total release size: 307.1 kB

Release files / pycordis_sdk-1.0.0.tar.gz

Download URL pycordis_sdk-1.0.0.tar.gz
Size 151.3 kB
Tags Source
SHA-256 checksum
How to use checksums
87de23a58f7741098b523f6827ea3749dbf79bfed859879877acbc24fa749012
BLAKE2b-256 checksum
How to use checksums
ffada3aeb871dba7a3196e460027494f4101216086da0b551b6931ab60e8f548
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.8

Release files / pycordis_sdk-1.0.0-py3-none-any.whl

Download URL pycordis_sdk-1.0.0-py3-none-any.whl
Size 155.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
55dc6519576324f4bac6db1441a0162a7febc88a56438bf3c118a97d24f81518
BLAKE2b-256 checksum
How to use checksums
e1c8d66dfb08406a674d61313af370c82e4d2937aaa7187bfdbe58c9e3a2375b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.8

Security for pycordis-sdk 1.0.0

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.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

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