Communication Library for Python implementing the most common communication patterns for CyberPhysical Systems.
Project description
commlib-py
Protocol-agnostic Pub/Sub ยท RPC ยท Actions ยท Task Queue for Python
Write your messaging logic once. Switch between MQTT, Redis, AMQP, and Kafka by changing a single import.
Table of Contents
- What is commlib-py?
- 30-Second Quickstart
- Why commlib-py?
- Communication Patterns
- Performance
- Installation
- API Reference
- Advanced
- Examples
- Testing
- Roadmap
- Contributing
- License
- Star History
๐ What is commlib-py?
commlib-py is a communication library for Python implementing the most common messaging patterns โ Pub/Sub, RPC, Actions, and Task Queue โ on top of any message broker, with a single unified API.
It abstracts away MQTT, Redis, AMQP, and Kafka behind a clean, Pydantic-typed interface. Whether you're building IoT pipelines, distributed microservices, or robotic control systems, your application code stays the same regardless of the broker underneath.
โก 30-Second Quickstart
from commlib.msg import PubSubMessage
from commlib.node import Node
# Change this one line to switch to Redis, AMQP, or Kafka โ nothing else changes
from commlib.transports.mqtt import ConnectionParameters
class SensorData(PubSubMessage):
temperature: float = 0.0
humidity: float = 0.0
node = Node(node_name='weather_station', connection_params=ConnectionParameters())
pub = node.create_publisher(msg_type=SensorData, topic='sensors.weather')
node.run()
pub.publish(SensorData(temperature=23.5, humidity=65.0))
Subscriber โ swap mqtt for redis, amqp, or kafka, nothing else changes:
from commlib.transports.redis import ConnectionParameters # swapped to Redis
node = Node(node_name='dashboard', connection_params=ConnectionParameters())
node.create_subscriber(
msg_type=SensorData,
topic='sensors.weather',
on_message=lambda msg: print(f'Temp: {msg.temperature}C Humidity: {msg.humidity}%')
)
node.run_forever()
๐ค Why commlib-py?
Building distributed systems in Python usually means picking a broker and writing boilerplate โ paho-mqtt, redis-py, pika, confluent-kafka all have different APIs, different patterns for RPC, and no built-in support for higher-level primitives like Actions or Task Queues.
commlib-py solves this with one consistent API across all brokers:
paho-mqtt |
redis-py |
pika (AMQP) |
commlib-py | |
|---|---|---|---|---|
| Pub/Sub | โ | โ | โ | โ |
| RPC (Request/Response) | โ DIY | โ DIY | โ DIY | โ built-in |
| Actions w/ feedback | โ | โ | โ | โ built-in |
| Task Queue | โ | โ | โ | โ built-in |
| Typed messages (Pydantic v2) | โ | โ | โ | โ |
| Swap broker in 1 line | โ | โ | โ | โ |
| Cross-broker bridges | โ | โ | โ | โ built-in |
| Automatic connection pooling | โ | manual | โ | โ |
| Wildcard subscriptions | โ | โ | โ | โ unified API |
๐ก Communication Patterns
commlib-py implements four production-grade patterns on top of any supported broker:
| Pattern | Description | Use Case |
|---|---|---|
| Pub/Sub | Fire-and-forget event publishing | Sensor streams, telemetry, events |
| RPC | Typed request/response with timeout | Service calls, queries, commands |
| Actions | Long-running tasks with cancellation & feedback | Robot motion, ML inference, batch jobs |
| Task Queue | Competing-consumer job distribution | Background workers, parallel processing |
All patterns work identically across MQTT, Redis, AMQP, and Kafka.
๐ Performance
- โ 6โ10ร fewer broker connections via connection pooling
- โ 35% faster AMQP throughput with optimized serialization
- โ 390+ tests with continuous benchmarking via GitHub Actions CI/CD
- โ Scaling tests for 1โ100 concurrent publishers
Serialization priority (auto-detected at runtime): orjson โ ujson โ json
See Performance Documentation for detailed benchmarks and analysis.
๐ ๏ธ Installation
Core (no broker dependencies):
pip install commlib-py
With specific broker support:
pip install "commlib-py[mqtt]" # MQTT via paho-mqtt
pip install "commlib-py[redis]" # Redis via redis-py + hiredis
pip install "commlib-py[amqp]" # AMQP via pika (RabbitMQ)
pip install "commlib-py[kafka]" # Kafka via confluent-kafka
pip install "commlib-py[all]" # All brokers
For maximum performance:
pip install "commlib-py[all,performance]" # Adds orjson, msgpack, lz4 compression
From source:
git clone https://github.com/robotics-4-all/commlib-py.git
cd commlib-py
pip install -e ".[dev]"
Requires Python 3.9+
๐ API Reference
Node
A Node is the central building block of commlib-py. It follows the Component-Port-Connector model โ each node binds to a single broker and exposes typed input/output ports for communication.
| Port Type | Endpoint | Description |
|---|---|---|
| Input | Subscriber |
Listens for messages on a topic |
| Input | RPCServer |
Handles RPC requests |
| Input | ActionService |
Executes long-running tasks with feedback |
| Output | Publisher |
Publishes messages to a topic |
| Output | RPCClient |
Sends RPC requests and waits for responses |
| Output | ActionClient |
Sends goals to an action service |
| InOut | TopicBridge |
Bridges Pub/Sub between two brokers |
| InOut | RPCBridge |
Bridges RPC between two brokers |
| InOut | PTopicBridge |
Wildcard-based cross-broker topic bridge |
Supported endpoint types across all transports:
| Interface Type | MQTT | Redis | AMQP | Kafka |
|---|---|---|---|---|
| RPCClient / RPCServer | โ | โ | โ | โ |
| Publisher / Subscriber | โ | โ | โ | โ |
| MPublisher (multi-topic) | โ | โ | โ | โ |
| PSubscriber (wildcard) | โ | โ | โ | โ |
| ActionService / ActionClient | โ | โ | โ | โ |
| TaskProducer / TaskWorker | โ | โ | โ | โ |
from commlib.node import Node
from commlib.msg import RPCMessage
from commlib.transports.redis import ConnectionParameters
class AddTwoIntMessage(RPCMessage):
class Request(RPCMessage.Request):
a: int = 0
b: int = 0
class Response(RPCMessage.Response):
c: int = 0
def add_two_int_handler(msg):
return AddTwoIntMessage.Response(c=msg.a + msg.b)
if __name__ == '__main__':
conn_params = ConnectionParameters()
node = Node(
node_name='add_two_ints_node',
connection_params=conn_params,
heartbeats=True,
heartbeat_uri='nodes.add_two_ints.heartbeat',
heartbeat_interval=10,
ctrl_services=True,
)
rpc = node.create_rpc(
msg_type=AddTwoIntMessage,
rpc_name='add_two_ints_node.add_two_ints',
on_request=add_two_int_handler
)
node.run_forever(sleep_rate=1)
Node constructor:
class Node:
def __init__(self,
node_name: Optional[str] = "",
connection_params: Optional[Any] = None,
debug: Optional[bool] = False,
heartbeats: Optional[bool] = True,
heartbeat_interval: Optional[float] = 10.0,
heartbeat_uri: Optional[str] = None,
compression: CompressionType = CompressionType.NO_COMPRESSION,
ctrl_services: Optional[bool] = False,
workers_rpc: Optional[int] = 4):
Node methods:
node.create_subscriber(...) # Pub/Sub subscriber
node.create_publisher(...) # Pub/Sub publisher
node.create_rpc(...) # RPC server
node.create_rpc_client(...) # RPC client
node.create_action(...) # Action service
node.create_action_client(...) # Action client
node.create_mpublisher(...) # Multi-topic publisher
node.create_psubscriber(...) # Wildcard subscriber
node.create_task_producer(...) # Task queue producer
node.create_task_worker(...) # Task queue worker
node.run_forever(sleep_rate=1) # Block and run
node.run(wait=True) # Start (optionally blocking)
node.stop() # Graceful shutdown
Req/Resp - RPCs
RPCs enable typed synchronous request/response between distributed components. Define your message schema once โ the same class is used by both client and server.
Server Side Example
from commlib.msg import RPCMessage
from commlib.node import Node
from commlib.transports.mqtt import ConnectionParameters
class AddTwoIntMessage(RPCMessage):
class Request(RPCMessage.Request):
a: int = 0
b: int = 0
class Response(RPCMessage.Response):
c: int = 0
# Callback function of the add_two_ints RPC
def add_two_int_handler(msg) -> AddTwoIntMessage.Response:
print(f'Request Message: {msg.__dict__}')
resp = AddTwoIntMessage.Response(c = msg.a + msg.b)
return resp
if __name__ == '__main__':
conn_params = ConnectionParameters()
node = Node(node_name='add_two_ints_node',
connection_params=conn_params)
rpc = node.create_rpc(
msg_type=AddTwoIntMessage,
rpc_name='add_two_ints_node.add_two_ints',
on_request=add_two_int_handler
)
node.run_forever(sleep_rate=1)
Client Side Example
import time
from commlib.msg import RPCMessage
from commlib.node import Node
from commlib.transports.mqtt import ConnectionParameters
class AddTwoIntMessage(RPCMessage):
class Request(RPCMessage.Request):
a: int = 0
b: int = 0
class Response(RPCMessage.Response):
c: int = 0
if __name__ == '__main__':
conn_params = ConnectionParameters()
node = Node(node_name='myclient', connection_params=conn_params)
rpc = node.create_rpc_client(
msg_type=AddTwoIntMessage,
rpc_name='add_two_ints_node.add_two_ints'
)
node.run()
msg = AddTwoIntMessage.Request()
while True:
resp = rpc.call(msg) # returns AddTwoIntMessage.Response
print(resp)
msg.a += 1
msg.b += 1
time.sleep(1)
Pub/Sub
Event-driven messaging with typed, Pydantic-validated messages. Publishers and subscribers are completely decoupled โ they don't need to know about each other.
Write a Simple Publisher
from commlib.msg import MessageHeader, PubSubMessage
from commlib.node import Node
from commlib.transports.mqtt import ConnectionParameters
class SonarMessage(PubSubMessage):
distance: float = 0.001
horizontal_fov: float = 30.0
vertical_fov: float = 14.0
if __name__ == "__main__":
conn_params = ConnectionParameters(host='localhost', port=1883)
node = Node(node_name='sensors.sonar.front', connection_params=conn_params)
pub = node.create_publisher(msg_type=SonarMessage, topic='sensors.sonar.front')
node.run()
msg = SonarMessage()
while True:
pub.publish(msg)
msg.distance += 0.1
time.sleep(1)
Write a Simple Subscriber
import time
from commlib.msg import MessageHeader, PubSubMessage
from commlib.node import Node
from commlib.transports.mqtt import ConnectionParameters
class SonarMessage(PubSubMessage):
header: MessageHeader = MessageHeader()
range: float = -1
hfov: float = 30.6
vfov: float = 14.2
def on_message(msg):
print(f'Received front sonar data: {msg}')
if __name__ == '__main__':
conn_params = ConnectionParameters()
node = Node(node_name='node.obstacle_avoidance', connection_params=conn_params)
node.create_subscriber(msg_type=SonarMessage,
topic='sensors.sonar.front',
on_message=on_message)
node.run_forever(sleep_rate=1)
Wildcard Subscriptions
Subscribe to multiple topics using a single pattern. Use PSubscriber for pattern-based subscriptions and MPublisher for multi-topic publishing:
from commlib.node import Node
from commlib.transports.mqtt import ConnectionParameters
def on_msg_callback(msg, topic):
print(f'Message at topic <{topic}>: {msg}')
if __name__ == '__main__':
conn_params = ConnectionParameters()
node = Node(node_name='wildcard_subscription_example',
connection_params=conn_params)
# Subscribe to all topic.* messages
node.create_psubscriber(topic='topic.*', on_message=on_msg_callback)
# Publish to multiple topics from a single instance
pub = node.create_mpublisher()
node.run(wait=True)
while True:
pub.publish({'a': 1}, 'topic.a')
pub.publish({'b': 1}, 'topic.b')
time.sleep(1)
Topic Notation Conversion
commlib-py uses a unified dot-notation (a.b.c) internally, converting automatically to/from each broker's native format.
| Protocol | Separator | Wildcard | Example |
|---|---|---|---|
| commlib (unified) | . |
* |
sensors.*.temperature |
| MQTT | / |
+ (single) / # (multi) |
sensors/+/temperature |
| Redis | . |
* |
sensors.*.temperature |
| AMQP | . |
* / # |
sensors.*.temperature |
| Kafka | - |
* |
sensors-*-temperature |
Conversion utilities:
from commlib.utils import (
convert_topic_notation,
topic_to_mqtt, topic_from_mqtt,
topic_to_redis, topic_from_redis,
topic_to_kafka, topic_from_kafka,
topic_to_amqp, topic_from_amqp,
)
# MQTT -> commlib
commlib_topic = topic_from_mqtt("sensors/+/temperature")
# Result: "sensors.*.temperature"
# commlib -> MQTT
mqtt_topic = topic_to_mqtt("sensors.*.temperature")
# Result: "sensors/+/temperature"
# Cross-protocol: Kafka -> MQTT
mqtt_topic = convert_topic_notation("sensors-temperature", "kafka", "mqtt")
# Result: "sensors/temperature"
# IoT hierarchy
commlib_topic = convert_topic_notation("home/+/sensors/+/temperature", "mqtt", "commlib")
# Result: "home.*.sensors.*.temperature"
Supported protocol names: "commlib", "mqtt", "redis", "amqp", "kafka"
Preemptive Services with Feedback (Actions)
Actions are pre-emptive services with asynchronous feedback publishing. Built for long-running tasks that can be cancelled mid-execution โ robot motion, ML inference, batch processing.
Each Action message defines three sub-messages: Goal, Result, and Feedback.
Write an Action Service
import time
from commlib.action import GoalStatus
from commlib.msg import ActionMessage
from commlib.node import Node
from commlib.transports.redis import ConnectionParameters
class MoveByDistanceMsg(ActionMessage):
class Goal(ActionMessage.Goal):
target_cm: int = 0
class Result(ActionMessage.Result):
dest_cm: int = 0
class Feedback(ActionMessage.Feedback):
current_cm: int = 0
def on_goal_request(goal_h):
c = 0
res = MoveByDistanceMsg.Result()
while c < goal_h.data.target_cm:
if goal_h.cancel_event.is_set(): # Supports mid-execution cancellation
break
goal_h.send_feedback(MoveByDistanceMsg.Feedback(current_cm=c))
c += 1
time.sleep(1)
res.dest_cm = c
return res
if __name__ == '__main__':
conn_params = ConnectionParameters()
node = Node(node_name='myrobot.node.motion', connection_params=conn_params)
node.create_action(
msg_type=MoveByDistanceMsg,
action_name='myrobot.move.distance',
on_goal=on_goal_request
)
node.run_forever()
Write an Action Client
import time
from commlib.action import GoalStatus
from commlib.msg import ActionMessage
from commlib.node import Node
from commlib.transports.redis import ConnectionParameters
class MoveByDistanceMsg(ActionMessage):
class Goal(ActionMessage.Goal):
target_cm: int = 0
class Result(ActionMessage.Result):
dest_cm: int = 0
class Feedback(ActionMessage.Feedback):
current_cm: int = 0
def on_feedback(feedback):
print(f'ActionClient <on-feedback> callback: {feedback}')
def on_result(result):
print(f'ActionClient <on-result> callback: {result}')
def on_goal_reached(result):
print(f'ActionClient <on-goal-reached> callback: {result}')
if __name__ == '__main__':
conn_params = ConnectionParameters()
node = Node(node_name='action_client_example_node',
connection_params=conn_params)
action_client = node.create_action_client(
msg_type=MoveByDistanceMsg,
action_name='myrobot.move.distance',
on_goal_reached=on_goal_reached,
on_feedback=on_feedback,
on_result=on_result
)
node.run()
action_client.send_goal(MoveByDistanceMsg.Goal(target_cm=5))
resp = action_client.get_result(wait=True)
print(f'Action Result: {resp}')
node.stop()
๐๏ธ Advanced
Endpoints (Low-level API)
For applications that don't fit the Node model, endpoints can be constructed directly without binding to a node:
from commlib.transports.redis import RPCService
from commlib.transports.amqp import Subscriber
from commlib.transports.mqtt import Publisher, RPCClient
Or use endpoint_factory for dynamic construction:
import time
from commlib.endpoints import endpoint_factory, EndpointType, TransportType
def callback(data):
print(data)
if __name__ == '__main__':
topic = 'endpoints_factory_example'
mqtt_sub = endpoint_factory(
EndpointType.Subscriber,
TransportType.MQTT)(topic=topic, on_message=callback)
mqtt_sub.run()
mqtt_pub = endpoint_factory(
EndpointType.Publisher,
TransportType.MQTT)(topic=topic, debug=True)
mqtt_pub.run()
data = {'a': 1, 'b': 2}
while True:
mqtt_pub.publish(data)
time.sleep(1)
All endpoint types:
| Endpoint | Description | Supported Protocols |
|---|---|---|
RPCClient / RPCServer |
Typed request/response | MQTT, Redis, AMQP, Kafka |
Publisher / Subscriber |
Fire-and-forget messaging | MQTT, Redis, AMQP, Kafka |
MPublisher |
Publish to multiple topics | MQTT, Redis, AMQP, Kafka |
PSubscriber |
Wildcard topic subscription | MQTT, Redis, AMQP, Kafka |
WPublisher / WSubscriber |
Wrapped endpoints | MQTT, Redis |
ActionService / ActionClient |
Long-running tasks w/ feedback | MQTT, Redis, AMQP, Kafka |
TaskProducer / TaskWorker |
Competing-consumer job queue | MQTT, Redis, AMQP, Kafka |
B2B Bridges
Bridge messages between brokers โ including across different protocols. Ideal for Edge-to-Cloud pipelines, multi-broker architectures, and protocol translation.
import commlib.transports.redis as rcomm
import commlib.transports.mqtt as mcomm
from commlib.bridges import RPCBridge, TopicBridge
def redis_to_mqtt_rpc_bridge():
"""[RPC Client] -> [Redis Broker] -> [MQTT Broker] -> [RPC Service]"""
br = RPCBridge(
from_uri='ops.start_navigation',
to_uri='thing.robotA.ops.start_navigation',
from_broker_params=rcomm.ConnectionParameters(),
to_broker_params=mcomm.ConnectionParameters(),
)
br.run()
def redis_to_mqtt_topic_bridge():
"""[Producer] -> [Redis Broker] -> [MQTT Broker] -> [Consumer]"""
br = TopicBridge(
from_uri='sonar.front',
to_uri='thing.robotA.sensors.sonar.front',
from_broker_params=rcomm.ConnectionParameters(),
to_broker_params=mcomm.ConnectionParameters(),
)
br.run()
Pattern-based bridge (PTopicBridge) โ bridge all topics matching a wildcard:
from commlib.msg import PubSubMessage
from commlib.bridges import PTopicBridge
import commlib.transports.redis as rcomm
import commlib.transports.mqtt as mcomm
class SonarMessage(PubSubMessage):
distance: float = 0.001
horizontal_fov: float = 30.0
vertical_fov: float = 14.0
if __name__ == '__main__':
br = PTopicBridge(
'sensors.*', # From: all sensor topics on Redis
'myrobot', # To: namespace on MQTT
rcomm.ConnectionParameters(),
mcomm.ConnectionParameters(),
msg_type=SonarMessage,
)
br.run()
Bridge class signatures:
class Bridge:
def __init__(self,
from_uri: str,
to_uri: str,
from_broker_params: BaseConnectionParameters,
to_broker_params: BaseConnectionParameters,
auto_transform_uris: bool = True,
debug: bool = False): ...
class RPCBridge(Bridge):
def __init__(self, msg_type: RPCMessage = None, *args, **kwargs): ...
class TopicBridge(Bridge):
def __init__(self, msg_type: PubSubMessage = None, *args, **kwargs): ...
class PTopicBridge(Bridge):
def __init__(self,
msg_type: PubSubMessage = None,
uri_transform: List = [],
*args, **kwargs): ...
TCP Bridge
Forwards raw TCP packets between two endpoints:
[Client] ------> [TCPBridge, port=xxxx] ---------> [TCP endpoint, port=xxxx]
A one-to-one connection is established between the bridge and the endpoint.
REST Proxy
Enables invocation of REST services via message brokers. An RPC call is translated into a proper HTTP request โ useful for exposing REST APIs into broker-based architectures.
class RESTProxyMessage(RPCMessage):
class Request(RPCMessage.Request):
base_url: str
path: str = '/'
verb: str = 'GET'
query_params: Dict[str, Any] = {}
path_params: Dict[str, Any] = {}
body_params: Dict[str, Any] = {}
headers: Dict[str, Any] = {}
class Response(RPCMessage.Response):
data: Union[str, Dict, int]
headers: Dict[str, Any]
status_code: int = 200
See commlib-rest-proxy for a ready-to-deploy Docker image.
Web Gateway
A WebSocket/HTTP gateway that exposes your broker topics and RPCs to web clients.
See commlib-web-gw for a ready-to-deploy Docker image.
๐ค Examples
The examples/ directory contains runnable examples for every pattern:
| Example | Pattern | Description |
|---|---|---|
simple_pubsub/ |
Pub/Sub | Basic publisher and subscriber |
simple_rpc/ |
RPC | Request/response service |
simple_action/ |
Action | Preemptive service with feedback |
node/ |
Node | Node with multiple endpoints |
node_decorators/ |
Node | Decorator-based node definition |
node_inherit/ |
Node | Inheritance-based node pattern |
bridges/ |
Bridge | Topic and RPC cross-broker bridges |
ptopic_bridge/ |
Bridge | Wildcard pattern bridge |
multitopic_publisher/ |
Pub/Sub | Multi-topic publishing |
minimize_conns/ |
Pub/Sub | Connection pooling example |
topic_aggregator/ |
Pub/Sub | Topic merge/aggregation |
endpoint_factory/ |
Low-level | Direct endpoint construction |
๐งช Testing
commlib-py uses pytest. Broker integration tests require Docker.
Quick test (unit only, no broker needed, ~15s):
make ci
With linting:
make ci-strict
Full suite including broker integration tests (~2min, requires Docker):
make ci-full
Individual steps:
pytest --ignore=tests/mqtt --ignore=tests/redis --ignore=tests/benchmarks -v # Unit only
pytest tests/benchmarks/ -v -m smoke # Benchmarks
make coverage # Coverage report
Standalone benchmarks (no broker needed):
python benchmark/bench_scaling.py --transport mock --test all
See benchmark/README.md for full benchmark documentation.
๐๏ธ Roadmap
- Protocol-agnostic architecture
- MQTT, Redis, AMQP support
- Kafka support (full endpoint parity)
- RPCServer for AMQP and Kafka
- Task Queue pattern across all transports
- Connection pooling (6-10x fewer connections)
- Optimized serialization (35% throughput improvement)
- Comprehensive integration testing
- AsyncIO transport backend
๐ค Contributing
- ๐ฌ Join the Discussions โ questions, ideas, feedback
- ๐ Report Issues โ bugs and feature requests
- ๐ก Submit Pull Requests โ contributions welcome
Contributing Guidelines
- Fork the repository
- Clone your fork:
git clone https://github.com/{YOUR_ACCOUNT}/commlib-py.git - Create a branch:
git checkout -b my-feature - Make your changes and run
make ci-strictto verify - Commit:
git commit -m 'Add my feature' - Push:
git push origin my-feature - Open a Pull Request
๐ License
commlib-py is released under the MIT License.
๐ Star History
If commlib-py is useful to you, a โญ helps the project grow and reach more developers!
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file commlib_py-0.13.2.tar.gz.
File metadata
- Download URL: commlib_py-0.13.2.tar.gz
- Upload date:
- Size: 135.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
692a1546d5cdae22aa4535304d9654dda5e630f067c6ccdc4235da373bcbffdc
|
|
| MD5 |
03f65c673511aa4b97f0a004f5e0044f
|
|
| BLAKE2b-256 |
05e09203195ad201cc5ca3ef6c112af525e79d4a710f6bce8b6d5af4ed7b1b11
|
File details
Details for the file commlib_py-0.13.2-py3-none-any.whl.
File metadata
- Download URL: commlib_py-0.13.2-py3-none-any.whl
- Upload date:
- Size: 97.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d01e0a22a9796e749492769f7bd0e5a02a5a0e568537a3fe5b6adc7db41ccbfd
|
|
| MD5 |
3ad5c38b85f85b18e20d0506a5a04866
|
|
| BLAKE2b-256 |
3e37cedfdde32f9c6022aab0f62dc6b20194c95102b2584472925aded4f79ad9
|