Skip to main content

siphon-control (Python)

Python client for the SIPhon external control plane (siphon-control.v1) — an ARI/ESL-class rail for driving handed-over calls out of process. Built with PyO3 over the async Rust client, so the wire is hidden: no manual JSON, no request-id bookkeeping.

Two connection modes

The plane runs in one of two modes; both are exposed here and share the SAME @on_call decorator and the SAME Call handle — only the transport differs.

  • Inbound-persistent (ControlClient) — the app dials siphon and holds one long-lived socket (does the hello handshake). Simplest to reason about; use it for development and single-process controllers.
  • Per-call-connect (ControlServer) — siphon dials the app per handed-over call, so the app is a WebSocket server. Each accepted connection owns exactly one call and the first frame is a pushed StasisStart (no hello). This is the documented production default for multi-pod controllers: because the accepting socket is the call, "the audio lands on the wrong pod" can't happen.

Inbound-persistent

import asyncio
from siphon_control import ControlClient, ControlError

client = ControlClient(app="ivr-app", token="s3cr3t",
                       url="ws://siphon:9090/control/ws")

@client.on_call
async def handle(call):
    await call.answer()
    try:
        await call.transfer("sip:agent@pbx")   # REFER, awaits correlated reply
    except ControlError as error:
        print("transfer rejected:", error.code)

async def main():
    async with client:          # closes on the way out — see Shutdown below
        await client.run()

asyncio.run(main())

Application-level events

Events an app opts into with control.apps[].events (RegistrationChanged, DialogStateChanged) concern no call, so they never reach on_call. Register a handler for them; run() installs it:

@client.on_app_event
async def on_app_event(event, payload):
    if event == "DialogStateChanged":
        print(payload["aor"], payload["state"])   # early / confirmed / terminated

Per-call-connect

import asyncio
from siphon_control import ControlServer, ControlError

server = ControlServer(app="ivr-app", token="s3cr3t", bind="0.0.0.0:8790")

@server.on_call
async def handle(call):
    await call.answer()
    try:
        await call.transfer("sip:agent@pbx")
    except ControlError as error:
        print("transfer rejected:", error.code)

async def main():
    async with server:
        await server.serve()

asyncio.run(main())

Shutdown

Both classes are async context managers, and async with is the recommended shape. close() is the same thing explicitly.

It matters more than it looks. run() / serve() are driven by a background tokio task, and every handed-over call is dispatched from another one. Nothing joins those tasks and the runtime outlives the interpreter, so an app that finishes without closing leaves them delivering results into an asyncio loop — and then into a Python — that is no longer there. Closing first means there is nothing in flight to strand.

Not closing is handled rather than fatal: a handover arriving after the loop or interpreter has gone is dropped, and a handler cancelled during teardown is not reported as a failure. That is damage control, not a substitute for closing.

API

ControlClient (inbound-persistent)

  • ControlClient(app, token, url=…, protocol=1, reply_timeout_ms=…, reconnect_backoff_ms=…)
  • @client.on_call — register an async (or sync) per-call handler.
  • await client.connect() / await client.run() — connect / drive (reconnect + resync).
  • await client.command(verb, module=None, target=None, args=None) — the generic {module, verb, target, args} primitive for any adapter (SIP today; SMPP/SS7 later).
  • await client.originate(channel, to=None, *, aor=None, strategy=None, total_timeout=None, media=False, sdp=None, body=None, content_type=None, …, session_timer=None) — place an outbound call under a channel id you choose; resolves to {"channel", "call_id", "sip_call_id"} once the INVITE is on the wire. Exactly one media plan (media=True, sdp= or body=). session_timer={"expires", "min_se", "refresher"} runs an RFC 4028 session timer on the call, each key left out taking the server's default. What the server would refuse raises ValueError before a frame goes out.
  • await client.describe() — adapter schema.
  • client.shutdown() — stop the client and unblock run().
  • client.close() — shutdown, plus drop the handler so nothing else is dispatched. async with client: does this on the way out. See Shutdown above.

ControlServer (per-call-connect)

  • ControlServer(app, token, bind="0.0.0.0:8790", reply_timeout_ms=…) — bind is the address the app listens on for siphon to dial; the token is validated on the incoming upgrade.
  • @server.on_call — the SAME decorator + Call handle as ControlClient.
  • await server.bind() — bind the listener; resolves to the bound address string (bind to …:0 to learn the ephemeral port before siphon dials in).
  • server.local_addr — the bound address once bind() / serve() has run, else None.
  • await server.serve() / await server.run() — accept siphon's per-call dials forever (stop by cancelling the task).
  • server.close() — drop the handler so no further accepted call is dispatched. async with server: does this on the way out. See Shutdown above.

