diff --git a/SConstruct b/SConstruct index ca5b7b6cb720b1..220ccd116336fe 100644 --- a/SConstruct +++ b/SConstruct @@ -176,7 +176,7 @@ Clean(["."], cache_dir) # Build common module SConscript(['common/SConscript']) Import('_common') -common = [_common, 'json11', 'zmq'] +common = [_common, 'json11'] Export('common') # Build messaging (cereal + msgq + socketmaster + their dependencies) @@ -189,7 +189,7 @@ SConscript(['opendbc_repo/SConscript'], exports={'env': env_swaglog}) SConscript(['cereal/SConscript']) Import('socketmaster', 'msgq') -messaging = [socketmaster, msgq, 'capnp', 'kj',] +messaging = [socketmaster, msgq, 'zmq', 'capnp', 'kj'] Export('messaging') diff --git a/cereal/SConscript b/cereal/SConscript index a58a9490ce6488..ceea0bfe046e4e 100644 --- a/cereal/SConscript +++ b/cereal/SConscript @@ -13,7 +13,7 @@ cereal = env.Library('cereal', [f'gen/cpp/{s}.c++' for s in schema_files]) # Build messaging services_h = env.Command(['services.h'], ['services.py'], 'python3 ' + cereal_dir.path + '/services.py > $TARGET') -env.Program('messaging/bridge', ['messaging/bridge.cc', 'messaging/msgq_to_zmq.cc'], LIBS=[msgq, common, 'pthread']) +env.Program('messaging/bridge', ['messaging/bridge.cc', 'messaging/msgq_to_zmq.cc'], LIBS=[msgq, 'zmq', common, 'pthread']) socketmaster = env.Library('socketmaster', ['messaging/socketmaster.cc']) diff --git a/common/SConscript b/common/SConscript index 1c68cf05c7aecd..e0ef94ad0554ba 100644 --- a/common/SConscript +++ b/common/SConscript @@ -14,10 +14,10 @@ Export('_common') if GetOption('extras'): env.Program('tests/test_common', ['tests/test_runner.cc', 'tests/test_params.cc', 'tests/test_util.cc', 'tests/test_swaglog.cc'], - LIBS=[_common, 'json11', 'zmq', 'pthread']) + LIBS=[_common, 'json11', 'pthread']) # Cython bindings -params_python = envCython.Program('params_pyx.so', 'params_pyx.pyx', LIBS=envCython['LIBS'] + [_common, 'zmq', 'json11']) +params_python = envCython.Program('params_pyx.so', 'params_pyx.pyx', LIBS=envCython['LIBS'] + [_common, 'json11']) common_python = [params_python] diff --git a/common/ipc.py b/common/ipc.py new file mode 100644 index 00000000000000..3b5fb3babc07c5 --- /dev/null +++ b/common/ipc.py @@ -0,0 +1,88 @@ +import os +import socket + + +class PushSocket: + """Non-blocking PUSH socket using Unix datagram sockets.""" + + def __init__(self): + self.sock: socket.socket | None = None + self.path: str | None = None + + def connect(self, ipc_path: str): + """Connect to a Unix domain socket. + + Args: + ipc_path: Socket path in format 'ipc:///path/to/socket' or '/path/to/socket' + """ + self.path = ipc_path.replace("ipc://", "") + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + self.sock.setblocking(False) + + def send(self, data: bytes) -> bool: + """Send data to the socket (non-blocking). + + Returns True if sent successfully, False if dropped. + Max message size is ~64KB on Linux (kernel limit for datagrams). + """ + if self.sock is None or self.path is None: + return False + try: + self.sock.sendto(data, self.path) + return True + except (BlockingIOError, ConnectionRefusedError, FileNotFoundError, OSError): + # Drop message on any send error (matches ZMQ NOBLOCK behavior) + # OSError includes EMSGSIZE (message too long) for large datagrams + return False + + def close(self): + if self.sock: + self.sock.close() + self.sock = None + + +class PullSocket: + """Blocking/non-blocking PULL socket using Unix datagram sockets.""" + + def __init__(self): + self.sock: socket.socket | None = None + self.path: str | None = None + + def bind(self, ipc_path: str): + """Bind to a Unix domain socket. + + Args: + ipc_path: Socket path in format 'ipc:///path/to/socket' or '/path/to/socket' + """ + self.path = ipc_path.replace("ipc://", "") + if os.path.exists(self.path): + os.unlink(self.path) + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + self.sock.bind(self.path) + + def recv(self, flags: int = 0) -> bytes: + """Receive data from the socket. + + Args: + flags: If non-zero, use non-blocking mode + + Returns: + Received bytes + + Raises: + BlockingIOError: If non-blocking and no data available + """ + if self.sock is None: + raise RuntimeError("Socket not bound") + if flags: # NOBLOCK + self.sock.setblocking(False) + else: + self.sock.setblocking(True) + return self.sock.recv(65536) + + def close(self): + if self.sock: + self.sock.close() + self.sock = None + if self.path and os.path.exists(self.path): + os.unlink(self.path) diff --git a/common/swaglog.cc b/common/swaglog.cc index 62a405a2b6e244..5834cfb644706d 100644 --- a/common/swaglog.cc +++ b/common/swaglog.cc @@ -9,8 +9,14 @@ #include #include -#include +#include +#include +#include #include +#include +#include +#include + #include "third_party/json11/json11.hpp" #include "common/version.h" #include "system/hardware/hw.h" @@ -18,13 +24,23 @@ class SwaglogState { public: SwaglogState() { - zctx = zmq_ctx_new(); - sock = zmq_socket(zctx, ZMQ_PUSH); - - // Timeout on shutdown for messages to be received by the logging process - int timeout = 100; - zmq_setsockopt(sock, ZMQ_LINGER, &timeout, sizeof(timeout)); - zmq_connect(sock, Path::swaglog_ipc().c_str()); + // Create Unix datagram socket + sock_fd = socket(AF_UNIX, SOCK_DGRAM, 0); + if (sock_fd >= 0) { + // Set non-blocking + int flags = fcntl(sock_fd, F_GETFL, 0); + fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK); + + // Set up destination address + memset(&sock_addr, 0, sizeof(sock_addr)); + sock_addr.sun_family = AF_UNIX; + std::string ipc_path = Path::swaglog_ipc(); + // Convert "ipc:///path" to "/path" + if (ipc_path.rfind("ipc://", 0) == 0) { + ipc_path = ipc_path.substr(6); + } + strncpy(sock_addr.sun_path, ipc_path.c_str(), sizeof(sock_addr.sun_path) - 1); + } // workaround for https://github.com/dropbox/json11/issues/38 setlocale(LC_NUMERIC, "C"); @@ -62,8 +78,9 @@ class SwaglogState { } ~SwaglogState() { - zmq_close(sock); - zmq_ctx_destroy(zctx); + if (sock_fd >= 0) { + close(sock_fd); + } } void log(int levelnum, const char* filename, int lineno, const char* func, const char* msg, const std::string& log_s) { @@ -71,12 +88,16 @@ class SwaglogState { if (levelnum >= print_level) { printf("%s: %s\n", filename, msg); } - zmq_send(sock, log_s.data(), log_s.length(), ZMQ_NOBLOCK); + if (sock_fd >= 0) { + // Non-blocking sendto, silently drops on failure (matches ZMQ NOBLOCK behavior) + sendto(sock_fd, log_s.data(), log_s.length(), 0, + (struct sockaddr*)&sock_addr, sizeof(sock_addr)); + } } std::mutex lock; - void* zctx = nullptr; - void* sock = nullptr; + int sock_fd = -1; + struct sockaddr_un sock_addr; int print_level; json11::Json::object ctx_j; }; diff --git a/common/swaglog.py b/common/swaglog.py index d009f00e76177d..629bba1510d07f 100644 --- a/common/swaglog.py +++ b/common/swaglog.py @@ -1,12 +1,10 @@ import logging import os import time -import warnings from pathlib import Path from logging.handlers import BaseRotatingHandler -import zmq - +from openpilot.common.ipc import PushSocket from openpilot.common.logging_extra import SwagLogger, SwagFormatter, SwagLogFileFormatter from openpilot.system.hardware.hw import Paths @@ -68,8 +66,6 @@ def __init__(self, formatter): logging.Handler.__init__(self) self.setFormatter(formatter) self.pid = None - - self.zctx = None self.sock = None def __del__(self): @@ -78,30 +74,20 @@ def __del__(self): def close(self): if self.sock is not None: self.sock.close() - if self.zctx is not None: - self.zctx.term() + self.sock = None def connect(self): - self.zctx = zmq.Context() - self.sock = self.zctx.socket(zmq.PUSH) - self.sock.setsockopt(zmq.LINGER, 10) + self.sock = PushSocket() self.sock.connect(Paths.swaglog_ipc()) self.pid = os.getpid() def emit(self, record): if os.getpid() != self.pid: - # TODO suppresses warning about forking proc with zmq socket, fix root cause - warnings.filterwarnings("ignore", category=ResourceWarning, message="unclosed.*") self.connect() msg = self.format(record).rstrip('\n') - # print("SEND".format(repr(msg))) - try: - s = chr(record.levelno)+msg - self.sock.send(s.encode('utf8'), zmq.NOBLOCK) - except zmq.error.Again: - # drop :/ - pass + s = chr(record.levelno) + msg + self.sock.send(s.encode('utf8')) # Non-blocking, drops on failure class ForwardingHandler(logging.Handler): diff --git a/common/tests/test_swaglog.cc b/common/tests/test_swaglog.cc index 09bc4c3795fd5e..50ec6087b141d6 100644 --- a/common/tests/test_swaglog.cc +++ b/common/tests/test_swaglog.cc @@ -1,5 +1,10 @@ -#include +#include +#include +#include +#include +#include +#include #include #include "catch2/catch.hpp" @@ -17,60 +22,10 @@ void log_thread(int thread_id, int msg_cnt) { for (int i = 0; i < msg_cnt; ++i) { LOGD("%d", thread_id); LINE_NO = __LINE__ - 1; - usleep(1); + usleep(100); // Small delay to avoid overwhelming the socket buffer } } -void recv_log(int thread_cnt, int thread_msg_cnt) { - void *zctx = zmq_ctx_new(); - void *sock = zmq_socket(zctx, ZMQ_PULL); - zmq_bind(sock, Path::swaglog_ipc().c_str()); - std::vector thread_msgs(thread_cnt); - int total_count = 0; - - for (auto start = std::chrono::steady_clock::now(), now = start; - now < start + std::chrono::seconds{1} && total_count < (thread_cnt * thread_msg_cnt); - now = std::chrono::steady_clock::now()) { - char buf[4096] = {}; - if (zmq_recv(sock, buf, sizeof(buf), ZMQ_DONTWAIT) <= 0) { - if (errno == EAGAIN || errno == EINTR || errno == EFSM) continue; - break; - } - - REQUIRE(buf[0] == CLOUDLOG_DEBUG); - std::string err; - auto msg = json11::Json::parse(buf + 1, err); - REQUIRE(!msg.is_null()); - - REQUIRE(msg["levelnum"].int_value() == CLOUDLOG_DEBUG); - REQUIRE_THAT(msg["filename"].string_value(), Catch::Contains("test_swaglog.cc")); - REQUIRE(msg["funcname"].string_value() == "log_thread"); - REQUIRE(msg["lineno"].int_value() == LINE_NO); - - auto ctx = msg["ctx"]; - - REQUIRE(ctx["daemon"].string_value() == daemon_name); - REQUIRE(ctx["dongle_id"].string_value() == dongle_id); - REQUIRE(ctx["dirty"].bool_value() == true); - - REQUIRE(ctx["version"].string_value() == COMMA_VERSION); - - std::string device = Hardware::get_name(); - REQUIRE(ctx["device"].string_value() == device); - - int thread_id = atoi(msg["msg"].string_value().c_str()); - REQUIRE((thread_id >= 0 && thread_id < thread_cnt)); - thread_msgs[thread_id]++; - total_count++; - } - for (int i = 0; i < thread_cnt; ++i) { - INFO("thread :" << i); - REQUIRE(thread_msgs[i] == thread_msg_cnt); - } - zmq_close(sock); - zmq_ctx_destroy(zctx); -} - TEST_CASE("swaglog") { setenv("MANAGER_DAEMON", daemon_name.c_str(), 1); setenv("DONGLE_ID", dongle_id.c_str(), 1); @@ -78,11 +33,81 @@ TEST_CASE("swaglog") { const int thread_cnt = 5; const int thread_msg_cnt = 100; + // Create and bind receiver socket BEFORE starting senders + // (datagram sockets drop messages if no receiver is bound) + int sock_fd = socket(AF_UNIX, SOCK_DGRAM, 0); + REQUIRE(sock_fd >= 0); + + // Increase receive buffer to handle burst of messages + int rcvbuf = 1024 * 1024; // 1MB buffer + setsockopt(sock_fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + + int flags = fcntl(sock_fd, F_GETFL, 0); + fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK); + + std::string ipc_path = Path::swaglog_ipc(); + if (ipc_path.rfind("ipc://", 0) == 0) { + ipc_path = ipc_path.substr(6); + } + unlink(ipc_path.c_str()); + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, ipc_path.c_str(), sizeof(addr.sun_path) - 1); + REQUIRE(bind(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0); + + // Shared state for receiver thread + std::vector thread_msgs(thread_cnt); + std::atomic total_count{0}; + std::atomic stop_receiver{false}; + + // Start receiver thread BEFORE senders to receive messages concurrently + std::thread receiver_thread([&]() { + while (!stop_receiver || total_count < (thread_cnt * thread_msg_cnt)) { + char buf[4096] = {}; + ssize_t len = recv(sock_fd, buf, sizeof(buf), 0); + if (len <= 0) { + if (errno == EAGAIN || errno == EINTR || errno == EWOULDBLOCK) { + usleep(100); + continue; + } + break; + } + + if (buf[0] != CLOUDLOG_DEBUG) continue; + std::string err; + auto msg = json11::Json::parse(buf + 1, err); + if (msg.is_null()) continue; + + int thread_id = atoi(msg["msg"].string_value().c_str()); + if (thread_id >= 0 && thread_id < thread_cnt) { + thread_msgs[thread_id]++; + total_count++; + } + + if (total_count >= thread_cnt * thread_msg_cnt) break; + } + }); + + // Now start senders std::vector log_threads; for (int i = 0; i < thread_cnt; ++i) { log_threads.push_back(std::thread(log_thread, i, thread_msg_cnt)); } for (auto &t : log_threads) t.join(); - recv_log(thread_cnt, thread_msg_cnt); + // Signal receiver to stop and wait for it + stop_receiver = true; + // Give receiver a bit more time to drain any remaining messages + usleep(100000); // 100ms + receiver_thread.join(); + + // Verify all messages were received + for (int i = 0; i < thread_cnt; ++i) { + INFO("thread :" << i); + REQUIRE(thread_msgs[i] == thread_msg_cnt); + } + close(sock_fd); + unlink(ipc_path.c_str()); } diff --git a/pyproject.toml b/pyproject.toml index 1be5c395f1c02c..c67b2813bcc241 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ dependencies = [ "onnx >= 1.14.0", # logging - "pyzmq", "sentry-sdk", "xattr", # used in place of 'os.getxattr' for macOS compatibility diff --git a/system/logmessaged.py b/system/logmessaged.py index c095c261926b8c..10777ace7e3464 100755 --- a/system/logmessaged.py +++ b/system/logmessaged.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -import zmq from typing import NoReturn import cereal.messaging as messaging +from openpilot.common.ipc import PullSocket from openpilot.common.logging_extra import SwagLogFileFormatter from openpilot.system.hardware.hw import Paths from openpilot.common.swaglog import get_file_handler @@ -13,8 +13,7 @@ def main() -> NoReturn: log_handler.setFormatter(SwagLogFileFormatter(None)) log_level = 20 # logging.INFO - ctx = zmq.Context.instance() - sock = ctx.socket(zmq.PULL) + sock = PullSocket() sock.bind(Paths.swaglog_ipc()) # and we publish them @@ -23,7 +22,7 @@ def main() -> NoReturn: try: while True: - dat = b''.join(sock.recv_multipart()) + dat = sock.recv() # Blocking recv, datagrams are atomic level = dat[0] record = dat[1:].decode("utf-8") if level >= log_level: @@ -43,7 +42,6 @@ def main() -> NoReturn: error_log_message_sock.send(msg.to_bytes()) finally: sock.close() - ctx.term() # can hit this if interrupted during a rollover try: diff --git a/system/statsd.py b/system/statsd.py index 33e9e9912d4a1c..bf8fa880323ffa 100755 --- a/system/statsd.py +++ b/system/statsd.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import os -import zmq import time import uuid from pathlib import Path @@ -8,6 +7,7 @@ from datetime import datetime, UTC from typing import NoReturn +from openpilot.common.ipc import PushSocket, PullSocket from openpilot.common.params import Params from cereal.messaging import SubMaster from openpilot.system.hardware.hw import Paths @@ -25,31 +25,23 @@ class METRIC_TYPE: class StatLog: def __init__(self): self.pid = None - self.zctx = None self.sock = None def connect(self) -> None: - self.zctx = zmq.Context() - self.sock = self.zctx.socket(zmq.PUSH) - self.sock.setsockopt(zmq.LINGER, 10) + self.sock = PushSocket() self.sock.connect(STATS_SOCKET) self.pid = os.getpid() def __del__(self): if self.sock is not None: self.sock.close() - if self.zctx is not None: - self.zctx.term() + self.sock = None def _send(self, metric: str) -> None: if os.getpid() != self.pid: self.connect() - try: - self.sock.send_string(metric, zmq.NOBLOCK) - except zmq.error.Again: - # drop :/ - pass + self.sock.send(metric.encode('utf8')) # Non-blocking, drops on failure def gauge(self, name: str, value: float) -> None: self._send(f"{name}:{value}|{METRIC_TYPE.GAUGE}") @@ -78,8 +70,7 @@ def get_influxdb_line(measurement: str, value: float | dict[str, float], timest return res # open statistics socket - ctx = zmq.Context.instance() - sock = ctx.socket(zmq.PULL) + sock = PullSocket() sock.bind(STATS_SOCKET) STATS_DIR = Paths.stats_root() @@ -115,7 +106,7 @@ def get_influxdb_line(measurement: str, value: float | dict[str, float], timest # Update metrics while True: try: - metric = sock.recv_string(zmq.NOBLOCK) + metric = sock.recv(flags=1).decode('utf8') # flags=1 for non-blocking try: metric_type = metric.split('|')[1] metric_name = metric.split(':')[0] @@ -129,7 +120,7 @@ def get_influxdb_line(measurement: str, value: float | dict[str, float], timest cloudlog.event("unknown metric type", metric_type=metric_type) except Exception: cloudlog.event("malformed metric", metric=metric) - except zmq.error.Again: + except BlockingIOError: break # flush when started state changes or after FLUSH_TIME_S @@ -174,7 +165,6 @@ def get_influxdb_line(measurement: str, value: float | dict[str, float], timest cloudlog.error("stats dir full") finally: sock.close() - ctx.term() if __name__ == "__main__": diff --git a/system/tests/test_logmessaged.py b/system/tests/test_logmessaged.py index 9ccc8ef53bc25c..f8288d70fabc7b 100644 --- a/system/tests/test_logmessaged.py +++ b/system/tests/test_logmessaged.py @@ -41,15 +41,20 @@ def test_simple_log(self): assert len(self._get_log_files()) >= 1 def test_big_log(self): + # Test with large (but realistic) log messages + # Unix datagram sockets support up to ~64KB, but JSON formatting adds overhead + # Use 20KB which is still much larger than typical logs (1-5KB) n = 10 - msg = "a"*3*1024*1024 + msg = "a"*20*1024 # 20KB message for _ in range(n): cloudlog.info(msg) - time.sleep(0.5) - - msgs = messaging.drain_sock(self.sock) - assert len(msgs) == 0 + time.sleep(1.0) + # 20KB messages should go through IPC and be written to log files logsize = sum([os.path.getsize(f) for f in self._get_log_files()]) - assert (n*len(msg)) < logsize < (n*(len(msg)+1024)) + assert (n*len(msg)) < logsize < (n*(len(msg)+2048)) + + # Messages are also published via messaging (under 2MB limit) + msgs = messaging.drain_sock(self.sock) + assert len(msgs) == n diff --git a/uv.lock b/uv.lock index 572305f08c05fb..193cf943ecc5f2 100644 --- a/uv.lock +++ b/uv.lock @@ -1298,7 +1298,6 @@ dependencies = [ { name = "pyjwt" }, { name = "pyopenssl" }, { name = "pyserial" }, - { name = "pyzmq" }, { name = "qrcode" }, { name = "raylib" }, { name = "requests" }, @@ -1402,7 +1401,6 @@ requires-dist = [ { name = "pytest-timeout", marker = "extra == 'testing'" }, { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, { name = "pywinctl", marker = "extra == 'dev'" }, - { name = "pyzmq" }, { name = "qrcode" }, { name = "raylib", specifier = ">5.5.0.3" }, { name = "requests" }, @@ -4495,42 +4493,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] -[[package]] -name = "pyzmq" -version = "27.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, - { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, - { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, - { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, - { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, - { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, - { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, - { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, - { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, - { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, - { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, -] - [[package]] name = "qrcode" version = "8.2"