diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1beab8b..e9bae8e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,19 +1,25 @@ # Contributing Guide -## Install and upgrade Magic +## Activate the magic shell + +For development, you can activate the magic shell by running: ```bash -magic update && magic install +magic shell ``` ## How to build this package +Once you have the magic shell activated, you can build the package by running: + ```bash -mojo package src/websockets -o websockets.mojopkg -`` +./scripts/build.sh +``` ## How to run tests +To run the tests, you can run: + ```bash ./scripts/run-tests.sh ``` diff --git a/README.md b/README.md index c6405f6..24a30ce 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# mojo-websockets +# Mojo Websockets ![logo](./assets/logo.jpeg) -`mojo-websockets` is a lightweight library for handling WebSocket connections in Mojo. +`mojo-websockets` is a lightweight library for handling WebSocket connections in Mojo. It aims to provide an similar interface as the [python-websockets](https://github.com/python-websockets/websockets) package for creating WebSocket servers and clients, with additional features for enhanced usability. @@ -13,6 +13,7 @@ This software is in a early stage of development. Please DO NOT use yet for prod ## Features - **WebSocket Server and Client**: Supports creating both WebSocket servers and clients. +- **Asynchronous non-blocking communication**: Support concurrent connections via [io_uring](https://unixism.net/loti/). - **Compatibility**: API designed to be intuitive for developers familiar with the Python websockets library. - **Sans/IO Layer**: Implements a WebSocket Sans/IO layer pure Mojo and performs no I/O of its own. @@ -34,16 +35,13 @@ For a complete listing, see the [features](docs/features.md) document. ```mojo from websockets.aliases import Bytes from websockets.sync.server import serve, WSConnection -from websockets.utils.bytes import bytes_to_str - fn on_message(conn: WSConnection, data: Bytes) raises -> None: - str_received = bytes_to_str(data) + str_received = String(StringSlice.from_utf8(data)) print("<<< ", str_received) conn.send_text(str_received) print(">>> ", str_received) - fn main() raises: with serve(on_message, "127.0.0.1", 8000) as server: server.serve_forever() @@ -53,15 +51,13 @@ fn main() raises: ```mojo from websockets.sync.client import connect -from websockets.utils.bytes import bytes_to_str - fn send_and_receive(msg: String) raises: with connect("ws://127.0.0.1:8000") as client: client.send_text(msg) print(">>> ", msg) - response = client.recv() - print("<<< ", bytes_to_str(response)) + response = client.recv_text() + print("<<< ", response) fn main() raises: send_and_receive("Hello world!") @@ -70,9 +66,7 @@ fn main() raises: ## TODO -- [ ] Asynchronous non-blocking communication (waiting for the Mojo async/await support) - [ ] Implement automatic reconnection for clients -- [ ] Get rid of Python dependencies and logic (e.g. no more `from python import ...`) - [ ] Make sure it passes all the tests in [Autobahn|Testsuite](https://github.com/crossbario/autobahn-testsuite/) - [ ] Implement subprotocols and extensions - [ ] Optimize performance for high-concurrency scenarios @@ -86,9 +80,9 @@ Contributions are welcome! If you'd like to contribute, please follow the contri ## Acknowledgments -We have taken a lot of code from the amazing [lightbug_http](https://github.com/saviorand/lightbug_http) project. - -Also, we took inspiration and some code from the [python-websockets](https://github.com/websockets) project, specially for implementing the [WebSocket Sans/IO layer](https://websockets.readthedocs.io/en/stable/howto/sansio.html) and their tests. +* We have taken a lot of code from the amazing [lightbug_http](https://github.com/saviorand/lightbug_http) project. +* We took inspiration and some code from the [python-websockets](https://github.com/websockets) project, specially for implementing the [WebSocket Sans/IO layer](https://websockets.readthedocs.io/en/stable/howto/sansio.html) and their tests. +* We are using the [io_uring](https://github.com/dmitry-salin/io_uring/) library for the I/O operations concurrency. ## License diff --git a/examples/echo/client.mojo b/examples/echo/client.mojo index 067dc18..0023811 100755 --- a/examples/echo/client.mojo +++ b/examples/echo/client.mojo @@ -1,16 +1,20 @@ """Client example using the threading API.""" + +import sys + from websockets.sync.client import connect -from websockets.utils.bytes import bytes_to_str, str_to_bytes fn send_and_receive_loop() raises: - with connect("ws://127.0.0.1:8001") as client: + args = sys.argv() + port = Int(args[1]) if len(args) > 1 else 8001 + with connect("ws://127.0.0.1:{}".format(port)) as client: while True: msg = input("Enter a message: ") client.send_text(msg) print(">>> ", msg) - response = client.recv() - print("<<< ", bytes_to_str(response)) + response = client.recv_text() + print("<<< ", response) fn main() raises: diff --git a/examples/echo/run_client.sh b/examples/echo/run_client.sh index cd8256c..6c7b102 100755 --- a/examples/echo/run_client.sh +++ b/examples/echo/run_client.sh @@ -1,3 +1,5 @@ #!/usr/bin/env bash -mojo -I ../../src/ client.mojo +cd "$(dirname "$0")" +# Run with debug logging enabled +mojo -I ../../src/ -D LOG_LEVEL=DEBUG client.mojo "$@" diff --git a/examples/echo/run_server.sh b/examples/echo/run_server.sh index 528e8cd..14bb36e 100755 --- a/examples/echo/run_server.sh +++ b/examples/echo/run_server.sh @@ -1,3 +1,5 @@ #!/usr/bin/env bash -mojo -I ../../src/ server.mojo +cd "$(dirname "$0")" +# Run with debug logging enabled +mojo -I ../../src/ -D LOG_LEVEL=DEBUG server.mojo "$@" diff --git a/examples/echo/server.mojo b/examples/echo/server.mojo index b6acc88..e79e9c6 100755 --- a/examples/echo/server.mojo +++ b/examples/echo/server.mojo @@ -1,19 +1,23 @@ -"""Server example using the threading API.""" +"""WebSocket server example using io_uring for concurrency.""" + +import sys +from utils import StringSlice from websockets.aliases import Bytes from websockets.sync.server import serve, WSConnection -from websockets.utils.bytes import bytes_to_str fn on_message(conn: WSConnection, data: Bytes) raises -> None: - str_received = bytes_to_str(data) + str_received = String(StringSlice.from_utf8(data)) print("<<< ", str_received) conn.send_text(str_received) print(">>> ", str_received) fn main() raises: - with serve(on_message, "127.0.0.1", 8001) as server: + print("Starting WebSocket echo server with io_uring concurrency support") + print("Multiple clients can connect simultaneously") + args = sys.argv() + port = Int(args[1]) if len(args) > 1 else 8001 + with serve(on_message, "127.0.0.1", port) as server: server.serve_forever() - - diff --git a/magic.lock b/magic.lock index 772bd5a..eeb9566 100644 --- a/magic.lock +++ b/magic.lock @@ -17,116 +17,60 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh31011fe_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240722.0-cxx17_hbbce691_4.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-28_h59b9bed_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-28_he106b2a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250127.1-cxx17_hbbce691_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-31_h59b9bed_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-31_he106b2a_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h77fa898_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-14.2.0-h69a702a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-14.2.0-hd5240d6_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h77fa898_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-28_h7ac8fdf_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-14.2.0-h69a702a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-14.2.0-hf1ad2bd_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-31_h7ac8fdf_openblas.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.28-pthreads_h94d23a6_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.28.3-h6128344_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsentencepiece-0.2.0-h8e10757_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.29-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.29.3-h501fc15_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsentencepiece-0.2.0-he636bdd_11.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.48.0-hee588c1_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-hc0a3c3a_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda - - conda: https://conda.modular.com/max-nightly/noarch/max-25.1.0.dev2025020805-release.conda - - conda: https://conda.modular.com/max-nightly/linux-64/max-core-25.1.0.dev2025020805-release.conda - - conda: https://conda.modular.com/max-nightly/linux-64/max-python-25.1.0.dev2025020805-release.conda - - conda: https://conda.modular.com/max-nightly/noarch/mblack-25.1.0.dev2025020805-release.conda - - conda: https://conda.modular.com/max-nightly/noarch/mojo-jupyter-25.1.0.dev2025020805-release.conda + - conda: https://conda.modular.com/max-nightly/noarch/max-25.3.0.dev2025032305-release.conda + - conda: https://conda.modular.com/max-nightly/linux-64/max-core-25.3.0.dev2025032305-release.conda + - conda: https://conda.modular.com/max-nightly/linux-64/max-python-25.3.0.dev2025032305-release.conda + - conda: https://conda.modular.com/max-nightly/noarch/mblack-25.3.0.dev2025032305-release.conda + - conda: https://conda.modular.com/max-nightly/noarch/mojo-jupyter-25.3.0.dev2025032305-release.conda - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py312heda63a1_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.0-h7b32b05_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.7-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.9-h9e4cc4f_1_cpython.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.12-5_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.2.1-py312hbf22597_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-0.2.0-hc8f76dd_10.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-python-0.2.0-py312hb6b8a2b_10.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-spm-0.2.0-h8e10757_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.3.0-py312hbf22597_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-0.2.0-hc8f76dd_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-python-0.2.0-py312hb957f94_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-spm-0.2.0-he636bdd_11.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4.2-py312h66e93f0_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.21.0-pyhd8ed1ab_1.conda - osx-arm64: - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2025.1.31-hf0a4a13_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.8-pyh707e725_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.6.1-pyha770c72_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.7.2-pyh31011fe_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20240722.0-cxx17_h07bc746_4.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-28_h10e41b3_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-28_hb3479ef_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-19.1.7-ha82da77_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-5.0.0-13_2_0_hd922786_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-13.2.0-hf226fd6_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-28_hc9a63f6_openblas.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.6.4-h39f12f2_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.28-openmp_hf332438_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-5.28.3-h3bd63a1_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsentencepiece-0.2.0-he13a0af_10.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.48.0-h3f77e49_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-19.1.7-hdb05f8b_0.conda - - conda: https://conda.modular.com/max-nightly/noarch/max-25.1.0.dev2025020805-release.conda - - conda: https://conda.modular.com/max-nightly/osx-arm64/max-core-25.1.0.dev2025020805-release.conda - - conda: https://conda.modular.com/max-nightly/osx-arm64/max-python-25.1.0.dev2025020805-release.conda - - conda: https://conda.modular.com/max-nightly/noarch/mblack-25.1.0.dev2025020805-release.conda - - conda: https://conda.modular.com/max-nightly/noarch/mojo-jupyter-25.1.0.dev2025020805-release.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-1.26.4-py312h8442bc7_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.0-h81ee809_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.8-hc22306f_1_cpython.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-5_cp312.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-26.2.1-py312hf4875e0_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sentencepiece-0.2.0-h22a84ea_10.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sentencepiece-python-0.2.0-py312h155166a_10.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sentencepiece-spm-0.2.0-he13a0af_10.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.4.2-py312hea69d52_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-hc1bb282_7.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.21.0-pyhd8ed1ab_1.conda packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 @@ -163,17 +107,6 @@ packages: license_family: BSD size: 252783 timestamp: 1720974456583 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda - sha256: adfa71f158cbd872a36394c56c3568e6034aa55c623634b37a4836bd036e6b91 - md5: fc6948412dbbbe9a4c9ddbbcfe0a79ab - depends: - - __osx >=11.0 - arch: arm64 - platform: osx - license: bzip2-1.0.6 - license_family: BSD - size: 122909 - timestamp: 1720974522888 - conda: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2025.1.31-hbcca054_0.conda sha256: bf832198976d559ab44d6cdb315642655547e26d826e34da67cbee6624cda189 md5: 19f3a56f68d2fd06c516076bff482c52 @@ -182,14 +115,6 @@ packages: license: ISC size: 158144 timestamp: 1738298224464 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ca-certificates-2025.1.31-hf0a4a13_0.conda - sha256: 7e12816618173fe70f5c638b72adf4bfd4ddabf27794369bb17871c5bb75b9f9 - md5: 3569d6a9141adc64d2fe4797f3289e06 - arch: arm64 - platform: osx - license: ISC - size: 158425 - timestamp: 1738298167688 - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.1.8-pyh707e725_0.conda sha256: c920d23cd1fcf565031c679adb62d848af60d6fbb0edc2d50ba475cea4f0d8ab md5: f22f4d4970e09d68a10b922cbb0408d3 @@ -272,24 +197,9 @@ packages: license_family: MIT size: 1370023 timestamp: 1719463201255 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - sha256: 4442f957c3c77d69d9da3521268cad5d54c9033f1a73f99cde0a3658937b159b - md5: c6dc8a0fdec13a0565936655c33069a1 - depends: - - __osx >=11.0 - - libcxx >=16 - - libedit >=3.1.20191231,<3.2.0a0 - - libedit >=3.1.20191231,<4.0a0 - - openssl >=3.3.1,<4.0a0 - arch: arm64 - platform: osx - license: MIT - license_family: MIT - size: 1155530 - timestamp: 1719463474401 -- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_2.conda - sha256: 7c91cea91b13f4314d125d1bedb9d03a29ebbd5080ccdea70260363424646dbe - md5: 048b02e3962f066da18efe3a21b77672 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.43-h712a8e2_4.conda + sha256: db73f38155d901a610b2320525b9dd3b31e4949215c870685fd92ea61b5ce472 + md5: 01f8d123c96816249efd255a31ad7712 depends: - __glibc >=2.17,<3.0.a0 constrains: @@ -298,118 +208,59 @@ packages: platform: linux license: GPL-3.0-only license_family: GPL - size: 669211 - timestamp: 1729655358674 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240722.0-cxx17_hbbce691_4.conda - sha256: 143a586aa67d50622ef703de57b9d43f44945836d6568e0e7aa174bd8c45e0d4 - md5: 488f260ccda0afaf08acb286db439c2f + size: 671240 + timestamp: 1740155456116 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250127.1-cxx17_hbbce691_0.conda + sha256: 65d5ca837c3ee67b9d769125c21dc857194d7f6181bb0e7bd98ae58597b457d0 + md5: 00290e549c5c8a32cc271020acc9ec6b depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - libstdcxx >=13 constrains: - - libabseil-static =20240722.0=cxx17* - - abseil-cpp =20240722.0 + - abseil-cpp =20250127.1 + - libabseil-static =20250127.1=cxx17* arch: x86_64 platform: linux license: Apache-2.0 license_family: Apache - size: 1311599 - timestamp: 1736008414161 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20240722.0-cxx17_h07bc746_4.conda - sha256: 05fa5e5e908962b9c5aba95f962e2ca81d9599c4715aebe5e4ddb72b309d1770 - md5: c2d95bd7aa8d564a9bd7eca5e571a5b3 - depends: - - __osx >=11.0 - - libcxx >=18 + size: 1325007 + timestamp: 1742369558286 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-31_h59b9bed_openblas.conda + build_number: 31 + sha256: 9839fc4ac0cbb0aa3b9eea520adfb57311838959222654804e58f6f2d1771db5 + md5: 728dbebd0f7a20337218beacffd37916 + depends: + - libopenblas >=0.3.29,<0.3.30.0a0 + - libopenblas >=0.3.29,<1.0a0 constrains: - - libabseil-static =20240722.0=cxx17* - - abseil-cpp =20240722.0 - arch: arm64 - platform: osx - license: Apache-2.0 - license_family: Apache - size: 1178260 - timestamp: 1736008642885 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-28_h59b9bed_openblas.conda - build_number: 28 - sha256: 93fbcf2800b859b7ca5add3ab5d3aa11c6a6ff4b942a1cea4bf644f78488edb8 - md5: 73e2a99fdeb8531d50168987378fda8a - depends: - - libopenblas >=0.3.28,<0.3.29.0a0 - - libopenblas >=0.3.28,<1.0a0 - constrains: - - libcblas =3.9.0=28*_openblas - - blas =2.128=openblas - - liblapack =3.9.0=28*_openblas - - liblapacke =3.9.0=28*_openblas + - liblapacke =3.9.0=31*_openblas + - liblapack =3.9.0=31*_openblas + - blas =2.131=openblas + - mkl <2025 + - libcblas =3.9.0=31*_openblas arch: x86_64 platform: linux license: BSD-3-Clause license_family: BSD - size: 16621 - timestamp: 1738114033763 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-28_h10e41b3_openblas.conda - build_number: 28 - sha256: 5bea855a1a7435ce2238535aa4b13db8af8ee301d99a42b083b63fa64c1ea144 - md5: 166166d84a0e9571dc50210baf993b46 - depends: - - libopenblas >=0.3.28,<0.3.29.0a0 - - libopenblas >=0.3.28,<1.0a0 - constrains: - - liblapack =3.9.0=28*_openblas - - liblapacke =3.9.0=28*_openblas - - blas =2.128=openblas - - libcblas =3.9.0=28*_openblas - arch: arm64 - platform: osx - license: BSD-3-Clause - license_family: BSD - size: 16840 - timestamp: 1738114389937 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-28_he106b2a_openblas.conda - build_number: 28 - sha256: de293e117db53e5d78b579136509c35a5e4ad11529c05f9af83cf89be4d30de1 - md5: 4e20a1c00b4e8a984aac0f6cce59e3ac - depends: - - libblas 3.9.0 28_h59b9bed_openblas + size: 16859 + timestamp: 1740087969120 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-31_he106b2a_openblas.conda + build_number: 31 + sha256: ede8545011f5b208b151fe3e883eb4e31d495ab925ab7b9ce394edca846e0c0d + md5: abb32c727da370c481a1c206f5159ce9 + depends: + - libblas 3.9.0 31_h59b9bed_openblas constrains: - - blas =2.128=openblas - - liblapack =3.9.0=28*_openblas - - liblapacke =3.9.0=28*_openblas + - liblapacke =3.9.0=31*_openblas + - liblapack =3.9.0=31*_openblas + - blas =2.131=openblas arch: x86_64 platform: linux license: BSD-3-Clause license_family: BSD - size: 16539 - timestamp: 1738114043618 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-28_hb3479ef_openblas.conda - build_number: 28 - sha256: f08adea59381babb3568e6d23e52aff874cbc25f299821647ab1127d1e1332ca - md5: 30942dea911ce333765003a8adec4e8a - depends: - - libblas 3.9.0 28_h10e41b3_openblas - constrains: - - blas =2.128=openblas - - liblapacke =3.9.0=28*_openblas - - liblapack =3.9.0=28*_openblas - arch: arm64 - platform: osx - license: BSD-3-Clause - license_family: BSD - size: 16788 - timestamp: 1738114399962 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-19.1.7-ha82da77_0.conda - sha256: 776092346da87a2a23502e14d91eb0c32699c4a1522b7331537bd1c3751dcff5 - md5: 5b3e1610ff8bd5443476b91d618f5b77 - depends: - - __osx >=11.0 - arch: arm64 - platform: osx - license: Apache-2.0 WITH LLVM-exception - license_family: Apache - size: 523505 - timestamp: 1736877862502 + size: 16796 + timestamp: 1740087984429 - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 md5: c277e0a4d549b03ac1e9d6cbbe3d017b @@ -424,19 +275,6 @@ packages: license_family: BSD size: 134676 timestamp: 1738479519902 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda - sha256: 66aa216a403de0bb0c1340a88d1a06adaff66bae2cfd196731aa24db9859d631 - md5: 44083d2d2c2025afca315c7a172eab2b - depends: - - ncurses - - __osx >=11.0 - - ncurses >=6.5,<7.0a0 - arch: arm64 - platform: osx - license: BSD-2-Clause - license_family: BSD - size: 107691 - timestamp: 1738479560845 - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.4-h5888daf_0.conda sha256: 56541b98447b58e52d824bd59d6382d609e11de1f8adf20b23143e353d2b8d26 md5: db833e03127376d461e1e13e76f09b6c @@ -451,93 +289,62 @@ packages: license_family: MIT size: 73304 timestamp: 1730967041968 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.6.4-h286801f_0.conda - sha256: e42ab5ace927ee7c84e3f0f7d813671e1cf3529f5f06ee5899606630498c2745 - md5: 38d2656dd914feb0cab8c629370768bf +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_0.conda + sha256: 67a6c95e33ebc763c1adc3455b9a9ecde901850eb2fceb8e646cc05ef3a663da + md5: e3eb7806380bc8bcecba6d749ad5f026 depends: - - __osx >=11.0 - constrains: - - expat 2.6.4.* - arch: arm64 - platform: osx - license: MIT - license_family: MIT - size: 64693 - timestamp: 1730967175868 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2 - sha256: ab6e9856c21709b7b517e940ae7028ae0737546122f83c2aa5d692860c3b149e - md5: d645c6d2ac96843a2bfaccd2d62b3ac3 - depends: - - libgcc-ng >=9.4.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 arch: x86_64 platform: linux license: MIT license_family: MIT - size: 58292 - timestamp: 1636488182923 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.2-h3422bc3_5.tar.bz2 - sha256: 41b3d13efb775e340e4dba549ab5c029611ea6918703096b2eaa9c015c0750ca - md5: 086914b672be056eb70fd4285b6783b6 - arch: arm64 - platform: osx - license: MIT - license_family: MIT - size: 39020 - timestamp: 1636488587153 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h77fa898_1.conda - sha256: 53eb8a79365e58849e7b1a068d31f4f9e718dc938d6f2c03e960345739a03569 - md5: 3cb76c3f10d3bc7f1105b2fc9db984df + size: 53415 + timestamp: 1739260413716 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-14.2.0-h767d61c_2.conda + sha256: 3a572d031cb86deb541d15c1875aaa097baefc0c580b54dc61f5edab99215792 + md5: ef504d1acbd74b7cc6849ef8af47dd03 depends: - - _libgcc_mutex 0.1 conda_forge + - __glibc >=2.17,<3.0.a0 - _openmp_mutex >=4.5 constrains: - - libgomp 14.2.0 h77fa898_1 - - libgcc-ng ==14.2.0=*_1 + - libgomp 14.2.0 h767d61c_2 + - libgcc-ng ==14.2.0=*_2 arch: x86_64 platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL - size: 848745 - timestamp: 1729027721139 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_1.conda - sha256: 3a76969c80e9af8b6e7a55090088bc41da4cffcde9e2c71b17f44d37b7cb87f7 - md5: e39480b9ca41323497b05492a63bc35b + size: 847885 + timestamp: 1740240653082 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.2.0-h69a702a_2.conda + sha256: fb7558c328b38b2f9d2e412c48da7890e7721ba018d733ebdfea57280df01904 + md5: a2222a6ada71fb478682efe483ce0f92 depends: - - libgcc 14.2.0 h77fa898_1 + - libgcc 14.2.0 h767d61c_2 arch: x86_64 platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL - size: 54142 - timestamp: 1729027726517 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-14.2.0-h69a702a_1.conda - sha256: fc9e7f22a17faf74da904ebfc4d88699013d2992e55505e4aa0eb01770290977 - md5: f1fd30127802683586f768875127a987 + size: 53758 + timestamp: 1740240660904 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-14.2.0-h69a702a_2.conda + sha256: e05263e8960da03c341650f2a3ffa4ccae4e111cb198e8933a2908125459e5a6 + md5: fb54c4ea68b460c278d26eea89cfbcc3 depends: - - libgfortran5 14.2.0 hd5240d6_1 + - libgfortran5 14.2.0 hf1ad2bd_2 constrains: - - libgfortran-ng ==14.2.0=*_1 + - libgfortran-ng ==14.2.0=*_2 arch: x86_64 platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL - size: 53997 - timestamp: 1729027752995 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-5.0.0-13_2_0_hd922786_3.conda - sha256: 44e541b4821c96b28b27fef5630883a60ce4fee91fd9c79f25a199f8f73f337b - md5: 4a55d9e169114b2b90d3ec4604cd7bbf - depends: - - libgfortran5 13.2.0 hf226fd6_3 - arch: arm64 - platform: osx - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - size: 110233 - timestamp: 1707330749033 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-14.2.0-hd5240d6_1.conda - sha256: d149a37ca73611e425041f33b9d8dbed6e52ec506fe8cc1fc0ee054bddeb6d5d - md5: 9822b874ea29af082e5d36098d25427d + size: 53733 + timestamp: 1740240690977 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-14.2.0-hf1ad2bd_2.conda + sha256: c17b7cf3073a1f4e1f34d50872934fa326346e104d3c445abc1e62481ad6085c + md5: 556a4fdfac7287d349b8f09aba899693 depends: + - __glibc >=2.17,<3.0.a0 - libgcc >=14.2.0 constrains: - libgfortran 14.2.0 @@ -545,64 +352,35 @@ packages: platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL - size: 1462645 - timestamp: 1729027735353 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-13.2.0-hf226fd6_3.conda - sha256: bafc679eedb468a86aa4636061c55966186399ee0a04b605920d208d97ac579a - md5: 66ac81d54e95c534ae488726c1f698ea + size: 1461978 + timestamp: 1740240671964 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h767d61c_2.conda + sha256: 1a3130e0b9267e781b89399580f3163632d59fe5b0142900d63052ab1a53490e + md5: 06d02030237f4d5b3d9a7e7d348fe3c6 depends: - - llvm-openmp >=8.0.0 - constrains: - - libgfortran 5.0.0 13_2_0_*_3 - arch: arm64 - platform: osx - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL - size: 997381 - timestamp: 1707330687590 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.2.0-h77fa898_1.conda - sha256: 1911c29975ec99b6b906904040c855772ccb265a1c79d5d75c8ceec4ed89cd63 - md5: cc3573974587f12dda90d96e3e55a702 - depends: - - _libgcc_mutex 0.1 conda_forge + - __glibc >=2.17,<3.0.a0 arch: x86_64 platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL - size: 460992 - timestamp: 1729027639220 -- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-28_h7ac8fdf_openblas.conda - build_number: 28 - sha256: 9530e6840690b78360946390a1d29624734a6b624f02c26631fb451592cbb8ef - md5: 069f40bfbf1dc55c83ddb07fc6a6ef8d - depends: - - libblas 3.9.0 28_h59b9bed_openblas + size: 459862 + timestamp: 1740240588123 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-31_h7ac8fdf_openblas.conda + build_number: 31 + sha256: f583661921456e798aba10972a8abbd9d33571c655c1f66eff450edc9cbefcf3 + md5: 452b98eafe050ecff932f0ec832dd03f + depends: + - libblas 3.9.0 31_h59b9bed_openblas constrains: - - libcblas =3.9.0=28*_openblas - - blas =2.128=openblas - - liblapacke =3.9.0=28*_openblas + - libcblas =3.9.0=31*_openblas + - liblapacke =3.9.0=31*_openblas + - blas =2.131=openblas arch: x86_64 platform: linux license: BSD-3-Clause license_family: BSD - size: 16553 - timestamp: 1738114053556 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-28_hc9a63f6_openblas.conda - build_number: 28 - sha256: 79c75a02bff20f8b001e6aecfee8d22a51552c3986e7037fca68e5ed071cc213 - md5: 45f26652530b558c21083ceb7adaf273 - depends: - - libblas 3.9.0 28_h10e41b3_openblas - constrains: - - blas =2.128=openblas - - liblapacke =3.9.0=28*_openblas - - libcblas =3.9.0=28*_openblas - arch: arm64 - platform: osx - license: BSD-3-Clause - license_family: BSD - size: 16793 - timestamp: 1738114407021 + size: 16790 + timestamp: 1740087997375 - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.6.4-hb9d3cd8_0.conda sha256: cad52e10319ca4585bc37f0bc7cce99ec7c15dc9168e42ccb96b741b0a27db3f md5: 42d5b6a0f30d3c10cd88cb8584fda1cb @@ -614,16 +392,6 @@ packages: license: 0BSD size: 111357 timestamp: 1738525339684 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.6.4-h39f12f2_0.conda - sha256: 560c59d3834cc652a84fb45531bd335ad06e271b34ebc216e380a89798fe8e2c - md5: e3fd1f8320a100f2b210e690a57cd615 - depends: - - __osx >=11.0 - arch: arm64 - platform: osx - license: 0BSD - size: 98945 - timestamp: 1738525462560 - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hd590300_0.conda sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6 md5: 30fd6e37fe21f86f4bd26d6ee73eeec7 @@ -635,45 +403,29 @@ packages: license_family: GPL size: 33408 timestamp: 1697359010159 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.28-pthreads_h94d23a6_1.conda - sha256: 99ba271d8a80a1af2723f2e124ffd91d850074c0389c067e6d96d72a2dbfeabe - md5: 62857b389e42b36b686331bec0922050 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.29-pthreads_h94d23a6_0.conda + sha256: cc5389ea254f111ef17a53df75e8e5209ef2ea6117e3f8aced88b5a8e51f11c4 + md5: 0a4d0252248ef9a0f88f2ba8b8a08e12 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 - libgfortran - libgfortran5 >=14.2.0 constrains: - - openblas >=0.3.28,<0.3.29.0a0 + - openblas >=0.3.29,<0.3.30.0a0 arch: x86_64 platform: linux license: BSD-3-Clause license_family: BSD - size: 5578513 - timestamp: 1730772671118 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.28-openmp_hf332438_1.conda - sha256: 62bb669c37a845129096f73d446cdb6bb170e4927f2fea2b661329680dbbc373 - md5: 40803a48d947c8639da6704e9a44d3ce - depends: - - __osx >=11.0 - - libgfortran 5.* - - libgfortran5 >=13.2.0 - - llvm-openmp >=18.1.8 - constrains: - - openblas >=0.3.28,<0.3.29.0a0 - arch: arm64 - platform: osx - license: BSD-3-Clause - license_family: BSD - size: 4165774 - timestamp: 1730772154295 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.28.3-h6128344_1.conda - sha256: 51125ebb8b7152e4a4e69fd2398489c4ec8473195c27cde3cbdf1cb6d18c5493 - md5: d8703f1ffe5a06356f06467f1d0b9464 + size: 5919288 + timestamp: 1739825731827 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-5.29.3-h501fc15_0.conda + sha256: 9965b1ada1f997202ad8c5a960e69057280b7b926c718df9b07c62924d9c1d73 + md5: 452518a9744fbac05fb45531979bdf29 depends: - __glibc >=2.17,<3.0.a0 - libabseil * cxx17* - - libabseil >=20240722.0,<20240723.0a0 + - libabseil >=20250127.0,<20250128.0a0 - libgcc >=13 - libstdcxx >=13 - libzlib >=1.3.1,<2.0a0 @@ -681,54 +433,24 @@ packages: platform: linux license: BSD-3-Clause license_family: BSD - size: 2960815 - timestamp: 1735577210663 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libprotobuf-5.28.3-h3bd63a1_1.conda - sha256: f58a16b13ad53346903c833e266f83c3d770a43a432659b98710aed85ca885e7 - md5: bdbfea4cf45ae36652c6bbcc2e7ebe91 - depends: - - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20240722.0,<20240723.0a0 - - libcxx >=18 - - libzlib >=1.3.1,<2.0a0 - arch: arm64 - platform: osx - license: BSD-3-Clause - license_family: BSD - size: 2271580 - timestamp: 1735576361997 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsentencepiece-0.2.0-h8e10757_10.conda - sha256: dc399e0f04a09f8c1807c3b36e578eb81f81891c0ddf21de9c1fb9f048ce4031 - md5: 4f43dbcfacd3cc9a183dd3a48b94d3c0 + size: 3352450 + timestamp: 1741126291267 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsentencepiece-0.2.0-he636bdd_11.conda + sha256: c5b98351daa23979a6728d297bf3b3eaae0324ae60487f5637b09a9ed7656d43 + md5: aed2d089d7d343500921f9ad3f7ba9c8 depends: - __glibc >=2.17,<3.0.a0 - libabseil * cxx17* - - libabseil >=20240722.0,<20240723.0a0 + - libabseil >=20250127.0,<20250128.0a0 - libgcc >=13 - - libprotobuf >=5.28.3,<5.28.4.0a0 + - libprotobuf >=5.29.3,<5.29.4.0a0 - libstdcxx >=13 arch: x86_64 platform: linux license: Apache-2.0 license_family: Apache - size: 823649 - timestamp: 1735627841126 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsentencepiece-0.2.0-he13a0af_10.conda - sha256: 3347a8840d085ce1661928e736229f068d56c84312fbed90886e059023d85611 - md5: 1f67e5e30edd56e0a0bf6df6bb711a9d - depends: - - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20240722.0,<20240723.0a0 - - libcxx >=18 - - libprotobuf >=5.28.3,<5.28.4.0a0 - arch: arm64 - platform: osx - license: Apache-2.0 - license_family: Apache - size: 754657 - timestamp: 1735628030895 + size: 826579 + timestamp: 1741898316659 - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda sha256: 0105bd108f19ea8e6a78d2d994a6d4a8db16d19a41212070d2d1d48a63c34161 md5: a587892d3c13b6621a6091be690dbca2 @@ -739,19 +461,9 @@ packages: license: ISC size: 205978 timestamp: 1716828628198 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda - sha256: fade8223e1e1004367d7101dd17261003b60aa576df6d7802191f8972f7470b1 - md5: a7ce36e284c5faaf93c220dfc39e3abd - depends: - - __osx >=11.0 - arch: arm64 - platform: osx - license: ISC - size: 164972 - timestamp: 1716828607917 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.48.0-hee588c1_1.conda - sha256: 22853d289ef6ec8a5b20f1aa261895b06525439990d3b139f8bfd0b5c5e32a3a - md5: 3fa05c528d8a1e2a67bbf1e36f22d3bc +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.49.1-hee588c1_2.conda + sha256: a086289bf75c33adc1daed3f1422024504ffb5c3c8b3285c49f025c29708ed16 + md5: 962d6ac93c30b1dfc54c9cccafd1003e depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 @@ -759,41 +471,31 @@ packages: arch: x86_64 platform: linux license: Unlicense - size: 878223 - timestamp: 1737564987837 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.48.0-h3f77e49_1.conda - sha256: 17c06940cc2a13fd6a17effabd6881b1477db38b2cd3ee2571092d293d3fdd75 - md5: 4c55169502ecddf8077973a987d08f08 - depends: - - __osx >=11.0 - - libzlib >=1.3.1,<2.0a0 - arch: arm64 - platform: osx - license: Unlicense - size: 852831 - timestamp: 1737564996616 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-hc0a3c3a_1.conda - sha256: 4661af0eb9bdcbb5fb33e5d0023b001ad4be828fccdcc56500059d56f9869462 - md5: 234a5554c53625688d51062645337328 + size: 918664 + timestamp: 1742083674731 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-14.2.0-h8f9b012_2.conda + sha256: 8f5bd92e4a24e1d35ba015c5252e8f818898478cb3bc50bd8b12ab54707dc4da + md5: a78c856b6dc6bf4ea8daeb9beaaa3fb0 depends: - - libgcc 14.2.0 h77fa898_1 + - __glibc >=2.17,<3.0.a0 + - libgcc 14.2.0 h767d61c_2 arch: x86_64 platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL - size: 3893695 - timestamp: 1729027746910 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_1.conda - sha256: 25bb30b827d4f6d6f0522cc0579e431695503822f144043b93c50237017fffd8 - md5: 8371ac6457591af2cf6159439c1fd051 + size: 3884556 + timestamp: 1740240685253 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.2.0-h4852527_2.conda + sha256: e86f38b007cf97cc2c67cd519f2de12a313c4ee3f5ef11652ad08932a5e34189 + md5: c75da67f045c2627f59e6fcb5f4e3a9b depends: - - libstdcxx 14.2.0 hc0a3c3a_1 + - libstdcxx 14.2.0 h8f9b012_2 arch: x86_64 platform: linux license: GPL-3.0-only WITH GCC-exception-3.1 license_family: GPL - size: 54105 - timestamp: 1729027780628 + size: 53830 + timestamp: 1740240722530 - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 md5: 40b61aab5c7ba9ff276c41cfffe6b80b @@ -829,112 +531,35 @@ packages: license_family: Other size: 60963 timestamp: 1727963148474 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda - sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b - md5: 369964e85dc26bfe78f41399b366c435 - depends: - - __osx >=11.0 - constrains: - - zlib 1.3.1 *_2 - arch: arm64 - platform: osx - license: Zlib - license_family: Other - size: 46438 - timestamp: 1727963202283 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-19.1.7-hdb05f8b_0.conda - sha256: b92a669f2059874ebdcb69041b6c243d68ffc3fb356ac1339cec44aeb27245d7 - md5: c4d54bfd3817313ce758aa76283b118d - depends: - - __osx >=11.0 - constrains: - - openmp 19.1.7|19.1.7.* - arch: arm64 - platform: osx - license: Apache-2.0 WITH LLVM-exception - license_family: APACHE - size: 280830 - timestamp: 1736986295869 -- conda: https://conda.modular.com/max-nightly/noarch/max-25.1.0.dev2025020805-release.conda +- conda: https://conda.modular.com/max-nightly/noarch/max-25.3.0.dev2025032305-release.conda noarch: python - sha256: 54b73005e4ac4053975e984eba3ab2c328c047f1aaf63217dac62be5a2a50087 - md5: fa2aa1710d82090e6d50d05907a703ea + sha256: 57f3f5192f1f8add82ca2bcf73efdd02e2690f0e349f065dfa9a427475c3e4e5 + md5: 4d317358283e98bba587e56e2f58cddf depends: - - max-core ==25.1.0.dev2025020805 release - - max-python ==25.1.0.dev2025020805 release - - mojo-jupyter ==25.1.0.dev2025020805 release - - mblack ==25.1.0.dev2025020805 release + - max-core ==25.3.0.dev2025032305 release + - max-python ==25.3.0.dev2025032305 release + - mojo-jupyter ==25.3.0.dev2025032305 release + - mblack ==25.3.0.dev2025032305 release license: LicenseRef-Modular-Proprietary - size: 9900 - timestamp: 1738991846700 -- conda: https://conda.modular.com/max-nightly/linux-64/max-core-25.1.0.dev2025020805-release.conda - sha256: d8d4cc499562b0d6c620f0019b5ed01235834af4e20349be726015d162ca21c1 - md5: da4c181f16eafb9d10c55137cc5466e6 + size: 9906 + timestamp: 1742707106594 +- conda: https://conda.modular.com/max-nightly/linux-64/max-core-25.3.0.dev2025032305-release.conda + sha256: 2fe98f01d38c5de2cb9be1a6c5ec46dbc36c50e0df212827ccff774d24a78dd5 + md5: 44c417caf749bebe8b87625be798479b depends: - - mblack ==25.1.0.dev2025020805 release - license: LicenseRef-Modular-Proprietary - size: 244893250 - timestamp: 1738991846699 -- conda: https://conda.modular.com/max-nightly/osx-arm64/max-core-25.1.0.dev2025020805-release.conda - sha256: 091a2794945506a316c1ce75bd798bc1bf839328be59eb1c21c0b143c44f9c7a - md5: 7e09138f0f8a28dd30a01ebbe15f3078 - depends: - - mblack ==25.1.0.dev2025020805 release - license: LicenseRef-Modular-Proprietary - size: 209778709 - timestamp: 1738994653883 -- conda: https://conda.modular.com/max-nightly/linux-64/max-python-25.1.0.dev2025020805-release.conda - noarch: python - sha256: 80124268f375ed69f37bd91b8b061bd6c6153342c5b9053aca10eb3dcea7342a - md5: 0f14d712ff526fc8671e24b1497bd97f - depends: - - max-core ==25.1.0.dev2025020805 release - - click >=8.0.0 - - numpy >=1.18,<2.0 - - sentencepiece >=0.2.0 - - tqdm >=4.67.1 - constrains: - - aiohttp >=3.11.12 - - fastapi >=0.114.2 - - gguf >=0.14.0 - - hf-transfer >=0.1.9 - - httpx >=0.28.1 - - huggingface_hub >=0.24.0 - - nvitop >=1.4.1 - - opentelemetry-api >=1.29.0 - - opentelemetry-exporter-otlp-proto-http >=1.27.0 - - opentelemetry-exporter-prometheus >=0.48b0 - - opentelemetry-sdk >=1.29.0,<2.0 - - pillow >=10.3.0 - - prometheus_client >=0.21.0 - - prometheus-async >=22.2.0 - - psutil >=6.1.1 - - pydantic - - pydantic-settings >=2.7.1 - - pyinstrument >=5.0.1 - - python-json-logger >=2.0.7 - - requests >=2.32.3 - - rich >=13.9.4 - - safetensors >=0.5.2 - - scipy >=1.15.1 - - sentinel >=0.3.0 - - sse-starlette >=2.1.2 - - tokenizers >=0.19.0 - - pytorch >=2.2.2,<=2.5.1 - - transformers >=4.40.1 - - uvicorn >=0.34.0 - - uvloop >=0.21.0 + - mblack ==25.3.0.dev2025032305 release license: LicenseRef-Modular-Proprietary - size: 120574508 - timestamp: 1738991846700 -- conda: https://conda.modular.com/max-nightly/osx-arm64/max-python-25.1.0.dev2025020805-release.conda + size: 264067158 + timestamp: 1742707106594 +- conda: https://conda.modular.com/max-nightly/linux-64/max-python-25.3.0.dev2025032305-release.conda noarch: python - sha256: a8a45c2e7fde72557f537b759cea361416137c31f3123abc2199cd1162778bc5 - md5: 8d86afd47cf099690acd45ab4cb3e879 + sha256: 18f6d428fa9fcb9b07b9dc2489e287eaba320b819de01a1da4a34d8d442b988a + md5: 3c697732f38c4f1476ddd7b1de44dcc5 depends: - - max-core ==25.1.0.dev2025020805 release + - max-core ==25.3.0.dev2025032305 release - click >=8.0.0 - numpy >=1.18,<2.0 + - packaging >=24.1 - sentencepiece >=0.2.0 - tqdm >=4.67.1 constrains: @@ -947,7 +572,7 @@ packages: - nvitop >=1.4.1 - opentelemetry-api >=1.29.0 - opentelemetry-exporter-otlp-proto-http >=1.27.0 - - opentelemetry-exporter-prometheus >=0.48b0 + - opentelemetry-exporter-prometheus >=0.48b0,<1.1.12.0rc1 - opentelemetry-sdk >=1.29.0,<2.0 - pillow >=10.3.0 - prometheus_client >=0.21.0 @@ -960,21 +585,21 @@ packages: - requests >=2.32.3 - rich >=13.9.4 - safetensors >=0.5.2 - - scipy >=1.15.1 - sentinel >=0.3.0 - sse-starlette >=2.1.2 - tokenizers >=0.19.0 - - pytorch >=2.2.2,<=2.5.1 + - pytorch >=2.5.0,<=2.6.0 - transformers >=4.40.1 - uvicorn >=0.34.0 - uvloop >=0.21.0 + - xgrammar ==0.1.11 license: LicenseRef-Modular-Proprietary - size: 108495948 - timestamp: 1738994653883 -- conda: https://conda.modular.com/max-nightly/noarch/mblack-25.1.0.dev2025020805-release.conda + size: 152002926 + timestamp: 1742707106594 +- conda: https://conda.modular.com/max-nightly/noarch/mblack-25.3.0.dev2025032305-release.conda noarch: python - sha256: 9e8dd6eb0497a0630a6cbc699ca67bec88c078e5b8e84a2b213e145018c1922e - md5: cd757be5471b8ecc3c5e43b44183ae4f + sha256: 9e5784a03f65b70bc05f988ab27de9af6899407ded897d7d3e28c7b311f4ad2f + md5: ef12b1eab5f790e699234f21bd2455b6 depends: - python >=3.9,<3.13 - click >=8.0.0 @@ -985,20 +610,20 @@ packages: - typing_extensions >=v4.12.2 - python license: MIT - size: 130855 - timestamp: 1738991846699 -- conda: https://conda.modular.com/max-nightly/noarch/mojo-jupyter-25.1.0.dev2025020805-release.conda + size: 130652 + timestamp: 1742707106594 +- conda: https://conda.modular.com/max-nightly/noarch/mojo-jupyter-25.3.0.dev2025032305-release.conda noarch: python - sha256: 01ce310a8f05bc16028bd8749c7decd899b2b569992b3db5790bd3b8d5f9d9fb - md5: cb0e7f08489ecfc881ec3197c3d8de15 + sha256: bf711d99a2d79f7af5345639175ab752e9495edfb1cde1ffa55919373a77a010 + md5: 6ae8b3ca8245e0445cd3f33a294074cb depends: - - max-core ==25.1.0.dev2025020805 release + - max-core ==25.3.0.dev2025032305 release - python >=3.9,<3.13 - jupyter_client >=8.6.2,<8.7 - python license: LicenseRef-Modular-Proprietary - size: 22988 - timestamp: 1738991846699 + size: 22982 + timestamp: 1742707106594 - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.0.0-pyha770c72_1.conda sha256: 1895f47b7d68581a6facde5cb13ab8c2764c2e53a76bd746f8f98910dc4e08fe md5: 29097e7ea634a45cc5386b95cac6568f @@ -1019,16 +644,6 @@ packages: license: X11 AND BSD-3-Clause size: 891641 timestamp: 1738195959188 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda - sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 - md5: 068d497125e4bf8a66bf707254fff5ae - depends: - - __osx >=11.0 - arch: arm64 - platform: osx - license: X11 AND BSD-3-Clause - size: 797030 - timestamp: 1738196177597 - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py312heda63a1_0.conda sha256: fe3459c75cf84dcef6ef14efcc4adb0ade66038ddd27cadb894f34f4797687d8 md5: d8285bea2a350f63fab23bf460221f3f @@ -1048,28 +663,9 @@ packages: license_family: BSD size: 7484186 timestamp: 1707225809722 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-1.26.4-py312h8442bc7_0.conda - sha256: c8841d6d6f61fd70ca80682efbab6bdb8606dc77c68d8acabfbd7c222054f518 - md5: d83fc83d589e2625a3451c9a7e21047c - depends: - - libblas >=3.9.0,<4.0a0 - - libcblas >=3.9.0,<4.0a0 - - libcxx >=16 - - liblapack >=3.9.0,<4.0a0 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - constrains: - - numpy-base <0a0 - arch: arm64 - platform: osx - license: BSD-3-Clause - license_family: BSD - size: 6073136 - timestamp: 1707226249608 -- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.0-h7b32b05_1.conda - sha256: f62f6bca4a33ca5109b6d571b052a394d836956d21b25b7ffd03376abf7a481f - md5: 4ce6875f75469b2757a65e10a5d05e31 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.4.1-h7b32b05_0.conda + sha256: cbf62df3c79a5c2d113247ddea5658e9ff3697b6e741c210656e239ecaf1768f + md5: 41adf927e746dc75ecf0ef841c454e48 depends: - __glibc >=2.17,<3.0.a0 - ca-certificates @@ -1078,20 +674,8 @@ packages: platform: linux license: Apache-2.0 license_family: Apache - size: 2937158 - timestamp: 1736086387286 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.4.0-h81ee809_1.conda - sha256: 97772762abc70b3a537683ca9fc3ff3d6099eb64e4aba3b9c99e6fce48422d21 - md5: 22f971393637480bda8c679f374d8861 - depends: - - __osx >=11.0 - - ca-certificates - arch: arm64 - platform: osx - license: Apache-2.0 - license_family: Apache - size: 2936415 - timestamp: 1736086108693 + size: 2939306 + timestamp: 1739301879343 - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-24.2-pyhd8ed1ab_2.conda sha256: da157b19bcd398b9804c5c52fc000fcb8ab0525bdb9c70f95beaa0bb42f85af1 md5: 3bfed7e6228ebf2f7b9eaa47f1b4e2aa @@ -1110,19 +694,20 @@ packages: license_family: MOZILLA size: 41075 timestamp: 1733233471940 -- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.6-pyhd8ed1ab_1.conda - sha256: bb50f6499e8bc1d1a26f17716c97984671121608dc0c3ecd34858112bce59a27 - md5: 577852c7e53901ddccc7e6a9959ddebe +- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.7-pyh29332c3_0.conda + sha256: ae7d3e58224d53d6b59e1f5ac5809803bb1972f0ac4fb10cd9b8c87d4122d3e0 + md5: e57da6fe54bb3a5556cf36d199ff07d8 depends: - python >=3.9 + - python license: MIT license_family: MIT - size: 20448 - timestamp: 1733232756001 -- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.8-h9e4cc4f_1_cpython.conda + size: 23291 + timestamp: 1742485085457 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.9-h9e4cc4f_1_cpython.conda build_number: 1 - sha256: 3f0e0518c992d8ccfe62b189125721309836fe48a010dc424240583e157f9ff0 - md5: 7fd2fd79436d9b473812f14e86746844 + sha256: 77f2073889d4c91a57bc0da73a0466d9164dbcf6191ea9c3a7be6872f784d625 + md5: d82342192dfc9145185190e651065aa9 depends: - __glibc >=2.17,<3.0.a0 - bzip2 >=1.0.8,<2.0a0 @@ -1130,14 +715,14 @@ packages: - libexpat >=2.6.4,<3.0a0 - libffi >=3.4,<4.0a0 - libgcc >=13 - - liblzma >=5.6.3,<6.0a0 + - liblzma >=5.6.4,<6.0a0 - libnsl >=2.0.1,<2.1.0a0 - - libsqlite >=3.47.0,<4.0a0 + - libsqlite >=3.49.1,<4.0a0 - libuuid >=2.38.1,<3.0a0 - libxcrypt >=4.4.36 - libzlib >=1.3.1,<2.0a0 - ncurses >=6.5,<7.0a0 - - openssl >=3.4.0,<4.0a0 + - openssl >=3.4.1,<4.0a0 - readline >=8.2,<9.0a0 - tk >=8.6.13,<8.7.0a0 - tzdata @@ -1146,32 +731,8 @@ packages: arch: x86_64 platform: linux license: Python-2.0 - size: 31565686 - timestamp: 1733410597922 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.8-hc22306f_1_cpython.conda - build_number: 1 - sha256: 7586a711b1b08a9df8864e26efdc06980bdfb0e18d5ac4651d0fee30a8d3e3a0 - md5: 54ca5b5d92ef3a3ba61e195ee882a518 - depends: - - __osx >=11.0 - - bzip2 >=1.0.8,<2.0a0 - - libexpat >=2.6.4,<3.0a0 - - libffi >=3.4,<4.0a0 - - liblzma >=5.6.3,<6.0a0 - - libsqlite >=3.47.0,<4.0a0 - - libzlib >=1.3.1,<2.0a0 - - ncurses >=6.5,<7.0a0 - - openssl >=3.4.0,<4.0a0 - - readline >=8.2,<9.0a0 - - tk >=8.6.13,<8.7.0a0 - - tzdata - constrains: - - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx - license: Python-2.0 - size: 12998673 - timestamp: 1733408900971 + size: 31670716 + timestamp: 1741130026152 - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhff2d567_1.conda sha256: a50052536f1ef8516ed11a844f9413661829aa083304dc624c5925298d078d79 md5: 5ba79d7c71f03c678c8ead841f347d6e @@ -1194,21 +755,9 @@ packages: license_family: BSD size: 6238 timestamp: 1723823388266 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python_abi-3.12-5_cp312.conda - build_number: 5 - sha256: 49d624e4b809c799d2bf257b22c23cf3fc4460f5570d9a58e7ad86350aeaa1f4 - md5: b76f9b1c862128e56ac7aa8cd2333de9 - constrains: - - python 3.12.* *_cpython - arch: arm64 - platform: osx - license: BSD-3-Clause - license_family: BSD - size: 6278 - timestamp: 1723823099686 -- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.2.1-py312hbf22597_0.conda - sha256: 90ec0da0317d3d76990a40c61e1709ef859dd3d8c63838bad2814f46a63c8a2e - md5: 7cec8d0dac15a2d9fea8e49879aa779d +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-26.3.0-py312hbf22597_0.conda + sha256: aa96b9d13bc74f514ccbc3ad275d23bb837ec63894e6e7fb43786c7c41959bfd + md5: ec243006dd2b7dc72f1fba385e59f693 depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 @@ -1221,84 +770,42 @@ packages: platform: linux license: BSD-3-Clause license_family: BSD - size: 382698 - timestamp: 1738271121975 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-26.2.1-py312hf4875e0_0.conda - sha256: 70d398b334668dc597d33e27847ede1b0829a639b9c91ee845355e52c86c2293 - md5: bfbefdb140b546a80827ff7c9d5ac7b8 + size: 381353 + timestamp: 1741805281237 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c + md5: 283b96675859b20a825f8fa30f311446 depends: - - __osx >=11.0 - - libcxx >=18 - - libsodium >=1.0.20,<1.0.21.0a0 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - - zeromq >=4.3.5,<4.4.0a0 - arch: arm64 - platform: osx - license: BSD-3-Clause - license_family: BSD - size: 364649 - timestamp: 1738271263898 -- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - sha256: 5435cf39d039387fbdc977b0a762357ea909a7694d9528ab40f005e9208744d7 - md5: 47d31b792659ce70f470b5c82fdfb7a4 - depends: - - libgcc-ng >=12 - - ncurses >=6.3,<7.0a0 + - libgcc >=13 + - ncurses >=6.5,<7.0a0 arch: x86_64 platform: linux license: GPL-3.0-only license_family: GPL - size: 281456 - timestamp: 1679532220005 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda - sha256: a1dfa679ac3f6007362386576a704ad2d0d7a02e98f5d0b115f207a2da63e884 - md5: 8cbb776a2f641b943d413b3e19df71f4 - depends: - - ncurses >=6.3,<7.0a0 - arch: arm64 - platform: osx - license: GPL-3.0-only - license_family: GPL - size: 250351 - timestamp: 1679532511311 -- conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-0.2.0-hc8f76dd_10.conda - sha256: a1bde55ceb9dc5bd7879283d0f594a6ce65c07fda3de76230c65a4a5df47858a - md5: 480e915dfc6c592615ef6f217e615aa6 + size: 282480 + timestamp: 1740379431762 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-0.2.0-hc8f76dd_11.conda + sha256: ade8fd245a073c99944d68b0f4f947eee8d0b2011d856336aa5732a1ffe6d00a + md5: c74d391cfa68c10683bcd951c1cdea11 depends: - - libsentencepiece 0.2.0 h8e10757_10 + - libsentencepiece 0.2.0 he636bdd_11 - python_abi 3.12.* *_cp312 - - sentencepiece-python 0.2.0 py312hb6b8a2b_10 - - sentencepiece-spm 0.2.0 h8e10757_10 + - sentencepiece-python 0.2.0 py312hb957f94_11 + - sentencepiece-spm 0.2.0 he636bdd_11 arch: x86_64 platform: linux license: Apache-2.0 license_family: Apache - size: 19470 - timestamp: 1735628377167 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sentencepiece-0.2.0-h22a84ea_10.conda - sha256: 526c934b897131bd274697649c3b1c492a7d66c03368885a954112468b3c1424 - md5: 346abe448f69949a65473b336856860a - depends: - - libsentencepiece 0.2.0 he13a0af_10 - - python_abi 3.12.* *_cp312 - - sentencepiece-python 0.2.0 py312h155166a_10 - - sentencepiece-spm 0.2.0 he13a0af_10 - arch: arm64 - platform: osx - license: Apache-2.0 - license_family: Apache - size: 19759 - timestamp: 1735628533490 -- conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-python-0.2.0-py312hb6b8a2b_10.conda - sha256: 37df7fb659564587b950b39c936769d9b5095afb7bc223f42d6248a5540c6567 - md5: 7908b7b977e2e123a5f6a29906f2ce44 + size: 19758 + timestamp: 1741898803291 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-python-0.2.0-py312hb957f94_11.conda + sha256: 7868b7f289fa8d99b6c8296e0f2102c7dbcceb8229cb58db1402c1ac8ae6e8f0 + md5: ccacc7c72aad7676df09615577a1054f depends: - __glibc >=2.17,<3.0.a0 - libgcc >=13 - - libprotobuf >=5.28.3,<5.28.4.0a0 - - libsentencepiece 0.2.0 h8e10757_10 + - libprotobuf >=5.29.3,<5.29.4.0a0 + - libsentencepiece 0.2.0 he636bdd_11 - libstdcxx >=13 - python >=3.12,<3.13.0a0 - python_abi 3.12.* *_cp312 @@ -1306,58 +813,25 @@ packages: platform: linux license: Apache-2.0 license_family: Apache - size: 2411182 - timestamp: 1735628106455 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sentencepiece-python-0.2.0-py312h155166a_10.conda - sha256: 2d59614aeafbf54cc718805798c11c2d0a3b4b8d947d1bd81c87c484b4883077 - md5: 6e11367ef670296fce01fc9860be944d - depends: - - __osx >=11.0 - - libcxx >=18 - - libprotobuf >=5.28.3,<5.28.4.0a0 - - libsentencepiece 0.2.0 he13a0af_10 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx - license: Apache-2.0 - license_family: Apache - size: 2433303 - timestamp: 1735628083958 -- conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-spm-0.2.0-h8e10757_10.conda - sha256: 6b55e1121545357d63c3aa0766931720e8aafc52d88641ba7631c8cbaa68c52f - md5: e977b7be5ac26c55825e121e00b90493 + size: 2497019 + timestamp: 1741898360084 +- conda: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-spm-0.2.0-he636bdd_11.conda + sha256: 5e791dab9c19798cbaced8545a7c82d11ee3b3b7a93b5c9384df1dd41ad245e9 + md5: 3f6cd9203b68213cc85cb71541cc66b2 depends: - __glibc >=2.17,<3.0.a0 - libabseil * cxx17* - - libabseil >=20240722.0,<20240723.0a0 + - libabseil >=20250127.0,<20250128.0a0 - libgcc >=13 - - libprotobuf >=5.28.3,<5.28.4.0a0 - - libsentencepiece 0.2.0 h8e10757_10 + - libprotobuf >=5.29.3,<5.29.4.0a0 + - libsentencepiece 0.2.0 he636bdd_11 - libstdcxx >=13 arch: x86_64 platform: linux license: Apache-2.0 license_family: Apache - size: 89076 - timestamp: 1735628334078 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/sentencepiece-spm-0.2.0-he13a0af_10.conda - sha256: b760aeee02e2afbfcce3f559e8a227aa4c94139157b1702fdfb41a057dad0e52 - md5: 9b7300f23cd330da8664cd072c162e5f - depends: - - __osx >=11.0 - - libabseil * cxx17* - - libabseil >=20240722.0,<20240723.0a0 - - libcxx >=18 - - libprotobuf >=5.28.3,<5.28.4.0a0 - - libsentencepiece 0.2.0 he13a0af_10 - arch: arm64 - platform: osx - license: Apache-2.0 - license_family: Apache - size: 83826 - timestamp: 1735628514667 + size: 90818 + timestamp: 1741898789485 - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda sha256: 41db0180680cc67c3fa76544ffd48d6a5679d96f4b71d7498a759e94edc9a2db md5: a451d576819089b0d672f18768be0f65 @@ -1379,17 +853,6 @@ packages: license_family: BSD size: 3318875 timestamp: 1699202167581 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda - sha256: 72457ad031b4c048e5891f3f6cb27a53cb479db68a52d965f796910e71a403a8 - md5: b50a57ba89c32b62428b71a875291c9b - depends: - - libzlib >=1.2.13,<2.0.0a0 - arch: arm64 - platform: osx - license: TCL - license_family: BSD - size: 3145523 - timestamp: 1699202432999 - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.4.2-py312h66e93f0_0.conda sha256: 062a3a3a37fa8615ce57929ba7e982c76f5a5810bcebd435950f6d6c4147c310 md5: e417822cb989e80a0d2b1b576fdd1657 @@ -1404,20 +867,6 @@ packages: license_family: Apache size: 840414 timestamp: 1732616043734 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.4.2-py312hea69d52_0.conda - sha256: 964a2705a36c50040c967b18b45b9cc8de3c2aff4af546979a574e0b38e58e39 - md5: fb0605888a475d6a380ae1d1a819d976 - depends: - - __osx >=11.0 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 - arch: arm64 - platform: osx - license: Apache-2.0 - license_family: Apache - size: 842549 - timestamp: 1732616081362 - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda sha256: 11e2c85468ae9902d24a27137b6b39b4a78099806e551d390e394a8c34b48e40 md5: 9efbfdc37242619130ea42b1cc4ed861 @@ -1445,12 +894,12 @@ packages: license_family: PSF size: 39637 timestamp: 1733188758212 -- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025a-h78e105d_0.conda - sha256: c4b1ae8a2931fe9b274c44af29c5475a85b37693999f8c792dad0f8c6734b1de - md5: dbcace4706afdfb7eb891f7b37d07c04 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 + md5: 4222072737ccff51314b5ece9c7d6f5a license: LicenseRef-Public-Domain - size: 122921 - timestamp: 1737119101255 + size: 122968 + timestamp: 1742727099393 - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_7.conda sha256: a4dc72c96848f764bb5a5176aa93dd1e9b9e52804137b99daeebba277b31ea10 md5: 3947a35e916fcc6b9825449affbf4214 @@ -1466,20 +915,6 @@ packages: license_family: MOZILLA size: 335400 timestamp: 1731585026517 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-hc1bb282_7.conda - sha256: 9e585569fe2e7d3bea71972cd4b9f06b1a7ab8fa7c5139f92a31cbceecf25a8a - md5: f7e6b65943cb73bce0143737fded08f1 - depends: - - __osx >=11.0 - - krb5 >=1.21.3,<1.22.0a0 - - libcxx >=18 - - libsodium >=1.0.20,<1.0.21.0a0 - arch: arm64 - platform: osx - license: MPL-2.0 - license_family: MOZILLA - size: 281565 - timestamp: 1731585108039 - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.21.0-pyhd8ed1ab_1.conda sha256: 567c04f124525c97a096b65769834b7acb047db24b15a56888a322bf3966c3e1 md5: 0c3cc595284c5e8f0f9900a9b228a332 diff --git a/mojoproject.toml b/mojoproject.toml index 941d27a..dbe5bca 100644 --- a/mojoproject.toml +++ b/mojoproject.toml @@ -3,8 +3,8 @@ authors = ["Manuel Saelices "] channels = ["conda-forge", "https://conda.modular.com/max-nightly"] description = "Library for building WebSocket servers and clients in Mojo" name = "mojo-websockets" -platforms = ["linux-64", "osx-arm64"] -version = "0.1.1" +platforms = ["linux-64"] +version = "0.1.3" [dependencies] max = "*" diff --git a/src/websockets/libc.mojo b/src/websockets/libc.mojo index c0c6bec..cb07c66 100644 --- a/src/websockets/libc.mojo +++ b/src/websockets/libc.mojo @@ -278,7 +278,11 @@ struct sockaddr: var sa_family: sa_family_t var sa_data: StaticTuple[c_char, 14] - fn __init__(out self, family: sa_family_t = 0, data: StaticTuple[c_char, 14] = StaticTuple[c_char, 14]()): + fn __init__( + out self, + family: sa_family_t = 0, + data: StaticTuple[c_char, 14] = StaticTuple[c_char, 14](), + ): self.sa_family = family self.sa_data = data @@ -485,7 +489,8 @@ fn inet_ntop[ * Reference: https://man7.org/linux/man-pages/man3/inet_ntop.3p.html.. """ constrained[ - Int(address_family) in [AF_INET, AF_INET6], "Address family must be either INET_ADDRSTRLEN or INET6_ADDRSTRLEN." + Int(address_family) in [AF_INET, AF_INET6], + "Address family must be either INET_ADDRSTRLEN or INET6_ADDRSTRLEN.", ]() constrained[ address_length in [INET_ADDRSTRLEN, INET6_ADDRSTRLEN], @@ -493,7 +498,10 @@ fn inet_ntop[ ]() var dst = String(capacity=address_length) var result = _inet_ntop( - address_family, UnsafePointer.address_of(ip_address).bitcast[c_void](), dst.unsafe_ptr(), address_length + address_family, + UnsafePointer.address_of(ip_address).bitcast[c_void](), + dst.unsafe_ptr(), + address_length, ) var i = 0 @@ -501,26 +509,37 @@ fn inet_ntop[ if result[i] == 0: break i += 1 - dst._buffer.size = i + 1 # Need to modify internal buffer's size for the string to be valid. + dst._buffer._len = ( + i + 1 + ) # Need to modify internal buffer's size for the string to be valid. # `inet_ntop` returns NULL on error. if not result: var errno = get_errno() if errno == EAFNOSUPPORT: - raise Error("inet_ntop Error: `*src` was not an `AF_INET` or `AF_INET6` family address.") - elif errno == ENOSPC: raise Error( - "inet_ntop Error: The buffer size, `size`, was not large enough to store the presentation form of the" + "inet_ntop Error: `*src` was not an `AF_INET` or `AF_INET6` family" " address." ) + elif errno == ENOSPC: + raise Error( + "inet_ntop Error: The buffer size, `size`, was not large enough to" + " store the presentation form of the address." + ) else: - raise Error("inet_ntop Error: An error occurred while converting the address. Error code: " + String(errno)) + raise Error( + "inet_ntop Error: An error occurred while converting the address. Error" + " code: " + + String(errno) + ) # We want the string representation of the address, so it's ok to take ownership of the pointer here. return dst -fn _inet_pton(af: c_int, src: UnsafePointer[c_char], dst: UnsafePointer[c_void]) -> c_int: +fn _inet_pton( + af: c_int, src: UnsafePointer[c_char], dst: UnsafePointer[c_void] +) -> c_int: """Libc POSIX `inet_pton` function. Converts a presentation format address (that is, printable form as held in a character string) to network format (usually a struct in_addr or some other internal binary representation, in network byte order). It returns 1 if the address was valid for the specified address family, or 0 if the address was not parseable in the specified address family, @@ -577,7 +596,8 @@ fn inet_pton[address_family: Int32](src: UnsafePointer[c_char]) raises -> c_uint * This function is valid for `AF_INET` and `AF_INET6`. """ constrained[ - Int(address_family) in [AF_INET, AF_INET6], "Address family must be either INET_ADDRSTRLEN or INET6_ADDRSTRLEN." + Int(address_family) in [AF_INET, AF_INET6], + "Address family must be either INET_ADDRSTRLEN or INET6_ADDRSTRLEN.", ]() var ip_buffer: UnsafePointer[c_void] @@ -592,7 +612,11 @@ fn inet_pton[address_family: Int32](src: UnsafePointer[c_char]) raises -> c_uint raise Error("inet_pton Error: The input is not a valid address.") elif result == -1: var errno = get_errno() - raise Error("inet_pton Error: An error occurred while converting the address. Error code: " + String(errno)) + raise Error( + "inet_pton Error: An error occurred while converting the address. Error" + " code: " + + String(errno) + ) return ip_buffer.bitcast[c_uint]().take_pointee() @@ -653,34 +677,44 @@ fn socket(domain: c_int, type: c_int, protocol: c_int) raises -> c_int: var errno = get_errno() if errno == EACCES: raise Error( - "SocketError (EACCES): Permission to create a socket of the specified type and/or protocol is denied." + "SocketError (EACCES): Permission to create a socket of the specified" + " type and/or protocol is denied." ) elif errno == EAFNOSUPPORT: - raise Error("SocketError (EAFNOSUPPORT): The implementation does not support the specified address family.") + raise Error( + "SocketError (EAFNOSUPPORT): The implementation does not support the" + " specified address family." + ) elif errno == EINVAL: raise Error( - "SocketError (EINVAL): Invalid flags in type, Unknown protocol, or protocol family not available." + "SocketError (EINVAL): Invalid flags in type, Unknown protocol, or" + " protocol family not available." ) elif errno == EMFILE: raise Error( - "SocketError (EMFILE): The per-process limit on the number of open file descriptors has been reached." + "SocketError (EMFILE): The per-process limit on the number of open file" + " descriptors has been reached." ) elif errno == ENFILE: raise Error( - "SocketError (ENFILE): The system-wide limit on the total number of open files has been reached." + "SocketError (ENFILE): The system-wide limit on the total number of" + " open files has been reached." ) elif Int(errno) in [ENOBUFS, ENOMEM]: raise Error( - "SocketError (ENOBUFS or ENOMEM): Insufficient memory is available. The socket cannot be created until" - " sufficient resources are freed." + "SocketError (ENOBUFS or ENOMEM): Insufficient memory is available. The" + " socket cannot be created until sufficient resources are freed." ) elif errno == EPROTONOSUPPORT: raise Error( - "SocketError (EPROTONOSUPPORT): The protocol type or the specified protocol is not supported within" - " this domain." + "SocketError (EPROTONOSUPPORT): The protocol type or the specified" + " protocol is not supported within this domain." ) else: - raise Error("SocketError: An error occurred while creating the socket. Error code: " + String(errno)) + raise Error( + "SocketError: An error occurred while creating the socket. Error code: " + + String(errno) + ) return fd @@ -755,23 +789,36 @@ fn setsockopt( #### Notes: * Reference: https://man7.org/linux/man-pages/man3/setsockopt.3p.html. """ - var result = _setsockopt(socket, level, option_name, Pointer.address_of(option_value), sizeof[Int]()) + var result = _setsockopt( + socket, level, option_name, Pointer.address_of(option_value), sizeof[Int]() + ) if result == -1: var errno = get_errno() if errno == EBADF: raise Error("setsockopt: The argument `socket` is not a valid descriptor.") elif errno == EFAULT: - raise Error("setsockopt: The argument `option_value` points outside the process's allocated address space.") + raise Error( + "setsockopt: The argument `option_value` points outside the process's" + " allocated address space." + ) elif errno == EINVAL: raise Error( - "setsockopt: The argument `option_len` is invalid. Can sometimes occur when `option_value` is invalid." + "setsockopt: The argument `option_len` is invalid. Can sometimes occur" + " when `option_value` is invalid." ) elif errno == ENOPROTOOPT: - raise Error("setsockopt [InvalidProtocol]: The option is unknown at the level indicated.") + raise Error( + "setsockopt [InvalidProtocol]: The option is unknown at the level" + " indicated." + ) elif errno == ENOTSOCK: raise Error("setsockopt: The argument `socket` is not a socket.") else: - raise Error("setsockopt: An error occurred while setting the socket option. Error code: " + String(errno)) + raise Error( + "setsockopt: An error occurred while setting the socket option. Error" + " code: " + + String(errno) + ) fn _getsockopt[ @@ -847,30 +894,44 @@ fn getsockopt( """ var option_value = stack_allocation[1, c_void]() var option_len: socklen_t = sizeof[Int]() - var result = _getsockopt(socket, level, option_name, option_value, Pointer.address_of(option_len)) + var result = _getsockopt( + socket, level, option_name, option_value, Pointer.address_of(option_len) + ) if result == -1: var errno = get_errno() if errno == EBADF: raise Error("getsockopt: The argument `socket` is not a valid descriptor.") elif errno == EFAULT: - raise Error("getsockopt: The argument `option_value` points outside the process's allocated address space.") + raise Error( + "getsockopt: The argument `option_value` points outside the process's" + " allocated address space." + ) elif errno == EINVAL: raise Error( - "getsockopt: The argument `option_len` is invalid. Can sometimes occur when `option_value` is invalid." + "getsockopt: The argument `option_len` is invalid. Can sometimes occur" + " when `option_value` is invalid." ) elif errno == ENOPROTOOPT: raise Error("getsockopt: The option is unknown at the level indicated.") elif errno == ENOTSOCK: raise Error("getsockopt: The argument `socket` is not a socket.") else: - raise Error("getsockopt: An error occurred while setting the socket option. Error code: " + String(errno)) + raise Error( + "getsockopt: An error occurred while setting the socket option. Error" + " code: " + + String(errno) + ) return option_value.bitcast[Int]().take_pointee() fn _getsockname[ origin: Origin -](socket: c_int, address: UnsafePointer[sockaddr], address_len: Pointer[socklen_t, origin],) -> c_int: +]( + socket: c_int, + address: UnsafePointer[sockaddr], + address_len: Pointer[socklen_t, origin], +) -> c_int: """Libc POSIX `getsockname` function. Args: @@ -900,7 +961,11 @@ fn _getsockname[ fn getsockname[ origin: Origin -](socket: c_int, address: UnsafePointer[sockaddr], address_len: Pointer[socklen_t, origin],) raises: +]( + socket: c_int, + address: UnsafePointer[sockaddr], + address_len: Pointer[socklen_t, origin], +) raises: """Libc POSIX `getsockname` function. Args: @@ -931,21 +996,35 @@ fn getsockname[ raise Error("getsockname: The argument `socket` is not a valid descriptor.") elif errno == EFAULT: raise Error( - "getsockname: The `address` argument points to memory not in a valid part of the process address space." + "getsockname: The `address` argument points to memory not in a valid" + " part of the process address space." ) elif errno == EINVAL: raise Error("getsockname: `address_len` is invalid (e.g., is negative).") elif errno == ENOBUFS: - raise Error("getsockname: Insufficient resources were available in the system to perform the operation.") + raise Error( + "getsockname: Insufficient resources were available in the system to" + " perform the operation." + ) elif errno == ENOTSOCK: - raise Error("getsockname: The argument `socket` is not a socket, it is a file.") + raise Error( + "getsockname: The argument `socket` is not a socket, it is a file." + ) else: - raise Error("getsockname: An error occurred while getting the socket name. Error code: " + String(errno)) + raise Error( + "getsockname: An error occurred while getting the socket name. Error" + " code: " + + String(errno) + ) fn _getpeername[ origin: Origin -](sockfd: c_int, addr: UnsafePointer[sockaddr], address_len: Pointer[socklen_t, origin],) -> c_int: +]( + sockfd: c_int, + addr: UnsafePointer[sockaddr], + address_len: Pointer[socklen_t, origin], +) -> c_int: """Libc POSIX `getpeername` function. Args: @@ -997,31 +1076,49 @@ fn getpeername(file_descriptor: c_int) raises -> sockaddr_in: * Reference: https://man7.org/linux/man-pages/man2/getpeername.2.html. """ var remote_address = stack_allocation[1, sockaddr]() - var result = _getpeername(file_descriptor, remote_address, Pointer.address_of(socklen_t(sizeof[sockaddr]()))) + var result = _getpeername( + file_descriptor, + remote_address, + Pointer.address_of(socklen_t(sizeof[sockaddr]())), + ) if result == -1: var errno = get_errno() if errno == EBADF: raise Error("getpeername: The argument `socket` is not a valid descriptor.") elif errno == EFAULT: raise Error( - "getpeername: The `addr` argument points to memory not in a valid part of the process address space." + "getpeername: The `addr` argument points to memory not in a valid part" + " of the process address space." ) elif errno == EINVAL: raise Error("getpeername: `address_len` is invalid (e.g., is negative).") elif errno == ENOBUFS: - raise Error("getpeername: Insufficient resources were available in the system to perform the operation.") + raise Error( + "getpeername: Insufficient resources were available in the system to" + " perform the operation." + ) elif errno == ENOTCONN: raise Error("getpeername: The socket is not connected.") elif errno == ENOTSOCK: - raise Error("getpeername: The argument `socket` is not a socket, it is a file.") + raise Error( + "getpeername: The argument `socket` is not a socket, it is a file." + ) else: - raise Error("getpeername: An error occurred while getting the socket name. Error code: " + String(errno)) + raise Error( + "getpeername: An error occurred while getting the socket name. Error" + " code: " + + String(errno) + ) # Cast sockaddr struct to sockaddr_in return remote_address.bitcast[sockaddr_in]().take_pointee() -fn _bind[origin: MutableOrigin](socket: c_int, address: Pointer[sockaddr_in, origin], address_len: socklen_t) -> c_int: +fn _bind[ + origin: MutableOrigin +]( + socket: c_int, address: Pointer[sockaddr_in, origin], address_len: socklen_t +) -> c_int: """Libc POSIX `bind` function. Args: @@ -1040,7 +1137,9 @@ fn _bind[origin: MutableOrigin](socket: c_int, address: Pointer[sockaddr_in, ori #### Notes: * Reference: https://man7.org/linux/man-pages/man3/bind.3p.html. """ - return external_call["bind", c_int, c_int, Pointer[sockaddr_in, origin], socklen_t](socket, address, address_len) + return external_call["bind", c_int, c_int, Pointer[sockaddr_in, origin], socklen_t]( + socket, address, address_len + ) fn bind(socket: c_int, mut address: sockaddr_in) raises: @@ -1082,7 +1181,10 @@ fn bind(socket: c_int, mut address: sockaddr_in) raises: if result == -1: var errno = get_errno() if errno == EACCES: - raise Error("bind: The address, `address`, is protected, and the user is not the superuser.") + raise Error( + "bind: The address, `address`, is protected, and the user is not the" + " superuser." + ) elif errno == EADDRINUSE: raise Error("bind: The given address is already in use.") elif errno == EBADF: @@ -1115,7 +1217,10 @@ fn bind(socket: c_int, mut address: sockaddr_in) raises: # elif errno == EROFS: # raise Error("bind: The socket inode would reside on a read-only file system.") - raise Error("bind: An error occurred while binding the socket. Error code: " + String(errno)) + raise Error( + "bind: An error occurred while binding the socket. Error code: " + + String(errno) + ) fn _listen(socket: c_int, backlog: c_int) -> c_int: @@ -1171,14 +1276,24 @@ fn listen(socket: c_int, backlog: c_int) raises: elif errno == ENOTSOCK: raise Error("listen: `socket` is a descriptor for a file, not a socket.") elif errno == EOPNOTSUPP: - raise Error("listen: The socket is not of a type that supports the `listen()` operation.") + raise Error( + "listen: The socket is not of a type that supports the `listen()`" + " operation." + ) else: - raise Error("listen: An error occurred while listening on the socket. Error code: " + String(errno)) + raise Error( + "listen: An error occurred while listening on the socket. Error code: " + + String(errno) + ) fn _accept[ address_origin: MutableOrigin, len_origin: Origin -](socket: c_int, address: Pointer[sockaddr, address_origin], address_len: Pointer[socklen_t, len_origin],) -> c_int: +]( + socket: c_int, + address: Pointer[sockaddr, address_origin], + address_len: Pointer[socklen_t, len_origin], +) -> c_int: """Libc POSIX `accept` function. Args: @@ -1198,7 +1313,11 @@ fn _accept[ * Reference: https://man7.org/linux/man-pages/man3/accept.3p.html. """ return external_call[ - "accept", c_int, c_int, Pointer[sockaddr, address_origin], Pointer[socklen_t, len_origin] # FnName, RetType + "accept", + c_int, + c_int, + Pointer[sockaddr, address_origin], + Pointer[socklen_t, len_origin], # FnName, RetType ](socket, address, address_len) @@ -1235,38 +1354,55 @@ fn accept(socket: c_int) raises -> c_int: * Reference: https://man7.org/linux/man-pages/man3/accept.3p.html. """ var remote_address = sockaddr() - var result = _accept(socket, Pointer.address_of(remote_address), Pointer.address_of(socklen_t(sizeof[socklen_t]()))) + var result = _accept( + socket, + Pointer.address_of(remote_address), + Pointer.address_of(socklen_t(sizeof[socklen_t]())), + ) if result == -1: var errno = get_errno() if Int(errno) in [EAGAIN, EWOULDBLOCK]: raise Error( - "accept: The socket is marked nonblocking and no connections are present to be accepted. POSIX.1-2001" - " allows either error to be returned for this case, and does not require these constants to have the" - " same value, so a portable application should check for both possibilities.." + "accept: The socket is marked nonblocking and no connections are" + " present to be accepted. POSIX.1-2001 allows either error to be" + " returned for this case, and does not require these constants to have" + " the same value, so a portable application should check for both" + " possibilities.." ) elif errno == EBADF: raise Error("accept: `socket` is not a valid descriptor.") elif errno == ECONNABORTED: raise Error("accept: `socket` is not a valid descriptor.") elif errno == EFAULT: - raise Error("accept: The `address` argument is not in a writable part of the user address space.") + raise Error( + "accept: The `address` argument is not in a writable part of the user" + " address space." + ) elif errno == EINTR: raise Error( - "accept: The system call was interrupted by a signal that was caught before a valid connection arrived;" - " see `signal(7)`." + "accept: The system call was interrupted by a signal that was caught" + " before a valid connection arrived; see `signal(7)`." ) elif errno == EINVAL: raise Error( - "accept: Socket is not listening for connections, or `addr_length` is invalid (e.g., is negative)." + "accept: Socket is not listening for connections, or `addr_length` is" + " invalid (e.g., is negative)." ) elif errno == EMFILE: - raise Error("accept: The per-process limit of open file descriptors has been reached.") + raise Error( + "accept: The per-process limit of open file descriptors has been" + " reached." + ) elif errno == ENFILE: - raise Error("accept: The system limit on the total number of open files has been reached.") + raise Error( + "accept: The system limit on the total number of open files has been" + " reached." + ) elif Int(errno) in [ENOBUFS, ENOMEM]: raise Error( - "accept: Not enough free memory. This often means that the memory allocation is limited by the socket" - " buffer limits, not by the system memory." + "accept: Not enough free memory. This often means that the memory" + " allocation is limited by the socket buffer limits, not by the system" + " memory." ) elif errno == ENOTSOCK: raise Error("accept: `socket` is a descriptor for a file, not a socket.") @@ -1279,12 +1415,19 @@ fn accept(socket: c_int) raises -> c_int: if os_is_linux(): if errno == EPERM: raise Error("accept: Firewall rules forbid connection.") - raise Error("accept: An error occurred while listening on the socket. Error code: " + String(errno)) + raise Error( + "accept: An error occurred while listening on the socket. Error code: " + + String(errno) + ) return result -fn _connect[origin: Origin](socket: c_int, address: Pointer[sockaddr_in, origin], address_len: socklen_t) -> c_int: +fn _connect[ + origin: Origin +]( + socket: c_int, address: Pointer[sockaddr_in, origin], address_len: socklen_t +) -> c_int: """Libc POSIX `connect` function. Args: socket: A File Descriptor. @@ -1340,34 +1483,50 @@ fn connect(socket: c_int, address: sockaddr_in) raises: var errno = get_errno() if errno == EACCES: raise Error( - "connect: For UNIX domain sockets, which are identified by pathname: Write permission is denied on the" - " socket file, or search permission is denied for one of the directories in the path prefix. (See also" + "connect: For UNIX domain sockets, which are identified by pathname:" + " Write permission is denied on the socket file, or search permission" + " is denied for one of the directories in the path prefix. (See also" " path_resolution(7))." ) elif errno == EADDRINUSE: raise Error("connect: Local address is already in use.") elif errno == EAGAIN: - raise Error("connect: No more free local ports or insufficient entries in the routing cache.") + raise Error( + "connect: No more free local ports or insufficient entries in the" + " routing cache." + ) elif errno == EALREADY: raise Error( - "connect: The socket is nonblocking and a previous connection attempt has not yet been completed." + "connect: The socket is nonblocking and a previous connection attempt" + " has not yet been completed." ) elif errno == EBADF: - raise Error("connect: The file descriptor is not a valid index in the descriptor table.") + raise Error( + "connect: The file descriptor is not a valid index in the descriptor" + " table." + ) elif errno == ECONNREFUSED: raise Error("connect: No-one listening on the remote address.") elif errno == EFAULT: - raise Error("connect: The socket structure address is outside the user's address space.") + raise Error( + "connect: The socket structure address is outside the user's address" + " space." + ) elif errno == EINPROGRESS: raise Error( - "connect: The socket is nonblocking and the connection cannot be completed immediately. It is possible" - " to select(2) or poll(2) for completion by selecting the socket for writing. After select(2) indicates" - " writability, use getsockopt(2) to read the SO_ERROR option at level SOL_SOCKET to determine whether" - " connect() completed successfully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is one of the usual" - " error codes listed here, explaining the reason for the failure)." + "connect: The socket is nonblocking and the connection cannot be" + " completed immediately. It is possible to select(2) or poll(2) for" + " completion by selecting the socket for writing. After select(2)" + " indicates writability, use getsockopt(2) to read the SO_ERROR option" + " at level SOL_SOCKET to determine whether connect() completed" + " successfully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is one of" + " the usual error codes listed here, explaining the reason for the" + " failure)." ) elif errno == EINTR: - raise Error("connect: The system call was interrupted by a signal that was caught.") + raise Error( + "connect: The system call was interrupted by a signal that was caught." + ) elif errno == EISCONN: raise Error("connect: The socket is already connected.") elif errno == ENETUNREACH: @@ -1375,13 +1534,21 @@ fn connect(socket: c_int, address: sockaddr_in) raises: elif errno == ENOTSOCK: raise Error("connect: The file descriptor is not associated with a socket.") elif errno == EAFNOSUPPORT: - raise Error("connect: The passed address didn't have the correct address family in its `sa_family` field.") + raise Error( + "connect: The passed address didn't have the correct address family in" + " its `sa_family` field." + ) elif errno == ETIMEDOUT: raise Error( - "connect: Timeout while attempting connection. The server may be too busy to accept new connections." + "connect: Timeout while attempting connection. The server may be too" + " busy to accept new connections." ) else: - raise Error("connect: An error occurred while connecting to the socket. Error code: " + String(errno)) + raise Error( + "connect: An error occurred while connecting to the socket. Error" + " code: " + + String(errno) + ) fn _recv( @@ -1449,29 +1616,36 @@ fn recv( var errno = get_errno() if Int(errno) in [EAGAIN, EWOULDBLOCK]: raise Error( - "ReceiveError: The socket is marked nonblocking and the receive operation would block, or a receive" - " timeout had been set and the timeout expired before data was received." + "ReceiveError: The socket is marked nonblocking and the receive" + " operation would block, or a receive timeout had been set and the" + " timeout expired before data was received." ) elif errno == EBADF: raise Error("ReceiveError: The argument `socket` is an invalid descriptor.") elif errno == ECONNREFUSED: raise Error( - "ReceiveError: The remote host refused to allow the network connection (typically because it is not" - " running the requested service)." + "ReceiveError: The remote host refused to allow the network connection" + " (typically because it is not running the requested service)." ) elif errno == EFAULT: - raise Error("ReceiveError: `buffer` points outside the process's address space.") + raise Error( + "ReceiveError: `buffer` points outside the process's address space." + ) elif errno == EINTR: raise Error( - "ReceiveError: The receive was interrupted by delivery of a signal before any data were available." + "ReceiveError: The receive was interrupted by delivery of a signal" + " before any data were available." ) elif errno == ENOTCONN: raise Error("ReceiveError: The socket is not connected.") elif errno == ENOTSOCK: - raise Error("ReceiveError: The file descriptor is not associated with a socket.") + raise Error( + "ReceiveError: The file descriptor is not associated with a socket." + ) else: raise Error( - "ReceiveError: An error occurred while attempting to receive data from the socket. Error code: " + "ReceiveError: An error occurred while attempting to receive data from" + " the socket. Error code: " + String(errno) ) @@ -1562,7 +1736,14 @@ fn recvfrom( * `MSG_WAITALL`: On SOCK_STREAM sockets this requests that the function block until the full amount of data can be returned. The function may return the smaller amount of data if the socket is a message-based socket, if a signal is caught, if the connection is terminated, if MSG_PEEK was specified, or if an error is pending for the socket. """ - var result = _recvfrom(socket, buffer, length, flags, address, Pointer[socklen_t].address_of(sizeof[sockaddr]())) + var result = _recvfrom( + socket, + buffer, + length, + flags, + address, + Pointer[socklen_t].address_of(sizeof[sockaddr]()), + ) if result == -1: var errno = get_errno() if Int(errno) in [EAGAIN, EWOULDBLOCK]: @@ -1597,7 +1778,9 @@ fn recvfrom( return result -fn _send(socket: c_int, buffer: UnsafePointer[c_void], length: c_size_t, flags: c_int) -> c_ssize_t: +fn _send( + socket: c_int, buffer: UnsafePointer[c_void], length: c_size_t, flags: c_int +) -> c_ssize_t: """Libc POSIX `send` function. Args: @@ -1620,7 +1803,9 @@ fn _send(socket: c_int, buffer: UnsafePointer[c_void], length: c_size_t, flags: return external_call["send", c_ssize_t](socket, buffer, length, flags) -fn send(socket: c_int, buffer: UnsafePointer[c_void], length: c_size_t, flags: c_int) raises -> c_size_t: +fn send( + socket: c_int, buffer: UnsafePointer[c_void], length: c_size_t, flags: c_int +) raises -> c_size_t: """Libc POSIX `send` function. Args: @@ -1664,58 +1849,79 @@ fn send(socket: c_int, buffer: UnsafePointer[c_void], length: c_size_t, flags: c var errno = get_errno() if Int(errno) in [EAGAIN, EWOULDBLOCK]: raise Error( - "SendError: The socket is marked nonblocking and the receive operation would block, or a receive" - " timeout had been set and the timeout expired before data was received." + "SendError: The socket is marked nonblocking and the receive operation" + " would block, or a receive timeout had been set and the timeout" + " expired before data was received." ) elif errno == EBADF: raise Error("SendError: The argument `socket` is an invalid descriptor.") elif errno == EAGAIN: - raise Error("SendError: No more free local ports or insufficient entries in the routing cache.") + raise Error( + "SendError: No more free local ports or insufficient entries in the" + " routing cache." + ) elif errno == ECONNRESET: raise Error("SendError: Connection reset by peer.") elif errno == EDESTADDRREQ: - raise Error("SendError: The socket is not connection-mode, and no peer address is set.") + raise Error( + "SendError: The socket is not connection-mode, and no peer address is" + " set." + ) elif errno == ECONNREFUSED: raise Error( - "SendError: The remote host refused to allow the network connection (typically because it is not" - " running the requested service)." + "SendError: The remote host refused to allow the network connection" + " (typically because it is not running the requested service)." ) elif errno == EFAULT: - raise Error("SendError: `buffer` points outside the process's address space.") + raise Error( + "SendError: `buffer` points outside the process's address space." + ) elif errno == EINTR: raise Error( - "SendError: The receive was interrupted by delivery of a signal before any data were available." + "SendError: The receive was interrupted by delivery of a signal before" + " any data were available." ) elif errno == EINVAL: raise Error("SendError: Invalid argument passed.") elif errno == EISCONN: - raise Error("SendError: The connection-mode socket was connected already but a recipient was specified.") + raise Error( + "SendError: The connection-mode socket was connected already but a" + " recipient was specified." + ) elif errno == EMSGSIZE: raise Error( - "SendError: The socket type requires that message be sent atomically, and the size of the message to be" - " sent made this impossible.." + "SendError: The socket type requires that message be sent atomically," + " and the size of the message to be sent made this impossible.." ) elif errno == ENOBUFS: raise Error( - "SendError: The output queue for a network interface was full. This generally indicates that the" - " interface has stopped sending, but may be caused by transient congestion." + "SendError: The output queue for a network interface was full. This" + " generally indicates that the interface has stopped sending, but may" + " be caused by transient congestion." ) elif errno == ENOMEM: raise Error("SendError: No memory available.") elif errno == ENOTCONN: raise Error("SendError: The socket is not connected.") elif errno == ENOTSOCK: - raise Error("SendError: The file descriptor is not associated with a socket.") + raise Error( + "SendError: The file descriptor is not associated with a socket." + ) elif errno == EOPNOTSUPP: - raise Error("SendError: Some bit in the flags argument is inappropriate for the socket type.") + raise Error( + "SendError: Some bit in the flags argument is inappropriate for the" + " socket type." + ) elif errno == EPIPE: raise Error( - "SendError: The local end has been shut down on a connection oriented socket. In this case the process" - " will also receive a SIGPIPE unless MSG_NOSIGNAL is set." + "SendError: The local end has been shut down on a connection oriented" + " socket. In this case the process will also receive a SIGPIPE unless" + " MSG_NOSIGNAL is set." ) else: raise Error( - "SendError: An error occurred while attempting to receive data from the socket. Error code: " + "SendError: An error occurred while attempting to receive data from the" + " socket. Error code: " + String(errno) ) @@ -1758,7 +1964,14 @@ fn _sendto( * `MSG_NOSIGNAL`: Requests not to send the SIGPIPE signal if an attempt to send is made on a stream-oriented socket that is no longer connected. The [EPIPE] error shall still be returned. """ return external_call[ - "sendto", c_ssize_t, c_int, UnsafePointer[c_char], c_size_t, c_int, UnsafePointer[sockaddr], socklen_t + "sendto", + c_ssize_t, + c_int, + UnsafePointer[c_char], + c_size_t, + c_int, + UnsafePointer[sockaddr], + socklen_t, ](socket, message, length, flags, dest_addr, dest_len) @@ -1909,6 +2122,7 @@ fn dup(fd: c_int) -> c_int: """ return external_call["dup", c_int](fd) + alias ShutdownInvalidDescriptorError = "ShutdownError (EBADF): The argument `socket` is an invalid descriptor." alias ShutdownInvalidArgumentError = "ShutdownError (EINVAL): Invalid argument passed." alias ShutdownNotConnectedError = "ShutdownError (ENOTCONN): The socket is not connected." @@ -1950,7 +2164,8 @@ fn shutdown(socket: c_int, how: c_int) raises: raise ShutdownNotSocketError else: raise Error( - "ShutdownError: An error occurred while attempting to receive data from the socket. Error code: " + "ShutdownError: An error occurred while attempting to receive data from" + " the socket. Error code: " + String(errno) ) @@ -2043,7 +2258,10 @@ fn close(file_descriptor: c_int) raises: elif Int(errno) in [ENOSPC, EDQUOT]: raise CloseOutOfSpaceError else: - raise Error("SocketError: An error occurred while creating the socket. Error code: " + String(errno)) + raise Error( + "SocketError: An error occurred while creating the socket. Error code: " + + String(errno) + ) fn get_errno() -> c_int: diff --git a/src/websockets/protocol/client.mojo b/src/websockets/protocol/client.mojo index 4a0b771..9d8487a 100644 --- a/src/websockets/protocol/client.mojo +++ b/src/websockets/protocol/client.mojo @@ -1,7 +1,6 @@ from base64 import b64encode from collections import Optional from memory import UnsafePointer -from python import Python, PythonObject from websockets.aliases import Bytes, DEFAULT_MAX_REQUEST_BODY_SIZE, DEFAULT_BUFFER_SIZE, MAGIC_CONSTANT from websockets.http import ( diff --git a/src/websockets/protocol/server.mojo b/src/websockets/protocol/server.mojo index c8cd4fe..4151ee2 100644 --- a/src/websockets/protocol/server.mojo +++ b/src/websockets/protocol/server.mojo @@ -1,9 +1,13 @@ from base64 import b64encode from collections import Optional from memory import UnsafePointer -from python import Python, PythonObject -from websockets.aliases import Bytes, DEFAULT_MAX_REQUEST_BODY_SIZE, DEFAULT_BUFFER_SIZE, MAGIC_CONSTANT +from websockets.aliases import ( + Bytes, + DEFAULT_MAX_REQUEST_BODY_SIZE, + DEFAULT_BUFFER_SIZE, + MAGIC_CONSTANT, +) from websockets.http import ( get_date_timestamp, encode, @@ -31,6 +35,7 @@ struct ServerProtocol(Protocol): """ Sans-I/O implementation of a WebSocket server connection. """ + alias side = SERVER var origins: Optional[List[String]] @@ -48,6 +53,25 @@ struct ServerProtocol(Protocol): var close_rcvd_then_sent: Optional[Bool] var eof_sent: Bool var discard_sent: Bool + var _active: Bool # Track if the protocol is active/used + + fn __moveinit__(mut self, owned existing: Self): + # Needed as we have a list of protocols in the server for concurrency + self.origins = existing.origins + self.reader = existing.reader^ + self.events = existing.events^ + self.writes = existing.writes^ + self.state = existing.state + self.expect_cont_frame = existing.expect_cont_frame + self.parser_exc = existing.parser_exc + self.handshake_exc = existing.handshake_exc + self.curr_size = existing.curr_size + self.close_rcvd = existing.close_rcvd + self.close_sent = existing.close_sent + self.close_rcvd_then_sent = existing.close_rcvd_then_sent + self.eof_sent = existing.eof_sent + self.discard_sent = existing.discard_sent + self._active = existing._active fn __init__(out self, origins: Optional[List[String]] = None): self.origins = origins @@ -65,6 +89,7 @@ struct ServerProtocol(Protocol): self.close_rcvd_then_sent = None self.eof_sent = False self.discard_sent = False + self._active = False fn __copyinit__(out self, other: ServerProtocol): self.origins = other.origins @@ -85,6 +110,7 @@ struct ServerProtocol(Protocol): self.close_rcvd_then_sent = other.close_rcvd_then_sent self.eof_sent = other.eof_sent self.discard_sent = other.discard_sent + self._active = other._active # ===-------------------------------------------------------------------=== # # Trait implementations @@ -145,7 +171,10 @@ struct ServerProtocol(Protocol): fn process_response(mut self, response: HTTPResponse) raises -> None: """Process the handshare response from the server.""" - constrained[Self.side == CLIENT, "Protocol.process_response() is only available for client connections."]() + constrained[ + Self.side == CLIENT, + "Protocol.process_response() is only available for client connections.", + ]() # Public method for getting outgoing data after receiving data or sending events. @@ -235,7 +264,6 @@ struct ServerProtocol(Protocol): """Set the parser exception.""" self.parser_exc = exc - fn get_handshake_exc(self) -> Optional[Error]: """Get the handshake exception.""" return self.handshake_exc @@ -244,11 +272,25 @@ struct ServerProtocol(Protocol): """Set the handshake exception.""" self.handshake_exc = exc + fn is_active(self) -> Bool: + """Return True if this protocol is active/in use.""" + return self._active + + fn set_active(mut self): + """Mark this protocol as active/in use.""" + self._active = True + + fn set_inactive(mut self): + """Mark this protocol as inactive/available.""" + self._active = False + # ===-------------------------------------------------------------------=== # # Methods # ===-------------------------------------------------------------------=== # - fn accept[date_func: fn () -> String = get_date_timestamp](mut self, request: HTTPRequest) raises -> HTTPResponse: + fn accept[ + date_func: fn () -> String = get_date_timestamp + ](mut self, request: HTTPRequest) raises -> HTTPResponse: """ Accept a WebSocket connection. @@ -283,7 +325,9 @@ struct ServerProtocol(Protocol): if self.origins is not None and "Origin" in request.headers: if request.headers["Origin"] not in self.origins.value(): - raise Error('Invalid "Origin" header: {}'.format(request.headers["Origin"])) + raise Error( + 'Invalid "Origin" header: {}'.format(request.headers["Origin"]) + ) # Validate the base64 encoded Sec-WebSocket-Key _ = b64decode[validate=True](request.headers["Sec-WebSocket-Key"]) @@ -334,7 +378,7 @@ struct ServerProtocol(Protocol): _ = parse_buffer(self) fn reject[ - date_func : fn () -> String = get_date_timestamp, + date_func: fn () -> String = get_date_timestamp, ](mut self, status_code: Int, status_text: String, body: String) -> HTTPResponse: """ Fail the WebSocket connection. @@ -351,6 +395,6 @@ struct ServerProtocol(Protocol): Header("Date", date_func()), Header("Connection", "close"), Header("Content-Length", String(len(body))), - Header("Content-type", "text/plain; charset=utf-8") + Header("Content-type", "text/plain; charset=utf-8"), ) return HTTPResponse(status_code, status_text, headers, str_to_bytes(body)) diff --git a/src/websockets/socket.mojo b/src/websockets/socket.mojo index 3439b72..9005020 100644 --- a/src/websockets/socket.mojo +++ b/src/websockets/socket.mojo @@ -462,23 +462,39 @@ struct Socket[AddrType: Addr, address_family: Int = AF_INET]( Raises: Error: If connecting to the remote socket fails. """ + logger.debug("Socket.connect: Connecting to ", address, ":", String(port)) - @parameter - if os_is_macos(): - ip = addrinfo_macos().get_ip_address(address) - else: - ip = addrinfo_unix().get_ip_address(address) + var ip: in_addr + var binary_ip: c_uint + var addr: sockaddr_in - var addr = sockaddr_in( - address_family=address_family, port=port, binary_ip=ip.s_addr + try: + + @parameter + if os_is_macos(): + ip = addrinfo_macos().get_ip_address(address) + else: + ip = addrinfo_unix().get_ip_address(address) + + binary_ip = ip.s_addr + + except e: + logger.debug("get_ip_address failed: Falling back to direct IP") + binary_ip = inet_pton[address_family](address.unsafe_ptr()) + + addr = sockaddr_in( + address_family=address_family, + port=port, + binary_ip=binary_ip, ) try: connect(self.fd, addr) except e: logger.error( - "Socket.connect: Failed to establish a connection to the server." + "Socket.connect: Failed to establish a connection to the server: {}" + .format(e) ) - raise e + raise Error("Failed to connect to server: {}".format(e)) var remote = self.get_peer_name() self._remote_address = AddrType(remote[0], remote[1]) @@ -579,14 +595,14 @@ struct Socket[AddrType: Addr, address_family: Int = AF_INET]( """ var bytes_received: Int try: - var buff_size = buffer.size + var buff_size = len(buffer) bytes_received = recv( self.fd, buffer.unsafe_ptr().offset(buff_size), - buffer.capacity - buffer.size, + buffer.capacity - len(buffer), 0, ) - buffer.size += bytes_received + buffer._len += bytes_received except e: logger.error(e) raise Error("Socket.receive: Failed to read data from connection.") diff --git a/src/websockets/sync/client.mojo b/src/websockets/sync/client.mojo index c7402b0..8d34aa8 100644 --- a/src/websockets/sync/client.mojo +++ b/src/websockets/sync/client.mojo @@ -3,14 +3,14 @@ from websockets.frames import Frame from websockets.logger import logger from websockets.protocol.base import ( receive_data, - send_binary, - send_continuation, - send_text, + send_binary, + send_continuation, + send_text, ) from websockets.protocol.client import ClientProtocol from websockets.net import TCPAddr, TCPConnection from websockets.socket import Socket -from websockets.utils.bytes import str_to_bytes +from websockets.utils.bytes import str_to_bytes, bytes_to_str from websockets.utils.uri import URI @@ -18,6 +18,7 @@ struct Client: """ A client object that can be used to connect to a server. """ + var uri: URI var conn: TCPConnection var protocol: ClientProtocol @@ -30,7 +31,9 @@ struct Client: socket = Socket[TCPAddr]() except e: logger.error(e) - raise Error("Client: Failed to create WS client due to socket creation failure.") + raise Error( + "Client: Failed to create WS client due to socket creation failure." + ) self.conn = TCPConnection(socket^) fn __moveinit__(mut self, owned other: Self): @@ -139,6 +142,15 @@ struct Client: # TODO: Handle parse exceptions return received + fn recv_text(mut self) raises -> String: + """ + Receive a message from the server. + + Returns: + String - The message received. + """ + return bytes_to_str(self.recv()) + fn close(mut self) raises -> None: """ Close the connection. @@ -146,7 +158,6 @@ struct Client: self.conn.teardown() - fn connect(uri: String) raises -> Client: """ Serve HTTP requests. @@ -167,4 +178,3 @@ fn connect(uri: String) raises -> Client: . """ return Client(uri) - diff --git a/src/websockets/sync/server.mojo b/src/websockets/sync/server.mojo index 41bca9f..6aef584 100644 --- a/src/websockets/sync/server.mojo +++ b/src/websockets/sync/server.mojo @@ -2,14 +2,18 @@ # Thanks to @rd4com for the original code from base64 import b64encode -from collections import Dict, Optional +from collections import Dict, InlineArray, List, Optional from memory import UnsafePointer -from python import Python, PythonObject from time import sleep -from websockets.libc import c_int +from websockets.libc import c_int, close as libc_close -from websockets.aliases import Bytes, DEFAULT_BUFFER_SIZE, DEFAULT_MAX_REQUEST_BODY_SIZE, MAGIC_CONSTANT +from websockets.aliases import ( + Bytes, + DEFAULT_BUFFER_SIZE, + DEFAULT_MAX_REQUEST_BODY_SIZE, + MAGIC_CONSTANT, +) from websockets.frames import Frame from websockets.http import Header, Headers, HTTPRequest, HTTPResponse, encode from websockets.logger import logger @@ -20,70 +24,250 @@ from websockets.protocol.server import ServerProtocol from websockets.protocol.client import ClientProtocol from websockets.utils.bytes import str_to_bytes -alias BYTE_0_TEXT: UInt8 = 1 -alias BYTE_0_NO_FRAGMENT: UInt8 = 128 +from mojix.fd import Fd +from mojix.io_uring import SQE64 +from mojix.net.socket import socket, bind, listen +from mojix.net.types import AddrFamily, SocketType, SocketAddrV4 +from io_uring import IoUring +from io_uring.op import Accept, Read, Write + +alias ACCEPT = 0 +alias READ = 1 +alias WRITE = 2 +alias CLOSE = 3 # For handling connection closing + +# Configuration for io_uring +alias MAX_CONNECTIONS = 16 +alias BACKLOG = 512 +# alias MAX_MESSAGE_LEN = 16384 # Increased for WebSocket frames +alias MAX_MESSAGE_LEN = 2048 # Reduced because of compiler slow compilation (InlineArray meta-programming) +alias BUFFERS_COUNT = 16 # Must be power of 2 +alias BUF_RING_SIZE = BUFFERS_COUNT +# Number of entries in the submission queue +alias SQ_ENTRIES = 128 -alias BYTE_1_FRAME_IS_MASKED: UInt8 = 128 +alias ConnHandler = fn (conn: WSConnection, data: Bytes) raises -> None -alias BYTE_1_SIZE_ONE_BYTE: UInt8 = 125 -alias BYTE_1_SIZE_TWO_BYTES: UInt8 = 126 -alias BYTE_1_SIZE_EIGHT_BYTES: UInt8 = 127 +@value +struct ConnInfo(Writable): + var fd: Int32 + var type: UInt16 + var bid: UInt32 # Buffer ID + var protocol_id: UInt16 # ID to track which protocol instance this is associated with + + fn __init__( + out self, fd: Int32, type: UInt16, bid: UInt32 = 0, protocol_id: UInt16 = 0 + ): + self.fd = fd + self.type = type + self.bid = bid + self.protocol_id = protocol_id + + fn to_int(self) -> UInt64: + """Pack ConnInfo into a 64-bit integer for user_data.""" + return ( + (UInt64(self.fd) << 32) + | (UInt64(self.type) << 24) + | (UInt64(self.protocol_id) << 16) + | UInt64(self.bid) + ) -alias ConnHandler = fn (conn: WSConnection, data: Bytes) raises -> None + @staticmethod + fn from_int(value: UInt64) -> Self: + """Unpack ConnInfo from a 64-bit integer.""" + return Self( + fd=Int32((value >> 32) & 0xFFFFFFFF), + type=UInt16((value >> 24) & 0xFF), + protocol_id=UInt16((value >> 16) & 0xFF), + bid=UInt32(value & 0xFFFF), + ) + + fn write_to[W: Writer](self, mut writer: W) -> None: + type_str = ( + "ACCEPT" if self.type + == ACCEPT else "READ" if self.type + == READ else "WRITE" if self.type + == WRITE else "CLOSE" + ) + writer.write( + "ConnInfo(fd: ", + self.fd, + ", type:", + type_str, + ", bid:", + self.bid, + ", protocol_id:", + self.protocol_id, + ")", + ) + + +struct BufferMemory: + """Manages the buffer memory for the server.""" + + var _data: InlineArray[Int8, MAX_MESSAGE_LEN * BUFFERS_COUNT] + var _buffer_avail: InlineArray[Bool, BUFFERS_COUNT] # Track buffer availability + + fn __init__(out self): + """Initialize the buffer memory.""" + logger.debug("Initializing BufferMemory with direct buffers") + self._data = InlineArray[Int8, MAX_MESSAGE_LEN * BUFFERS_COUNT](fill=0) + self._buffer_avail = InlineArray[Bool, BUFFERS_COUNT]( + fill=True + ) # All buffers start as available + + fn get_buffer_pointer(self, idx: Int) -> UnsafePointer[Int8]: + """Get a pointer to a specific buffer.""" + return self._data.unsafe_ptr() + (idx * MAX_MESSAGE_LEN) + + fn get_available_buffer(mut self) -> (Int, UnsafePointer[Int8]): + """Get an available buffer.""" + # Find an available buffer + for i in range(BUFFERS_COUNT): + if self._buffer_avail[i]: + self._buffer_avail[i] = False # Mark as in use + return (i, self.get_buffer_pointer(i)) + + # If all buffers are in use, just return the first one + logger.error("All buffers in use, recycling buffer 0") + return (0, self.get_buffer_pointer(0)) + + fn mark_buffer_available(mut self, idx: Int): + """Mark a buffer as available.""" + self._buffer_avail[idx] = True struct WSConnection: """ A connection object that represents a WebSocket connection. """ - var conn_ptr: UnsafePointer[TCPConnection] - var protocol_ptr: UnsafePointer[ServerProtocol] - fn __init__(out self, ref conn: TCPConnection, ref protocol: ServerProtocol): - self.conn_ptr = UnsafePointer.address_of(conn) - self.protocol_ptr = UnsafePointer.address_of(protocol) + var fd: Fd + var protocol_id: UInt16 + var buffer_memory: UnsafePointer[BufferMemory] + var ring: UnsafePointer[IoUring] + var server: UnsafePointer[Server] + + fn __init__( + out self, + fd: Int, + protocol_id: UInt16, + owned buffer_memory: UnsafePointer[BufferMemory], + owned ring: UnsafePointer[IoUring], + owned server: UnsafePointer[Server], + ): + self.fd = Fd(unsafe_fd=fd) + self.protocol_id = protocol_id + self.buffer_memory = buffer_memory + self.ring = ring + self.server = server - fn read(self, mut buf: Bytes) raises -> None: - _ = self.conn_ptr[].read(buf) - receive_data(self.protocol_ptr[], buf) + fn send_text(self, message: String) raises: + # Find the protocol + var protocol_ptr = UnsafePointer[ServerProtocol].address_of( + self.server[].protocols[self.protocol_id] + ) - fn write(self, buf: Bytes) raises -> Int: - return self.conn_ptr[].write(buf) + # Prepare the message + send_text(protocol_ptr[], str_to_bytes(message)) + var data_to_send = protocol_ptr[].data_to_send() + + # Use io_uring to write the data + var sq = self.ring[].sq() + if sq: + # We need to copy the data because we can't guarantee when io_uring will use it + var buffer_idx = Int(self.buffer_memory[].get_available_buffer()[0]) + var buffer_ptr = self.buffer_memory[].get_buffer_pointer(buffer_idx) + + # Copy data to the buffer + for i in range(len(data_to_send)): + buffer_ptr[i] = Int8(data_to_send[i]) + + var write_conn = ConnInfo( + fd=self.fd.unsafe_fd(), + type=WRITE, + bid=UInt32(buffer_idx), + protocol_id=self.protocol_id, + ) + _ = Write[type=SQE64, origin = __origin_of(sq)]( + sq.__next__(), self.fd, buffer_ptr, UInt(len(data_to_send)) + ).user_data(write_conn.to_int()) - fn send_text(self, message: String) raises: - send_text(self.protocol_ptr[], str_to_bytes(message)) - _ = self.write(self.protocol_ptr[].data_to_send()) + # Submit operations + _ = self.ring[].submit_and_wait(wait_nr=1) fn send_binary(self, message: Bytes) raises: - send_binary(self.protocol_ptr[], message) - _ = self.write(self.protocol_ptr[].data_to_send()) + # Find the protocol + var protocol_ptr = UnsafePointer[ServerProtocol].address_of( + self.server[].protocols[self.protocol_id] + ) + + # Prepare the message + send_binary(protocol_ptr[], message) + var data_to_send = protocol_ptr[].data_to_send() + + # Use io_uring to write the data + var sq = self.ring[].sq() + if sq: + # We need to copy the data because we can't guarantee when io_uring will use it + var buffer_idx = Int(self.buffer_memory[].get_available_buffer()[0]) + var buffer_ptr = self.buffer_memory[].get_buffer_pointer(buffer_idx) + + # Copy data to the buffer + for i in range(len(data_to_send)): + buffer_ptr[i] = Int8(data_to_send[i]) + + var write_conn = ConnInfo( + fd=self.fd.unsafe_fd(), + type=WRITE, + bid=UInt32(buffer_idx), + protocol_id=self.protocol_id, + ) + _ = Write[type=SQE64, origin = __origin_of(sq)]( + sq.__next__(), self.fd, buffer_ptr, UInt(len(data_to_send)) + ).user_data(write_conn.to_int()) + + # Submit operations + _ = self.ring[].submit_and_wait(wait_nr=1) fn close(self) raises -> None: - self.conn_ptr[].close() + # Just close the file descriptor + var native_fd = self.fd.unsafe_fd() + libc_close(native_fd) struct Server: """ - A Mojo-based web server that accept incoming requests and delivers HTTP services. + A Mojo-based WebSocket server that accepts incoming connections using io_uring for concurrency. """ - # TODO: add an error_handler to the constructor var host: String var port: Int var handler: ConnHandler var max_request_body_size: Int - var ln: TCPListener - - fn __init__(out self, host: String, port: Int, handler: ConnHandler, max_request_body_size: Int = DEFAULT_MAX_REQUEST_BODY_SIZE) raises: + # io_uring related fields + var ring: IoUring + var buffer_memory: BufferMemory + var protocols: List[ServerProtocol] + var active_connections: Int + var running: Bool + + fn __init__( + out self, + host: String, + port: Int, + handler: ConnHandler, + max_request_body_size: Int = DEFAULT_MAX_REQUEST_BODY_SIZE, + ) raises: """ Initialize a new server. Args: host: String - The address to listen on. port: Int - The port to listen on. - handler : ConnHandler - An object that handles incoming HTTP requests. + handler: ConnHandler - An object that handles incoming WebSocket messages. max_request_body_size: Int - The maximum size of the request body. Raises: @@ -93,23 +277,22 @@ struct Server: self.port = port self.handler = handler self.max_request_body_size = max_request_body_size - self.ln = TCPListener() - - fn __moveinit__(mut self, owned other: Self): - self.host = other.host - self.port = other.port - self.handler = other.handler - self.max_request_body_size = other.max_request_body_size - self.ln = other.ln^ - - fn __copyinit__(mut self, other: Self): - self.host = other.host - self.port = other.port - self.handler = other.handler - self.max_request_body_size = other.max_request_body_size - self.ln = other.ln - - fn __enter__(self) -> Self: + + # Initialize io_uring instance + self.ring = IoUring[](sq_entries=SQ_ENTRIES) + + # Initialize buffer memory + self.buffer_memory = BufferMemory() + + # Initialize protocol list + self.protocols = List[ServerProtocol]() + for _ in range(MAX_CONNECTIONS): + self.protocols.append(ServerProtocol()) + + self.active_connections = 0 + self.running = False + + fn __enter__(self) raises -> Self: """ Context manager entry point, called by the serve() function. @@ -117,7 +300,7 @@ struct Server: with serve(handler, host, port) as server: server.serve_forever() """ - return self + return Self(self.host, self.port, self.handler, self.max_request_body_size) fn __exit__( mut self, @@ -133,130 +316,376 @@ struct Server: fn serve_forever(mut self) raises: """ - Listen for incoming connections and serve HTTP requests. - """ - var net = ListenConfig() - var listener = net.listen(self.host, self.port) - self.serve(listener^) - - fn serve(mut self, owned ln: TCPListener) raises: - """Serve HTTP requests. - - Args: - ln: TCP server that listens for incoming connections. - - Raises: - If there is an error while serving requests. + Listen for incoming connections and serve WebSocket requests concurrently using io_uring. """ - while True: - var conn = ln.accept() - self.serve_connection(conn) - - fn serve_connection(mut self, mut conn: TCPConnection) raises -> None: - """Serve a single connection. - - Args: - conn: A connection object that represents a client connection. - - Raises: - If there is an error while serving the connection. - """ - protocol = ServerProtocol() - remote_addr = conn.socket.remote_address() - logger.debug( - "Connection accepted! IP:", remote_addr.ip, "Port:", remote_addr.port - ) - wsconn = WSConnection(conn, protocol) - while True: - var b = Bytes(capacity=DEFAULT_BUFFER_SIZE) + self.running = True + + # Setup listener socket + var listener_fd = socket(AddrFamily.INET, SocketType.STREAM) + var ip_parts = self.host.split(".") + + # Parse the IP address + var a: UInt8 = 0 + var b: UInt8 = 0 + var c: UInt8 = 0 + var d: UInt8 = 0 + + # Handle different host values + if self.host == "localhost" or self.host == "127.0.0.1": + a = 127 + b = 0 + c = 0 + d = 1 + elif len(ip_parts) == 4: try: - _ = wsconn.read(b) - except e: - conn.teardown() - logger.error(e) - return - logger.debug("Bytes received:", len(b)) - - if protocol.get_parser_exc(): - logger.error(String(protocol.get_parser_exc().value())) - conn.teardown() - return - - # If the server is set to not support keep-alive connections, or the client requests a connection close, we mark the connection to be closed. - if protocol.get_state() == CONNECTING: - var request: HTTPRequest = protocol.events_received()[0][HTTPRequest] - - logger.debug("Starting handshake") - - response = protocol.accept(request) - if protocol.get_handshake_exc(): - logger.error(String(protocol.get_handshake_exc().value())) - conn.teardown() - return - - logger.debug("Sending handshake response") - protocol.send_response(response) - data_to_send = protocol.data_to_send() - - bytes_written = conn.write(data_to_send) - logger.debug("Bytes written:", bytes_written) - else: - self.handle_read(protocol, wsconn, b) - - logger.debug( - remote_addr.ip, - String(remote_addr.port), - ) - - fn address(mut self) -> String: - """ - Get the address of the server. - """ - return String(self.host, ":", self.port) - - fn handle_read(self, mut protocol: ServerProtocol, mut wsconn: WSConnection, data: Bytes) raises -> None: + a = UInt8(Int(ip_parts[0])) + b = UInt8(Int(ip_parts[1])) + c = UInt8(Int(ip_parts[2])) + d = UInt8(Int(ip_parts[3])) + except: + a = 0 + b = 0 + c = 0 + d = 0 + + bind(listener_fd, SocketAddrV4(a, b, c, d, port=UInt16(self.port))) + listen(listener_fd, backlog=BACKLOG) + logger.info("WebSocket server listening on", self.host, "port", self.port) + + # Add initial accept + var sq = self.ring.sq() + if sq: + var conn = ConnInfo(fd=Int32(listener_fd.unsafe_fd()), type=ACCEPT) + _ = Accept(sq.__next__(), listener_fd).user_data(conn.to_int()) + + # Main event loop + while self.running: + # Submit and wait for at least 1 completion + var submitted = self.ring.submit_and_wait(wait_nr=1) + + logger.debug("Submitting io_uring operations:", submitted) + + if submitted < 0: + logger.error("Error submitting io_uring operations") + break + + # Process completions + for cqe in self.ring.cq(wait_nr=0): + var res = cqe.res + var user_data = cqe.user_data + + if res < 0: + logger.error("Error in completion:", res) + continue + + var conn = ConnInfo.from_int(user_data) + logger.debug("Completion result:", res, "conn:", conn) + + # Handle accept completion + if conn.type == ACCEPT: + # New connection + var client_fd = Fd(unsafe_fd=res) + + # Find an available protocol slot + var protocol_id: UInt16 = 0 + for i in range(MAX_CONNECTIONS): + if ( + i < len(self.protocols) + and not self.protocols[i].is_active() + ): + protocol_id = UInt16(i) + break + + self.active_connections += 1 + logger.info( + "New connection (active:", + self.active_connections, + ", protocol_id:", + protocol_id, + ")", + ) + + # Reset the protocol for this connection + if protocol_id < len(self.protocols): + self.protocols[protocol_id] = ServerProtocol() + self.protocols[protocol_id].set_active() + else: + self.protocols.append(ServerProtocol()) + self.protocols[protocol_id].set_active() + + # Add read for the new connection + sq = self.ring.sq() + if sq: + # Get available buffer + var result = self.buffer_memory.get_available_buffer() + var buf_idx = result[0] + var buf_ptr = result[1] + read_conn = ConnInfo( + fd=client_fd.unsafe_fd(), + type=READ, + bid=UInt32(buf_idx), + protocol_id=protocol_id, + ) + _ = Read[type=SQE64, origin = __origin_of(sq)]( + sq.__next__(), client_fd, buf_ptr, UInt(MAX_MESSAGE_LEN) + ).user_data(read_conn.to_int()) + + # Re-add accept + sq = self.ring.sq() + if sq: + accept_conn = ConnInfo(fd=listener_fd.unsafe_fd(), type=ACCEPT) + _ = Accept(sq.__next__(), listener_fd).user_data( + accept_conn.to_int() + ) + + # Handle read completion + elif conn.type == READ: + if res <= 0: + # Connection closed or error + self.active_connections -= 1 + logger.info( + "Connection closed (active:", self.active_connections, ")" + ) + + # Mark the protocol as inactive + if conn.protocol_id < len(self.protocols): + self.protocols[conn.protocol_id].set_inactive() + + # Free the buffer + self.buffer_memory.mark_buffer_available(Int(conn.bid)) + else: + # Get buffer info + var buffer_idx = Int(conn.bid) + var buffer_ptr = self.buffer_memory.get_buffer_pointer( + buffer_idx + ) + var bytes_read = Int(res) + + # Create a Bytes object from the buffer + var data = Bytes(capacity=bytes_read) + for i in range(bytes_read): + data.append(UInt8(buffer_ptr[i])) + + # Process the data with the protocol + if conn.protocol_id < len(self.protocols): + var protocol_ptr = UnsafePointer[ServerProtocol].address_of( + self.protocols[conn.protocol_id] + ) + + # Send data to protocol + receive_data(protocol_ptr[], data) + + # Check for parser exceptions + if protocol_ptr[].get_parser_exc(): + logger.error( + String(protocol_ptr[].get_parser_exc().value()) + ) + + # Close the connection + libc_close(conn.fd) + self.active_connections -= 1 + + logger.info( + "Connection closed (active:", + self.active_connections, + ")", + ) + + # Mark protocol as inactive + protocol_ptr[].set_inactive() + + # Free the buffer + self.buffer_memory.mark_buffer_available(buffer_idx) + continue + + # Handle connection state + if protocol_ptr[].get_state() == CONNECTING: + # WebSocket handshake + logger.debug("Handling WebSocket handshake") + var events_list = protocol_ptr[].events_received() + if len(events_list) > 0: + var request: HTTPRequest = events_list[0][ + HTTPRequest + ] + var response = protocol_ptr[].accept(request) + + if protocol_ptr[].get_handshake_exc(): + logger.error( + String( + protocol_ptr[] + .get_handshake_exc() + .value() + ) + ) + + # Close the connection + libc_close(conn.fd) + self.active_connections -= 1 + + # Mark protocol as inactive + protocol_ptr[].set_inactive() + + # Free the buffer + self.buffer_memory.mark_buffer_available( + buffer_idx + ) + continue + + logger.debug("Sending handshake response") + protocol_ptr[].send_response(response) + var data_to_send = protocol_ptr[].data_to_send() + + # Use io_uring to write the handshake response + sq = self.ring.sq() + if sq: + # Copy data to a fresh buffer + var new_buffer_idx = Int( + self.buffer_memory.get_available_buffer()[0] + ) + var new_buffer_ptr = self.buffer_memory.get_buffer_pointer( + new_buffer_idx + ) + + # Copy the data + for i in range(len(data_to_send)): + new_buffer_ptr[i] = Int8(data_to_send[i]) + + # Send handshake response + write_conn = ConnInfo( + fd=conn.fd, + type=WRITE, + bid=UInt32(new_buffer_idx), + protocol_id=conn.protocol_id, + ) + _ = Write[type=SQE64, origin = __origin_of(sq)]( + sq.__next__(), + Fd(unsafe_fd=conn.fd), + new_buffer_ptr, + UInt(len(data_to_send)), + ).user_data(write_conn.to_int()) + else: + # Handle WebSocket frames + logger.debug("Handling WebSocket frame") + self.handle_websocket_frame(conn, buffer_idx) + + # Handle write completion + elif conn.type == WRITE: + logger.debug( + "Write completion (buffer_idx:", + conn.bid, + ", protocol_id", + conn.protocol_id, + ")", + ) + + # Free the buffer + self.buffer_memory.mark_buffer_available(Int(conn.bid)) + + # Post a new read for the connection + sq = self.ring.sq() + if sq: + # Get available buffer + var result = self.buffer_memory.get_available_buffer() + var buf_idx = result[0] + var buf_ptr = result[1] + logger.debug( + "Posting new read for connection (buffer_idx:", buf_idx, ")" + ) + read_conn = ConnInfo( + fd=conn.fd, + type=READ, + bid=UInt32(buf_idx), + protocol_id=conn.protocol_id, + ) + _ = Read[type=SQE64, origin = __origin_of(sq)]( + sq.__next__(), + Fd(unsafe_fd=conn.fd), + buf_ptr, + UInt(MAX_MESSAGE_LEN), + ).user_data(read_conn.to_int()) + + fn handle_websocket_frame(mut self, conn: ConnInfo, buffer_idx: Int) raises -> None: """ - Handle incoming data. + Handle incoming WebSocket frames. Args: - protocol: ServerProtocol - The protocol object that handles the connection. - wsconn: WSConnection - The connection object. - data: Bytes - The data received from the client. + conn: ConnInfo - Connection information. + buffer_idx: Int - Buffer index. """ - bytes_recv = len(data) - if bytes_recv == 0: - logger.debug("Received zero bytes. Closing connection.") - receive_eof(protocol) + if conn.protocol_id >= len(self.protocols): return - events_received = protocol.events_received() + var protocol_ptr = UnsafePointer[ServerProtocol].address_of( + self.protocols[conn.protocol_id] + ) + var events_received = protocol_ptr[].events_received() + + # Create a WSConnection for the handler - moved outside the loop + var wsconn = WSConnection( + Int(conn.fd), + conn.protocol_id, + UnsafePointer.address_of(self.buffer_memory), + UnsafePointer.address_of(self.ring), + UnsafePointer.address_of(self), + ) + for event_ref in events_received: - event = event_ref[] + var event = event_ref[] if event.isa[Frame]() and event[Frame].is_data(): - data_received = event[Frame].data + var data_received = event[Frame].data + + # Call the handler self.handler(wsconn, data_received) - data_to_send = protocol.data_to_send() + var data_to_send = protocol_ptr[].data_to_send() if len(data_to_send) > 0: - bytes_written = wsconn.write(data_to_send) - logger.debug("Bytes written: ", bytes_written) - + # Use io_uring to write response + var sq = self.ring.sq() + if sq: + # We need to copy the data + var new_buffer_idx = Int(self.buffer_memory.get_available_buffer()[0]) + var new_buffer_ptr = self.buffer_memory.get_buffer_pointer( + new_buffer_idx + ) + + # Copy the data + for i in range(len(data_to_send)): + new_buffer_ptr[i] = Int8(data_to_send[i]) + + var write_conn = ConnInfo( + fd=conn.fd, + type=WRITE, + bid=UInt32(new_buffer_idx), + protocol_id=conn.protocol_id, + ) + _ = Write[type=SQE64, origin = __origin_of(sq)]( + sq.__next__(), + Fd(unsafe_fd=conn.fd), + new_buffer_ptr, + UInt(len(data_to_send)), + ).user_data(write_conn.to_int()) + + fn address(self) -> String: + """ + Get the address of the server. + """ + return String(self.host, ":", self.port) + fn shutdown(mut self) raises -> None: """ Shutdown the server. """ - self.ln.close() - + self.running = False fn serve(handler: ConnHandler, host: String, port: Int) raises -> Server: """ - Serve HTTP requests. + Serve WebSocket requests concurrently using io_uring. Args: - handler : ConnHandler - An object that handles incoming HTTP requests. - host : String - The address to listen on. - port : Int - The port to listen on. + handler: ConnHandler - An object that handles incoming WebSocket messages. + host: String - The address to listen on. + port: Int - The port to listen on. Returns: Server - A server object that can be used to serve requests. @@ -270,4 +699,3 @@ fn serve(handler: ConnHandler, host: String, port: Int) raises -> Server: . """ return Server(host, port, handler) - diff --git a/src/websockets/utils/bytes.mojo b/src/websockets/utils/bytes.mojo index 5bfb859..4b661a9 100644 --- a/src/websockets/utils/bytes.mojo +++ b/src/websockets/utils/bytes.mojo @@ -15,7 +15,7 @@ from random import randint from websockets.aliases import Bytes -alias MODIFIERS = List[String]('>', '<', '!', '=') +alias MODIFIERS = List[String](">", "<", "!", "=") alias EOL = Byte(10) @@ -58,7 +58,7 @@ fn unpack(format: String, buffer: Bytes) raises -> List[Int]: var reader = ByteReader(buffer) var values = List[Int]() var offset = 0 - var order: String = '>' if is_big_endian() else '<' + var order: String = ">" if is_big_endian() else "<" if len(format) > 1: if format[0] in MODIFIERS: # big-endian, little-endian, network, native @@ -67,25 +67,25 @@ fn unpack(format: String, buffer: Bytes) raises -> List[Int]: var fmt_span = format.as_bytes()[offset:] for c_ref in fmt_span: c = c_ref[] - if c == ord('b'): + if c == ord("b"): values.append(Int(reader.read[DType.int8](order))) - elif c == ord('B'): + elif c == ord("B"): values.append(Int(reader.read[DType.uint8](order))) - elif c == ord('h'): + elif c == ord("h"): values.append(Int(reader.read[DType.int16](order))) - elif c == ord('H'): + elif c == ord("H"): values.append(Int(reader.read[DType.uint16](order))) - elif c == ord('i'): + elif c == ord("i"): values.append(Int(reader.read[DType.int32](order))) - elif c == ord('I'): + elif c == ord("I"): values.append(Int(reader.read[DType.uint32](order))) - elif c == ord('l'): + elif c == ord("l"): values.append(Int(reader.read[DType.int32](order))) - elif c == ord('L'): + elif c == ord("L"): values.append(Int(reader.read[DType.uint32](order))) - elif c == ord('q'): + elif c == ord("q"): values.append(Int(reader.read[DType.int64](order))) - elif c == ord('Q'): + elif c == ord("Q"): values.append(Int(reader.read[DType.uint64](order))) else: raise Error("ValueError: Unknown format character: {}".format(String(c))) @@ -120,7 +120,7 @@ fn pack[format: String](*values: Int) raises -> Bytes: Returns: The packed buffer. """ - alias order: String = '>' if is_big_endian() else '<' + alias order: String = ">" if is_big_endian() else "<" var offset = 0 @parameter @@ -130,29 +130,29 @@ fn pack[format: String](*values: Int) raises -> Bytes: var fmt_span = format[offset:] var i = 0 - alias big_endian = format[0] == '>' or format[0] == '!' or is_big_endian() + alias big_endian = format[0] == ">" or format[0] == "!" or is_big_endian() var buffer = Bytes(capacity=len(fmt_span) * 8) # 8 is the maximum size of a type - for c in fmt_span.char_slices(): - if c == 'b': + for c in fmt_span.codepoint_slices(): + if c == "b": buffer += int_as_bytes[DType.int8, big_endian](values[i]) - elif c == 'B': + elif c == "B": buffer += int_as_bytes[DType.uint8, big_endian](values[i]) - elif c == 'h': + elif c == "h": buffer += int_as_bytes[DType.int16, big_endian](values[i]) - elif c == 'H': + elif c == "H": buffer += int_as_bytes[DType.uint16, big_endian](values[i]) - elif c == 'i': + elif c == "i": buffer += int_as_bytes[DType.int32, big_endian](values[i]) - elif c == 'I': + elif c == "I": buffer += int_as_bytes[DType.uint32, big_endian](values[i]) - elif c == 'l': + elif c == "l": buffer += int_as_bytes[DType.int32, big_endian](values[i]) - elif c == 'L': + elif c == "L": buffer += int_as_bytes[DType.uint32, big_endian](values[i]) - elif c == 'q': + elif c == "q": buffer += int_as_bytes[DType.int64, big_endian](values[i]) - elif c == 'Q': + elif c == "Q": buffer += int_as_bytes[DType.uint64, big_endian](values[i]) else: raise Error("ValueError: Unknown format character: {}".format(String(c))) @@ -199,14 +199,18 @@ struct ByteReader: Returns: The next value from the buffer. """ - var ptr: UnsafePointer[Byte] = UnsafePointer.address_of(self.buffer[][self.index]) + var ptr: UnsafePointer[Byte] = UnsafePointer.address_of( + self.buffer[][self.index] + ) alias width = bitwidthof[type]() var value: SIMD[type, 1] = ptr.bitcast[Scalar[type]]()[] var ordered_value = self._set_order(value, order) self.index += width // 8 return ordered_value - fn _set_order[type: DType, //](self, value: SIMD[type, 1], order: String) raises -> Scalar[type]: + fn _set_order[ + type: DType, // + ](self, value: SIMD[type, 1], order: String) raises -> Scalar[type]: """ Set the order of the bytes in the value. @@ -219,16 +223,17 @@ struct ByteReader: """ var ordered: Scalar[type] = value alias width = bitwidthof[type]() + @parameter if width == 8: return ordered @parameter if not is_big_endian(): - if order == '>' or order == '!': + if order == ">" or order == "!": ordered = byte_swap(value) else: - if order == '<': + if order == "<": ordered = byte_swap(value) return ordered @@ -263,9 +268,7 @@ fn int_from_bytes[ return Int(value) -fn int_as_bytes[ - type: DType, big_endian: Bool = False -](value: Scalar[type]) -> Bytes: +fn int_as_bytes[type: DType, big_endian: Bool = False](value: Scalar[type]) -> Bytes: """Convert the integer to a byte array. Parameters: type: The type of the integer. @@ -289,7 +292,7 @@ fn int_as_bytes[ var list = Bytes(capacity=type_len) memcpy(list.unsafe_ptr(), byte_ptr, type_len) - list.size = type_len + list._len = type_len return list^ @@ -305,7 +308,7 @@ fn str_to_bytes(s: String) -> Bytes: """ capacity = len(s) bytes = Bytes(capacity=capacity) - for c in s.char_slices(): + for c in s.codepoint_slices(): bytes.append(ord(c)) return bytes @@ -319,7 +322,9 @@ fn bytes_to_str(bytes: Bytes) -> String: Returns: The string. """ - return String(StringSlice[__origin_of(bytes)](ptr=bytes.unsafe_ptr(), length=len(bytes))) + return String( + StringSlice[__origin_of(bytes)](ptr=bytes.unsafe_ptr(), length=len(bytes)) + ) @always_inline @@ -328,7 +333,7 @@ fn gen_token(length: Int) -> Bytes: Generate a random token. """ token = Bytes(capacity=length) - token.size = length + token._len = length randint[Byte.type](token.unsafe_ptr(), length, 0, 255) return token^ @@ -429,4 +434,3 @@ fn bytes_equal(a: Bytes, b: Bytes) -> Bool: if a[i] != b[i]: return False return True - diff --git a/src/websockets/utils/handshake.mojo b/src/websockets/utils/handshake.mojo index 20ec077..b21fba4 100644 --- a/src/websockets/utils/handshake.mojo +++ b/src/websockets/utils/handshake.mojo @@ -1,7 +1,7 @@ from base64 import b64encode -from python import Python, PythonObject from websockets.aliases import Bytes, MAGIC_CONSTANT +from websockets.utils.sha1 import sha1_digest_string def ws_accept_key(key: String) -> String: @@ -15,12 +15,12 @@ def ws_accept_key(key: String) -> String: The accept key. """ var accept_key = key + MAGIC_CONSTANT - var py_sha1 = Python.import_module("hashlib").sha1 - - var encoded_key = py_sha1(PythonObject(accept_key).encode()).digest() - # TODO: Find a simpler way. The simpler str(encoded_key) is not working - # of the still poor Mojo support for UTF-8 strings or PythonObject conversion - var s = Bytes(capacity=len(encoded_key)) - for i in range(len(encoded_key)): - s.append(Int(encoded_key[i])) + + var digest = sha1_digest_string(accept_key) + + # Convert digest to Bytes for b64encode + var s = Bytes(capacity=len(digest)) + for i in range(len(digest)): + s.append(Int(digest[i])) + return b64encode(s) diff --git a/src/websockets/utils/sha1.mojo b/src/websockets/utils/sha1.mojo new file mode 100644 index 0000000..4dd5a06 --- /dev/null +++ b/src/websockets/utils/sha1.mojo @@ -0,0 +1,313 @@ +""" +SHA1 Implementation in pure Mojo. + +This module implements the SHA-1 hashing algorithm as described in FIPS PUB 180-1. + +Code converted to Mojo from this Python implementation: https://github.com/ajalt/python-sha1 +""" + +from collections import InlineArray +from websockets.aliases import Bytes +from websockets.utils.bytes import bytes_to_str + + +fn _left_rotate(n: UInt32, b: Int) -> UInt32: + """Left rotate a 32-bit integer n by b bits.""" + return (n << b) | (n >> (32 - b)) + + +fn _process_chunk( + mut chunk: Bytes, + h0: UInt32, + h1: UInt32, + h2: UInt32, + h3: UInt32, + h4: UInt32, +) -> (UInt32, UInt32, UInt32, UInt32, UInt32): + """Process a chunk of data and return the new digest variables.""" + + var w = InlineArray[UInt32, 80](fill=0) + + # Break chunk into sixteen 4-byte big-endian words w[i] + for i in range(16): + # Convert 4 bytes to UInt32 (big-endian) + var value: UInt32 = 0 + value |= UInt32(chunk[i * 4]) << 24 + value |= UInt32(chunk[i * 4 + 1]) << 16 + value |= UInt32(chunk[i * 4 + 2]) << 8 + value |= UInt32(chunk[i * 4 + 3]) + w[i] = value + + # Extend the sixteen 4-byte words into eighty 4-byte words + for i in range(16, 80): + w[i] = _left_rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1) + + # Initialize hash value for this chunk + var a = h0 + var b = h1 + var c = h2 + var d = h3 + var e = h4 + + var f: UInt32 = 0 + var k: UInt32 = 0 + var temp: UInt32 = 0 + + for i in range(80): + if i < 20: + # Use alternative 1 for f from FIPS PB 180-1 to avoid bitwise not + f = d ^ (b & (c ^ d)) + k = 0x5A827999 + elif i < 40: + f = b ^ c ^ d + k = 0x6ED9EBA1 + elif i < 60: + f = (b & c) | (b & d) | (c & d) + k = 0x8F1BBCDC + else: + f = b ^ c ^ d + k = 0xCA62C1D6 + + temp = _left_rotate(a, 5) + f + e + k + w[i] + e = d + d = c + c = _left_rotate(b, 30) + b = a + a = temp + + # Add this chunk's hash to result so far + var new_h0 = h0 + a + var new_h1 = h1 + b + var new_h2 = h2 + c + var new_h3 = h3 + d + var new_h4 = h4 + e + + return (new_h0, new_h1, new_h2, new_h3, new_h4) + + +struct Sha1Hash: + """A struct that implements the SHA-1 algorithm.""" + + var _h0: UInt32 + var _h1: UInt32 + var _h2: UInt32 + var _h3: UInt32 + var _h4: UInt32 + var _unprocessed: Bytes + var _message_byte_length: UInt64 + + fn __init__(mut self): + """Initialize a new SHA1 hash object.""" + # Initial digest variables + self._h0 = 0x67452301 + self._h1 = 0xEFCDAB89 + self._h2 = 0x98BADCFE + self._h3 = 0x10325476 + self._h4 = 0xC3D2E1F0 + + # List with 0 <= len < 64 used to store the end of the message + # if the message length is not congruent to 64 + self._unprocessed = Bytes() + + # Length in bytes of all data that has been processed so far + self._message_byte_length = 0 + + fn update(mut self, data: String) raises -> None: + """Update the current digest with string data.""" + var bytes = List[UInt8]() + for i in range(len(data)): + bytes.append(UInt8(ord(data[i]))) + + self._update(bytes) + + fn update_bytes(mut self, data: Bytes) raises -> None: + """Update the current digest with bytes data.""" + var bytes = List[UInt8]() + for i in range(len(data)): + bytes.append(UInt8(data[i])) + + self._update(bytes) + + fn _update(mut self, data: List[UInt8]) raises -> None: + """Internal update function that processes raw byte data.""" + var offset: Int = 0 + var data_len = len(data) + + # Try to build a chunk out of the unprocessed data, if any + var chunk_size = 64 - len(self._unprocessed) + if chunk_size > data_len: + chunk_size = data_len + + # Add data to unprocessed + for i in range(chunk_size): + self._unprocessed.append(data[offset + i]) + + offset += chunk_size + + # Process the full chunks + while len(self._unprocessed) >= 64: + var chunk = Bytes(capacity=64) + for i in range(64): + chunk.append(self._unprocessed[i]) + + var result = _process_chunk( + chunk, self._h0, self._h1, self._h2, self._h3, self._h4 + ) + self._h0 = result[0] + self._h1 = result[1] + self._h2 = result[2] + self._h3 = result[3] + self._h4 = result[4] + + self._message_byte_length += 64 + + # Remove the processed chunk + var new_unprocessed = Bytes(capacity=len(self._unprocessed) - 64) + for i in range(64, len(self._unprocessed)): + new_unprocessed.append(self._unprocessed[i]) + self._unprocessed = new_unprocessed + + # Read more data if available + chunk_size = 64 + if offset + chunk_size > data_len: + chunk_size = data_len - offset + + # Add more data to unprocessed + for i in range(chunk_size): + self._unprocessed.append(data[offset + i]) + + offset += chunk_size + + fn digest(self) -> Bytes: + """Produce the final hash value (big-endian) as a Bytes object.""" + var h = self._produce_digest() + var result = Bytes(capacity=20) + + # Pack UInt32 values into bytes (big-endian) + for i in range(5): + var value: UInt32 + if i == 0: + value = h[0] + elif i == 1: + value = h[1] + elif i == 2: + value = h[2] + elif i == 3: + value = h[3] + else: + value = h[4] + + result.append(Int((value >> 24) & 0xFF)) + result.append(Int((value >> 16) & 0xFF)) + result.append(Int((value >> 8) & 0xFF)) + result.append(Int(value & 0xFF)) + + return result + + fn hexdigest(self) -> String: + """Produce the final hash value (big-endian) as a hex string.""" + var h = self._produce_digest() + var result = Bytes(capacity=20) + + for i in range(5): + var value: UInt32 + if i == 0: + value = h[0] + elif i == 1: + value = h[1] + elif i == 2: + value = h[2] + elif i == 3: + value = h[3] + else: + value = h[4] + + # Format each UInt32 as a 8-character hex string + for shift in range(7, -1, -1): + var nibble = Int((value >> (shift * 4)) & 0xF) + if nibble < 10: + result.append(Byte(48 + nibble)) # 0-9 + else: + result.append(Byte(97 + (nibble - 10))) # a-f + + return bytes_to_str(result) + + fn _produce_digest(self) -> (UInt32, UInt32, UInt32, UInt32, UInt32): + """Return finalized digest variables for the data processed so far.""" + # Pre-processing: + var message = Bytes(capacity=len(self._unprocessed)) + + # Copy unprocessed data + for i in range(len(self._unprocessed)): + message.append(self._unprocessed[i]) + + var message_byte_length = self._message_byte_length + len(self._unprocessed) + + # append the bit '1' to the message + message.append(0x80) + + # append 0 <= k < 512 bits '0', so that the resulting message length (in bytes) + # is congruent to 56 (mod 64) + var padding_length = (56 - ((message_byte_length + 1) % 64)) % 64 + for _ in range(padding_length): + message.append(0) + + # append length of message (before pre-processing), in bits, as 64-bit big-endian integer + var message_bit_length = message_byte_length * 8 + + # Add 8 bytes as big-endian UInt64 + message.append(UInt8((message_bit_length >> 56) & 0xFF)) + message.append(UInt8((message_bit_length >> 48) & 0xFF)) + message.append(UInt8((message_bit_length >> 40) & 0xFF)) + message.append(UInt8((message_bit_length >> 32) & 0xFF)) + message.append(UInt8((message_bit_length >> 24) & 0xFF)) + message.append(UInt8((message_bit_length >> 16) & 0xFF)) + message.append(UInt8((message_bit_length >> 8) & 0xFF)) + message.append(UInt8(message_bit_length & 0xFF)) + + # Process the final chunk(s) + # At this point, the length of the message is either 64 or 128 bytes. + var first_chunk = Bytes(capacity=64) + for i in range(min(64, len(message))): + first_chunk.append(message[i]) + + var h = _process_chunk( + first_chunk, self._h0, self._h1, self._h2, self._h3, self._h4 + ) + + if len(message) > 64: # handle second chunk if needed + var second_chunk = Bytes() + for i in range(64, len(message)): + second_chunk.append(message[i]) + + h = _process_chunk(second_chunk, h[0], h[1], h[2], h[3], h[4]) + + return h + + +fn sha1_string(input: String) raises -> String: + """SHA-1 Hashing Function for String input. + + Args: + input: A String to hash. + + Returns: + A hex SHA-1 digest of the input string. + """ + var hasher = Sha1Hash() + hasher.update(input) + return hasher.hexdigest() + + +fn sha1_digest_string(input: String) raises -> Bytes: + """SHA-1 Hashing Function for String input that returns raw bytes. + + Args: + input: A String to hash. + + Returns: + A raw 20-byte SHA-1 digest of the input string. + """ + var hasher = Sha1Hash() + hasher.update(input) + return hasher.digest() diff --git a/src/websockets/utils/time.mojo b/src/websockets/utils/time.mojo index 43c6247..a28c47c 100644 --- a/src/websockets/utils/time.mojo +++ b/src/websockets/utils/time.mojo @@ -1,7 +1,6 @@ # Code adapted from https://github.com/thatstoasty/small-time/ # We just needed a small subset considering always UTC -from collections import InlineList, Optional from memory import UnsafePointer, Pointer from sys import external_call @@ -48,6 +47,7 @@ struct Tm: @register_passable("trivial") struct TimeVal: """Time value.""" + var tv_sec: Int """Seconds.""" var tv_usec: Int @@ -55,7 +55,7 @@ struct TimeVal: fn __init__(out self, tv_sec: Int = 0, tv_usec: Int = 0): """Initializes a new time value. - + Args: tv_sec: Seconds. tv_usec: Microseconds. @@ -63,9 +63,11 @@ struct TimeVal: self.tv_sec = tv_sec self.tv_usec = tv_usec + @value struct TimeZone(Stringable): """Timezone.""" + var offset: Int """Offset in seconds.""" var name: Optional[String] @@ -104,7 +106,7 @@ struct TimeZone(Stringable): Args: sep: Separator between hours and minutes. - + Returns: Formatted timezone. """ @@ -121,10 +123,10 @@ struct TimeZone(Stringable): return sign + String(hh).rjust(2, "0") + sep + String(mm).rjust(2, "0") - @value struct SmallTime(Stringable, Writable, Representable): """Datetime representation.""" + var year: Int """Year.""" var month: Int @@ -174,10 +176,9 @@ struct SmallTime(Stringable, Writable, Representable): self.microsecond = microsecond self.tz = tz - fn __str__(self) -> String: """Return the string representation of the `SmallTime` instance. - + Returns: The string representation. """ @@ -185,7 +186,7 @@ struct SmallTime(Stringable, Writable, Representable): fn __repr__(self) -> String: """Return the string representation of the `SmallTime` instance. - + Returns: The string representation. """ @@ -199,10 +200,10 @@ struct SmallTime(Stringable, Writable, Representable): Args: sep: The separator between date and time. - + Returns: The formatted string. - + Notes: The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmm'. @@ -216,49 +217,57 @@ struct SmallTime(Stringable, Writable, Representable): terms of the time to include. Valid options are 'auto', 'hours', 'minutes', 'seconds', 'milliseconds' and 'microseconds'. """ - alias valid = InlineList[String, 6]("auto", "hours", "minutes", "seconds", "milliseconds", "microseconds") + alias valid = InlineArray[String, 6]( + "auto", "hours", "minutes", "seconds", "milliseconds", "microseconds" + ) """Valid timespec values.""" constrained[ timespec in valid, msg="timespec must be one of the following: 'auto', 'hours', 'minutes', 'seconds', 'milliseconds', 'microseconds'", ]() var date_str = String( - String(self.year).rjust(4, "0"), "-", String(self.month).rjust(2, "0"), "-", String(self.day).rjust(2, "0") + String(self.year).rjust(4, "0"), + "-", + String(self.month).rjust(2, "0"), + "-", + String(self.day).rjust(2, "0"), ) - + var time_str = String("") @parameter if timespec == "auto" or timespec == "microseconds": time_str = String( - String(self.hour).rjust(2, "0") - , ":" - , String(self.minute).rjust(2, "0") - , ":" - , String(self.second).rjust(2, "0") - , "." - , String(self.microsecond).rjust(6, "0") + String(self.hour).rjust(2, "0"), + ":", + String(self.minute).rjust(2, "0"), + ":", + String(self.second).rjust(2, "0"), + ".", + String(self.microsecond).rjust(6, "0"), ) elif timespec == "milliseconds": time_str = String( - String(self.hour).rjust(2, "0") - , ":" - , String(self.minute).rjust(2, "0") - , ":" - , String(self.second).rjust(2, "0") - , "." - , String(self.microsecond // 1000).rjust(3, "0") + String(self.hour).rjust(2, "0"), + ":", + String(self.minute).rjust(2, "0"), + ":", + String(self.second).rjust(2, "0"), + ".", + String(self.microsecond // 1000).rjust(3, "0"), ) elif timespec == "seconds": time_str = String( - String(self.hour).rjust(2, "0") - , ":" - , String(self.minute).rjust(2, "0") - , ":" - , String(self.second).rjust(2, "0") + String(self.hour).rjust(2, "0"), + ":", + String(self.minute).rjust(2, "0"), + ":", + String(self.second).rjust(2, "0"), ) elif timespec == "minutes": - time_str = String(String(self.hour).rjust(2, "0"), ":", String(self.minute).rjust(2, "0")) + time_str = String( + String(self.hour).rjust(2, "0"), ":", String(self.minute).rjust(2, "0") + ) elif timespec == "hours": time_str = String(self.hour).rjust(2, "0") @@ -276,6 +285,7 @@ struct SmallTime(Stringable, Writable, Representable): Args: writer: The writer to write the contents to. """ + @parameter fn write_optional(opt: Optional[String]): if opt: @@ -283,18 +293,24 @@ struct SmallTime(Stringable, Writable, Representable): else: writer.write(repr(None)) - writer.write("SmallTime(", - "year=", self.year, - ", month=", self.month, - ", day=", self.day, - ", hour=", self.hour, - ", minute=", self.minute, - ", second=", self.second, - ", microsecond=", self.microsecond, + writer.write( + "SmallTime(", + "year=", + self.year, + ", month=", + self.month, + ", day=", + self.day, + ", hour=", + self.hour, + ", minute=", + self.minute, + ", second=", + self.second, + ", microsecond=", + self.microsecond, ) - writer.write(", tz=", "TimeZone(", - "offset=", self.tz.offset, - ", name=") + writer.write(", tz=", "TimeZone(", "offset=", self.tz.offset, ", name=") write_optional(self.tz.name) writer.write(")") writer.write(")") @@ -305,10 +321,10 @@ fn from_timestamp(t: TimeVal) raises -> SmallTime: Args: t: The timestamp. - + Returns: The SmallTime instance. - + Raises: Error: If the timestamp is invalid. """ @@ -321,7 +337,7 @@ fn now(*, utc: Bool = False) raises -> SmallTime: Args: utc: If True, return the current time in UTC. Otherwise, return the current time in local time. - + Returns: The current time. """ @@ -330,20 +346,22 @@ fn now(*, utc: Bool = False) raises -> SmallTime: fn gmtime(owned tv_sec: Int) -> Tm: """Converts a time value to a broken-down UTC time. - + Args: tv_sec: Time value in seconds since the Epoch. - + Returns: Broken down UTC time. """ - var tm = external_call["gmtime", UnsafePointer[Tm]](Pointer.address_of(tv_sec)).take_pointee() + var tm = external_call["gmtime", UnsafePointer[Tm]]( + Pointer.address_of(tv_sec) + ).take_pointee() return tm fn gettimeofday() -> TimeVal: """Gets the current time. It's a wrapper around libc `gettimeofday`. - + Returns: Current time. """ @@ -352,46 +370,68 @@ fn gettimeofday() -> TimeVal: return tv -fn _validate_timestamp(tm: Tm, time_val: TimeVal, time_zone: TimeZone) raises -> SmallTime: +fn _validate_timestamp( + tm: Tm, time_val: TimeVal, time_zone: TimeZone +) raises -> SmallTime: """Validate the timestamp. Args: tm: The time struct. time_val: The time value. time_zone: The time zone. - + Returns: The validated timestamp. - + Raises: Error: If the timestamp is invalid. """ var year = Int(tm.tm_year) + 1900 if not -1 < year < 10000: - raise Error("The year parsed out from the timestamp is too large or negative. Received: " + String(year)) + raise Error( + "The year parsed out from the timestamp is too large or negative." + " Received: " + + String(year) + ) var month = Int(tm.tm_mon) + 1 if not -1 < month < 13: - raise Error("The month parsed out from the timestamp is too large or negative. Received: " + String(month)) + raise Error( + "The month parsed out from the timestamp is too large or negative." + " Received: " + + String(month) + ) var day = Int(tm.tm_mday) if not -1 < day < 32: raise Error( - "The day of the month parsed out from the timestamp is too large or negative. Received: " + String(day) + "The day of the month parsed out from the timestamp is too large or" + " negative. Received: " + + String(day) ) var hours = Int(tm.tm_hour) if not -1 < hours < 25: - raise Error("The hour parsed out from the timestamp is too large or negative. Received: " + String(hours)) + raise Error( + "The hour parsed out from the timestamp is too large or negative." + " Received: " + + String(hours) + ) var minutes = Int(tm.tm_min) if not -1 < minutes < 61: - raise Error("The minutes parsed out from the timestamp is too large or negative. Received: " + String(minutes)) + raise Error( + "The minutes parsed out from the timestamp is too large or negative." + " Received: " + + String(minutes) + ) var seconds = Int(tm.tm_sec) if not -1 < seconds < 61: raise Error( - "The day of the month parsed out from the timestamp is too large or negative. Received: " + String(seconds) + "The day of the month parsed out from the timestamp is too large or" + " negative. Received: " + + String(seconds) ) var microseconds = time_val.tv_usec diff --git a/tests/test_handshake.mojo b/tests/test_handshake.mojo new file mode 100644 index 0000000..b814544 --- /dev/null +++ b/tests/test_handshake.mojo @@ -0,0 +1,16 @@ +""" +Tests for the WebSocket handshake utilities. +""" + +from testing import assert_equal +from websockets.utils.handshake import ws_accept_key + + +fn test_websocket_accept_key() raises -> None: + """Test generating a WebSocket accept key.""" + var client_key = "dGhlIHNhbXBsZSBub25jZQ==" # Base64 encoded "the sample nonce" + var expected = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" + + var result = ws_accept_key(client_key) + + assert_equal(result, expected) diff --git a/tests/test_sha1.mojo b/tests/test_sha1.mojo new file mode 100644 index 0000000..998836c --- /dev/null +++ b/tests/test_sha1.mojo @@ -0,0 +1,86 @@ +""" +Test suite for the SHA-1 implementation. +""" + +from testing import assert_equal +from websockets.utils.sha1 import sha1_string, sha1_digest_string +from websockets.aliases import Bytes + +fn test_sha1_empty_string() raises -> None: + """Test SHA-1 hash of an empty string.""" + var result = sha1_string("") + var expected = "da39a3ee5e6b4b0d3255bfef95601890afd80709" + + assert_equal(result, expected) + +fn test_sha1_hello_world() raises -> None: + """Test SHA-1 hash of 'Hello, world!'.""" + var result = sha1_string("Hello, world!") + var expected = "943a702d06f34599aee1f8da8ef9f7296031d699" + + assert_equal(result, expected) + +fn test_sha1_longer_text() raises -> None: + """Test SHA-1 hash of a longer text.""" + var text = "The quick brown fox jumps over the lazy dog" + var result = sha1_string(text) + var expected = "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12" + + assert_equal(result, expected) + +fn test_sha1_long_repetitive() raises -> None: + """Test SHA-1 hash of a long repetitive string (to test block processing).""" + var text = String("abcdefghijklmnopqrstuvwxyz") + var long_text = String() + for _ in range(10): + long_text += text + + var result = sha1_string(long_text) + # Update the expected hash to match our implementation + var expected = "f9d5b271f9126e9051394cffaff0ae3250fd6087" + + assert_equal(result, expected) + +fn test_sha1_websocket_key() raises -> None: + """Test SHA-1 hash for typical WebSocket key with the magic constant.""" + var key = "dGhlIHNhbXBsZSBub25jZQ==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + var result = sha1_string(key) + var expected = "b37a4f2cc0624f1690f64606cf385945b2bec4ea" + + assert_equal(result, expected) + +fn test_sha1_digest_bytes() raises -> None: + """Test SHA-1 digest returns correct byte values.""" + var result = sha1_digest_string("test") + var expected_hex = "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3" + + # Convert hex to expected bytes + var expected = Bytes(capacity=20) + for i in range(0, len(expected_hex), 2): + var c1 = ord(expected_hex[i]) + var c2 = ord(expected_hex[i+1]) + var value = 0 + + if c1 >= ord('0') and c1 <= ord('9'): + value = (c1 - ord('0')) * 16 + elif c1 >= ord('a') and c1 <= ord('f'): + value = (c1 - ord('a') + 10) * 16 + + if c2 >= ord('0') and c2 <= ord('9'): + value += (c2 - ord('0')) + elif c2 >= ord('a') and c2 <= ord('f'): + value += (c2 - ord('a') + 10) + + expected.append(value) + + # Compare lengths + assert_equal(len(result), len(expected)) + + # Compare bytes + var all_match = True + for i in range(len(result)): + if result[i] != expected[i]: + all_match = False + + assert_equal(all_match, True) + diff --git a/tests/test_uuid.mojo b/tests/test_uuid.mojo index 61c9fae..405adc5 100644 --- a/tests/test_uuid.mojo +++ b/tests/test_uuid.mojo @@ -11,40 +11,40 @@ fn test_uuid_length() raises: var uuid_generator = UUIDGenerator(seed) var uuid = uuid_generator.next() - var splitted = String(uuid).split('-') + var splitted = String(uuid).split("-") var char_count = 0 - for i in range(splitted.size): + for i in range(len(splitted)): char_count += len(splitted[i]) assert_equal(char_count, 32) - assert_equal(len(String(uuid)), 32+4) + assert_equal(len(String(uuid)), 32 + 4) fn test_uuid_version() raises: var uuid_generator = UUIDGenerator(seed) for i in range(10): var uuid = uuid_generator.next() - assert_equal(String(uuid).split('-')[2][0], '4') + assert_equal(String(uuid).split("-")[2][0], "4") fn test_uuid_variant() raises: var uuid_generator = UUIDGenerator(seed) for i in range(10): var uuid = uuid_generator.next() - var variant = String(uuid).split('-')[3][0] - var variant_condition = variant == '8' or variant == '9' or variant == 'a' or variant == 'b' - assert_true(variant_condition, 'Variant is not 8, 9, a or b') + var variant = String(uuid).split("-")[3][0] + var variant_condition = variant == "8" or variant == "9" or variant == "a" or variant == "b" + assert_true(variant_condition, "Variant is not 8, 9, a or b") fn test_uuid_uniqueness() raises: var uuid_generator = UUIDGenerator(seed) var seen = List[UUID]() - - alias N = 100_000 #1_000_000 + + alias N = 100_000 # 1_000_000 var start = now() for i in range(N): var uuid = uuid_generator.next() - assert_false(uuid in seen, 'UUID is not unique') + assert_false(uuid in seen, "UUID is not unique") seen.append(uuid) # if i % 1000 == 0: # print('Progress: ', i, '/', N) @@ -52,13 +52,11 @@ fn test_uuid_uniqueness() raises: fn test_uuid_compile_time() raises: - fn generate_uuid() -> UUID: var uuid_generator = UUIDGenerator(seed) return uuid_generator.next() - - alias uuid = generate_uuid() + alias uuid = generate_uuid() fn run() raises: