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 thehellohandshake). 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 pushedStasisStart(nohello). 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 theCallit placed once the INVITE is on the wire, so the answer, a rejection or the ring timeout arrive onawait call.next_event()andawait call.hangup()CANCELs or BYEs it (call.payloadis the server's reply). A channel id still live on this client isControlErrorconflict, refused before anything is sent. Exactly one media plan (media=True,sdp=orbody=).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 withaor=), for a peer that identifies the tenant by it, andcontact_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 raisesValueErrorbefore a frame goes out.await client.describe()— adapter schema.client.shutdown()— stop the client and unblockrun().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=…)—bindis the address the app listens on for siphon to dial; the token is validated on the incoming upgrade.@server.on_call— the SAME decorator +Callhandle asControlClient.await server.bind()— bind the listener; resolves to the bound address string (bind to…:0to learn the ephemeral port before siphon dials in).server.local_addr— the bound address oncebind()/serve()has run, elseNone.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 whileserve()is still running included.async with server:does this on the way out. See Shutdown above.
Call (shared by both modes)
Callverbs: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,ringandprogresstakecontact_host=, the host siphon presents in its own Contact toward the caller (fixed once an 18x or 2xx went out; another host isinvalid_state), anddial/routetake the same ascaller_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. Alsoset_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 (ofdialorroute) 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 (ofdial,{"uri"}or{"aor"}, or ofroute) 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'stls.verify_server. Adialtarget of either form may carry"reroute_after_progress": False(abool, anything else raisesTypeError; server defaultTrue): 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. Adialtarget of either form may also carry"profile", the media profile for that branch alone over the dial's ownprofile; 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 noprofileanchors 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 anon_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 refusesofferon a connecting dial. Withon_answer="bridge"it instead rings phones for a caller the app already answered and anchored (after a greeting or a menu), playsringback(a tone preset or cadence,Truefor the default,Falsefor none) while they alert, and bridges the first to pick up; the result addsgroup_id,total_timeoutand thebranchesrung.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 byDialBranchFailedwith causecancelled, and the dial ends inDialFailedwith code 487;reasonis thecauseof a bridging dial'sDialFailed(defaultcancelled). RaisesControlError(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 (aTransferRequestedevent).modeis"terminate"(siphon dials the target),"transparent"(siphon relays the REFER) or"controller": siphon answers202and dials nothing, this app moves the parties itself and then reports withawait call.complete_refer(code, reason=None), withintimeoutseconds (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 onesdialtakes, for the leg the transfer dials;number_policynames a number policy configured on the server for the numbers in them, orformatgives one format ("e164","plain","international","national"). A target URI together withaor=,mode="controller"with any argument that describes a leg, andtimeoutwith another mode each raiseValueErrorbefore 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 oftargetandaor=(ValueErrorotherwise). The reply says the INVITE is on the wire;PeerReplaced/ReplaceFailedis the outcome.await call.bridge(with_channel, on_peer_hangup=None)joins this call to another leg the app owns, andawait call.unbridge(reason=None)parts them, both legs staying answered and held. The outcome arrives asChannelBridged/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: afile, adb_id, ablob, atone(a preset such as"ringback_eu"or a cadence) or aurl(HTTP or HTTPS).repeatis a total play count, or"inf"to play until stopped; anything else raisesValueError.gain_decibelsplays louder (positive) or quieter (negative).play_file(file),stop(),dtmf(digits, …),hold()andunhold()are the other media verbs.holdis 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"), andawait 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 oftoandaor, andstrategy/total_timeoutonly withaor. Resolves to the placedCalltoo, whosepayloadis{"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 therecording_idthatawait call.record_stop(recording_id=None)addresses (no id stops every recording on the call). The reply is the accept; theRecordingFinishedevent 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: a404confirms the number to an enumeration sweep, silence does not. An answered call raisesControlErrorwithcode == "invalid_state"(its dialog is owed a BYE — that ishangup); the reason reaches siphon's log and the CDR, not the peer.ban=Truealso 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_verbis what a verb raises when the configured media backend cannot carry it out: the stream and record verbs on rtpengine or rtpproxy, andplay(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.0
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 | |
|---|---|---|---|---|
| siphon_control-0.10.0.tar.gz | 189.1 kB | With provenance | Details |
Built distributions (wheels)
Total release size: 18.2 MB
Release files / siphon_control-0.10.0.tar.gz
| Download URL | siphon_control-0.10.0.tar.gz |
|---|---|
| Size | 189.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
99941471cb1d64462a1fbc3fc1625c049a314c72b5f4cbfb3431fc4904825653
|
|
BLAKE2b-256 checksum How to use checksums |
15f4bfff59f97686e5beaa48995a9ffdf47ca7cfe1527a58fb42056ed03b10c9
|
| 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 Oct 20, 2026.
Transparency logRelease files / siphon_control-0.10.0-cp314-cp314t-win_amd64.whl
| Download URL | siphon_control-0.10.0-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 |
d641ab40da4ec2591d5876b77243baccf36e76c46aa527c2b2ee2353b19fa443
|
|
BLAKE2b-256 checksum How to use checksums |
3e39661c0dcc4d6ce15ba07edf8be81bae400566b2d595631aabd116bf4e08e4
|
| 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 Oct 20, 2026.
Transparency logRelease files / siphon_control-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | siphon_control-0.10.0-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 |
89a7a4554666768040d94c4beaef4e822a2081f8ec0d7ff7592c24258c75e4a1
|
|
BLAKE2b-256 checksum How to use checksums |
4d559fa942e43ad4f35ad5b784f71f012cb9303ad8f292f58dd904a71b7d3d5f
|
| 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 Oct 20, 2026.
Transparency logRelease files / siphon_control-0.10.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | siphon_control-0.10.0-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 |
c27b3a5a74449b5e1291b80e24121369aa4631271548ae102ac6d7252ffcf55d
|
|
BLAKE2b-256 checksum How to use checksums |
2fe58881d30767b623435855704186219dcdd3305fd83bf796d3bb7c432e8cef
|
| 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 Oct 20, 2026.
Transparency logRelease files / siphon_control-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl
| Download URL | siphon_control-0.10.0-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 |
cb18400bd8704c26b5d821c261a803b117d3402496ee95c3527ff33e9c84e476
|
|
BLAKE2b-256 checksum How to use checksums |
0cfdd38081d293190f3e99711827b203739e175110b2b85b0017341ef2f99ed5
|
| 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 Oct 20, 2026.
Transparency logRelease files / siphon_control-0.10.0-cp314-cp314-win_amd64.whl
| Download URL | siphon_control-0.10.0-cp314-cp314-win_amd64.whl |
|---|---|
| Size | 2.1 MB |
| Tags | CPython 3.14 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
0ffe247522b40dc612837c7b5f57d2fc7fcaa8faea1b006d614881868f9775d5
|
|
BLAKE2b-256 checksum How to use checksums |
125d60e01cf6ab57882bdd9443b33417c9cbaa5b90f7b70ffbbe29220e50906b
|
| 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 Oct 20, 2026.
Transparency logRelease files / siphon_control-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | siphon_control-0.10.0-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 |
9e3ab2823fd60e4700a0363778d127ee93dbaf43bd14719a5f6d446648709e06
|
|
BLAKE2b-256 checksum How to use checksums |
efb9f7ae82acceac86a184d21f03486bae8f6c6dec045e6f6648ad8f7fb3c410
|
| 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 Oct 20, 2026.
Transparency logRelease files / siphon_control-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | siphon_control-0.10.0-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 |
b3868d7ba1e1582f3135948b6a89aa3c8dae810fa171cce0cb5db0da201bc2b0
|
|
BLAKE2b-256 checksum How to use checksums |
367e15c3ee870cc3d5c4310e8d6806d32cc1f066a4f855b03e3708bcd00ac916
|
| 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 Oct 20, 2026.
Transparency logRelease files / siphon_control-0.10.0-cp314-cp314-macosx_11_0_arm64.whl
| Download URL | siphon_control-0.10.0-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 |
5bcbfb814c78116ac70388d0c7230662114b0d925be265f1cf36ce6822d49200
|
|
BLAKE2b-256 checksum How to use checksums |
ce9710d30647bc11613b4ecf74db50fe8f126e3d671c5fa05afc40b8a3934c9b
|
| 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 Oct 20, 2026.
Transparency logSecurity for siphon-control 0.10.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.
Full provenance
All 9 files in this release have been uploaded with provenance metadata.
Attestation for siphon_control-0.10.0.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 20, 2026.
Transparency logA 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.