Call (shared by both modes)

  • Call verbs: answer(), answer_with(code, …), progress(), reject(code, reason), hangup(reason=None), refer(to) / transfer(to), set_header(name, value), get_header(name), set_var(key, value), get_var(key), command(verb, args=None), next_event().
  • await call.dial(targets, strategy=None, timeout=None, headers=None) rings B-legs while the caller stays unanswered and this app keeps the channel. Each target is a dict: {"uri": ...} is dialed as written, {"aor": ...} is forked to every registered contact over that contact's own captured flow, which is the only way to reach a phone registered on TCP, TLS or WSS behind NAT. A bare string is refused, because it does not say which of the two was meant. With on_answer="bridge" it instead rings phones for a caller the app already answered and anchored (after a greeting or a menu), plays ringback (a tone preset or cadence, True for the default, False for none) while they alert, and bridges the first to pick up; the result adds group_id, total_timeout and the branches rung.
  • await client.originate(channel, aor=..., strategy=None, total_timeout=None, …) rings every phone registered at the AoR, each over its own flow and Path; the first to answer becomes the channel's call. Exactly one of to and aor, and strategy / total_timeout only with aor. Resolves to {"channel", "group_id", "aor", "strategy", "total_timeout", "branches"}.
  • await call.record_start(direction=None, channels=None, max_duration_ms=None, silence_ms=None, path=None) records the call's decoded audio to a wav file and returns the recording_id that await call.record_stop(recording_id=None) addresses (no id stops every recording on the call). The reply is the accept; the RecordingFinished event says the file is closed. siphon-rtp backend only.
  • await call.drop(reason=None, *, ban=False) abandons an unanswered call with nothing on the wire — no final response, no CANCEL — and releases it. Use it for traffic addressed to nothing your controller serves: a 404 confirms the number to an enumeration sweep, silence does not. An answered call raises ControlError with code == "invalid_state" (its dialog is owed a BYE — that is hangup); the reason reaches siphon's log and the CDR, not the peer. ban=True also scores the caller's source toward an auto-ban (security.failed_auth_ban), so a source the controller keeps dropping is refused at the transport.
  • Media verbs play_file(file) / dtmf(digits) raise ControlError with code == "unsupported_verb" until the server implements media.

Errors

A rejected command raises ControlError carrying a stable .code (not_found, forbidden, unsupported_verb, unauthorized, …).

Build

maturin develop        # into the active venv
maturin build --release

The target interpreter is free-threaded CPython 3.14t (the SIPhon runtime); the wheel also loads on a standard GIL build.

License

MIT

Metadata

Release files for siphon-control 0.6.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 siphon-control 0.6.0
File Size Uploaded
siphon_control-0.6.0.tar.gz 142.0 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for siphon-control 0.6.0
File
siphon_control-0.6.0-cp314-cp314t-win_amd64.whl CPython 3.14 CPython 3.14 free-threading Windows x86-64 Details
siphon_control-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.14 CPython 3.14 free-threading Linux glibc 2.17+ x86-64 Details
siphon_control-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.14 CPython 3.14 free-threading Linux glibc 2.17+ ARM64 Details
siphon_control-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl CPython 3.14 CPython 3.14 free-threading macOS 11.0+ ARM64 Details
siphon_control-0.6.0-cp314-cp314-win_amd64.whl CPython 3.14 CPython 3.14 Windows x86-64 Details
siphon_control-0.6.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.14 CPython 3.14 Linux glibc 2.17+ x86-64 Details
siphon_control-0.6.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.14 CPython 3.14 Linux glibc 2.17+ ARM64 Details
siphon_control-0.6.0-cp314-cp314-macosx_11_0_arm64.whl CPython 3.14 CPython 3.14 macOS 11.0+ ARM64 Details

Total release size: 16.8 MB

Release files / siphon_control-0.6.0.tar.gz

Download URL siphon_control-0.6.0.tar.gz
Size 142.0 kB
Tags Source
SHA-256 checksum
How to use checksums
92f14529f683d38c6deaf7193d57a7f35741cbd85c1c98eb782a20a6bbadb960
BLAKE2b-256 checksum
How to use checksums
e9693a6c9b4280a53cdd09c2b764f1f6b92a8fe33c8ddede637340c587161e54
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / siphon_control-0.6.0-cp314-cp314t-win_amd64.whl

Download URL siphon_control-0.6.0-cp314-cp314t-win_amd64.whl
Size 1.9 MB
Tags CPython 3.14 CPython 3.14 free-threading Windows x86-64
SHA-256 checksum
How to use checksums
515626c558753918f932d61bde4e046375fb0e1c43dae1d259c3bcf539948fb7
BLAKE2b-256 checksum
How to use checksums
cb9fb00bcfe30e40744ca917fa563fccaea1e9cce17654d904ef43a34e79044f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / siphon_control-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL siphon_control-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 2.1 MB
Tags CPython 3.14 CPython 3.14 free-threading Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
fecaa278891066ceca28443752cc9a3a094abaf798f5f4a65c7e7a9ea2bd1249
BLAKE2b-256 checksum
How to use checksums
4a6af31666443f98149c6037bcb391fb90fb450f76cc8df3268cf5c354497948
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / siphon_control-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL siphon_control-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 2.3 MB
Tags CPython 3.14 CPython 3.14 free-threading Linux glibc 2.17+ ARM64
SHA-256 checksum
How to use checksums
c978f75b0722c4cbfcecd62ece63a161c3774ff76b0da9c155c6f0fa57c6e0dc
BLAKE2b-256 checksum
How to use checksums
65fef3fd484ca74340d48b4ce3f438928480db6008ae9adf67933c1e2878de9d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / siphon_control-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl

Download URL siphon_control-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl
Size 2.0 MB
Tags CPython 3.14 CPython 3.14 free-threading macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
141a0c94494fcec2a4e06f939ba5d02825510f0267739571cf4030a2e5e00406
BLAKE2b-256 checksum
How to use checksums
678c650b26804a6bf2730a0bd89bf6cda1511d74c6eb10a65c1db78da9fc9210
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / siphon_control-0.6.0-cp314-cp314-win_amd64.whl

Download URL siphon_control-0.6.0-cp314-cp314-win_amd64.whl
Size 1.9 MB
Tags CPython 3.14 Windows x86-64
SHA-256 checksum
How to use checksums
ce201a351fe002ebb49d96c94e8c060460f8ebc8856282ab4584f755116c5546
BLAKE2b-256 checksum
How to use checksums
d7fa6a671d1c3d28a0aabb781240c2c32ac6c9c2b91bf4588c3c745103b5b74a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / siphon_control-0.6.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL siphon_control-0.6.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 2.1 MB
Tags CPython 3.14 Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
acd3069bb1c49eb8f6a8ba851f6f1a48b3fa9b81d0f5a5c064fe860c7787b7a4
BLAKE2b-256 checksum
How to use checksums
c44a7afbcf31d4f243fb9536bd7c7ec798fa8e613061b899abbd3fd6de4da79b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / siphon_control-0.6.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL siphon_control-0.6.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 2.3 MB
Tags CPython 3.14 Linux glibc 2.17+ ARM64
SHA-256 checksum
How to use checksums
f56b5d577038f9c4260418bdea7f60c26fc76978a67ab7696eae1cac676691f1
BLAKE2b-256 checksum
How to use checksums
156709e9e4912e01a031e6c9a43f6e85ccb8b9e326c5d15949b1b28b03d61660
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / siphon_control-0.6.0-cp314-cp314-macosx_11_0_arm64.whl

Download URL siphon_control-0.6.0-cp314-cp314-macosx_11_0_arm64.whl
Size 2.0 MB
Tags CPython 3.14 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
9f16a0615b65c3cc265433d4d78c3a7be5182b8a90a54ce4befae7d7ba0c2708
BLAKE2b-256 checksum
How to use checksums
842025f9bf98902c6347ed6db8525c204522fb2f5f75b677ce0062c35ccc0302
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release history Release notifications | RSS feed

0.8.0

9 release files

0.7.0

9 release files

This release

0.6.0 This release

9 release files

0.5.0

9 release files

0.4.0

9 release files

0.3.0

9 release files

0.2.0

9 release files

0.1.3

9 release files

0.1.2

9 release files

0.1.1

9 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