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

RegistrationChanged (events: [registration]) and RegistrantChanged (events: [outbound_registration]) carry a seq that rises by one per event. On connecting, and whenever it skips a number, ask for the whole set and apply only events numbered above its seq:

state = await client.registrations()          # {"seq": ..., "registrations": [...]}
trunks = await client.outbound_registrations()

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, contact_host=None, contact_user=None) — place an outbound call under a channel id you choose; resolves to the Call it placed once the INVITE is on the wire, so the answer, a rejection or the ring timeout arrive on await call.next_event() and await call.hangup() CANCELs or BYEs it (call.payload is the server's reply). A channel id still live on this client is ControlError conflict, refused before anything is sent. 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. contact_host="tenant-a.example.com" presents that host in siphon's own Contact on the call (on every leg with aor=), for a peer that identifies the tenant by it, and contact_user="trunk-7" the user part of that Contact, which is otherwise userless (<sip:trunk-7@sbc.example.net:5061;transport=tls>). 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, one accepted while serve() is still running included. async with server: does this on the way out. See Shutdown above.

Call (shared by both modes)

  • Call verbs: answer(), answer_with(code, …), answer_anchored(profile=None, ws_uri=None), ring(reason=None), progress(), reject(code, reason), hangup(reason=None), refer(to) / transfer(to), route(targets, strategy="sequential", headers=None, *, caller_contact_host=None, caller_contact_user=None). answer, answer_with, answer_anchored, ring and progress take contact_host=, the host siphon presents in its own Contact toward the caller (fixed once an 18x or 2xx went out; another host is invalid_state), and dial / route take the same as caller_contact_host=. contact_user= / caller_contact_user= on the same verbs set the user part of that Contact, which is otherwise userless, and stick the same way. Also set_header(name, value), get_header(name), remove_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, profile=None, from_uri=None, from_display=None, p_asserted_identity=None, privacy=None, on_answer=None, ringback=None, caller_contact_host=None, caller_contact_user=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. A {"uri": ...} target (of dial or route) may carry "route", the route set its INVITE carries, topmost first: ["<sip:term@scscf.example;lr;odi=4f1c>"]. That is how an IMS application server sends a session back through the S-CSCF with the Route the S-CSCF gave it. Any target (of dial, {"uri"} or {"aor"}, or of route) may carry "contact_host", the host siphon presents in its own Contact on that leg in place of the address it advertises, for a peer that identifies the tenant or account by that host. Only the host changes. Any target may also carry "contact_user", the user part of that Contact (<sip:trunk-7@sbc.example.net:5061;transport=tls>), alone or beside "contact_host". Any target may also carry "verify_server" (a bool): verify the server certificate when that leg opens a new outbound TLS connection, over the gateway it routes to and the server's tls.verify_server. A dial target of either form may carry "reroute_after_progress": False (a bool, anything else raises TypeError; server default True): a sequential dial then ends on that target once it has rung, its ring timeout, busy or other failure being the dial's, for a hunt across several routes to one destination. A target that never rang, and a 503 with Retry-After, still move the hunt on; a 6xx ends a sequential dial either way. A dial target of either form may also carry "profile", the media profile for that branch alone over the dial's own profile; a target without one inherits the dial's. One ring can then reach a phone behind NAT that needs a latching relay and a trunk that asks for SDES-SRTP. A connecting dial that names no profile anchors every branch or none, so either every target names one or none does. offer="early" (or "offer" on a target, which wins) sends each phone of an on_answer="bridge" dial an SDP offer in its INVITE, allocated on the media engine from the caller's media, for peers that refuse a delayed-offer INVITE; "delayed" is the server's default. The server refuses offer on a connecting dial. 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 call.cancel_dial(reason=None) gives up on the dial that is ringing and leaves the caller as the dial found it. Every phone still ringing is CANCELled, each reported by DialBranchFailed with cause cancelled, and the dial ends in DialFailed with code 487; reason is the cause of a bridging dial's DialFailed (default cancelled). Raises ControlError (invalid_state) when nothing is ringing, and once a phone has answered and is being bridged.
  • await call.accept_refer(target=None, next_hop=None, mode=None, profile=None, *, aor=None, from_uri=None, from_display=None, p_asserted_identity=None, privacy=None, headers=None, timeout=None, number_policy=None, format=None) accepts a pending inbound REFER (a TransferRequested event). mode is "terminate" (siphon dials the target), "transparent" (siphon relays the REFER) or "controller": siphon answers 202 and dials nothing, this app moves the parties itself and then reports with await call.complete_refer(code, reason=None), within timeout seconds (default 60, at most 180). aor= names the target by its registered address-of-record in place of a URI: it is dialled over the flow its phone registered on, every registered contact rings and the first to answer is kept. The identity arguments are the ones dial takes, for the leg the transfer dials; number_policy names a number policy configured on the server for the numbers in them, or format gives one format ("e164", "plain", "international", "national"). A target URI together with aor=, mode="controller" with any argument that describes a leg, and timeout with another mode each raise ValueError before a frame goes out. await call.reject_refer(code, reason=None) declines the REFER.
  • await call.replace_peer(target=None, next_hop=None, replace_a_leg=None, profile=None, timeout=None, *, aor=None, from_uri=None, from_display=None, p_asserted_identity=None, privacy=None, headers=None, number_policy=None, format=None) swaps one party of an answered call for a freshly dialled target, with no REFER involved; the replaced leg stays up while the target rings. Exactly one of target and aor= (ValueError otherwise). The reply says the INVITE is on the wire; PeerReplaced / ReplaceFailed is the outcome.
  • await call.bridge(with_channel, on_peer_hangup=None) joins this call to another leg the app owns, and await call.unbridge(reason=None) parts them, both legs staying answered and held. The outcome arrives as ChannelBridged / BridgeFailed.
  • await call.play(file=None, db_id=None, blob=None, repeat=None, start_ms=None, duration_ms=None, to_tag=None, *, tone=None, url=None, gain_decibels=None) plays an announcement on the caller's media. Exactly one source: a file, a db_id, a blob, a tone (a preset such as "ringback_eu" or a cadence) or a url (HTTP or HTTPS). repeat is a total play count, or "inf" to play until stopped; anything else raises ValueError. gain_decibels plays louder (positive) or quieter (negative). play_file(file), stop(), dtmf(digits, …), hold() and unhold() are the other media verbs. hold is a media gate, not a SIP hold, and is refused (invalid_state) on a call the engine only relays.
  • await call.stream_start(ws_uri, direction=None, channels=None, *, mode="tee", sample_rate=None, profile=None) streams the call's audio to a WebSocket server, as a copy ("tee") or a takeover ("bridge"), and await call.stream_stop(*, mode="tee") detaches it. siphon-rtp backend only.
  • 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 the placed Call too, whose payload is {"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.
  • unsupported_verb is what a verb raises when the configured media backend cannot carry it out: the stream and record verbs on rtpengine or rtpproxy, and play(repeat="inf") there.

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.10.1

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.10.1
File Size Uploaded Provenance Actions
siphon_control-0.10.1.tar.gz 189.1 kB With provenance Details for siphon_control-0.10.1.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for siphon-control 0.10.1
File Actions
siphon_control-0.10.1-cp314-cp314t-win_amd64.whl CPython 3.14 CPython 3.14 free-threading Windows x86-64 Details for siphon_control-0.10.1-cp314-cp314t-win_amd64.whl
siphon_control-0.10.1-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 for siphon_control-0.10.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
siphon_control-0.10.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.14 CPython 3.14 free-threading Linux glibc 2.17+ ARM64 Details for siphon_control-0.10.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
siphon_control-0.10.1-cp314-cp314t-macosx_11_0_arm64.whl CPython 3.14 CPython 3.14 free-threading macOS 11.0+ ARM64 Details for siphon_control-0.10.1-cp314-cp314t-macosx_11_0_arm64.whl
siphon_control-0.10.1-cp314-cp314-win_amd64.whl CPython 3.14 CPython 3.14 Windows x86-64 Details for siphon_control-0.10.1-cp314-cp314-win_amd64.whl
siphon_control-0.10.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.14 CPython 3.14 Linux glibc 2.17+ x86-64 Details for siphon_control-0.10.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
siphon_control-0.10.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.14 CPython 3.14 Linux glibc 2.17+ ARM64 Details for siphon_control-0.10.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
siphon_control-0.10.1-cp314-cp314-macosx_11_0_arm64.whl CPython 3.14 CPython 3.14 macOS 11.0+ ARM64 Details for siphon_control-0.10.1-cp314-cp314-macosx_11_0_arm64.whl

Total release size: 18.2 MB

Release files / siphon_control-0.10.1.tar.gz

Download URL siphon_control-0.10.1.tar.gz
Size 189.1 kB
Tags Source
SHA-256 checksum
How to use checksums
6aa91d870d370920ea44b6fc16f54a8872ef953e2e284e2afb13b15e51604cc5
BLAKE2b-256 checksum
How to use checksums
7cb6c99d2b741d451ec25f832ce83bf1c4a154deb101726e59266d0116e77a69
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 22, 2026.

Transparency log

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

Download URL siphon_control-0.10.1-cp314-cp314t-win_amd64.whl
Size 2.1 MB
Tags CPython 3.14 CPython 3.14 free-threading Windows x86-64
SHA-256 checksum
How to use checksums
35074f2195c7b1b052fc60e838e201ae03fe03b0d2638d9da813849b74e91ad1
BLAKE2b-256 checksum
How to use checksums
c6bdbfb4f6c0401f12d5446a3781e90e28f6e351f747b2450eee48f79794305b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 22, 2026.

Transparency log

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

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

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 Oct 22, 2026.

Transparency log

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

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

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 Oct 22, 2026.

Transparency log

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

Download URL siphon_control-0.10.1-cp314-cp314t-macosx_11_0_arm64.whl
Size 2.1 MB
Tags CPython 3.14 CPython 3.14 free-threading macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
29e07990ac068d42c09ab956a4ee5a65916575d27a0bf52767b70edfe5549b95
BLAKE2b-256 checksum
How to use checksums
c2ea8cb3d6dc60bf4dc6a0f34bb6e2b6c23ea3ca859bb5d38cbef54ddcee3bac
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 22, 2026.

Transparency log

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

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

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 Oct 22, 2026.

Transparency log

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

Download URL siphon_control-0.10.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 2.3 MB
Tags CPython 3.14 Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
8b1f899c238c8b5d3772e6132ffe3f6a60bac25582f3b4e20050a18494976c77
BLAKE2b-256 checksum
How to use checksums
4bea78c10e377e25ea42dc4e01d6358f548cb6ccd4b68f30172b0812d35b17c8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 22, 2026.

Transparency log

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

Download URL siphon_control-0.10.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 2.5 MB
Tags CPython 3.14 Linux glibc 2.17+ ARM64
SHA-256 checksum
How to use checksums
32be1b0bbd2dc48ab94711f997ebfdf1bc7ab1f8b25c94a1f1ffb78348582ef3
BLAKE2b-256 checksum
How to use checksums
50eba8cd9ac10b38e13c6ad66ae95b81eeca5f2f51c42a5a338b09fbc0c6d325
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 22, 2026.

Transparency log

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

Download URL siphon_control-0.10.1-cp314-cp314-macosx_11_0_arm64.whl
Size 2.2 MB
Tags CPython 3.14 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
9d24772f12b3cacd6a33de2f5c180172f88ab1561b6ff5308d248c1cecccd87b
BLAKE2b-256 checksum
How to use checksums
ff6506df8ed20a783e45aef31929f67cad4fe90f68edd4dbb11ff24d00a3c19f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Oct 22, 2026.

Transparency log

Security for siphon-control 0.10.1

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.

Full provenance

All 9 files in this release have been uploaded with provenance metadata.

Select from 9 files

Attestation for siphon_control-0.10.1.tar.gz

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 Oct 22, 2026.

Transparency log

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.

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