smpp34
A pure-Rust implementation of the SMPP 3.4 protocol (Short Message Peer-to-Peer): a PDU codec plus an asynchronous (tokio) client and server built on a listener-trait model.
The same engine ships two ways from one source tree, one version: a Rust
crate (cargo add smpp34, zero Python dependencies) and a Rust-backed Python
wheel (pip install smpp34) that exposes the identical async client/server to
asyncio. The whole SMPP machinery — framing, windowing, timers, codec — runs in
the Rust/tokio core; Python crosses the GIL only once per message, and the
extension is free-threaded ("no-GIL") ready. See Python below.
- Client — bind to an SMSC as an ESME (transmitter, receiver or
transceiver), send
submit_sm/data_sm, and receivedeliver_smand session events through your ownSmppClientListener. - Server — accept ESME binds and dispatch
submit_sm,data_sm,cancel_sm, … to yourSmppServerListener; push MT traffic back out withdeliver_sm. - Codec — encode/decode every SMPP 3.4 PDU, including optional TLV parameters.
Sessions handle the SMPP timers (session_init, enquire_link, inactivity,
response), sequence-number windowing, and optional TLS.
Supported PDUs
bind_transmitter · bind_receiver · bind_transceiver (+ resps) · outbind
· unbind · enquire_link · submit_sm · submit_sm_multi · deliver_sm ·
data_sm · query_sm · cancel_sm · replace_sm · alert_notification ·
generic_nack, plus arbitrary TLV (optional parameter) encode/decode.
Install
cargo add smpp34
Quick start — server
Implement SmppServerListener and hand it to an SmppServer. Every trait
method has a default, so you override only the PDUs you care about — typically
on_bind_transceiver and on_submit_sm.
use std::sync::Arc;
use std::net::{IpAddr, Ipv4Addr};
use async_trait::async_trait;
use smpp34::{
SmppServer, SmppServerListener, SmppConnectionInformation, SmppError,
bind_transceiver, bind_transceiver_resp, submit_sm, submit_sm_resp,
};
struct MyEsmeHandler;
#[async_trait]
impl SmppServerListener for MyEsmeHandler {
async fn on_bind_transceiver(
&self, req: bind_transceiver, _c: &SmppConnectionInformation, _s: &String,
) -> bind_transceiver_resp {
if req.system_id == "esme1" && req.password == "secret" {
req.accept("MySMSC".to_string(), Some(0x34)) // 0x34 = SMPP v3.4
} else {
req.reject(SmppError::ESME_RINVPASWD)
}
}
async fn on_submit_sm(
&self, req: submit_sm, _c: &SmppConnectionInformation, _s: &String,
) -> submit_sm_resp {
// ... route the message ...
req.accept("message-id-1".to_string())
}
// Every other method has a sensible default (binds reject, on_data_sm /
// on_cancel_sm reject, on_unbind acks, notifications no-op), so you override
// only what you need.
}
#[tokio::main]
async fn main() {
let listener = Arc::new(MyEsmeHandler);
let mut server = SmppServer::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2775, listener);
server.start().await; // spawns the accept loop
tokio::signal::ctrl_c().await.unwrap();
server.stop().await;
}
Quick start — client
Bind to an SMSC, then submit once the bind completes (the on_smsc_bound
callback hands you an SMSC you can submit through).
use std::sync::Arc;
use smpp34::client::{SmppClient, BIND_TYPE};
// `listener` implements SmppClientListener; its on_smsc_bound callback hands
// you the bound `SMSC` handle to submit through.
let listener = Arc::new(MyClientHandler::new());
let mut client = SmppClient::new(
"smsc.example.net".to_string(), 2775, /* tls = */ false,
BIND_TYPE::TRX,
"esme1".to_string(), "secret".to_string(), "GATEWAY".to_string(),
/* addr_ton */ 0, /* addr_npi */ 0, /* address_range */ String::new(),
listener.clone(),
/* window_size */ 20,
);
client.start().await;
// inside on_smsc_bound(smsc, _): use the fluent builder (only set what you need)
// smsc.submit_sm()
// .source_addr("12345")
// .destination_addr("31600000000")
// .short_message(b"hello")
// .send().await?;
Python
pip install smpp34 gives a Rust-backed wheel exposing the same async
client/server to asyncio. The hot path stays in the Rust/tokio core; the Python
layer is an event-pull bridge that crosses the GIL once per message. abi3 wheels
cover CPython 3.9+, plus free-threaded ("no-GIL") builds.
pip install smpp34
Client — connect() binds and returns an Smsc; await sends and inbound
events:
import asyncio, smpp34
async def main():
client = smpp34.Client(host="smsc.example.net", port=2775,
system_id="esme1", password="secret", bind_type="TRX")
smsc = await client.connect()
resp = await smsc.submit_sm(destination_addr="31600000000",
source_addr="12345", short_message=b"hello")
print(resp.message_id, resp.is_success)
# pull inbound deliver_sm (MO / DLR) — one GIL crossing per message
ev = await smsc.next() # DeliverSmEvent | Disconnected
if isinstance(ev, smpp34.DeliverSmEvent):
print(ev.source_addr, ev.short_message)
await smsc.unbind()
asyncio.run(main())
Server — await next() yields an Esme when a client binds, a
SubmitSmEvent per inbound message (answer with accept/reject), or Unbound:
import asyncio, smpp34
async def main():
server = smpp34.Server(host="0.0.0.0", port=2775, system_id="MySMSC")
await server.start()
sessions = {}
while True:
ev = await server.next()
if isinstance(ev, smpp34.Esme):
sessions[ev.session_id] = ev # stash the MT send handle
elif isinstance(ev, smpp34.SubmitSmEvent):
ev.accept(message_id="abc") # or ev.reject(smpp34.ESME_RMSGQFUL)
# push MT back to that ESME (here, a delivery receipt):
await sessions[ev.session_id].deliver_sm(
source_addr=ev.destination_addr, destination_addr=ev.source_addr,
esm_class=0x04, short_message=b"id:abc stat:DELIVRD")
asyncio.run(main())
Codec — encode/decode PDUs without any I/O:
pdu = smpp34.SubmitSm(source_addr="12345", destination_addr="31600000000",
short_message=b"hello")
wire = pdu.encode()
msg = smpp34.decode(wire) # -> typed SubmitSm / DeliverSm / RawPdu
Runnable samples: python/examples/ (and the Rust
equivalents in examples/ — client.rs / server.rs). A throughput
and leak harness lives in python/perf/, mirroring the Rust
examples/{perf_loopback,leak_check}.rs.
Optional parameters (TLVs)
Inbound TLVs are on pdu.tlvs in wire order, with typed accessors from the
TlvList trait. Outbound, the builders take them directly:
use smpp34::{Tlv, TlvTag, TlvList};
// ESME -> SMSC
smsc.submit_sm()
.source_addr("12345")
.destination_addr("31600000000")
.tlv(TlvTag::MessagePayload, "a body past the 254-byte short_message limit")
.tlv(TlvTag::UserMessageReference, 42u16.to_be_bytes())
.tlv_raw(0x1403, [0xAA, 0xBB]) // vendor-specific tag
.send().await?;
// SMSC -> ESME: a delivery receipt is TLVs, nothing else
esme.deliver_sm()
.source_addr("31600000000")
.destination_addr("12345")
.esm_class(0x04)
.tlvs([
Tlv::from_c_string(TlvTag::ReceiptedMessageId, "abc"),
Tlv::from_u8(TlvTag::MessageStateTlv, 2), // DELIVERED
])
.send().await?;
// reading, on either side
let dlr_id = request.tlvs.receipted_message_id();
For a PDU built up front (relaying a decoded one, or data_sm, whose message
body only exists as the message_payload TLV) use the *_pdu send methods —
the session assigns the sequence number:
let pdu = data_sm::new(0, String::new(), 1, 1, "12345".into(), 1, 1, "31600000000".into(), 0, 0, 8)
.with_tlvs([Tlv::from_tag(TlvTag::MessagePayload, body)]);
smsc.send_data_sm_pdu(pdu).await?;
In Python, tlvs= takes a list of Tlv(tag, value) and the tag constants are on
the module:
await smsc.submit_sm("31600000000", b"hi", source_addr="12345", tlvs=[
smpp34.Tlv(smpp34.TLV_USER_MESSAGE_REFERENCE, (42).to_bytes(2, "big")),
smpp34.Tlv(smpp34.TLV_MESSAGE_PAYLOAD, b"the real body"),
])
ev = await smsc.next()
print([(hex(t.tag), t.value) for t in ev.tlvs])
All 44 spec-defined tags are in TlvTag / smpp34.TLV_*; unknown and
vendor-specific tags (0x1400-0x3FFF) pass through byte for byte.
TLS
Pass tls = true to SmppClient::new to bind over TLS. The TLS transport is
rustls (via tokio-rustls, ring
backend), so no system TLS library is needed to build or run.
- TLS 1.2 and 1.3, ECDHE key exchange with AEAD ciphers only. A peer limited to RSA key exchange or CBC suites cannot be reached.
- The server certificate is verified against the operating system's trust store
(
SSL_CERT_FILE/SSL_CERT_DIRare honoured), and against the address you passed, hostname or IP. It must carry a matching subjectAltName; a certificate that only names the host in its CN is rejected. - No client certificate (mTLS) and no per-client CA yet.
Timers & windowing
SmppServer::new / SmppClient::new use sensible default timers. Use the
*_with_default_timers constructors to set session_init, enquire_link,
inactivity and response timers (milliseconds) and the buffer / window size
explicitly.
Performance
Measured on a 24-core AMD Ryzen AI 9 HX 370 (single box — the SMSC and ESME are
co-located, so the load generator competes for cores; separate hosts scale
further). Reproduce with cargo bench and perf/docker-compose.yml.
Codec — single core, criterion (cargo bench):
| Operation | Throughput |
|---|---|
submit_sm decode |
~6.6 M PDU/s |
submit_sm encode |
~12.3 M PDU/s |
deliver_sm decode / encode |
~6.8 M / ~12.6 M PDU/s |
| TLV decode / encode | ~20 M / ~14 M /s |
End-to-end — real TCP, full submit_sm round-trip (send → submit_sm_resp):
| Load | Throughput | Peak RSS |
|---|---|---|
| 1 ESME, window 64 | ~180k submit_sm/s | 6 MB |
| 4 ESMEs, window 64 | ~635k submit_sm/s | 7 MB |
The SMSC server sustains 625k+ submit_sm/s even pinned to half the cores; past a few sessions the single-box benchmark is bound by the co-located load generator, not by smpp34.
Rust vs Python (and free-threading)
Both languages drive the same Rust/tokio engine, so the protocol work — framing, windowing, timers, codec — runs at full Rust speed either way. The difference is the Python boundary: the event-pull bridge crosses the GIL once per message.
| Path | Throughput | Notes |
|---|---|---|
| Rust — 4 ESMEs, window 64 | ~635k submit_sm/s | full multi-threaded tokio core |
Python — single asyncio loop |
~25k submit_sm/s | one GIL crossing per message (release wheel) |
| Codec decode — GIL CPython | ~4.5M PDU/s (flat vs threads) | the GIL serialises the pyo3 call |
| Codec decode — free-threaded, 4 threads | ~12M PDU/s | no GIL → real parallelism |
A few things worth knowing:
- Build matters. A
maturin developdebug build is ~3× slower; published wheels are--release. The ~25k/s figure is a release build. - Tune the tokio pool with
SMPP34_WORKER_THREADS(default2). On a many-core box, a large pool thrashes the GIL on every future completion and throughput drops (24 threads ≈ 17k/s vs 1 thread ≈ 28k/s); on a free-threaded build, raise it. - Free-threaded ("no-GIL") is correctness-clean — the full test suite passes
with the GIL off (
sys._is_gil_enabled() == False) — and lets CPU-bound Python work (the codec, your per-message handlers) run in parallel, as the codec row shows. Single-process async I/O is bounded by the shared tokio runtime, so scale that across processes. - ~25k submit_sm/s on one event loop is already well above the per-bind rate limits carriers typically impose.
Reproduce: python/perf/perf_loopback.py
(COUNT=200000 SESSIONS=4 WINDOW=64), the thread-scaling demo
python/perf/perf_threads.py, and the leak check
python/perf/leak_check.py — all after a release
build (maturin develop --release).
Profiling
A CPU flamegraph of the loopback under load (scripts/flamegraph.sh) puts
~99.98% of samples in the kernel (the socket/syscall path) — the user-space
codec barely appears. smpp34 is syscall-bound, not CPU-bound: at these rates
the cost is the ~600k+ read/write syscalls per second plus the runtime's
wakeups, not parsing (the codec runs at millions of PDUs/sec). The remaining
headroom is therefore I/O batching — coalescing writes and cutting wakeups —
not faster code.
Memory
scripts/mem_leak_test.sh installs a counting global allocator and asserts
live bytes stay flat (exact — unlike noisy RSS) across both codec round-trips
and bind/unbind churn. RSS holds at single-digit MB while pushing millions of
PDUs. (The bind/unbind check found and now guards a real session-teardown leak —
see the changelog.)
SMPP 3.4 compliance
The PDU / TLV / session support matrix is in
docs/COMPLIANCE.md;
docs/COMPARISON.md
maps smpp34 against other SMPP stacks.
MSRV
Rust 1.80.
Stability
The SMPP 3.4 codec and the client/server session handling are used in
production, and the crate follows Semantic Versioning: the
public Rust API is the contract. The 17-argument send_submit_sm /
send_deliver_sm remain available, but the submit_sm() / deliver_sm()
builders are the recommended way to send.
License
MIT © Real Time Telecom B.V.
Maintained by Real Time Telecom B.V. — carrier-grade telecom infrastructure in Rust.
Metadata
Release files for smpp34 1.5.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 | Actions |
|---|---|---|---|---|
| smpp34-1.5.0.tar.gz | 139.5 kB | With provenance | Details for smpp34-1.5.0.tar.gz |
Built distributions (wheels)
Total release size: 13.8 MB
Release files / smpp34-1.5.0.tar.gz
| Download URL | smpp34-1.5.0.tar.gz |
|---|---|
| Size | 139.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
23bd7286f1e26f5816a8386bf5d54333adbc5bf91718e0657e3e438e309f1ea6
|
|
BLAKE2b-256 checksum How to use checksums |
baf7a70b013ce94e412eae9ad6f5fbb088f05c685674e5135aeb33ef38f4f995
|
| 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 4, 2026.
Transparency logRelease files / smpp34-1.5.0-cp314-cp314t-win_amd64.whl
| Download URL | smpp34-1.5.0-cp314-cp314t-win_amd64.whl |
|---|---|
| Size | 1.5 MB |
| Tags | CPython 3.14 CPython 3.14 free-threading Windows x86-64 |
|
SHA-256 checksum How to use checksums |
5c702f308d6181e9e467c2940b1ca8bb98519ba0cc6e1de81c63784f3ccc5abe
|
|
BLAKE2b-256 checksum How to use checksums |
0645ab3b23de9894dcd03012afcbd8a7ef60549d50c7e0228fda1d7c4ce0b162
|
| 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 4, 2026.
Transparency logRelease files / smpp34-1.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | smpp34-1.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 1.8 MB |
| Tags | CPython 3.14 CPython 3.14 free-threading Linux glibc 2.17+ x86-64 |
|
SHA-256 checksum How to use checksums |
8b69ae10914ecf988d03a88f9fcd0a516412beefbcfe56a3e64e835d1909b1fe
|
|
BLAKE2b-256 checksum How to use checksums |
65b3742664f97b5b7042a70c09d843f9f5c088f099b4c84e1f732cd2aa27cda8
|
| 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 4, 2026.
Transparency logRelease files / smpp34-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl
| Download URL | smpp34-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.6 MB |
| Tags | CPython 3.14 CPython 3.14 free-threading macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
5cf2f96ca730a7a1e509712747ac17d891d75cdb04cfd685a2a0ad6619887c93
|
|
BLAKE2b-256 checksum How to use checksums |
8512c8d277a1a32e671c854e138e3bc566d90d37b4eea35068b86d1f1c111f24
|
| 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 4, 2026.
Transparency logRelease files / smpp34-1.5.0-cp39-abi3-win_amd64.whl
| Download URL | smpp34-1.5.0-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 1.6 MB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
cedf2b52f948a4e4c67d9c57da9d9fe24c97f0867efca606c96fe718fbef6394
|
|
BLAKE2b-256 checksum How to use checksums |
8c95a6558f11ba4a543b1d44b0a50e86e118df76b026436f65195a5fdbb9a3d5
|
| 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 4, 2026.
Transparency logRelease files / smpp34-1.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | smpp34-1.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 1.8 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
abd0a7f65d2e6732fb42b2f76e5ba966252c4757f80a1eb5decbb00fe209adcc
|
|
BLAKE2b-256 checksum How to use checksums |
d7a4502538de56fc855c5b235d505c57aa6683f81cf0facc9a901256d88077d7
|
| 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 4, 2026.
Transparency logRelease files / smpp34-1.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | smpp34-1.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 1.9 MB |
| Tags | CPython 3.9 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
e84b35d283e25e9739df61aee5b36b2dd6a6f9a9c28dbc0e51ffbae8286aabdc
|
|
BLAKE2b-256 checksum How to use checksums |
aa26129d06236721ac0ad2bb14948a7315a2f55a3ad4212a198446676095d34e
|
| 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 4, 2026.
Transparency logRelease files / smpp34-1.5.0-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | smpp34-1.5.0-cp39-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.7 MB |
| Tags | CPython 3.9 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
85254d0161b43780096d46028e965d0832b1326a962c33404ca9f6965926d5ef
|
|
BLAKE2b-256 checksum How to use checksums |
7614668ec342a5c0219ce6df598ce0e2530c34fb2653141e55e1b3ac0ed20753
|
| 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 4, 2026.
Transparency logRelease files / smpp34-1.5.0-cp39-abi3-macosx_10_12_x86_64.whl
| Download URL | smpp34-1.5.0-cp39-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.7 MB |
| Tags | CPython 3.9 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
317633487adbfe8efe881978289184fa13bb2dc900ac64d35d73824e23b676dd
|
|
BLAKE2b-256 checksum How to use checksums |
cae79041faacc509edf315387250990849107a132a2a1bc5ceb5a7d4d0ed96ea
|
| 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 4, 2026.
Transparency logSecurity for smpp34 1.5.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 smpp34-1.5.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 4, 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.
To learn more about protecting yourself from malicious actors, visit our package security documentation.