├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ └── release.yml ├── release.yml └── workflows │ ├── check-labels.yml │ ├── ci.yml │ ├── release.yml │ └── update-release-project.yml ├── .gitignore ├── .markdownlint.yaml ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── MANIFEST.in ├── NOTICE.md ├── README.md ├── ci └── scripts │ └── bump-and-tag.bash ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── requirements.txt └── stubs_to_sources.py ├── examples ├── README.md ├── common │ ├── __init__.py │ └── common.py ├── entity.proto ├── z_bytes.py ├── z_delete.py ├── z_get.py ├── z_get_liveliness.py ├── z_info.py ├── z_liveliness.py ├── z_ping.py ├── z_pong.py ├── z_pub.py ├── z_pub_thr.py ├── z_pull.py ├── z_put.py ├── z_put_float.py ├── z_querier.py ├── z_queryable.py ├── z_scout.py ├── z_storage.py ├── z_sub.py ├── z_sub_liveliness.py ├── z_sub_queued.py └── z_sub_thr.py ├── pyproject.toml ├── requirements-dev.txt ├── rust-toolchain.toml ├── src ├── bytes.rs ├── config.rs ├── ext.rs ├── handlers.rs ├── key_expr.rs ├── lib.rs ├── liveliness.rs ├── logging.rs ├── macros.rs ├── pubsub.rs ├── qos.rs ├── query.rs ├── sample.rs ├── scouting.rs ├── session.rs ├── time.rs └── utils.rs ├── tests ├── examples_check.py ├── stubs_check.py ├── test_serializer.py └── test_session.py ├── zenoh-dragon.png └── zenoh ├── __init__.py ├── __init__.pyi ├── ext.py ├── ext.pyi ├── handlers.pyi └── py.typed /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Report a bug 2 | description: | 3 | Create a bug report to help us improve Zenoh. 4 | title: "[Bug] " 5 | labels: ["bug"] 6 | body: 7 | - type: textarea 8 | id: summary 9 | attributes: 10 | label: "Describe the bug" 11 | description: | 12 | A clear and concise description of the expected behaviour and what the bug is. 13 | placeholder: | 14 | E.g. zenoh peers can not automatically establish a connection. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: reproduce 19 | attributes: 20 | label: To reproduce 21 | description: "Steps to reproduce the behavior:" 22 | placeholder: | 23 | 1. Start a subscriber "..." 24 | 2. Start a publisher "...." 25 | 3. See error 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: system 30 | attributes: 31 | label: System info 32 | description: "Please complete the following information:" 33 | placeholder: | 34 | - Platform: [e.g. Ubuntu 20.04 64-bit] 35 | - CPU [e.g. AMD Ryzen 3800X] 36 | - Zenoh version/commit [e.g. 6f172ea985d42d20d423a192a2d0d46bb0ce0d11] 37 | validations: 38 | required: true 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://github.com/eclipse-zenoh/roadmap/discussions/categories/zenoh 5 | about: Open to the Zenoh community. Share your feedback with the Zenoh team. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Request a feature 2 | description: | 3 | Suggest a new feature specific to this repository. NOTE: for generic Zenoh ideas use "Ask a question". 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | **Guidelines for a good issue** 9 | 10 | *Is your feature request related to a problem?* 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | *Describe the solution you'd like* 14 | A clear and concise description of what you want to happen. 15 | 16 | *Describe alternatives you've considered* 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | *Additional context* 20 | Add any other context about the feature request here. 21 | - type: textarea 22 | id: feature 23 | attributes: 24 | label: "Describe the feature" 25 | validations: 26 | required: true 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/release.yml: -------------------------------------------------------------------------------- 1 | name: Add an issue to the next release 2 | description: | 3 | Add an issue as part of next release. 4 | This will be added to the current release project. 5 | You must be a contributor to use this template. 6 | labels: ["release"] 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | **Guidelines for a good issue** 12 | 13 | *Is your release item related to a problem?* 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | *Describe the solution you'd like* 17 | A clear and concise description of what you want to happen. 18 | 19 | *Describe alternatives you've considered* 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | *Additional context* 23 | Add any other context about the release item request here. 24 | - type: textarea 25 | id: item 26 | attributes: 27 | label: "Describe the release item" 28 | validations: 29 | required: true 30 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2023 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | 15 | changelog: 16 | categories: 17 | - title: Breaking changes 💥 18 | labels: 19 | - breaking-change 20 | - title: New features 🎉 21 | labels: 22 | - enhancement 23 | - new feature 24 | exclude: 25 | labels: 26 | - internal 27 | - title: Bug fixes 🐞 28 | labels: 29 | - bug 30 | exclude: 31 | labels: 32 | - internal 33 | - title: Documentation 📝 34 | labels: 35 | - documentation 36 | exclude: 37 | labels: 38 | - internal 39 | - title: Dependencies 👷 40 | labels: 41 | - dependencies 42 | exclude: 43 | labels: 44 | - internal 45 | - title: Other changes 46 | labels: 47 | - "*" 48 | exclude: 49 | labels: 50 | - internal -------------------------------------------------------------------------------- /.github/workflows/check-labels.yml: -------------------------------------------------------------------------------- 1 | name: Check required labels 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, synchronize, reopened, labeled] 6 | branches: ["**"] 7 | 8 | jobs: 9 | check-labels: 10 | name: Check PR labels 11 | uses: eclipse-zenoh/ci/.github/workflows/check-labels.yml@main 12 | secrets: 13 | github-token: ${{ secrets.GITHUB_TOKEN }} 14 | permissions: 15 | pull-requests: write 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: ["**"] 9 | pull_request: 10 | branches: ["**"] 11 | schedule: 12 | - cron: "0 6 * * 1-5" 13 | workflow_dispatch: 14 | 15 | jobs: 16 | check: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Clone this repository 20 | uses: actions/checkout@v4 21 | with: 22 | submodules: true 23 | 24 | - name: Run isort 25 | uses: isort/isort-action@v1 26 | 27 | - name: Run black 28 | uses: psf/black@stable 29 | with: 30 | options: "--check --diff -C" 31 | 32 | - name: Install rust components 33 | run: | 34 | rustup component add clippy rustfmt 35 | 36 | - name: Run rustfmt 37 | run: cargo fmt --check -- --config "unstable_features=true,imports_granularity=Crate,group_imports=StdExternalCrate" 38 | 39 | - name: Clippy no-default-features 40 | run: cargo +stable clippy --no-default-features --all-targets -- --deny warnings 41 | 42 | - name: Clippy 43 | run: cargo +stable clippy --all-features --all-targets -- --deny warnings 44 | 45 | check_rust: 46 | name: Check zenoh-python using Rust 1.75 47 | runs-on: ubuntu-latest 48 | strategy: 49 | fail-fast: false 50 | 51 | steps: 52 | - name: Clone this repository 53 | uses: actions/checkout@v4 54 | 55 | - name: Update Rust 1.75.0 toolchain 56 | run: rustup update 1.75.0 57 | 58 | - name: Setup rust-cache 59 | uses: Swatinem/rust-cache@v2 60 | with: 61 | cache-bin: false 62 | 63 | - name: Check zenoh with rust 1.75.0 64 | run: cargo +1.75.0 check --release --bins --lib 65 | 66 | build: 67 | needs: check 68 | runs-on: ubuntu-latest 69 | strategy: 70 | fail-fast: false 71 | matrix: 72 | python-version: 73 | - "3.9" 74 | - "3.10" 75 | - "3.11" 76 | - "3.12" 77 | - "3.13-dev" 78 | steps: 79 | - name: Clone this repository 80 | uses: actions/checkout@v4 81 | with: 82 | submodules: true 83 | 84 | - name: Set up Python ${{ matrix.python-version }} 85 | uses: actions/setup-python@v5 86 | with: 87 | python-version: ${{ matrix.python-version }} 88 | 89 | - name: Build zenoh-python 90 | uses: messense/maturin-action@v1 91 | 92 | - name: Install zenoh-python 93 | run: pip3 install ./target/wheels/*.whl 94 | 95 | - name: Run stubs check 96 | run: python3 tests/stubs_check.py 97 | 98 | - name: Install pytest 99 | run: pip3 install pytest pytest-xdist fixtures 100 | 101 | - name: Run examples check 102 | run: pytest tests/examples_check.py -v --durations=0 103 | 104 | - name: Run pytest 105 | run: pytest -n auto --import-mode=append 106 | 107 | markdown_lint: 108 | runs-on: ubuntu-latest 109 | steps: 110 | - uses: actions/checkout@v4 111 | - uses: DavidAnson/markdownlint-cli2-action@v18 112 | with: 113 | config: '.markdownlint.yaml' 114 | globs: '**/README.md' 115 | 116 | # NOTE: In GitHub repository settings, the "Require status checks to pass 117 | # before merging" branch protection rule ensures that commits are only merged 118 | # from branches where specific status checks have passed. These checks are 119 | # specified manually as a list of workflow job names. Thus we use this extra 120 | # job to signal whether all CI checks have passed. 121 | ci: 122 | name: CI status checks 123 | runs-on: ubuntu-latest 124 | needs: [check_rust, build, markdown_lint] 125 | if: always() 126 | steps: 127 | - name: Check whether all jobs pass 128 | run: echo '${{ toJson(needs) }}' | jq -e 'all(.result == "success")' 129 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | schedule: 5 | - cron: "0 1 * * 1-5" 6 | workflow_dispatch: 7 | inputs: 8 | live-run: 9 | type: boolean 10 | description: Live-run 11 | required: false 12 | version: 13 | type: string 14 | description: Release number 15 | required: false 16 | zenoh-version: 17 | type: string 18 | description: Release number of Zenoh 19 | required: false 20 | branch: 21 | type: string 22 | description: Release branch 23 | required: false 24 | 25 | jobs: 26 | tag: 27 | name: Branch, Bump & tag 28 | runs-on: ubuntu-latest 29 | outputs: 30 | version: ${{ steps.create-release-branch.outputs.version }} 31 | branch: ${{ steps.create-release-branch.outputs.branch }} 32 | steps: 33 | - id: create-release-branch 34 | uses: eclipse-zenoh/ci/create-release-branch@main 35 | with: 36 | repo: ${{ github.repository }} 37 | live-run: ${{ inputs.live-run || false }} 38 | version: ${{ inputs.version }} 39 | branch: ${{ inputs.branch }} 40 | github-token: ${{ secrets.BOT_TOKEN_WORKFLOW }} 41 | 42 | - name: Checkout this repository 43 | uses: actions/checkout@v4 44 | with: 45 | ref: ${{ steps.create-release-branch.outputs.branch }} 46 | 47 | - name: Bump and tag project 48 | run: bash ci/scripts/bump-and-tag.bash 49 | env: 50 | LIVE_RUN: ${{ inputs.live-run || false }} 51 | VERSION: ${{ steps.create-release-branch.outputs.version }} 52 | BUMP_DEPS_VERSION: ${{ inputs.zenoh-version }} 53 | BUMP_DEPS_PATTERN: ${{ inputs.zenoh-version && 'zenoh.*' || '' }} 54 | BUMP_DEPS_BRANCH: ${{ inputs.zenoh-version && format('release/{0}', inputs.zenoh-version) || '' }} 55 | GIT_USER_NAME: eclipse-zenoh-bot 56 | GIT_USER_EMAIL: eclipse-zenoh-bot@users.noreply.github.com 57 | 58 | build-macos: 59 | needs: tag 60 | runs-on: macos-latest 61 | steps: 62 | - name: Checkout this repository 63 | uses: actions/checkout@v4 64 | with: 65 | ref: ${{ needs.tag.outputs.branch }} 66 | 67 | - name: Install Rust toolchain 68 | run: | 69 | rustup set profile minimal 70 | rustup show 71 | 72 | - name: Build wheels - x86_64 73 | uses: messense/maturin-action@v1 74 | with: 75 | target: x86_64 76 | args: --release --out dist --sdist # Note: this step builds also the sources distrib 77 | 78 | - name: Build wheels - universal2 79 | uses: messense/maturin-action@v1 80 | with: 81 | target: universal2-apple-darwin 82 | args: --release --out dist 83 | 84 | - name: Upload wheels 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: wheels-macos 88 | path: dist 89 | 90 | build-windows: 91 | needs: tag 92 | runs-on: windows-latest 93 | strategy: 94 | matrix: 95 | target: [x64] 96 | # target: [x64, x86] NOTE: x86 deactivated because of strange error: failed to run custom build command for `pyo3-ffi v0.17.1` 97 | steps: 98 | - name: Checkout this repository 99 | uses: actions/checkout@v4 100 | with: 101 | ref: ${{ needs.tag.outputs.branch }} 102 | 103 | - name: Install Rust toolchain 104 | run: | 105 | rustup set profile minimal 106 | rustup show 107 | 108 | - name: Build wheels 109 | uses: messense/maturin-action@v1 110 | with: 111 | target: ${{ matrix.target }} 112 | args: --release --out dist 113 | 114 | - name: Upload wheels 115 | uses: actions/upload-artifact@v4 116 | with: 117 | name: wheels-windows-${{ matrix.target }} 118 | path: dist 119 | 120 | build-linux: 121 | needs: tag 122 | runs-on: ubuntu-latest 123 | strategy: 124 | matrix: 125 | target: [x86_64, i686, armv7] 126 | steps: 127 | - name: Checkout this repository 128 | uses: actions/checkout@v4 129 | with: 130 | ref: ${{ needs.tag.outputs.branch }} 131 | 132 | - name: Build wheels 133 | uses: messense/maturin-action@v1 134 | with: 135 | target: ${{ matrix.target }} 136 | manylinux: auto 137 | args: --release --out dist 138 | 139 | - name: Upload wheels 140 | uses: actions/upload-artifact@v4 141 | with: 142 | name: wheels-linux-${{ matrix.target }} 143 | path: dist 144 | 145 | build-linux-aarch64: 146 | needs: tag 147 | runs-on: ubuntu-latest 148 | steps: 149 | - name: Checkout this repository 150 | uses: actions/checkout@v4 151 | with: 152 | ref: ${{ needs.tag.outputs.branch }} 153 | 154 | - name: Build wheels 155 | uses: messense/maturin-action@v1 156 | with: 157 | target: aarch64 158 | # NOTE(fuzzypixelz): We manually specify a more recent manylinux platform tag for aarch64: 159 | # - zenoh-link-quic indirectly depends on ring 0.17 through rustls-webpki. 160 | # - ring 0.17 depends on a version of BoringSSL that requires GCC/Clang to provide __ARM_ARCH 161 | # - When setting the manylinux tag to 'auto', messense/maturin-action@v1 uses manylinux2014 to compile for for aarch64 162 | # - the GCC included in the manylinux2014 docker image doesn't provide __ARM_ARCH 163 | # See: https://github.com/briansmith/ring/issues/1728 164 | manylinux: manylinux_2_28 165 | args: --release --out dist 166 | 167 | - name: Upload wheels 168 | uses: actions/upload-artifact@v4 169 | with: 170 | name: wheels-linux-aarch64 171 | path: dist 172 | 173 | build-linux-armv6: 174 | needs: tag 175 | runs-on: macos-latest 176 | steps: 177 | - name: Checkout this repository 178 | uses: actions/checkout@v4 179 | with: 180 | ref: ${{ needs.tag.outputs.branch }} 181 | 182 | - name: Install Rust toolchain 183 | run: | 184 | rustup set profile minimal 185 | rustup target add arm-unknown-linux-gnueabihf 186 | 187 | - name: Install cross toolchain 188 | run: | 189 | brew tap messense/macos-cross-toolchains 190 | brew install arm-unknown-linux-gnueabihf 191 | 192 | export CC_arm_unknown_linux_gnueabihf=arm-unknown-linux-gnueabihf-gcc 193 | export CXX_arm_unknown_linux_gnueabihf=arm-unknown-linux-gnueabihf-g++ 194 | export AR_arm_unknown_linux_gnueabihf=arm-unknown-linux-gnueabihf-ar 195 | export CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-unknown-linux-gnueabihf-gcc 196 | 197 | python3 -m venv venv 198 | source venv/bin/activate 199 | pip3 install -r requirements-dev.txt 200 | maturin build --release --target arm-unknown-linux-gnueabihf --out dist 201 | 202 | - name: Upload wheels 203 | uses: actions/upload-artifact@v4 204 | with: 205 | name: wheels-linux-armv6 206 | path: dist 207 | 208 | publish-pypi: 209 | needs: 210 | [ 211 | tag, 212 | build-macos, 213 | build-windows, 214 | build-linux, 215 | build-linux-armv6, 216 | build-linux-aarch64, 217 | ] 218 | name: Deploy wheels to pypi 219 | runs-on: ubuntu-latest 220 | steps: 221 | - uses: actions/download-artifact@v4 222 | with: 223 | path: dist 224 | pattern: wheels-* 225 | merge-multiple: true 226 | 227 | - name: Check dist 228 | run: ls -al ./dist/* 229 | 230 | - name: Publish 231 | if: ${{ inputs.live-run || false }} 232 | uses: pypa/gh-action-pypi-publish@release/v1 233 | with: 234 | password: ${{ secrets.PYPI_ORG_TOKEN }} 235 | 236 | publish-github: 237 | needs: 238 | [ 239 | tag, 240 | build-macos, 241 | build-windows, 242 | build-linux, 243 | build-linux-armv6, 244 | build-linux-aarch64, 245 | ] 246 | runs-on: ubuntu-latest 247 | steps: 248 | - uses: eclipse-zenoh/ci/publish-crates-github@main 249 | with: 250 | repo: ${{ github.repository }} 251 | live-run: ${{ inputs.live-run || false }} 252 | version: ${{ needs.tag.outputs.version }} 253 | branch: ${{ needs.tag.outputs.branch }} 254 | github-token: ${{ secrets.BOT_TOKEN_WORKFLOW }} 255 | archive-patterns: "^$" 256 | -------------------------------------------------------------------------------- /.github/workflows/update-release-project.yml: -------------------------------------------------------------------------------- 1 | name: Update release project 2 | 3 | on: 4 | issues: 5 | types: [opened, edited, labeled] 6 | pull_request_target: 7 | types: [closed] 8 | branches: 9 | - main 10 | 11 | jobs: 12 | main: 13 | uses: eclipse-zenoh/zenoh/.github/workflows/update-release-project.yml@main 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | **/target 4 | 5 | # Generated by setup.py 6 | *.egg-info 7 | dist/ 8 | build/ 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 14 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 15 | #Cargo.lock 16 | 17 | # These are backup files generated by rustfmt 18 | **/*.rs.bk 19 | 20 | # sphinx build directories 21 | docs/_* 22 | 23 | # CLion project directory 24 | .idea 25 | 26 | # Emacs temps 27 | *~ 28 | 29 | # MacOS Related 30 | .DS_Store 31 | 32 | # others 33 | *.log 34 | *.BAK 35 | *.bak 36 | 37 | .vscode/ 38 | 39 | **/pycache 40 | **/__pycache__ 41 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, # Line length limitation 3 | "MD033": false, # Enable Inline HTML 4 | "MD041": false, # Allow first line heading 5 | "MD045": false, # Allow Images have no alternate text 6 | } -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: fmt 5 | name: fmt 6 | entry: cargo fmt -- --config "unstable_features=true,imports_granularity=Crate,group_imports=StdExternalCrate" 7 | language: system 8 | types: [rust] 9 | - repo: https://github.com/pycqa/isort 10 | rev: 5.12.0 11 | hooks: 12 | - id: isort 13 | - repo: https://github.com/psf/black-pre-commit-mirror 14 | rev: 24.4.2 15 | hooks: 16 | - id: black 17 | args: [-C] -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | 15 | # .readthedocs.yml 16 | # Read the Docs configuration file 17 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 18 | 19 | # Required 20 | version: 2 21 | 22 | # Build documentation in the docs/ directory with Sphinx 23 | sphinx: 24 | configuration: docs/conf.py 25 | 26 | build: 27 | os: ubuntu-22.04 28 | tools: 29 | python: "3.11" 30 | rust: "latest" 31 | jobs: 32 | pre_build: 33 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH maturin develop 34 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH python docs/stubs_to_sources.py 35 | 36 | # Optionally build your docs in additional formats such as PDF 37 | formats: 38 | - pdf 39 | 40 | # Optionally set the version of Python and requirements required to build your docs 41 | python: 42 | install: 43 | - requirements: docs/requirements.txt 44 | - requirements: requirements-dev.txt 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | All changes for each release are tracked via [GitHub Releases](https://github.com/eclipse-zenoh/zenoh-python/releases). -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Eclipse zenoh 2 | 3 | Thanks for your interest in this project. 4 | 5 | ## Project description 6 | 7 | Eclipse zenoh provides is a stack designed to 8 | 1. minimize network overhead, 9 | 2. support extremely constrained devices, 10 | 3. supports devices with low duty-cycle by allowing the negotiation of data exchange modes and schedules, 11 | 4. provide a rich set of abstraction for distributing, querying and storing data along the entire system, and 12 | 5. provide extremely low latency and high throughput. 13 | 14 | * https://projects.eclipse.org/projects/iot.zenoh 15 | 16 | ## Developer resources 17 | 18 | Information regarding source code management, builds, coding standards, and 19 | more. 20 | 21 | * https://projects.eclipse.org/projects/iot.zenoh/developer 22 | 23 | The project maintains the following source code repositories 24 | 25 | * https://github.com/eclipse-zenoh 26 | 27 | ## Eclipse Contributor Agreement 28 | 29 | Before your contribution can be accepted by the project team contributors must 30 | electronically sign the Eclipse Contributor Agreement (ECA). 31 | 32 | * http://www.eclipse.org/legal/ECA.php 33 | 34 | Commits that are provided by non-committers must have a Signed-off-by field in 35 | the footer indicating that the author is aware of the terms by which the 36 | contribution has been provided to the project. The non-committer must 37 | additionally have an Eclipse Foundation account and must have a signed Eclipse 38 | Contributor Agreement (ECA) on file. 39 | 40 | For more information, please see the Eclipse Committer Handbook: 41 | https://www.eclipse.org/projects/handbook/#resources-commit 42 | 43 | ## Contact 44 | 45 | Contact the project developers via the project's "dev" list. 46 | 47 | * https://accounts.eclipse.org/mailing-list/zenoh-dev 48 | 49 | Or via the Gitter channel. 50 | 51 | * https://gitter.im/atolab/zenoh 52 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors to Eclipse zenoh 2 | 3 | These are the contributors to Eclipse zenoh (the initial contributors and the contributors listed in the Git log). 4 | 5 | 6 | | GitHub username | Name | 7 | | --------------- | ---------------------------------| 8 | | kydos | Angelo Corsaro (ZettaScale) | 9 | | JEnoch | Julien Enoch (ZettaScale) | 10 | | OlivierHecart | Olivier Hécart (ZettaScale) | 11 | | gabrik | Gabriele Baldoni (ZettaScale) | 12 | | Mallets | Luca Cominardi (ZettaScale) | 13 | | IvanPaez | Ivan Paez (ZettaScale) | 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # Copyright (c) 2022 ZettaScale Technology 4 | # 5 | # This program and the accompanying materials are made available under the 6 | # terms of the Eclipse Public License 2.0 which is available at 7 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | # 10 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | # 12 | # Contributors: 13 | # ZettaScale Zenoh Team, 14 | # 15 | # 16 | [package] 17 | name = "zenoh-python" 18 | version = "1.4.0" 19 | authors = [ 20 | "kydos ", 21 | "Julien Enoch ", 22 | "Olivier Hécart ", 23 | "Luca Cominardi ", 24 | "Pierre Avital ", 25 | ] 26 | edition = "2021" 27 | license = "EPL-2.0 OR Apache-2.0" 28 | categories = ["network-programming"] 29 | description = "The Zenoh Python API" 30 | readme = "README.md" 31 | 32 | [lib] 33 | name = "zenoh" 34 | crate-type = ["cdylib"] 35 | 36 | [features] 37 | default = ["zenoh/default", "zenoh-ext"] 38 | 39 | [badges] 40 | maintenance = { status = "actively-developed" } 41 | 42 | [dependencies] 43 | flume = "0.11.0" 44 | json5 = "0.4.1" 45 | paste = "1.0.14" 46 | pyo3 = { version = "0.21.2", features = [ 47 | "extension-module", 48 | "abi3-py38", 49 | "experimental-async", 50 | "experimental-declarative-modules", 51 | ] } 52 | validated_struct = "2.1.0" 53 | zenoh = { version = "1.4.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["unstable", "internal"], default-features = false } 54 | zenoh-ext = { version = "1.4.0", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", features = ["internal"], optional = true } 55 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include Cargo.toml 2 | include Cargo.lock 3 | include requirements-dev.txt 4 | recursive-include src * 5 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | # Notices for Eclipse zenoh-python 2 | 3 | This content is produced and maintained by the Eclipse zenoh project. 4 | 5 | * Project home: https://projects.eclipse.org/projects/iot.zenoh 6 | 7 | ## Trademarks 8 | 9 | Eclipse zenoh is trademark of the Eclipse Foundation. 10 | Eclipse, and the Eclipse Logo are registered trademarks of the Eclipse Foundation. 11 | 12 | ## Copyright 13 | 14 | All content is the property of the respective authors or their employers. 15 | For more information regarding authorship of content, please consult the 16 | listed source code repository logs. 17 | 18 | ## Declared Project Licenses 19 | 20 | This program and the accompanying materials are made available under the 21 | terms of the Eclipse Public License 2.0 which is available at 22 | http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 23 | which is available at https://www.apache.org/licenses/LICENSE-2.0. 24 | 25 | SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 26 | 27 | ## Source Code 28 | 29 | The project maintains the following source code repositories: 30 | 31 | * https://github.com/eclipse-zenoh/zenoh.git 32 | * https://github.com/eclipse-zenoh/zenoh-c.git 33 | * https://github.com/eclipse-zenoh/zenoh-java.git 34 | * https://github.com/eclipse-zenoh/zenoh-go.git 35 | * https://github.com/eclipse-zenoh/zenoh-python.git 36 | 37 | ## Third-party Content 38 | 39 | *To be completed...* 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![CI](https://github.com/eclipse-zenoh/zenoh-python/workflows/CI/badge.svg)](https://github.com/eclipse-zenoh/zenoh-python/actions?query=workflow%3A%22CI%22) 4 | [![Documentation Status](https://readthedocs.org/projects/zenoh-python/badge/?version=latest)](https://zenoh-python.readthedocs.io/en/latest/?badge=latest) 5 | [![Discussion](https://img.shields.io/badge/discussion-on%20github-blue)](https://github.com/eclipse-zenoh/roadmap/discussions) 6 | [![Discord](https://img.shields.io/badge/chat-on%20discord-blue)](https://discord.gg/2GJ958VuHs) 7 | [![License](https://img.shields.io/badge/License-EPL%202.0-blue)](https://choosealicense.com/licenses/epl-2.0/) 8 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 9 | 10 | # Eclipse Zenoh 11 | 12 | The Eclipse Zenoh: Zero Overhead Pub/sub, Store/Query and Compute. 13 | 14 | Zenoh (pronounce _/zeno/_) unifies data in motion, data at rest and computations. It carefully blends traditional pub/sub with geo-distributed storages, queries and computations, while retaining a level of time and space efficiency that is well beyond any of the mainstream stacks. 15 | 16 | Check the website [zenoh.io](http://zenoh.io) and the [roadmap](https://github.com/eclipse-zenoh/roadmap) for more detailed information. 17 | 18 | ------------------------------- 19 | 20 | # Python API 21 | 22 | This repository provides a Python binding based on the main [Zenoh implementation written in Rust](https://github.com/eclipse-zenoh/zenoh). 23 | 24 | ------------------------------- 25 | 26 | ## How to install it 27 | 28 | The Eclipse zenoh-python library is available on [Pypi.org](https://pypi.org/project/eclipse-zenoh/). 29 | Install the latest available version using `pip` in a [virtual environment](https://packaging.python.org/en/latest/guides/installing-using-pip-and-virtual-environments/): 30 | 31 | ```bash 32 | pip install eclipse-zenoh 33 | ``` 34 | 35 | :warning:WARNING:warning: zenoh-python is developped in Rust. 36 | On Pypi.org we provide binary wheels for the most common platforms (Linux x86_64, i686, ARMs, MacOS universal2 and Windows amd64). But also a source distribution package for other platforms. 37 | However, for `pip` to be able to build this source distribution, there are some prerequisites: 38 | 39 | - `pip` version 19.3.1 minimum (for full support of PEP 517). 40 | (if necessary upgrade it with command: `'sudo pip install --upgrade pip'` ) 41 | - Have a Rust toolchain installed (instructions at [rustup.rs](https://rustup.rs/)) 42 | 43 | ### Supported Python versions and platforms 44 | 45 | zenoh-python has been tested with Python 3.8, 3.9, 3.10, 3.11 and 3.12 46 | 47 | It relies on the [zenoh](https://github.com/eclipse-zenoh/zenoh/tree/main/zenoh) Rust API which require the full `std` library. See the list in [Rust Platform Support](https://doc.rust-lang.org/nightly/rustc/platform-support.html). 48 | 49 | ### Enable zenoh features 50 | 51 | To enable some compilation features of the Rust library that are disabled by default, for example `shared-memory`, execute the following command: 52 | 53 | ```bash 54 | pip install eclipse-zenoh --no-binary :all: --config-settings build-args="--features=zenoh/shared-memory" 55 | ``` 56 | 57 | ------------------------------- 58 | 59 | ## How to build it 60 | 61 | Requirements: 62 | 63 | - Python >= 3.8 64 | - pip >= 19.3.1 65 | - (Optional) A Python virtual environment (for instance [virtualenv](https://docs.python.org/3.10/tutorial/venv.html) or [miniconda](https://docs.conda.io/en/latest/miniconda.html)) 66 | - [Rust and Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html). If you already have the Rust toolchain installed, make sure it is up-to-date with: 67 | 68 | ```bash 69 | rustup update 70 | ``` 71 | 72 | Steps: 73 | 74 | - Install developments requirements: 75 | 76 | ```bash 77 | pip install -r requirements-dev.txt 78 | ``` 79 | 80 | - Ensure your system can find the building tool `maturin` (installed by previous step). 81 | For example, it is placed at _$HOME/.local/bin/maturin_ by default on Ubuntu 20.04. 82 | 83 | ```bash 84 | export PATH="$HOME/.local/bin:$PATH" 85 | ``` 86 | 87 | - Build and install zenoh-python: 88 | 89 | - With a virtual environment active: 90 | 91 | ```bash 92 | maturin develop --release 93 | ``` 94 | 95 | - Without one: 96 | 97 | ```bash 98 | maturin build --release 99 | pip install ./target/wheels/ 100 | ``` 101 | 102 | ------------------------------- 103 | 104 | ## Running the Examples 105 | 106 | You can install Zenoh Router first (See [the instructions](https://github.com/eclipse-zenoh/zenoh/?tab=readme-ov-file#how-to-install-it)). 107 | Then, run the zenoh-python examples following the instructions in [examples/README.md](https://github.com/eclipse-zenoh/zenoh-python/tree/main/examples#readme) 108 | -------------------------------------------------------------------------------- /ci/scripts/bump-and-tag.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xeo pipefail 4 | 5 | readonly live_run=${LIVE_RUN:-false} 6 | # Release number 7 | version=${VERSION:?input VERSION is required} 8 | # Dependencies' pattern 9 | readonly bump_deps_pattern=${BUMP_DEPS_PATTERN:-''} 10 | # Dependencies' version 11 | readonly bump_deps_version=${BUMP_DEPS_VERSION:-''} 12 | # Dependencies' git branch 13 | readonly bump_deps_branch=${BUMP_DEPS_BRANCH:-''} 14 | # Git actor name 15 | readonly git_user_name=${GIT_USER_NAME:?input GIT_USER_NAME is required} 16 | # Git actor email 17 | readonly git_user_email=${GIT_USER_EMAIL:?input GIT_USER_EMAIL is required} 18 | 19 | cargo +stable install toml-cli 20 | 21 | # NOTE(fuzzypixelz): toml-cli doesn't yet support in-place modification 22 | # See: https://github.com/gnprice/toml-cli?tab=readme-ov-file#writing-ish-toml-set 23 | function toml_set_in_place() { 24 | local tmp=$(mktemp) 25 | toml set "$1" "$2" "$3" > "$tmp" 26 | mv "$tmp" "$1" 27 | } 28 | 29 | export GIT_AUTHOR_NAME=$git_user_name 30 | export GIT_AUTHOR_EMAIL=$git_user_email 31 | export GIT_COMMITTER_NAME=$git_user_name 32 | export GIT_COMMITTER_EMAIL=$git_user_email 33 | 34 | # For development releases (e.g. 1.0.0-dev-21-g2ca8632), transform the version 35 | # into a PEP-440 compatible version, since maturin>1 requires strict version compliance. 36 | if [[ "${version}" =~ [0-9]+\.[0-9]+\.[0-9]+-[0-9]+-g[a-f0-9]+ ]]; then 37 | version=$(echo $version | sed 's/-/+/') 38 | fi 39 | 40 | # Bump Cargo version 41 | toml_set_in_place Cargo.toml "package.version" "$version" 42 | # Propagate version change to pyproject.toml 43 | toml_set_in_place pyproject.toml "project.version" "$version" 44 | 45 | git commit Cargo.toml pyproject.toml -m "chore: Bump version to $version" 46 | 47 | # Select all package dependencies that match $bump_deps_pattern and bump them to $bump_deps_version 48 | if [[ "$bump_deps_pattern" != '' ]]; then 49 | deps=$(toml get Cargo.toml dependencies | jq -r "keys[] | select(test(\"$bump_deps_pattern\"))") 50 | for dep in $deps; do 51 | if [[ -n $bump_deps_version ]]; then 52 | toml_set_in_place Cargo.toml "dependencies.$dep.version" "$bump_deps_version" 53 | fi 54 | 55 | if [[ -n $bump_deps_branch ]]; then 56 | toml_set_in_place Cargo.toml "dependencies.$dep.branch" "$bump_deps_branch" 57 | fi 58 | done 59 | # Update lockfile 60 | cargo check 61 | 62 | if [[ -n $bump_deps_version || -n $bump_deps_branch ]]; then 63 | git commit Cargo.toml Cargo.lock -m "chore: Bump $bump_deps_pattern version to $bump_deps_version" 64 | else 65 | echo "warn: no changes have been made to any dependencies matching $bump_deps_pattern" 66 | fi 67 | fi 68 | 69 | if [[ ${live_run} ]]; then 70 | git tag --force "$version" -m "v$version" 71 | fi 72 | git log -10 73 | git show-ref --tags 74 | git push origin 75 | git push --force origin "$version" 76 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # Copyright (c) 2022 ZettaScale Technology 4 | # 5 | # This program and the accompanying materials are made available under the 6 | # terms of the Eclipse Public License 2.0 which is available at 7 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | # 10 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | # 12 | # Contributors: 13 | # ZettaScale Zenoh Team, 14 | # 15 | # 16 | 17 | 18 | # Configuration file for the Sphinx documentation builder. 19 | # 20 | # This file only contains a selection of the most common options. For a full 21 | # list see the documentation: 22 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 23 | 24 | # -- Project setup -------------------------------------------------------------- 25 | 26 | import tomllib 27 | 28 | import zenoh 29 | 30 | # -- Project information ----------------------------------------------------- 31 | 32 | project = "zenoh-python" 33 | copyright = "2020, ZettaScale Zenoh team, " 34 | author = "ZettaScale Zenoh team, " 35 | 36 | # Extract the release number from the Cargo manifest 37 | with open("../Cargo.toml", "rb") as f: 38 | release = tomllib.load(f)["package"]["version"] 39 | 40 | 41 | # -- General configuration --------------------------------------------------- 42 | 43 | # Add any Sphinx extension module names here, as strings. They can be 44 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 45 | # ones. 46 | extensions = [ 47 | "sphinx.ext.autodoc", 48 | "sphinx.ext.autosummary", 49 | "sphinx.ext.intersphinx", 50 | "sphinx_rtd_theme", 51 | "enum_tools.autoenum", 52 | ] 53 | 54 | # Add any paths that contain templates here, relative to this directory. 55 | templates_path = ["_templates"] 56 | 57 | # The suffix(es) of source filenames. 58 | # You can specify multiple suffix as a list of string: 59 | # 60 | # source_suffix = ['.rst', '.md'] 61 | source_suffix = ".rst" 62 | 63 | # The master toctree document. 64 | master_doc = "index" 65 | 66 | # The language for content autogenerated by Sphinx. Refer to documentation 67 | # for a list of supported languages. 68 | # 69 | # This is also used if you do content translation via gettext catalogs. 70 | # Usually you set "language" from the command line for these cases. 71 | language = "python" 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | # This pattern also affects html_static_path and html_extra_path. 76 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**/*.rs"] 77 | 78 | # The name of the Pygments (syntax highlighting) style to use. 79 | pygments_style = None 80 | 81 | autodoc_typehints = "description" 82 | autodoc_mock_imports = ["zenoh"] 83 | 84 | # -- Options for HTML output ------------------------------------------------- 85 | 86 | # The theme to use for HTML and HTML Help pages. See the documentation for 87 | # a list of builtin themes. 88 | # 89 | html_theme = "sphinx_rtd_theme" 90 | 91 | # Add any paths that contain custom static files (such as style sheets) here, 92 | # relative to this directory. They are copied after the builtin static files, 93 | # so a file named "default.css" will overwrite the builtin "default.css". 94 | html_static_path = ["_static"] 95 | autodoc_member_order = "groupwise" 96 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. 2 | .. Copyright (c) 2017, 2022 ZettaScale Technology 3 | .. 4 | .. This program and the accompanying materials are made available under the 5 | .. terms of the Eclipse Public License 2.0 which is available at 6 | .. http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | .. which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | .. 9 | .. SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | .. 11 | .. Contributors: 12 | .. ZettaScale Zenoh team, 13 | .. 14 | 15 | ******************* 16 | Zenoh API Reference 17 | ******************* 18 | 19 | `Zenoh `_ /zeno/ is a stack that unifies data in motion, data at 20 | rest and computations. It elegantly blends traditional pub/sub with geo distributed 21 | storage, queries and computations, while retaining a level of time and space efficiency 22 | that is well beyond any of the mainstream stacks. 23 | 24 | Before delving into the examples, we need to introduce few **Zenoh** concepts. 25 | First off, in Zenoh you will deal with **Resources**, where a resource is made up of a 26 | key and a value. The other concept you'll have to familiarize yourself with are 27 | **key expressions**, such as ``robot/sensor/temp``, ``robot/sensor/*``, ``robot/**``, etc. 28 | As you can gather, the above key expression denotes set of keys, while the ``*`` and ``**`` 29 | are wildcards representing respectively (1) a single chunk (non-empty sequence of characters that doesn't contain ``/``), and (2) any amount of chunks (including 0). 30 | 31 | Below are some examples that highlight these key concepts and show how easy it is to get 32 | started with. 33 | 34 | Quick start examples: 35 | ^^^^^^^^^^^^^^^^^^^^^ 36 | 37 | Publish a key/value pair onto Zenoh 38 | """"""""""""""""""""""""""""""""""" 39 | 40 | >>> import zenoh 41 | >>> with zenoh.open() as session: 42 | >>> session.put('demo/example/hello', 'Hello World!') 43 | 44 | Subscribe to a set of keys with Zenoh 45 | """"""""""""""""""""""""""""""""""""" 46 | 47 | >>> import zenoh, time 48 | >>> def listener(sample): 49 | >>> print(f"{sample.key_expr} => {sample.payload.to_string()}") 50 | >>> 51 | >>> with zenoh.open() as session: 52 | >>> with session.declare_subscriber('demo/example/**', listener) as subscriber: 53 | >>> time.sleep(60) 54 | 55 | Get keys/values from zenoh 56 | """""""""""""""""""""""""" 57 | 58 | >>> import zenoh 59 | >>> with zenoh.open() as session: 60 | >>> for response in session.get('demo/example/**'): 61 | >>> response = response.ok 62 | >>> print(f"{response.key_expr} => {response.payload.to_string()}") 63 | 64 | module zenoh 65 | ============ 66 | 67 | .. automodule:: zenoh 68 | :members: 69 | :undoc-members: 70 | 71 | module zenoh.handlers 72 | ===================== 73 | 74 | .. automodule:: zenoh.handlers 75 | :members: 76 | :undoc-members: 77 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==7.2.6 2 | sphinx_rtd_theme==2.0.0 3 | enum-tools[sphinx] 4 | -------------------------------------------------------------------------------- /docs/stubs_to_sources.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | """Transform Python stubs into Python code. 15 | 16 | Rename `*.pyi` to `*.py`. Also, because overloaded functions doesn't render nicely, 17 | overloaded functions are rewritten in a non-overloaded form. Handler parameter types 18 | are merged, and return type is unspecialized, while handler delegated methods are 19 | kept without the `Never` overload. `serializer`/`deserializer` are kept untouched, 20 | because it's ok. 21 | Moreover, all function parameters annotations are stringified in order to allow 22 | referencing a type not declared yet (i.e. forward reference).""" 23 | 24 | import ast 25 | import inspect 26 | from collections import defaultdict 27 | from pathlib import Path 28 | 29 | PACKAGE = (Path(__file__) / "../../zenoh").resolve() 30 | __INIT__ = PACKAGE / "__init__.py" 31 | 32 | 33 | def _unstable(item): 34 | warning = ".. warning:: This API has been marked as unstable: it works as advertised, but it may be changed in a future release." 35 | if item.__doc__: 36 | item.__doc__ += "\n" + warning 37 | else: 38 | item.__doc__ = warning 39 | return item 40 | 41 | 42 | class RemoveOverload(ast.NodeTransformer): 43 | def __init__(self): 44 | self.current_cls = None 45 | # only the first overloaded signature is modified, others are removed 46 | # modified functions are stored here 47 | self.overloaded_by_class: defaultdict[str | None, set[str]] = defaultdict(set) 48 | 49 | def visit_ClassDef(self, node: ast.ClassDef): 50 | # register the current class for method name disambiguation 51 | self.current_cls = node.name 52 | res = self.generic_visit(node) 53 | self.current_cls = None 54 | return res 55 | 56 | def visit_FunctionDef(self, node: ast.FunctionDef): 57 | for decorator in node.decorator_list: 58 | if isinstance(decorator, ast.Name) and decorator.id == "overload": 59 | if node.name in self.overloaded_by_class[self.current_cls]: 60 | # there is no implementation in stub, so one has to be added 61 | # for (de)serializer 62 | if node.name in ("serializer", "deserializer"): 63 | func = ast.parse( 64 | f"def {node.name}(arg, /): {ast.unparse(node.body[0])}" 65 | ) 66 | return [node, func] 67 | # remove already modified overloaded signature 68 | return None 69 | self.overloaded_by_class[self.current_cls].add(node.name) 70 | # (de)serializer is kept overloaded 71 | if node.name in ("serializer", "deserializer"): 72 | return node 73 | # remove overloaded decorator 74 | node.decorator_list.clear() 75 | if node.name not in ("recv", "try_recv", "__iter__"): 76 | # retrieve the handled type (Scout/Reply/etc.) from the return type 77 | assert isinstance(node.returns, ast.Subscript) 78 | if isinstance(node.returns.slice, ast.Subscript): 79 | # `Subscriber[Handler[Sample]]` case 80 | tp = node.returns.slice.slice 81 | else: 82 | # `Handler[Reply]` case 83 | tp = node.returns.slice 84 | assert isinstance(tp, ast.Name) 85 | # replace `handler` parameter annotation 86 | annotation = f"_RustHandler[{tp.id}] | tuple[Callable[[{tp.id}], Any], Any] | Callable[[{tp.id}], Any] | None" 87 | for arg in (*node.args.args, *node.args.kwonlyargs): 88 | if arg.arg == "handler": 89 | arg.annotation = ast.parse(annotation) 90 | node.returns = node.returns.value 91 | # stringify all parameters and return annotation 92 | for arg in (*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs): 93 | if ann := arg.annotation: 94 | arg.annotation = ast.Constant(f"{ast.unparse(ann)}") 95 | if ret := node.returns: 96 | node.returns = ast.Constant(f"{ast.unparse(ret)}") 97 | return node 98 | 99 | 100 | def main(): 101 | # remove __init__.pyi 102 | __INIT__.unlink() 103 | # rename stubs 104 | for entry in PACKAGE.glob("*.pyi"): 105 | entry.rename(PACKAGE / f"{entry.stem}.py") 106 | # read stub code 107 | with open(__INIT__) as f: 108 | stub: ast.Module = ast.parse(f.read()) 109 | # replace _unstable 110 | for i, stmt in enumerate(stub.body): 111 | if isinstance(stmt, ast.FunctionDef) and stmt.name == "_unstable": 112 | stub.body[i] = ast.parse(inspect.getsource(_unstable)) 113 | # remove overload 114 | stub = RemoveOverload().visit(stub) 115 | # write modified code 116 | with open(__INIT__, "w") as f: 117 | f.write(ast.unparse(stub)) 118 | 119 | 120 | if __name__ == "__main__": 121 | main() 122 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Zenoh Python Examples 2 | 3 | ## Get Started 4 | 5 | ```bash 6 | python3 7 | ``` 8 | 9 | Each example accepts the `-h` or `--help` option that provides a description of its arguments and their default values. 10 | 11 | If you run the tests against the zenoh router running in a Docker container, you need to add the 12 | `-e tcp/localhost:7447` option to your examples. That's because Docker doesn't support UDP multicast 13 | transport, and therefore the zenoh scouting and discrovery mechanism cannot work with. 14 | 15 | ## Examples description 16 | 17 | ### z_scout 18 | 19 | Scouts for zenoh peers and routers available on the network. 20 | 21 | Typical usage: 22 | 23 | ```bash 24 | python3 z_scout.py 25 | ``` 26 | 27 | ### z_info 28 | 29 | Gets information about the Zenoh session. 30 | 31 | Typical usage: 32 | 33 | ```bash 34 | python3 z_info.py 35 | ``` 36 | 37 | ### z_put 38 | 39 | Puts a path/payload into Zenoh. 40 | The path/payload will be received by all matching subscribers, for instance the [z_sub](#z_sub) 41 | and [z_storage](#z_storage) examples. 42 | 43 | Typical usage: 44 | 45 | ```bash 46 | python3 z_put.py 47 | ``` 48 | 49 | or 50 | 51 | ```bash 52 | python3 z_put.py -k demo/example/test -p 'Hello World' 53 | ``` 54 | 55 | ### z_pub 56 | 57 | Declares a resource with a path and a publisher on this resource. Then puts a payload using the numerical resource id. 58 | The path/payload will be received by all matching subscribers, for instance the [z_sub](#z_sub) 59 | and [z_storage](#z_storage) examples. 60 | 61 | Typical usage: 62 | 63 | ```bash 64 | python3 z_pub.py 65 | ``` 66 | 67 | or 68 | 69 | ```bash 70 | python3 z_pub.py -k demo/example/test -p 'Hello World' 71 | ``` 72 | 73 | ### z_sub 74 | 75 | Creates a subscriber with a key expression. 76 | The subscriber will be notified of each put made on any key expression matching 77 | the subscriber's key expression, and will print this notification. 78 | 79 | Typical usage: 80 | 81 | ```bash 82 | python3 z_sub.py 83 | ``` 84 | 85 | or 86 | 87 | ```bash 88 | python3 z_sub.py -k 'demo/**' 89 | ``` 90 | 91 | ### z_get 92 | 93 | Sends a query message for a selector. 94 | The queryables with a matching path or selector (for instance [z_queryable](#z_queryable) and [z_storage](#z_storage)) 95 | will receive this query and reply with paths/payloads that will be received by the query callback. 96 | 97 | Typical usage: 98 | 99 | ```bash 100 | python3 z_get.py 101 | ``` 102 | 103 | or 104 | 105 | ```bash 106 | python3 z_get.py -s 'demo/**' 107 | ``` 108 | 109 | ### z_querier 110 | 111 | Continuously sends query messages for a selector. 112 | The queryables with a matching path or selector (for instance [z_queryable](#z_queryable) and [z_storage](#z_storage)) 113 | will receive these queries and reply with paths/payloads that will be received by the querier's query callback. 114 | 115 | Typical usage: 116 | 117 | ```bash 118 | python3 z_querier.py 119 | ``` 120 | 121 | or 122 | 123 | ```bash 124 | python3 z_get.py -s 'demo/**' 125 | ``` 126 | 127 | ### z_queryable 128 | 129 | Creates a queryable function with a key expression. 130 | This queryable function will be triggered by each call to a get operation on zenoh 131 | with a selector that matches the key expression, and will return a payload to the querier. 132 | 133 | Typical usage: 134 | 135 | ```bash 136 | python3 z_queryable.py 137 | ``` 138 | 139 | or 140 | 141 | ```bash 142 | python3 z_queryable.py -k demo/example/queryable -p 'This is the result' 143 | ``` 144 | 145 | ### z_storage 146 | 147 | Trivial implementation of a storage in memory. 148 | This examples creates a subscriber and a queryable on the same key expression. 149 | The subscriber callback will store the received key/values in an hashmap. 150 | The queryable callback will answer to queries with the key/values stored in the hashmap 151 | and that match the queried selector. 152 | 153 | Typical usage: 154 | 155 | ```bash 156 | python3 z_storage.py 157 | ``` 158 | 159 | or 160 | 161 | ```bash 162 | python3 z_storage.py -k 'demo/**' 163 | ``` 164 | 165 | ### z_pub_thr & z_sub_thr 166 | 167 | Pub/Sub throughput test. 168 | This example allows to perform throughput measurements between a pubisher performing 169 | put operations and a subscriber receiving notifications of those puts. 170 | 171 | Typical Subscriber usage: 172 | 173 | ```bash 174 | python3 z_sub_thr.py 175 | ``` 176 | 177 | Typical Publisher usage: 178 | 179 | ```bash 180 | python3 z_pub_thr.py 1024 181 | ``` 182 | -------------------------------------------------------------------------------- /examples/common/__init__.py: -------------------------------------------------------------------------------- 1 | from .common import * 2 | -------------------------------------------------------------------------------- /examples/common/common.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | 4 | import zenoh 5 | 6 | 7 | def add_config_arguments(parser: argparse.ArgumentParser): 8 | parser.add_argument( 9 | "--mode", 10 | "-m", 11 | dest="mode", 12 | choices=["peer", "client"], 13 | type=str, 14 | help="The zenoh session mode.", 15 | ) 16 | parser.add_argument( 17 | "--connect", 18 | "-e", 19 | dest="connect", 20 | metavar="ENDPOINT", 21 | action="append", 22 | type=str, 23 | help="Endpoints to connect to.", 24 | ) 25 | parser.add_argument( 26 | "--listen", 27 | "-l", 28 | dest="listen", 29 | metavar="ENDPOINT", 30 | action="append", 31 | type=str, 32 | help="Endpoints to listen on.", 33 | ) 34 | parser.add_argument( 35 | "--config", 36 | "-c", 37 | dest="config", 38 | metavar="FILE", 39 | type=str, 40 | help="A configuration file.", 41 | ) 42 | parser.add_argument( 43 | "--no-multicast-scouting", 44 | dest="no_multicast_scouting", 45 | default=False, 46 | action="store_true", 47 | help="Disable multicast scouting.", 48 | ) 49 | parser.add_argument( 50 | "--cfg", 51 | dest="cfg", 52 | metavar="CFG", 53 | default=[], 54 | action="append", 55 | type=str, 56 | help="Allows arbitrary configuration changes as column-separated KEY:VALUE pairs. Where KEY must be a valid config path and VALUE must be a valid JSON5 string that can be deserialized to the expected type for the KEY field. Example: --cfg='transport/unicast/max_links:2'.", 57 | ) 58 | 59 | 60 | def get_config_from_args(args) -> zenoh.Config: 61 | conf = ( 62 | zenoh.Config.from_file(args.config) 63 | if args.config is not None 64 | else zenoh.Config() 65 | ) 66 | if args.mode is not None: 67 | conf.insert_json5("mode", json.dumps(args.mode)) 68 | if args.connect is not None: 69 | conf.insert_json5("connect/endpoints", json.dumps(args.connect)) 70 | if args.listen is not None: 71 | conf.insert_json5("listen/endpoints", json.dumps(args.listen)) 72 | if args.no_multicast_scouting: 73 | conf.insert_json5("scouting/multicast/enabled", json.dumps(False)) 74 | 75 | for c in args.cfg: 76 | try: 77 | [key, value] = c.split(":", 1) 78 | except: 79 | print(f"`--cfg` argument: expected KEY:VALUE pair, got {c}") 80 | raise 81 | conf.insert_json5(key, value) 82 | 83 | return conf 84 | -------------------------------------------------------------------------------- /examples/entity.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Entity { 4 | uint32 id = 1; 5 | string name = 2; 6 | } 7 | -------------------------------------------------------------------------------- /examples/z_bytes.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | from zenoh import ZBytes 15 | 16 | 17 | def main(): 18 | # Raw bytes 19 | input = b"raw bytes" 20 | payload = ZBytes(input) 21 | output = payload.to_bytes() # equivalent to `bytes(payload)` 22 | assert input == output 23 | # Corresponding encoding to be used in operations like `.put()`, `.reply()`, etc. 24 | # encoding = Encoding.ZENOH_BYTES; 25 | 26 | # Raw utf8 bytes, i.e. string 27 | input = "raw bytes" 28 | payload = ZBytes(input) 29 | output = payload.to_string() # equivalent to `str(payload)` 30 | assert input == output 31 | # Corresponding encoding to be used in operations like `.put()`, `.reply()`, etc. 32 | # encoding = Encoding.ZENOH_STRING; 33 | 34 | # JSON 35 | import json 36 | 37 | input = {"name": "John Doe", "age": 43, "phones": ["+44 1234567", "+44 2345678"]} 38 | payload = ZBytes(json.dumps(input)) 39 | output = json.loads(payload.to_string()) 40 | assert input == output 41 | # Corresponding encoding to be used in operations like `.put()`, `.reply()`, etc. 42 | # encoding = Encoding.APPLICATION_JSON; 43 | 44 | # Protobuf 45 | try: 46 | import entity_pb2 47 | 48 | input = entity_pb2.Entity(id=1234, name="John Doe") 49 | payload = ZBytes(input.SerializeToString()) 50 | output = entity_pb2.Entity() 51 | output.ParseFromString(payload.to_bytes()) 52 | assert input == output 53 | # Corresponding encoding to be used in operations like `.put()`, `.reply()`, etc. 54 | # encoding = Encoding.APPLICATION_PROTOBUF; 55 | except ImportError: 56 | # You must install protobuf and generate the protobuf classes from the schema with 57 | # $ pip install protobuf 58 | # $ protoc --python_out=. --pyi_out=. examples/entity.proto 59 | pass 60 | 61 | # zenoh.ext serialization 62 | from zenoh.ext import UInt32, z_deserialize, z_serialize 63 | 64 | if True: 65 | # Numeric: UInt8, UInt16, Uint32, UInt64, UInt128, Int8, Int16, Int32, Int64, 66 | # Int128, int (handled as int32), Float32, Float64, float (handled as Float64) 67 | input = UInt32(1234) 68 | payload = z_serialize(input) 69 | output = z_deserialize(UInt32, payload) 70 | assert input == output 71 | 72 | # list 73 | input = [0.0, 1.5, 42.0] # all items must have the same type 74 | payload = z_serialize(input) 75 | output = z_deserialize(list[float], payload) 76 | assert input == output 77 | 78 | # dict 79 | input = {0: "abc", 1: "def"} 80 | payload = z_serialize(input) 81 | output = z_deserialize(dict[int, str], payload) 82 | assert input == output 83 | 84 | # tuple 85 | input = (0.42, "string") 86 | payload = z_serialize(input) 87 | output = z_deserialize(tuple[float, str], payload) 88 | assert input == output 89 | 90 | 91 | if __name__ == "__main__": 92 | main() 93 | -------------------------------------------------------------------------------- /examples/z_delete.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import zenoh 15 | 16 | 17 | def main(conf: zenoh.Config, key: str): 18 | # initiate logging 19 | zenoh.init_log_from_env_or("error") 20 | 21 | print("Opening session...") 22 | with zenoh.open(conf) as session: 23 | print(f"Deleting resources matching '{key}'...") 24 | session.delete(key) 25 | 26 | 27 | # --- Command line argument parsing --- --- --- --- --- --- 28 | if __name__ == "__main__": 29 | import argparse 30 | 31 | import common 32 | 33 | parser = argparse.ArgumentParser(prog="z_delete", description="zenoh put example") 34 | common.add_config_arguments(parser) 35 | parser.add_argument( 36 | "--key", 37 | "-k", 38 | dest="key", 39 | default="demo/example/zenoh-python-put", 40 | type=str, 41 | help="The key expression matching resources to delete.", 42 | ) 43 | 44 | args = parser.parse_args() 45 | conf = common.get_config_from_args(args) 46 | 47 | key = args.key 48 | 49 | main(conf, key) 50 | -------------------------------------------------------------------------------- /examples/z_get.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import zenoh 15 | 16 | 17 | def main( 18 | conf: zenoh.Config, 19 | selector: str, 20 | target: zenoh.QueryTarget, 21 | payload: str, 22 | timeout: float, 23 | ): 24 | # initiate logging 25 | zenoh.init_log_from_env_or("error") 26 | 27 | print("Opening session...") 28 | with zenoh.open(conf) as session: 29 | print(f"Sending Query '{selector}'...") 30 | replies = session.get(selector, target=target, payload=payload, timeout=timeout) 31 | for reply in replies: 32 | try: 33 | print( 34 | f">> Received ('{reply.ok.key_expr}': '{reply.ok.payload.to_string()}')" 35 | ) 36 | except: 37 | print(f">> Received (ERROR: '{reply.err.payload.to_string()}')") 38 | 39 | 40 | if __name__ == "__main__": 41 | # --- Command line argument parsing --- --- --- --- --- --- 42 | import argparse 43 | import json 44 | 45 | import common 46 | 47 | parser = argparse.ArgumentParser(prog="z_get", description="zenoh get example") 48 | common.add_config_arguments(parser) 49 | parser.add_argument( 50 | "--selector", 51 | "-s", 52 | dest="selector", 53 | default="demo/example/**", 54 | type=str, 55 | help="The selection of resources to query.", 56 | ) 57 | parser.add_argument( 58 | "--target", 59 | "-t", 60 | dest="target", 61 | choices=["ALL", "BEST_MATCHING", "ALL_COMPLETE", "NONE"], 62 | default="BEST_MATCHING", 63 | type=str, 64 | help="The target queryables of the query.", 65 | ) 66 | parser.add_argument( 67 | "--payload", 68 | "-p", 69 | dest="payload", 70 | type=str, 71 | help="An optional payload to send in the query.", 72 | ) 73 | parser.add_argument( 74 | "--timeout", 75 | "-o", 76 | dest="timeout", 77 | default=10.0, 78 | type=float, 79 | help="The query timeout", 80 | ) 81 | 82 | args = parser.parse_args() 83 | conf = common.get_config_from_args(args) 84 | 85 | target = { 86 | "ALL": zenoh.QueryTarget.ALL, 87 | "BEST_MATCHING": zenoh.QueryTarget.BEST_MATCHING, 88 | "ALL_COMPLETE": zenoh.QueryTarget.ALL_COMPLETE, 89 | }.get(args.target) 90 | 91 | main(conf, args.selector, target, args.payload, args.timeout) 92 | -------------------------------------------------------------------------------- /examples/z_get_liveliness.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import zenoh 15 | 16 | 17 | def main(conf: zenoh.Config, key: str, timeout: float): 18 | # initiate logging 19 | zenoh.init_log_from_env_or("error") 20 | 21 | print("Opening session...") 22 | with zenoh.open(conf) as session: 23 | print(f"Sending Liveliness Query '{key}'...") 24 | replies = session.liveliness().get(key, timeout=timeout) 25 | for reply in replies: 26 | try: 27 | print(f">> Alive token ('{reply.ok.key_expr}')") 28 | except: 29 | print(f">> Received (ERROR: '{reply.err.payload.to_string()}')") 30 | 31 | 32 | # --- Command line argument parsing --- --- --- --- --- --- 33 | if __name__ == "__main__": 34 | import argparse 35 | 36 | import common 37 | 38 | parser = argparse.ArgumentParser( 39 | prog="z_get_liveliness", description="zenoh put example" 40 | ) 41 | common.add_config_arguments(parser) 42 | parser.add_argument( 43 | "--key", 44 | "-k", 45 | dest="key", 46 | default="group1/**", 47 | type=str, 48 | help="The key expression to write.", 49 | ) 50 | parser.add_argument( 51 | "--timeout", 52 | "-o", 53 | dest="timeout", 54 | default=10.0, 55 | type=float, 56 | help="The query timeout.", 57 | ) 58 | 59 | args = parser.parse_args() 60 | conf = common.get_config_from_args(args) 61 | 62 | main(conf, args.key, args.timeout) 63 | -------------------------------------------------------------------------------- /examples/z_info.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import zenoh 15 | 16 | 17 | def main(conf: zenoh.Config): 18 | # initiate logging 19 | zenoh.init_log_from_env_or("error") 20 | 21 | print("Opening session...") 22 | with zenoh.open(conf) as session: 23 | info = session.info 24 | print(f"zid: {info.zid()}") 25 | print(f"routers: {info.routers_zid()}") 26 | print(f"peers: {info.peers_zid()}") 27 | 28 | 29 | # --- Command line argument parsing --- --- --- --- --- --- 30 | if __name__ == "__main__": 31 | import argparse 32 | import json 33 | 34 | import common 35 | 36 | parser = argparse.ArgumentParser(prog="z_info", description="zenoh info example") 37 | common.add_config_arguments(parser) 38 | 39 | args = parser.parse_args() 40 | conf = common.get_config_from_args(args) 41 | 42 | main(conf) 43 | -------------------------------------------------------------------------------- /examples/z_liveliness.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import time 15 | 16 | import zenoh 17 | 18 | 19 | def main(conf: zenoh.Config, key: str): 20 | # initiate logging 21 | zenoh.init_log_from_env_or("error") 22 | 23 | print("Opening session...") 24 | with zenoh.open(conf) as session: 25 | print(f"Declaring LivelinessToken on '{key}'...") 26 | with session.liveliness().declare_token(key) as token: 27 | print("Press CTRL-C to quit...") 28 | while True: 29 | time.sleep(1) 30 | 31 | 32 | # --- Command line argument parsing --- --- --- --- --- --- 33 | if __name__ == "__main__": 34 | import argparse 35 | 36 | import common 37 | 38 | parser = argparse.ArgumentParser( 39 | prog="z_liveliness", description="zenoh put example" 40 | ) 41 | common.add_config_arguments(parser) 42 | parser.add_argument( 43 | "--key", 44 | "-k", 45 | dest="key", 46 | default="group1/zenoh-py", 47 | type=str, 48 | help="The key expression to write.", 49 | ) 50 | 51 | args = parser.parse_args() 52 | conf = common.get_config_from_args(args) 53 | 54 | main(conf, args.key) 55 | -------------------------------------------------------------------------------- /examples/z_ping.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import time 15 | 16 | import zenoh 17 | 18 | 19 | def main(conf: zenoh.Config, payload_size: int, warmup: int, samples: int): 20 | # initiate logging 21 | zenoh.init_log_from_env_or("error") 22 | 23 | print("Opening session...") 24 | with zenoh.open(conf) as session: 25 | sub = session.declare_subscriber("test/pong") 26 | pub = session.declare_publisher( 27 | "test/ping", congestion_control=zenoh.CongestionControl.BLOCK 28 | ) 29 | data = bytes(i % 10 for i in range(0, payload_size)) 30 | 31 | print(f"Warming up for {warmup}...") 32 | warmup_end = time.time() + warmup 33 | while time.time() < warmup_end: 34 | pub.put(data) 35 | sub.recv() 36 | 37 | sample_list = [] 38 | for i in range(samples): 39 | write_time = time.time() 40 | pub.put(data) 41 | sub.recv() 42 | sample_list.append(round((time.time() - write_time) * 1_000_000)) 43 | 44 | for i, rtt in enumerate(sample_list): 45 | print(f"{payload_size} bytes: seq={i} rtt={rtt}µs lat={rtt / 2}µs") 46 | 47 | 48 | # --- Command line argument parsing --- --- --- --- --- --- 49 | if __name__ == "__main__": 50 | import argparse 51 | 52 | import common 53 | 54 | parser = argparse.ArgumentParser(prog="z_ping", description="zenoh get example") 55 | common.add_config_arguments(parser) 56 | parser.add_argument( 57 | "--warmup", 58 | "-w", 59 | dest="warmup", 60 | metavar="WARMUP", 61 | type=float, 62 | default=1.0, 63 | help="The number of seconds to warmup (float)", 64 | ) 65 | parser.add_argument( 66 | "--samples", 67 | "-n", 68 | dest="samples", 69 | metavar="SAMPLES", 70 | type=int, 71 | default=100, 72 | help="The number of round-trip to measure", 73 | ) 74 | parser.add_argument( 75 | "payload_size", 76 | metavar="PAYLOAD_SIZE", 77 | type=int, 78 | help="Sets the size of the payload to publish.", 79 | ) 80 | 81 | args = parser.parse_args() 82 | conf = common.get_config_from_args(args) 83 | 84 | main(conf, args.payload_size, args.warmup, args.samples) 85 | -------------------------------------------------------------------------------- /examples/z_pong.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import time 15 | 16 | import zenoh 17 | 18 | 19 | def main(conf: zenoh.Config, express: bool): 20 | # initiate logging 21 | zenoh.init_log_from_env_or("error") 22 | 23 | print("Opening session...") 24 | with zenoh.open(conf) as session: 25 | pub = session.declare_publisher( 26 | "test/pong", 27 | congestion_control=zenoh.CongestionControl.BLOCK, 28 | express=express, 29 | ) 30 | session.declare_subscriber("test/ping", lambda s: pub.put(s.payload)) 31 | 32 | print("Press CTRL-C to quit...") 33 | while True: 34 | time.sleep(1) 35 | 36 | 37 | # --- Command line argument parsing --- --- --- --- --- --- 38 | if __name__ == "__main__": 39 | import argparse 40 | 41 | import common 42 | 43 | parser = argparse.ArgumentParser(prog="z_pong", description="zenoh get example") 44 | common.add_config_arguments(parser) 45 | parser.add_argument( 46 | "--express", 47 | dest="express", 48 | metavar="EXPRESS", 49 | type=bool, 50 | default=False, 51 | help="Express publishing", 52 | ) 53 | 54 | args = parser.parse_args() 55 | conf = common.get_config_from_args(args) 56 | 57 | main(conf, args.express) 58 | -------------------------------------------------------------------------------- /examples/z_pub.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import time 15 | from typing import Optional 16 | 17 | import zenoh 18 | 19 | 20 | def main( 21 | conf: zenoh.Config, key: str, payload: str, iter: Optional[int], interval: int 22 | ): 23 | # initiate logging 24 | zenoh.init_log_from_env_or("error") 25 | 26 | print("Opening session...") 27 | with zenoh.open(conf) as session: 28 | print(f"Declaring Publisher on '{key}'...") 29 | pub = session.declare_publisher(key) 30 | 31 | print("Press CTRL-C to quit...") 32 | for idx in itertools.count() if iter is None else range(iter): 33 | time.sleep(interval) 34 | buf = f"[{idx:4d}] {payload}" 35 | print(f"Putting Data ('{key}': '{buf}')...") 36 | pub.put(buf) 37 | 38 | 39 | # --- Command line argument parsing --- --- --- --- --- --- 40 | if __name__ == "__main__": 41 | import argparse 42 | import itertools 43 | 44 | import common 45 | 46 | parser = argparse.ArgumentParser(prog="z_pub", description="zenoh pub example") 47 | common.add_config_arguments(parser) 48 | parser.add_argument( 49 | "--key", 50 | "-k", 51 | dest="key", 52 | default="demo/example/zenoh-python-pub", 53 | type=str, 54 | help="The key expression to publish onto.", 55 | ) 56 | parser.add_argument( 57 | "--payload", 58 | "-p", 59 | dest="payload", 60 | default="Pub from Python!", 61 | type=str, 62 | help="The payload to publish.", 63 | ) 64 | parser.add_argument( 65 | "--iter", dest="iter", type=int, help="How many puts to perform" 66 | ) 67 | parser.add_argument( 68 | "--interval", 69 | dest="interval", 70 | type=float, 71 | default=1.0, 72 | help="Interval between each put", 73 | ) 74 | 75 | args = parser.parse_args() 76 | conf = common.get_config_from_args(args) 77 | 78 | main(conf, args.key, args.payload, args.iter, args.interval) 79 | -------------------------------------------------------------------------------- /examples/z_pub_thr.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import zenoh 15 | 16 | 17 | def main(conf: zenoh.Config, payload_size: int): 18 | # initiate logging 19 | zenoh.init_log_from_env_or("error") 20 | 21 | data = bytearray() 22 | for i in range(0, payload_size): 23 | data.append(i % 10) 24 | data = zenoh.ZBytes(data) 25 | congestion_control = zenoh.CongestionControl.BLOCK 26 | 27 | with zenoh.open(conf) as session: 28 | pub = session.declare_publisher( 29 | "test/thr", congestion_control=congestion_control 30 | ) 31 | 32 | print("Press CTRL-C to quit...") 33 | while True: 34 | pub.put(data) 35 | 36 | 37 | # --- Command line argument parsing --- --- --- --- --- --- 38 | if __name__ == "__main__": 39 | import argparse 40 | import json 41 | 42 | import common 43 | 44 | parser = argparse.ArgumentParser( 45 | prog="z_pub_thr", description="zenoh throughput pub example" 46 | ) 47 | common.add_config_arguments(parser) 48 | parser.add_argument( 49 | "payload_size", type=int, help="Sets the size of the payload to publish." 50 | ) 51 | 52 | args = parser.parse_args() 53 | conf = common.get_config_from_args(args) 54 | 55 | main(conf, args.payload_size) 56 | -------------------------------------------------------------------------------- /examples/z_pull.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import time 15 | 16 | import zenoh 17 | 18 | 19 | def main(conf: zenoh.Config, key: str, size: int, interval: int): 20 | # initiate logging 21 | zenoh.init_log_from_env_or("error") 22 | 23 | print("Opening session...") 24 | with zenoh.open(conf) as session: 25 | print(f"Declaring Subscriber on '{key}'...") 26 | # Subscriber doesn't receive messages over the RingBuffer size. 27 | # The oldest message is overwritten by the latest one. 28 | sub = session.declare_subscriber(key, zenoh.handlers.RingChannel(size)) 29 | 30 | print("Press CTRL-C to quit...") 31 | while True: 32 | time.sleep(interval) 33 | while True: 34 | sample = sub.try_recv() 35 | if sample is None: 36 | break 37 | print( 38 | f">> [Subscriber] Received {sample.kind} ('{sample.key_expr}': '{sample.payload.to_string()}')" 39 | ) 40 | 41 | 42 | # --- Command line argument parsing --- --- --- --- --- --- 43 | if __name__ == "__main__": 44 | import argparse 45 | 46 | import common 47 | 48 | parser = argparse.ArgumentParser(prog="z_pull", description="zenoh pull example") 49 | common.add_config_arguments(parser) 50 | parser.add_argument( 51 | "--key", 52 | "-k", 53 | dest="key", 54 | default="demo/example/**", 55 | type=str, 56 | help="The key expression matching resources to pull.", 57 | ) 58 | parser.add_argument( 59 | "--size", dest="size", default=3, type=int, help="The size of the ringbuffer" 60 | ) 61 | parser.add_argument( 62 | "--interval", 63 | dest="interval", 64 | default=1.0, 65 | type=float, 66 | help="The interval for pulling the ringbuffer", 67 | ) 68 | 69 | args = parser.parse_args() 70 | conf = common.get_config_from_args(args) 71 | 72 | main(conf, args.key, args.size, args.interval) 73 | -------------------------------------------------------------------------------- /examples/z_put.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import zenoh 15 | 16 | 17 | def main(conf: zenoh.Config, key: str, payload: str): 18 | # initiate logging 19 | zenoh.init_log_from_env_or("error") 20 | 21 | print("Opening session...") 22 | with zenoh.open(conf) as session: 23 | print(f"Putting Data ('{key}': '{payload}')...") 24 | # Refer to z_bytes.py to see how to serialize different types of message 25 | session.put(key, payload) 26 | 27 | 28 | # --- Command line argument parsing --- --- --- --- --- --- 29 | if __name__ == "__main__": 30 | import argparse 31 | 32 | import common 33 | 34 | parser = argparse.ArgumentParser(prog="z_put", description="zenoh put example") 35 | common.add_config_arguments(parser) 36 | parser.add_argument( 37 | "--key", 38 | "-k", 39 | dest="key", 40 | default="demo/example/zenoh-python-put", 41 | type=str, 42 | help="The key expression to write.", 43 | ) 44 | parser.add_argument( 45 | "--payload", 46 | "-p", 47 | dest="payload", 48 | default="Put from Python!", 49 | type=str, 50 | help="The payload to write.", 51 | ) 52 | 53 | args = parser.parse_args() 54 | conf = common.get_config_from_args(args) 55 | 56 | main(conf, args.key, args.payload) 57 | -------------------------------------------------------------------------------- /examples/z_put_float.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import zenoh 15 | from zenoh.ext import z_serialize 16 | 17 | 18 | def main(conf: zenoh.Config, key: str, payload: float): 19 | # initiate logging 20 | zenoh.init_log_from_env_or("error") 21 | 22 | print("Opening session...") 23 | with zenoh.open(conf) as session: 24 | print(f"Putting Data ('{key}': '{payload}')...") 25 | # Refer to z_bytes.py to see how to serialize different types of message 26 | session.put(key, z_serialize(payload)) 27 | 28 | 29 | # --- Command line argument parsing --- --- --- --- --- --- 30 | if __name__ == "__main__": 31 | import argparse 32 | 33 | import common 34 | 35 | parser = argparse.ArgumentParser( 36 | prog="z_put_float", description="zenoh put example" 37 | ) 38 | common.add_config_arguments(parser) 39 | parser.add_argument( 40 | "--key", 41 | "-k", 42 | dest="key", 43 | default="demo/example/zenoh-python-put", 44 | type=str, 45 | help="The key expression to write.", 46 | ) 47 | parser.add_argument( 48 | "--payload", 49 | "-p", 50 | dest="payload", 51 | default=42.0, 52 | type=float, 53 | help="The payload to write.", 54 | ) 55 | 56 | args = parser.parse_args() 57 | conf = common.get_config_from_args(args) 58 | 59 | main(conf, args.key, args.payload) 60 | -------------------------------------------------------------------------------- /examples/z_querier.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import itertools 15 | import time 16 | from typing import Optional, Tuple 17 | 18 | import zenoh 19 | 20 | 21 | def main( 22 | conf: zenoh.Config, 23 | selector: str, 24 | target: zenoh.QueryTarget, 25 | payload: str, 26 | timeout: float, 27 | iter: Optional[int], 28 | ): 29 | # initiate logging 30 | zenoh.init_log_from_env_or("error") 31 | print("Opening session...") 32 | with zenoh.open(conf) as session: 33 | query_selector = zenoh.Selector(selector) 34 | 35 | print(f"Declaring Querier on '{query_selector.key_expr}'...") 36 | querier = session.declare_querier( 37 | query_selector.key_expr, target=target, timeout=timeout 38 | ) 39 | 40 | print("Press CTRL-C to quit...") 41 | for idx in itertools.count() if iter is None else range(iter): 42 | time.sleep(1.0) 43 | buf = f"[{idx:4d}] {payload if payload else ''}" 44 | print(f"Querying '{selector}' with payload '{buf}')...") 45 | 46 | replies = querier.get(parameters=query_selector.parameters, payload=buf) 47 | for reply in replies: 48 | try: 49 | print( 50 | f">> Received ('{reply.ok.key_expr}': '{reply.ok.payload.to_string()}')" 51 | ) 52 | except: 53 | print(f">> Received (ERROR: '{reply.err.payload.to_string()}')") 54 | 55 | 56 | if __name__ == "__main__": 57 | # --- Command line argument parsing --- --- --- --- --- --- 58 | import argparse 59 | import json 60 | 61 | import common 62 | 63 | parser = argparse.ArgumentParser( 64 | prog="z_querier", description="zenoh querier example" 65 | ) 66 | common.add_config_arguments(parser) 67 | parser.add_argument( 68 | "--selector", 69 | "-s", 70 | dest="selector", 71 | default="demo/example/**", 72 | type=str, 73 | help="The selection of resources to query.", 74 | ) 75 | parser.add_argument( 76 | "--target", 77 | "-t", 78 | dest="target", 79 | choices=["ALL", "BEST_MATCHING", "ALL_COMPLETE", "NONE"], 80 | default="BEST_MATCHING", 81 | type=str, 82 | help="The target queryables of the query.", 83 | ) 84 | parser.add_argument( 85 | "--payload", 86 | "-p", 87 | dest="payload", 88 | type=str, 89 | help="An optional payload to send in the query.", 90 | ) 91 | parser.add_argument( 92 | "--timeout", 93 | "-o", 94 | dest="timeout", 95 | default=10.0, 96 | type=float, 97 | help="The query timeout", 98 | ) 99 | parser.add_argument( 100 | "--iter", dest="iter", type=int, help="How many gets to perform" 101 | ) 102 | 103 | args = parser.parse_args() 104 | conf = common.get_config_from_args(args) 105 | 106 | target = { 107 | "ALL": zenoh.QueryTarget.ALL, 108 | "BEST_MATCHING": zenoh.QueryTarget.BEST_MATCHING, 109 | "ALL_COMPLETE": zenoh.QueryTarget.ALL_COMPLETE, 110 | }.get(args.target) 111 | 112 | main(conf, args.selector, target, args.payload, args.timeout, args.iter) 113 | -------------------------------------------------------------------------------- /examples/z_queryable.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import time 15 | 16 | import zenoh 17 | 18 | 19 | def main(conf: zenoh.Config, key: str, payload: str, complete: bool): 20 | # initiate logging 21 | zenoh.init_log_from_env_or("error") 22 | 23 | print("Opening session...") 24 | with zenoh.open(conf) as session: 25 | print(f"Declaring Queryable on '{key}'...") 26 | queryable = session.declare_queryable(key, complete=complete) 27 | 28 | print("Press CTRL-C to quit...") 29 | while True: 30 | with queryable.recv() as query: 31 | if query.payload is not None: 32 | print( 33 | f">> [Queryable ] Received Query '{query.selector}'" 34 | f" with payload: '{query.payload.to_string()}'" 35 | ) 36 | else: 37 | print(f">> [Queryable ] Received Query '{query.selector}'") 38 | query.reply(key, payload) 39 | # it's possible to call `query.drop()` after handling it 40 | # instead of using a context manager 41 | 42 | 43 | # --- Command line argument parsing --- --- --- --- --- --- 44 | if __name__ == "__main__": 45 | import argparse 46 | import json 47 | 48 | import common 49 | 50 | parser = argparse.ArgumentParser( 51 | prog="z_queryable", description="zenoh queryable example" 52 | ) 53 | common.add_config_arguments(parser) 54 | parser.add_argument( 55 | "--key", 56 | "-k", 57 | dest="key", 58 | default="demo/example/zenoh-python-queryable", 59 | type=str, 60 | help="The key expression matching queries to reply to.", 61 | ) 62 | parser.add_argument( 63 | "--payload", 64 | "-p", 65 | dest="payload", 66 | default="Queryable from Python!", 67 | type=str, 68 | help="The payload to reply to queries.", 69 | ) 70 | parser.add_argument( 71 | "--complete", 72 | dest="complete", 73 | default=False, 74 | action="store_true", 75 | help="Declare the queryable as complete w.r.t. the key expression.", 76 | ) 77 | 78 | args = parser.parse_args() 79 | conf = common.get_config_from_args(args) 80 | 81 | main(conf, args.key, args.payload, args.complete) 82 | -------------------------------------------------------------------------------- /examples/z_scout.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import threading 15 | 16 | import zenoh 17 | 18 | 19 | def main(): 20 | # initiate logging 21 | zenoh.init_log_from_env_or("error") 22 | 23 | print("Scouting...") 24 | scout = zenoh.scout(what="peer|router") 25 | threading.Timer(1.0, lambda: scout.stop()).start() 26 | 27 | for hello in scout: 28 | print(hello) 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /examples/z_storage.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import time 15 | 16 | import zenoh 17 | 18 | store = {} 19 | 20 | 21 | def listener(sample: zenoh.Sample): 22 | print( 23 | f">> [Subscriber] Received {sample.kind} ('{sample.key_expr}': '{sample.payload.to_string()}')" 24 | ) 25 | if sample.kind == zenoh.SampleKind.DELETE: 26 | store.pop(sample.key_expr, None) 27 | else: 28 | store[sample.key_expr] = sample 29 | 30 | 31 | def query_handler(query: zenoh.Query): 32 | print(f">> [Queryable ] Received Query '{query.selector}'") 33 | for stored_name, sample in store.items(): 34 | if query.key_expr.intersects(stored_name): 35 | query.reply( 36 | sample.key_expr, 37 | sample.payload, 38 | encoding=sample.encoding, 39 | congestion_control=sample.congestion_control, 40 | priority=sample.priority, 41 | express=sample.express, 42 | ) 43 | 44 | 45 | def main(conf: zenoh.Config, key: str, complete: bool): 46 | # initiate logging 47 | zenoh.init_log_from_env_or("error") 48 | 49 | print("Opening session...") 50 | with zenoh.open(conf) as session: 51 | print(f"Declaring Subscriber on '{key}'...") 52 | session.declare_subscriber(key, listener) 53 | 54 | print(f"Declaring Queryable on '{key}'...") 55 | session.declare_queryable(key, query_handler, complete=complete) 56 | 57 | print("Press CTRL-C to quit...") 58 | while True: 59 | time.sleep(1) 60 | 61 | 62 | # --- Command line argument parsing --- --- --- --- --- --- 63 | if __name__ == "__main__": 64 | import argparse 65 | import json 66 | 67 | import common 68 | 69 | parser = argparse.ArgumentParser( 70 | prog="z_storage", description="zenoh storage example" 71 | ) 72 | common.add_config_arguments(parser) 73 | parser.add_argument( 74 | "--key", 75 | "-k", 76 | dest="key", 77 | default="demo/example/**", 78 | type=str, 79 | help="The key expression matching resources to store.", 80 | ) 81 | parser.add_argument( 82 | "--complete", 83 | dest="complete", 84 | default=False, 85 | action="store_true", 86 | help="Declare the storage as complete w.r.t. the key expression.", 87 | ) 88 | 89 | args = parser.parse_args() 90 | conf = common.get_config_from_args(args) 91 | 92 | main(conf, args.key, args.complete) 93 | -------------------------------------------------------------------------------- /examples/z_sub.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import time 15 | 16 | import zenoh 17 | 18 | 19 | def main(conf: zenoh.Config, key: str): 20 | # initiate logging 21 | zenoh.init_log_from_env_or("error") 22 | 23 | print("Opening session...") 24 | with zenoh.open(conf) as session: 25 | print(f"Declaring Subscriber on '{key}'...") 26 | 27 | def listener(sample: zenoh.Sample): 28 | print( 29 | f">> [Subscriber] Received {sample.kind} ('{sample.key_expr}': '{sample.payload.to_string()}')" 30 | ) 31 | 32 | session.declare_subscriber(key, listener) 33 | 34 | print("Press CTRL-C to quit...") 35 | while True: 36 | time.sleep(1) 37 | 38 | 39 | # --- Command line argument parsing --- --- --- --- --- --- 40 | if __name__ == "__main__": 41 | import argparse 42 | 43 | import common 44 | 45 | parser = argparse.ArgumentParser(prog="z_sub", description="zenoh sub example") 46 | common.add_config_arguments(parser) 47 | parser.add_argument( 48 | "--key", 49 | "-k", 50 | dest="key", 51 | default="demo/example/**", 52 | type=str, 53 | help="The key expression to subscribe to.", 54 | ) 55 | 56 | args = parser.parse_args() 57 | conf = common.get_config_from_args(args) 58 | 59 | main(conf, args.key) 60 | -------------------------------------------------------------------------------- /examples/z_sub_liveliness.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import zenoh 15 | 16 | 17 | def main(conf: zenoh.Config, key: str, history: bool): 18 | # initiate logging 19 | zenoh.init_log_from_env_or("error") 20 | 21 | print("Opening session...") 22 | with zenoh.open(conf) as session: 23 | print(f"Declaring Liveliness Subscriber on '{key}'...") 24 | with session.liveliness().declare_subscriber(key, history=history) as sub: 25 | for sample in sub: 26 | if sample.kind == zenoh.SampleKind.PUT: 27 | print( 28 | f">> [LivelinessSubscriber] New alive token ('{sample.key_expr}')" 29 | ) 30 | elif sample.kind == zenoh.SampleKind.DELETE: 31 | print( 32 | f">> [LivelinessSubscriber] Dropped token ('{sample.key_expr}')" 33 | ) 34 | 35 | 36 | # --- Command line argument parsing --- --- --- --- --- --- 37 | if __name__ == "__main__": 38 | import argparse 39 | 40 | import common 41 | 42 | parser = argparse.ArgumentParser( 43 | prog="z_sub_liveliness", description="zenoh sub example" 44 | ) 45 | common.add_config_arguments(parser) 46 | parser.add_argument( 47 | "--key", 48 | "-k", 49 | dest="key", 50 | default="group1/**", 51 | type=str, 52 | help="The key expression to subscribe to.", 53 | ) 54 | parser.add_argument( 55 | "--history", 56 | dest="history", 57 | default=False, 58 | type=bool, 59 | help="Get historical liveliness tokens.", 60 | ) 61 | 62 | args = parser.parse_args() 63 | conf = common.get_config_from_args(args) 64 | 65 | main(conf, args.key, args.history) 66 | -------------------------------------------------------------------------------- /examples/z_sub_queued.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import zenoh 15 | 16 | 17 | def main(conf: zenoh.Config, key: str): 18 | # initiate logging 19 | zenoh.init_log_from_env_or("error") 20 | 21 | print("Opening session...") 22 | with zenoh.open(conf) as session: 23 | print(f"Declaring Subscriber on '{key}'...") 24 | with session.declare_subscriber(key) as sub: 25 | print("Press CTRL-C to quit...") 26 | for sample in sub: 27 | print( 28 | f">> [Subscriber] Received {sample.kind} ('{sample.key_expr}': '{sample.payload.to_string()}')" 29 | ) 30 | 31 | 32 | # --- Command line argument parsing --- --- --- --- --- --- 33 | if __name__ == "__main__": 34 | import argparse 35 | import json 36 | 37 | import common 38 | 39 | parser = argparse.ArgumentParser( 40 | prog="z_sub_queued", description="zenoh sub example" 41 | ) 42 | common.add_config_arguments(parser) 43 | parser.add_argument( 44 | "--key", 45 | "-k", 46 | dest="key", 47 | default="demo/example/**", 48 | type=str, 49 | help="The key expression to subscribe to.", 50 | ) 51 | 52 | args = parser.parse_args() 53 | conf = common.get_config_from_args(args) 54 | 55 | main(conf, args.key) 56 | -------------------------------------------------------------------------------- /examples/z_sub_thr.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2022 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import time 15 | 16 | import zenoh 17 | 18 | batch_count = 0 19 | count = 0 20 | start = None 21 | global_start = None 22 | 23 | 24 | def main(conf: zenoh.Config, number: int): 25 | def listener(_sample: zenoh.Sample): 26 | global count, batch_count, start, global_start 27 | if count == 0: 28 | start = time.time() 29 | if global_start is None: 30 | global_start = start 31 | count += 1 32 | elif count < number: 33 | count += 1 34 | else: 35 | stop = time.time() 36 | print(f"{number / (stop - start):.6f} msgs/sec") 37 | batch_count += 1 38 | count = 0 39 | 40 | def report(): 41 | assert global_start is not None 42 | end = time.time() 43 | total = batch_count * number + count 44 | print( 45 | f"Received {total} messages in {end - global_start}: averaged {total / (end - global_start):.6f} msgs/sec" 46 | ) 47 | 48 | # initiate logging 49 | zenoh.init_log_from_env_or("error") 50 | 51 | with zenoh.open(conf) as session: 52 | session.declare_subscriber( 53 | "test/thr", zenoh.handlers.Callback(listener, report) 54 | ) 55 | 56 | print("Press CTRL-C to quit...") 57 | while True: 58 | time.sleep(1) 59 | 60 | 61 | if __name__ == "__main__": 62 | # --- Command line argument parsing --- --- --- --- --- --- 63 | import argparse 64 | import json 65 | 66 | import common 67 | 68 | parser = argparse.ArgumentParser( 69 | prog="z_sub_thr", description="zenoh throughput sub example" 70 | ) 71 | common.add_config_arguments(parser) 72 | parser.add_argument( 73 | "--number", 74 | "-n", 75 | dest="number", 76 | default=50000, 77 | metavar="NUMBER", 78 | type=int, 79 | help="Number of messages in each throughput measurements.", 80 | ) 81 | 82 | args = parser.parse_args() 83 | conf = common.get_config_from_args(args) 84 | 85 | main(conf, args.number) 86 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # Copyright (c) 2022 ZettaScale Technology 4 | # 5 | # This program and the accompanying materials are made available under the 6 | # terms of the Eclipse Public License 2.0 which is available at 7 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | # 10 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | # 12 | # Contributors: 13 | # ZettaScale Zenoh Team, 14 | # 15 | # 16 | 17 | [build-system] 18 | requires = ["maturin>1"] 19 | build-backend = "maturin" 20 | 21 | [project] 22 | name = "eclipse-zenoh" 23 | version = "1.4.0" 24 | description = "The python API for Eclipse zenoh" 25 | requires-python = ">=3.8" 26 | authors = [ 27 | { name = "ZettaScale Zenoh team", email = "zenoh@zettascale.tech" } 28 | ] 29 | classifiers = [ 30 | "Programming Language :: Python :: 3 :: Only", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Programming Language :: Python :: 3.10", 34 | "Programming Language :: Python :: 3.11", 35 | "Programming Language :: Python :: 3.12", 36 | "Programming Language :: Rust", 37 | "Intended Audience :: Developers", 38 | "Development Status :: 4 - Beta", 39 | "Topic :: System :: Networking", 40 | "License :: OSI Approved :: Apache Software License", 41 | "License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)", 42 | "Operating System :: POSIX :: Linux", 43 | "Operating System :: MacOS :: MacOS X", 44 | "Operating System :: Microsoft :: Windows", 45 | ] 46 | 47 | [project.urls] 48 | "Bug Tracker" = "https://github.com/eclipse-zenoh/zenoh-python/issues" 49 | "Source Code" = "https://github.com/eclipse-zenoh/zenoh-python" 50 | "Documentation" = "https://readthedocs.org/projects/zenoh-python/" 51 | 52 | [tool.isort] 53 | profile = "black" 54 | 55 | [package] 56 | version = "1.0.0-dev" 57 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | maturin>1 2 | fixtures 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85.0" 3 | -------------------------------------------------------------------------------- /src/bytes.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | // 3 | // Copyright (c) 2024 ZettaScale Technology 4 | // 5 | // This program and the accompanying materials are made available under the 6 | // terms of the Eclipse Public License 2.0 which is available at 7 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 8 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 9 | // 10 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 11 | // 12 | // Contributors: 13 | // ZettaScale Zenoh Team, 14 | // 15 | use std::io::Read; 16 | 17 | use pyo3::{ 18 | exceptions::{PyTypeError, PyValueError}, 19 | prelude::*, 20 | types::{PyByteArray, PyBytes, PyString}, 21 | }; 22 | 23 | use crate::{ 24 | macros::{downcast_or_new, wrapper}, 25 | utils::{IntoPyResult, MapInto}, 26 | }; 27 | 28 | wrapper!(zenoh::bytes::ZBytes: Clone, Default); 29 | downcast_or_new!(ZBytes); 30 | 31 | #[pymethods] 32 | impl ZBytes { 33 | #[new] 34 | fn new(obj: Option<&Bound>) -> PyResult { 35 | let Some(obj) = obj else { 36 | return Ok(Self::default()); 37 | }; 38 | if let Ok(bytes) = obj.downcast::() { 39 | Ok(Self(bytes.to_vec().into())) 40 | } else if let Ok(bytes) = obj.downcast::() { 41 | Ok(Self(bytes.as_bytes().into())) 42 | } else if let Ok(string) = obj.downcast::() { 43 | Ok(Self(string.to_string().into())) 44 | } else { 45 | Err(PyTypeError::new_err(format!( 46 | "expected bytes/str type, found '{}'", 47 | obj.get_type().name().unwrap() 48 | ))) 49 | } 50 | } 51 | 52 | fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { 53 | // Not using `ZBytes::to_bytes` 54 | PyBytes::new_bound_with(py, self.0.len(), |bytes| { 55 | self.0.reader().read_exact(bytes).into_pyres() 56 | }) 57 | } 58 | 59 | fn to_string(&self) -> PyResult> { 60 | self.0 61 | .try_to_string() 62 | .map_err(|_| PyValueError::new_err("not an UTF8 error")) 63 | } 64 | 65 | fn __len__(&self) -> usize { 66 | self.0.len() 67 | } 68 | 69 | fn __bool__(&self) -> bool { 70 | !self.0.is_empty() 71 | } 72 | 73 | fn __bytes__<'py>(&self, py: Python<'py>) -> PyResult> { 74 | self.to_bytes(py) 75 | } 76 | 77 | fn __str__(&self) -> PyResult> { 78 | self.to_string() 79 | } 80 | 81 | fn __eq__(&self, other: &Bound) -> PyResult { 82 | Ok(self.0 == Self::from_py(other)?.0) 83 | } 84 | 85 | fn __hash__(&self, py: Python) -> PyResult { 86 | self.__bytes__(py)?.hash() 87 | } 88 | 89 | fn __repr__(&self) -> String { 90 | format!("{:?}", self.0) 91 | } 92 | } 93 | 94 | wrapper!(zenoh::bytes::Encoding: Clone, Default); 95 | downcast_or_new!(Encoding => Option); 96 | 97 | #[pymethods] 98 | impl Encoding { 99 | #[new] 100 | fn new(s: Option) -> PyResult { 101 | Ok(s.map_into().map(Self).unwrap_or_default()) 102 | } 103 | 104 | fn with_schema(&self, schema: String) -> Self { 105 | Self(self.0.clone().with_schema(schema)) 106 | } 107 | 108 | // Cannot use `#[pyo3(from_py_with = "...")]`, see https://github.com/PyO3/pyo3/issues/4113 109 | fn __eq__(&self, other: &Bound) -> PyResult { 110 | Ok(self.0 == Self::from_py(other)?.0) 111 | } 112 | 113 | fn __hash__(&self, py: Python) -> PyResult { 114 | PyString::new_bound(py, &self.__str__()).hash() 115 | } 116 | 117 | fn __repr__(&self) -> String { 118 | format!("{:?}", self.0) 119 | } 120 | 121 | fn __str__(&self) -> String { 122 | format!("{}", self.0) 123 | } 124 | 125 | #[classattr] 126 | const ZENOH_BYTES: Self = Self(zenoh::bytes::Encoding::ZENOH_BYTES); 127 | #[classattr] 128 | const ZENOH_STRING: Self = Self(zenoh::bytes::Encoding::ZENOH_STRING); 129 | #[classattr] 130 | const ZENOH_SERIALIZED: Self = Self(zenoh::bytes::Encoding::ZENOH_SERIALIZED); 131 | #[classattr] 132 | const APPLICATION_OCTET_STREAM: Self = Self(zenoh::bytes::Encoding::APPLICATION_OCTET_STREAM); 133 | #[classattr] 134 | const TEXT_PLAIN: Self = Self(zenoh::bytes::Encoding::TEXT_PLAIN); 135 | #[classattr] 136 | const APPLICATION_JSON: Self = Self(zenoh::bytes::Encoding::APPLICATION_JSON); 137 | #[classattr] 138 | const TEXT_JSON: Self = Self(zenoh::bytes::Encoding::TEXT_JSON); 139 | #[classattr] 140 | const APPLICATION_CDR: Self = Self(zenoh::bytes::Encoding::APPLICATION_CDR); 141 | #[classattr] 142 | const APPLICATION_CBOR: Self = Self(zenoh::bytes::Encoding::APPLICATION_CBOR); 143 | #[classattr] 144 | const APPLICATION_YAML: Self = Self(zenoh::bytes::Encoding::APPLICATION_YAML); 145 | #[classattr] 146 | const TEXT_YAML: Self = Self(zenoh::bytes::Encoding::TEXT_YAML); 147 | #[classattr] 148 | const TEXT_JSON5: Self = Self(zenoh::bytes::Encoding::TEXT_JSON5); 149 | #[classattr] 150 | const APPLICATION_PYTHON_SERIALIZED_OBJECT: Self = 151 | Self(zenoh::bytes::Encoding::APPLICATION_PYTHON_SERIALIZED_OBJECT); 152 | #[classattr] 153 | const APPLICATION_PROTOBUF: Self = Self(zenoh::bytes::Encoding::APPLICATION_PROTOBUF); 154 | #[classattr] 155 | const APPLICATION_JAVA_SERIALIZED_OBJECT: Self = 156 | Self(zenoh::bytes::Encoding::APPLICATION_JAVA_SERIALIZED_OBJECT); 157 | #[classattr] 158 | const APPLICATION_OPENMETRICS_TEXT: Self = 159 | Self(zenoh::bytes::Encoding::APPLICATION_OPENMETRICS_TEXT); 160 | #[classattr] 161 | const IMAGE_PNG: Self = Self(zenoh::bytes::Encoding::IMAGE_PNG); 162 | #[classattr] 163 | const IMAGE_JPEG: Self = Self(zenoh::bytes::Encoding::IMAGE_JPEG); 164 | #[classattr] 165 | const IMAGE_GIF: Self = Self(zenoh::bytes::Encoding::IMAGE_GIF); 166 | #[classattr] 167 | const IMAGE_BMP: Self = Self(zenoh::bytes::Encoding::IMAGE_BMP); 168 | #[classattr] 169 | const IMAGE_WEBP: Self = Self(zenoh::bytes::Encoding::IMAGE_WEBP); 170 | #[classattr] 171 | const APPLICATION_XML: Self = Self(zenoh::bytes::Encoding::APPLICATION_XML); 172 | #[classattr] 173 | const APPLICATION_X_WWW_FORM_URLENCODED: Self = 174 | Self(zenoh::bytes::Encoding::APPLICATION_X_WWW_FORM_URLENCODED); 175 | #[classattr] 176 | const TEXT_HTML: Self = Self(zenoh::bytes::Encoding::TEXT_HTML); 177 | #[classattr] 178 | const TEXT_XML: Self = Self(zenoh::bytes::Encoding::TEXT_XML); 179 | #[classattr] 180 | const TEXT_CSS: Self = Self(zenoh::bytes::Encoding::TEXT_CSS); 181 | #[classattr] 182 | const TEXT_JAVASCRIPT: Self = Self(zenoh::bytes::Encoding::TEXT_JAVASCRIPT); 183 | #[classattr] 184 | const TEXT_MARKDOWN: Self = Self(zenoh::bytes::Encoding::TEXT_MARKDOWN); 185 | #[classattr] 186 | const TEXT_CSV: Self = Self(zenoh::bytes::Encoding::TEXT_CSV); 187 | #[classattr] 188 | const APPLICATION_SQL: Self = Self(zenoh::bytes::Encoding::APPLICATION_SQL); 189 | #[classattr] 190 | const APPLICATION_COAP_PAYLOAD: Self = Self(zenoh::bytes::Encoding::APPLICATION_COAP_PAYLOAD); 191 | #[classattr] 192 | const APPLICATION_JSON_PATCH_JSON: Self = 193 | Self(zenoh::bytes::Encoding::APPLICATION_JSON_PATCH_JSON); 194 | #[classattr] 195 | const APPLICATION_JSON_SEQ: Self = Self(zenoh::bytes::Encoding::APPLICATION_JSON_SEQ); 196 | #[classattr] 197 | const APPLICATION_JSONPATH: Self = Self(zenoh::bytes::Encoding::APPLICATION_JSONPATH); 198 | #[classattr] 199 | const APPLICATION_JWT: Self = Self(zenoh::bytes::Encoding::APPLICATION_JWT); 200 | #[classattr] 201 | const APPLICATION_MP4: Self = Self(zenoh::bytes::Encoding::APPLICATION_MP4); 202 | #[classattr] 203 | const APPLICATION_SOAP_XML: Self = Self(zenoh::bytes::Encoding::APPLICATION_SOAP_XML); 204 | #[classattr] 205 | const APPLICATION_YANG: Self = Self(zenoh::bytes::Encoding::APPLICATION_YANG); 206 | #[classattr] 207 | const AUDIO_AAC: Self = Self(zenoh::bytes::Encoding::AUDIO_AAC); 208 | #[classattr] 209 | const AUDIO_FLAC: Self = Self(zenoh::bytes::Encoding::AUDIO_FLAC); 210 | #[classattr] 211 | const AUDIO_MP4: Self = Self(zenoh::bytes::Encoding::AUDIO_MP4); 212 | #[classattr] 213 | const AUDIO_OGG: Self = Self(zenoh::bytes::Encoding::AUDIO_OGG); 214 | #[classattr] 215 | const AUDIO_VORBIS: Self = Self(zenoh::bytes::Encoding::AUDIO_VORBIS); 216 | #[classattr] 217 | const VIDEO_H261: Self = Self(zenoh::bytes::Encoding::VIDEO_H261); 218 | #[classattr] 219 | const VIDEO_H263: Self = Self(zenoh::bytes::Encoding::VIDEO_H263); 220 | #[classattr] 221 | const VIDEO_H264: Self = Self(zenoh::bytes::Encoding::VIDEO_H264); 222 | #[classattr] 223 | const VIDEO_H265: Self = Self(zenoh::bytes::Encoding::VIDEO_H265); 224 | #[classattr] 225 | const VIDEO_H266: Self = Self(zenoh::bytes::Encoding::VIDEO_H266); 226 | #[classattr] 227 | const VIDEO_MP4: Self = Self(zenoh::bytes::Encoding::VIDEO_MP4); 228 | #[classattr] 229 | const VIDEO_OGG: Self = Self(zenoh::bytes::Encoding::VIDEO_OGG); 230 | #[classattr] 231 | const VIDEO_RAW: Self = Self(zenoh::bytes::Encoding::VIDEO_RAW); 232 | #[classattr] 233 | const VIDEO_VP8: Self = Self(zenoh::bytes::Encoding::VIDEO_VP8); 234 | #[classattr] 235 | const VIDEO_VP9: Self = Self(zenoh::bytes::Encoding::VIDEO_VP9); 236 | } 237 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::path::PathBuf; 15 | 16 | use pyo3::{prelude::*, types::PyType}; 17 | 18 | use crate::{ 19 | macros::{downcast_or_new, enum_mapper, wrapper}, 20 | utils::{IntoPyResult, IntoRust}, 21 | }; 22 | 23 | wrapper!(zenoh::Config: Default, Clone); 24 | 25 | #[pymethods] 26 | impl Config { 27 | #[new] 28 | fn new() -> Self { 29 | Self::default() 30 | } 31 | 32 | #[classattr] 33 | const DEFAULT_CONFIG_PATH_ENV: &'static str = zenoh::Config::DEFAULT_CONFIG_PATH_ENV; 34 | 35 | #[classmethod] 36 | fn from_env(_cls: &Bound) -> PyResult { 37 | Ok(Self(zenoh::config::Config::from_env().into_pyres()?)) 38 | } 39 | 40 | #[classmethod] 41 | fn from_file(_cls: &Bound, path: PathBuf) -> PyResult { 42 | Ok(Self(zenoh::config::Config::from_file(path).into_pyres()?)) 43 | } 44 | 45 | #[classmethod] 46 | fn from_json5(_cls: &Bound, json: &str) -> PyResult { 47 | Ok(Self(zenoh::config::Config::from_json5(json).into_pyres()?)) 48 | } 49 | 50 | fn get_json(&self, key: &str) -> PyResult { 51 | self.0.get_json(key).into_pyres() 52 | } 53 | 54 | fn insert_json5(&mut self, key: &str, value: &str) -> PyResult<()> { 55 | self.0.insert_json5(key, value).into_pyres() 56 | } 57 | 58 | fn __repr__(&self) -> String { 59 | format!("{:?}", self.0) 60 | } 61 | 62 | fn __str__(&self) -> String { 63 | format!("{}", self.0) 64 | } 65 | } 66 | 67 | enum_mapper!(zenoh::config::WhatAmI: u8 { 68 | Router = 0b001, 69 | Peer = 0b010, 70 | Client = 0b100, 71 | }); 72 | downcast_or_new!(WhatAmI => String); 73 | 74 | #[pymethods] 75 | impl WhatAmI { 76 | #[new] 77 | fn new(s: String) -> PyResult { 78 | Ok(s.parse::().into_pyres()?.into()) 79 | } 80 | 81 | fn __or__(&self, #[pyo3(from_py_with = "WhatAmI::from_py")] other: WhatAmI) -> WhatAmIMatcher { 82 | (self.into_rust() | other.into_rust()).into() 83 | } 84 | 85 | fn __repr__(&self) -> String { 86 | format!("{:?}", self.into_rust()) 87 | } 88 | 89 | fn __str__(&self) -> &str { 90 | (*self).into_rust().to_str() 91 | } 92 | } 93 | 94 | wrapper!(zenoh::config::WhatAmIMatcher: Clone, Copy); 95 | downcast_or_new!(WhatAmIMatcher => Option); 96 | 97 | impl Default for WhatAmIMatcher { 98 | fn default() -> Self { 99 | zenoh::config::WhatAmIMatcher::empty() 100 | .router() 101 | .peer() 102 | .client() 103 | .into() 104 | } 105 | } 106 | 107 | #[pymethods] 108 | impl WhatAmIMatcher { 109 | #[new] 110 | pub(crate) fn new(s: Option) -> PyResult { 111 | let Some(s) = s else { 112 | return Ok(Self(zenoh::config::WhatAmIMatcher::empty())); 113 | }; 114 | let res = s.parse().map_err(|_| "invalid WhatAmI matcher"); 115 | Ok(Self(res.into_pyres()?)) 116 | } 117 | 118 | #[classmethod] 119 | fn empty(_cls: &Bound) -> Self { 120 | Self(zenoh::config::WhatAmIMatcher::empty()) 121 | } 122 | 123 | fn router(&self) -> Self { 124 | Self(zenoh::config::WhatAmIMatcher::router(self.0)) 125 | } 126 | 127 | fn peer(&self) -> Self { 128 | Self(zenoh::config::WhatAmIMatcher::peer(self.0)) 129 | } 130 | 131 | fn client(&self) -> Self { 132 | Self(zenoh::config::WhatAmIMatcher::client(self.0)) 133 | } 134 | 135 | fn is_empty(&self) -> bool { 136 | self.0.is_empty() 137 | } 138 | 139 | fn matches(&self, #[pyo3(from_py_with = "WhatAmI::from_py")] whatami: WhatAmI) -> bool { 140 | self.0.matches(whatami.into()) 141 | } 142 | 143 | fn __contains__(&self, #[pyo3(from_py_with = "WhatAmI::from_py")] whatami: WhatAmI) -> bool { 144 | self.0.matches(whatami.into()) 145 | } 146 | 147 | fn __repr__(&self) -> String { 148 | format!("{:?}", self.0) 149 | } 150 | 151 | fn __str__(&self) -> &'static str { 152 | self.0.to_str() 153 | } 154 | } 155 | 156 | wrapper!(zenoh::config::ZenohId); 157 | 158 | #[pymethods] 159 | impl ZenohId { 160 | fn __repr__(&self) -> String { 161 | format!("{:?}", self.0) 162 | } 163 | 164 | fn __str__(&self) -> String { 165 | format!("{}", self.0) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/handlers.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::{fmt, marker::PhantomData, sync::Arc, time::Duration}; 15 | 16 | use pyo3::{ 17 | exceptions::PyValueError, 18 | prelude::*, 19 | types::{PyCFunction, PyDict, PyType}, 20 | }; 21 | use zenoh::handlers::IntoHandler; 22 | 23 | use crate::{ 24 | macros::{import, py_static}, 25 | utils::{generic, short_type_name, IntoPyErr, IntoPyResult, IntoPython, IntoRust}, 26 | ZError, 27 | }; 28 | 29 | type RustCallback = zenoh::handlers::Callback; 30 | 31 | const CHECK_SIGNALS_INTERVAL: Duration = Duration::from_millis(100); 32 | const DROP_CALLBACK_WARNING: &str = "Passing drop-callback using a tuple \ 33 | `(callback, drop-callback)` no longer works in 1.0;\n\ 34 | `zenoh.handlers.Callback(callback, drop_callback)` must be used instead.\n\ 35 | The tuple form is reserved for passing a handler with `(callback, handler)`.\n\ 36 | If you are already passing a handler and this warning is still incorrectly displayed, \ 37 | you can silence it with:\n\ 38 | warnings.filterwarnings(\"ignore\", message=\"Passing drop-callback\")"; 39 | 40 | fn log_error(py: Python, result: PyResult) { 41 | if let Err(err) = result { 42 | let kwargs = PyDict::new_bound(py); 43 | kwargs.set_item("exc_info", err.into_value(py)).unwrap(); 44 | py_static!(py, PyAny, || Ok(import!(py, logging.getLogger) 45 | .call1(("zenoh.handlers",))? 46 | .getattr("error")? 47 | .unbind())) 48 | .unwrap() 49 | .call(("callback error",), Some(&kwargs)) 50 | .ok(); 51 | } 52 | } 53 | 54 | #[pyclass] 55 | #[derive(Clone)] 56 | pub(crate) struct DefaultHandler; 57 | 58 | impl IntoRust for DefaultHandler { 59 | type Into = zenoh::handlers::DefaultHandler; 60 | 61 | fn into_rust(self) -> Self::Into { 62 | Self::Into::default() 63 | } 64 | } 65 | 66 | #[pymethods] 67 | impl DefaultHandler { 68 | #[new] 69 | fn new() -> Self { 70 | Self 71 | } 72 | } 73 | 74 | #[pyclass] 75 | #[derive(Clone)] 76 | pub(crate) struct FifoChannel(usize); 77 | 78 | impl IntoRust for FifoChannel { 79 | type Into = zenoh::handlers::FifoChannel; 80 | 81 | fn into_rust(self) -> Self::Into { 82 | Self::Into::new(self.0) 83 | } 84 | } 85 | 86 | #[pymethods] 87 | impl FifoChannel { 88 | #[new] 89 | fn new(capacity: usize) -> Self { 90 | Self(capacity) 91 | } 92 | } 93 | 94 | #[pyclass] 95 | #[derive(Clone)] 96 | pub(crate) struct RingChannel(usize); 97 | 98 | impl IntoRust for RingChannel { 99 | type Into = zenoh::handlers::RingChannel; 100 | 101 | fn into_rust(self) -> Self::Into { 102 | Self::Into::new(self.0) 103 | } 104 | } 105 | 106 | #[pymethods] 107 | impl RingChannel { 108 | #[new] 109 | fn new(capacity: usize) -> Self { 110 | Self(capacity) 111 | } 112 | } 113 | 114 | pub(crate) trait Receiver { 115 | fn type_name(&self) -> &'static str; 116 | fn try_recv(&self, py: Python) -> PyResult; 117 | fn recv(&self, py: Python) -> PyResult; 118 | } 119 | 120 | #[pyclass] 121 | pub(crate) struct Handler(Box); 122 | 123 | #[pymethods] 124 | impl Handler { 125 | #[classmethod] 126 | fn __class_getitem__(cls: &Bound, args: &Bound) -> PyObject { 127 | generic(cls, args) 128 | } 129 | 130 | fn try_recv(&self, py: Python) -> PyResult { 131 | self.0.try_recv(py) 132 | } 133 | 134 | fn recv(&self, py: Python) -> PyResult { 135 | self.0.recv(py) 136 | } 137 | 138 | fn __iter__(this: Py) -> Py { 139 | this 140 | } 141 | 142 | fn __next__(&self, py: Python) -> PyResult> { 143 | match self.0.recv(py) { 144 | Ok(obj) => Ok(Some(obj)), 145 | Err(err) if err.is_instance_of::(py) => Ok(None), 146 | Err(err) => Err(err), 147 | } 148 | } 149 | 150 | fn __repr__(&self) -> String { 151 | format!("Handler[{}]", self.0.type_name()) 152 | } 153 | } 154 | 155 | #[pyclass] 156 | #[derive(Clone, Debug)] 157 | pub(crate) struct Callback { 158 | #[pyo3(get)] 159 | callback: PyObject, 160 | #[pyo3(get)] 161 | drop: Option, 162 | #[pyo3(get)] 163 | indirect: bool, 164 | } 165 | 166 | #[pymethods] 167 | impl Callback { 168 | #[new] 169 | #[pyo3(signature = (callback, drop, *, indirect = true))] 170 | fn new(callback: PyObject, drop: Option, indirect: bool) -> Self { 171 | Self { 172 | callback, 173 | drop, 174 | indirect, 175 | } 176 | } 177 | 178 | fn __call__(&self, arg: &Bound) -> PyResult { 179 | self.callback.call1(arg.py(), (arg,)) 180 | } 181 | 182 | fn __repr__(&self) -> String { 183 | format!("{self:?}") 184 | } 185 | } 186 | 187 | pub(crate) struct PythonCallback(Callback); 188 | 189 | impl PythonCallback { 190 | fn new(obj: &Bound) -> Self { 191 | if let Ok(cb) = Callback::extract_bound(obj) { 192 | return Self(cb); 193 | } 194 | Self(Callback::new(obj.clone().unbind(), None, true)) 195 | } 196 | 197 | fn call(&self, py: Python, t: T) { 198 | log_error(py, self.0.callback.call1(py, (t.into_pyobject(py),))); 199 | } 200 | } 201 | 202 | impl Drop for PythonCallback { 203 | fn drop(&mut self) { 204 | if let Some(drop) = &self.0.drop { 205 | Python::with_gil(|gil| log_error(gil, drop.call0(gil))); 206 | } 207 | } 208 | } 209 | 210 | // the generic type is not useful per se, it just there to make typing 211 | // prettier, e.g. to have `get` returning a `PyResult>` 212 | pub(crate) enum HandlerImpl { 213 | Rust(Py, PhantomData), 214 | Python(PyObject), 215 | } 216 | 217 | impl fmt::Debug for HandlerImpl { 218 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 219 | match self { 220 | Self::Rust(..) => write!(f, "Handler[{}]", short_type_name::()), 221 | Self::Python(obj) => write!(f, "{obj:?}"), 222 | } 223 | } 224 | } 225 | 226 | impl IntoPy for HandlerImpl { 227 | fn into_py(self, _: Python<'_>) -> PyObject { 228 | match self { 229 | Self::Rust(obj, _) => obj.into_any(), 230 | Self::Python(obj) => obj, 231 | } 232 | } 233 | } 234 | 235 | impl ToPyObject for HandlerImpl { 236 | fn to_object(&self, py: Python<'_>) -> PyObject { 237 | match self { 238 | Self::Rust(obj, _) => obj.clone_ref(py).into_any(), 239 | Self::Python(obj) => obj.clone_ref(py), 240 | } 241 | } 242 | } 243 | 244 | impl HandlerImpl { 245 | pub(crate) fn try_recv(&self, py: Python) -> PyResult { 246 | match self { 247 | Self::Rust(handler, _) => handler.borrow(py).try_recv(py), 248 | Self::Python(handler) => handler.call_method0(py, "try_recv"), 249 | } 250 | } 251 | 252 | pub(crate) fn recv(&self, py: Python) -> PyResult { 253 | match self { 254 | Self::Rust(handler, _) => handler.borrow(py).recv(py), 255 | Self::Python(handler) => handler.call_method0(py, "recv"), 256 | } 257 | } 258 | } 259 | 260 | struct RustHandler 261 | where 262 | H::Into: IntoHandler, 263 | { 264 | handler: >::Handler, 265 | _phantom: PhantomData, 266 | } 267 | 268 | fn try_recv( 269 | py: Python, 270 | f: impl FnOnce() -> Result + Send, 271 | ) -> PyResult { 272 | Ok(py.allow_threads(f).into_pyres()?.into_pyobject(py)) 273 | } 274 | 275 | fn recv( 276 | py: Python, 277 | f: impl Fn() -> Result + Sync, 278 | is_timeout: impl Fn(&E) -> bool, 279 | ) -> PyResult { 280 | loop { 281 | match py.allow_threads(&f) { 282 | Ok(obj) => return Ok(obj.into_pyobject(py)), 283 | Err(err) if is_timeout(&err) => py.check_signals()?, 284 | Err(err) => return Err(err.into_pyerr()), 285 | } 286 | } 287 | } 288 | 289 | enum DeadlineError { 290 | Timeout, 291 | Error(E), 292 | } 293 | impl fmt::Display for DeadlineError { 294 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 295 | match self { 296 | Self::Error(err) => write!(f, "{err}"), 297 | Self::Timeout => unreachable!(), 298 | } 299 | } 300 | } 301 | 302 | impl Receiver for RustHandler { 303 | fn type_name(&self) -> &'static str { 304 | short_type_name::() 305 | } 306 | 307 | fn try_recv(&self, py: Python) -> PyResult { 308 | try_recv(py, || PyResult::Ok(self.handler.try_recv().ok())) 309 | } 310 | 311 | fn recv(&self, py: Python) -> PyResult { 312 | recv( 313 | py, 314 | || match self.handler.recv_timeout(CHECK_SIGNALS_INTERVAL) { 315 | Ok(Some(x)) => Ok(x), 316 | Ok(None) => Err(DeadlineError::Timeout), 317 | Err(err) => Err(DeadlineError::Error(err)), 318 | }, 319 | |err| matches!(err, DeadlineError::Timeout), 320 | ) 321 | } 322 | } 323 | 324 | impl Receiver for RustHandler { 325 | fn type_name(&self) -> &'static str { 326 | short_type_name::() 327 | } 328 | 329 | fn try_recv(&self, py: Python) -> PyResult { 330 | try_recv(py, || PyResult::Ok(self.handler.try_recv().ok())) 331 | } 332 | 333 | fn recv(&self, py: Python) -> PyResult { 334 | recv( 335 | py, 336 | || match self.handler.recv_timeout(CHECK_SIGNALS_INTERVAL) { 337 | Ok(Some(x)) => Ok(x), 338 | Ok(None) => Err(DeadlineError::Timeout), 339 | Err(err) => Err(DeadlineError::Error(err)), 340 | }, 341 | |err| matches!(err, DeadlineError::Timeout), 342 | ) 343 | } 344 | } 345 | 346 | impl Receiver for RustHandler { 347 | fn type_name(&self) -> &'static str { 348 | short_type_name::() 349 | } 350 | 351 | fn try_recv(&self, py: Python) -> PyResult { 352 | try_recv(py, || self.handler.try_recv()) 353 | } 354 | 355 | fn recv(&self, py: Python) -> PyResult { 356 | recv( 357 | py, 358 | || match self.handler.recv_timeout(CHECK_SIGNALS_INTERVAL) { 359 | Ok(Some(x)) => Ok(x), 360 | Ok(None) => Err(DeadlineError::Timeout), 361 | Err(err) => Err(DeadlineError::Error(err)), 362 | }, 363 | |err| matches!(err, DeadlineError::Timeout), 364 | ) 365 | } 366 | } 367 | 368 | fn rust_handler( 369 | py: Python, 370 | into_handler: H, 371 | ) -> (RustCallback, HandlerImpl) 372 | where 373 | H::Into: IntoHandler, 374 | >::Handler: Send + Sync, 375 | RustHandler: Receiver, 376 | { 377 | let (callback, handler) = into_handler.into_rust().into_handler(); 378 | let rust_handler = RustHandler:: { 379 | handler, 380 | _phantom: PhantomData, 381 | }; 382 | let handler = Py::new(py, Handler(Box::new(rust_handler))).unwrap(); 383 | (callback, HandlerImpl::Rust(handler, PhantomData)) 384 | } 385 | 386 | fn python_callback(callback: &Bound) -> PyResult> { 387 | let py = callback.py(); 388 | let callback = PythonCallback::new(callback); 389 | Ok(if callback.0.indirect { 390 | let (rust_callback, receiver) = DefaultHandler.into_rust().into_handler(); 391 | let kwargs = PyDict::new_bound(py); 392 | let target = PyCFunction::new_closure_bound(py, None, None, move |args, _| { 393 | let py = args.py(); 394 | // No need to call `Python::check_signals` because it's not the main thread. 395 | while let Ok(x) = py.allow_threads(|| receiver.recv()) { 396 | callback.call(py, x); 397 | } 398 | })?; 399 | kwargs.set_item("target", target)?; 400 | let thread = import!(py, threading.Thread).call((), Some(&kwargs))?; 401 | thread.call_method0("start")?; 402 | rust_callback 403 | } else { 404 | RustCallback::new(Arc::new(move |t| { 405 | Python::with_gil(|gil| callback.call(gil, t)) 406 | })) 407 | }) 408 | } 409 | 410 | pub(crate) fn into_handler( 411 | py: Python, 412 | obj: Option<&Bound>, 413 | ) -> PyResult<(impl IntoHandler>, bool)> { 414 | let mut background = false; 415 | let Some(obj) = obj else { 416 | return Ok((rust_handler(py, DefaultHandler), background)); 417 | }; 418 | let into_handler = if let Ok(handler) = obj.extract::() { 419 | rust_handler(py, handler) 420 | } else if let Ok(handler) = obj.extract::() { 421 | rust_handler(py, handler) 422 | } else if let Ok(handler) = obj.extract::() { 423 | rust_handler(py, handler) 424 | } else if obj.is_callable() { 425 | background = true; 426 | (python_callback(obj)?, HandlerImpl::Python(py.None())) 427 | } else if let Some((cb, handler)) = obj 428 | .extract::<(Bound, PyObject)>() 429 | .ok() 430 | .filter(|(cb, _)| cb.is_callable()) 431 | { 432 | if handler.bind(py).is_callable() { 433 | import!(py, warnings.warn).call1((DROP_CALLBACK_WARNING,))?; 434 | } 435 | (python_callback(&cb)?, HandlerImpl::Python(handler)) 436 | } else { 437 | return Err(PyValueError::new_err(format!( 438 | "Invalid handler type {}", 439 | obj.get_type().name()? 440 | ))); 441 | }; 442 | Ok((into_handler, background)) 443 | } 444 | -------------------------------------------------------------------------------- /src/key_expr.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | 15 | use pyo3::{ 16 | prelude::*, 17 | types::{PyString, PyType}, 18 | }; 19 | 20 | use crate::{ 21 | macros::{downcast_or_new, enum_mapper, wrapper}, 22 | utils::{IntoPyResult, MapInto}, 23 | }; 24 | 25 | enum_mapper!(zenoh::key_expr::SetIntersectionLevel: u8 { 26 | Disjoint, 27 | Intersects, 28 | Includes, 29 | Equals, 30 | }); 31 | 32 | wrapper!(zenoh::key_expr::KeyExpr<'static>: Clone); 33 | downcast_or_new!(KeyExpr => String); 34 | 35 | #[pymethods] 36 | impl KeyExpr { 37 | #[new] 38 | pub(crate) fn new(s: String) -> PyResult { 39 | Ok(Self(s.parse().into_pyres()?)) 40 | } 41 | 42 | #[classmethod] 43 | fn autocanonize(_cls: &Bound, key_expr: String) -> PyResult { 44 | zenoh::key_expr::KeyExpr::autocanonize(key_expr) 45 | .into_pyres() 46 | .map_into() 47 | } 48 | 49 | fn intersects(&self, #[pyo3(from_py_with = "KeyExpr::from_py")] other: KeyExpr) -> bool { 50 | self.0.intersects(&other.0) 51 | } 52 | 53 | fn includes(&self, #[pyo3(from_py_with = "KeyExpr::from_py")] other: KeyExpr) -> bool { 54 | self.0.includes(&other.0) 55 | } 56 | 57 | fn relation_to( 58 | &self, 59 | #[pyo3(from_py_with = "KeyExpr::from_py")] other: KeyExpr, 60 | ) -> SetIntersectionLevel { 61 | self.0.relation_to(&other.0).into() 62 | } 63 | 64 | fn join(&self, other: String) -> PyResult { 65 | self.0.join(&other).into_pyres().map_into() 66 | } 67 | 68 | fn concat(&self, other: String) -> PyResult { 69 | self.0.concat(&other).into_pyres().map_into() 70 | } 71 | 72 | // Cannot use `#[pyo3(from_py_with = "...")]`, see https://github.com/PyO3/pyo3/issues/4113 73 | fn __eq__(&self, other: &Bound) -> PyResult { 74 | Ok(self.0 == Self::from_py(other)?.0) 75 | } 76 | 77 | fn __repr__(&self) -> String { 78 | format!("{:?}", self.0) 79 | } 80 | 81 | fn __str__(&self) -> String { 82 | format!("{}", self.0) 83 | } 84 | 85 | fn __hash__(&self, py: Python) -> PyResult { 86 | PyString::new_bound(py, self.0.as_str()).hash() 87 | } 88 | 89 | // Cannot use `#[pyo3(from_py_with = "...")]`, see https://github.com/PyO3/pyo3/issues/4113 90 | fn __truediv__(&self, other: &Bound) -> PyResult { 91 | Ok(Self(&self.0 / &Self::from_py(other)?.0)) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | // TODO https://github.com/eclipse-zenoh/zenoh-python/pull/235#discussion_r1644498390 15 | // mod logging; 16 | mod bytes; 17 | mod config; 18 | #[cfg(feature = "zenoh-ext")] 19 | mod ext; 20 | mod handlers; 21 | mod key_expr; 22 | mod liveliness; 23 | mod macros; 24 | mod pubsub; 25 | mod qos; 26 | mod query; 27 | mod sample; 28 | mod scouting; 29 | mod session; 30 | mod time; 31 | mod utils; 32 | 33 | use pyo3::prelude::*; 34 | 35 | pyo3::create_exception!(zenoh, ZError, pyo3::exceptions::PyException); 36 | // must be defined here or exporting doesn't work 37 | pyo3::create_exception!(zenoh, ZDeserializeError, pyo3::exceptions::PyException); 38 | 39 | #[pymodule] 40 | pub(crate) mod zenoh { 41 | use pyo3::prelude::*; 42 | 43 | #[pyfunction] 44 | fn try_init_log_from_env() { 45 | zenoh::try_init_log_from_env(); 46 | } 47 | 48 | #[pyfunction] 49 | fn init_log_from_env_or(level: &str) { 50 | zenoh::init_log_from_env_or(level); 51 | } 52 | 53 | #[pymodule_export] 54 | use crate::{ 55 | bytes::{Encoding, ZBytes}, 56 | config::{Config, WhatAmI, WhatAmIMatcher, ZenohId}, 57 | handlers::Handler, 58 | key_expr::{KeyExpr, SetIntersectionLevel}, 59 | liveliness::{Liveliness, LivelinessToken}, 60 | pubsub::{Publisher, Subscriber}, 61 | qos::{CongestionControl, Priority, Reliability}, 62 | query::{ 63 | ConsolidationMode, Parameters, Querier, Query, QueryConsolidation, QueryTarget, 64 | Queryable, Reply, ReplyError, Selector, 65 | }, 66 | sample::{Sample, SampleKind}, 67 | scouting::{scout, Hello, Scout}, 68 | session::{open, Session, SessionInfo}, 69 | time::Timestamp, 70 | ZError, 71 | }; 72 | 73 | #[pymodule] 74 | mod handlers { 75 | #[pymodule_export] 76 | use crate::handlers::{Callback, DefaultHandler, FifoChannel, Handler, RingChannel}; 77 | } 78 | 79 | #[cfg(feature = "zenoh-ext")] 80 | #[pymodule] 81 | mod _ext { 82 | #[pymodule_export] 83 | use crate::ext::{z_deserialize, z_serialize}; 84 | #[pymodule_export] 85 | use crate::ZDeserializeError; 86 | } 87 | 88 | #[pymodule_init] 89 | fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { 90 | let sys_modules = m.py().import_bound("sys")?.getattr("modules")?; 91 | sys_modules.set_item("zenoh.handlers", m.getattr("handlers")?)?; 92 | #[cfg(feature = "zenoh-ext")] 93 | sys_modules.set_item("zenoh._ext", m.getattr("_ext")?)?; 94 | // TODO 95 | // crate::logging::init_logger(m.py())?; 96 | Ok(()) 97 | } 98 | } 99 | 100 | // Test should be runned with `cargo test --no-default-features` 101 | #[test] 102 | #[cfg(not(feature = "default"))] 103 | fn test_no_default_features() { 104 | assert_eq!(::zenoh::FEATURES, concat!(" zenoh/unstable")); 105 | } 106 | -------------------------------------------------------------------------------- /src/liveliness.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use pyo3::{ 4 | prelude::*, 5 | types::{PyDict, PyTuple}, 6 | }; 7 | 8 | use crate::{ 9 | handlers::{into_handler, HandlerImpl}, 10 | key_expr::KeyExpr, 11 | macros::{build, option_wrapper}, 12 | pubsub::Subscriber, 13 | query::Reply, 14 | utils::{timeout, wait, MapInto}, 15 | }; 16 | 17 | #[pyclass] 18 | pub(crate) struct Liveliness(pub(crate) zenoh::Session); 19 | 20 | #[pymethods] 21 | impl Liveliness { 22 | fn declare_token( 23 | &self, 24 | py: Python, 25 | #[pyo3(from_py_with = "KeyExpr::from_py")] key_expr: KeyExpr, 26 | ) -> PyResult { 27 | let liveliness = self.0.liveliness(); 28 | let builder = liveliness.declare_token(key_expr); 29 | wait(py, builder).map_into() 30 | } 31 | 32 | #[pyo3(signature = (key_expr, handler = None, *, history = None))] 33 | fn declare_subscriber( 34 | &self, 35 | py: Python, 36 | #[pyo3(from_py_with = "KeyExpr::from_py")] key_expr: KeyExpr, 37 | handler: Option<&Bound>, 38 | history: Option, 39 | ) -> PyResult { 40 | let (handler, background) = into_handler(py, handler)?; 41 | let liveliness = self.0.liveliness(); 42 | let builder = build!(liveliness.declare_subscriber(key_expr), history); 43 | let mut subscriber = wait(py, builder.with(handler))?; 44 | if background { 45 | subscriber.set_background(true); 46 | } 47 | Ok(subscriber.into()) 48 | } 49 | 50 | #[pyo3(signature = (key_expr, handler = None, *, timeout = None))] 51 | fn get( 52 | &self, 53 | py: Python, 54 | #[pyo3(from_py_with = "KeyExpr::from_py")] key_expr: KeyExpr, 55 | handler: Option<&Bound>, 56 | #[pyo3(from_py_with = "timeout")] timeout: Option, 57 | ) -> PyResult> { 58 | let (handler, _) = into_handler(py, handler)?; 59 | let liveliness = self.0.liveliness(); 60 | let builder = build!(liveliness.get(key_expr), timeout); 61 | wait(py, builder.with(handler)).map_into() 62 | } 63 | } 64 | 65 | option_wrapper!( 66 | zenoh::liveliness::LivelinessToken, 67 | "Undeclared LivelinessToken" 68 | ); 69 | 70 | #[pymethods] 71 | impl LivelinessToken { 72 | fn __enter__<'a, 'py>(this: &'a Bound<'py, Self>) -> PyResult<&'a Bound<'py, Self>> { 73 | Self::check(this) 74 | } 75 | 76 | #[pyo3(signature = (*_args, **_kwargs))] 77 | fn __exit__( 78 | &mut self, 79 | py: Python, 80 | _args: &Bound, 81 | _kwargs: Option<&Bound>, 82 | ) -> PyResult { 83 | self.undeclare(py)?; 84 | Ok(py.None()) 85 | } 86 | 87 | fn undeclare(&mut self, py: Python) -> PyResult<()> { 88 | wait(py, self.take()?.undeclare()) 89 | } 90 | 91 | fn __repr__(&self) -> PyResult { 92 | Ok(format!("{:?}", self.get_ref()?)) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::{ 15 | collections::HashMap, 16 | sync::{ 17 | atomic::{AtomicUsize, Ordering}, 18 | Arc, 19 | }, 20 | thread, 21 | }; 22 | 23 | use pyo3::{exceptions::PyKeyError, prelude::*, sync::GILOnceCell, types::PyDict}; 24 | use tracing::level_filters::LevelFilter; 25 | use zenoh_util::LogRecord; 26 | 27 | use crate::{macros::import, utils::MapInto}; 28 | 29 | const LOGGER_NAME: &str = "zenoh"; 30 | static LOGGER: GILOnceCell = GILOnceCell::new(); 31 | 32 | pub(crate) fn init_logger(py: Python) -> PyResult<()> { 33 | LOGGER.get_or_try_init(py, || { 34 | import!(py, logging.getLogger) 35 | .call1((LOGGER_NAME,)) 36 | .map_into() 37 | })?; 38 | Ok(()) 39 | } 40 | 41 | fn handle_record( 42 | py: Python, 43 | loggers: &mut HashMap, 44 | record: LogRecord, 45 | ) -> PyResult<()> { 46 | let mut logger_name = record.target.replace("::", "."); 47 | if !logger_name.starts_with("zenoh") { 48 | logger_name.insert_str(0, "zenoh.dependency."); 49 | } 50 | let logger = loggers 51 | .entry(record.target.clone()) 52 | .or_insert_with(|| { 53 | import!(py, logging.getLogger) 54 | .call1((&logger_name,)) 55 | .map_or_else(|_| py.None(), Bound::unbind) 56 | }) 57 | .bind(py); 58 | if logger.is_none() { 59 | return Ok(()); 60 | } 61 | let level = match record.level { 62 | tracing::Level::TRACE => 5, 63 | tracing::Level::DEBUG => 10, 64 | tracing::Level::INFO => 20, 65 | tracing::Level::WARN => 30, 66 | tracing::Level::ERROR => 40, 67 | }; 68 | if !logger 69 | .call_method1("isEnabledFor", (level,)) 70 | .and_then(|obj| bool::extract_bound(&obj)) 71 | .unwrap_or(false) 72 | { 73 | return Ok(()); 74 | } 75 | let extra = PyDict::new_bound(py); 76 | for (k, v) in &record.attributes { 77 | extra.set_item(k, v)?; 78 | } 79 | extra.set_item("raw_attributes", extra.copy()?)?; 80 | let formatted_attributes = record 81 | .attributes 82 | .iter() 83 | .flat_map(|(k, v)| [k, "=", v]) 84 | .collect::>() 85 | .join("="); 86 | extra.set_item("formatted_attributes", formatted_attributes)?; 87 | let record = logger.call_method1( 88 | "makeRecord", 89 | ( 90 | logger_name, 91 | level, 92 | record.file, 93 | record.line, 94 | record.message, 95 | py.None(), 96 | py.None(), 97 | py.None(), 98 | extra, 99 | ), 100 | )?; 101 | logger.call_method1("handle", (record,))?; 102 | Ok(()) 103 | } 104 | 105 | #[derive(Clone)] 106 | struct LogFilter(Arc); 107 | 108 | impl LogFilter { 109 | // These constants normally matches `LevelFilter` internal representation 110 | const TRACE: usize = 0; 111 | const DEBUG: usize = 1; 112 | const INFO: usize = 2; 113 | const WARN: usize = 3; 114 | const ERROR: usize = 4; 115 | const OFF: usize = 5; 116 | 117 | fn new(py: Python) -> Self { 118 | let this = Self(Arc::new(AtomicUsize::new(Self::OFF))); 119 | this.reset(py); 120 | this 121 | } 122 | 123 | fn reset(&self, py: Python) { 124 | let logger = LOGGER.get(py).unwrap().bind(py); 125 | let level = logger 126 | .call_method0("getEffectiveLevel") 127 | .unwrap() 128 | .extract::() 129 | .unwrap(); 130 | let filter = match level { 131 | l if l <= 5 => Self::TRACE, 132 | l if l <= 10 => Self::DEBUG, 133 | l if l <= 20 => Self::INFO, 134 | l if l <= 30 => Self::WARN, 135 | l if l <= 40 => Self::ERROR, 136 | _ => Self::OFF, 137 | }; 138 | self.0.store(filter, Ordering::Relaxed); 139 | } 140 | 141 | fn filter(&self, level: tracing::Level) -> bool { 142 | let filter = match self.0.load(Ordering::Relaxed) { 143 | Self::TRACE => LevelFilter::TRACE, 144 | Self::DEBUG => LevelFilter::DEBUG, 145 | Self::INFO => LevelFilter::INFO, 146 | Self::WARN => LevelFilter::WARN, 147 | Self::ERROR => LevelFilter::ERROR, 148 | Self::OFF => LevelFilter::OFF, 149 | _ => unreachable!(), 150 | }; 151 | level <= filter 152 | } 153 | } 154 | 155 | #[pyclass] 156 | struct LoggerCache { 157 | inner: Py, 158 | filter: LogFilter, 159 | } 160 | 161 | #[pymethods] 162 | impl LoggerCache { 163 | fn __getitem__<'py>( 164 | &self, 165 | py: Python<'py>, 166 | key: &Bound<'py, PyAny>, 167 | ) -> PyResult> { 168 | match self.inner.bind(py).get_item(key)? { 169 | Some(value) => Ok(value), 170 | None => Err(PyKeyError::new_err(key.clone().unbind())), 171 | } 172 | } 173 | fn __setitem__(&self, py: Python, key: &Bound, value: &Bound) -> PyResult<()> { 174 | self.inner.bind(py).set_item(key, value) 175 | } 176 | fn clear(&self, py: Python) { 177 | self.inner.bind(py).clear(); 178 | self.filter.reset(py); 179 | tracing::callsite::rebuild_interest_cache(); 180 | } 181 | } 182 | 183 | #[pyfunction] 184 | #[pyo3(signature = (*, raw = false, basic_config = true, **kwargs))] 185 | pub(crate) fn init_logging( 186 | py: Python, 187 | raw: bool, 188 | basic_config: bool, 189 | kwargs: Option<&Bound>, 190 | ) -> PyResult<()> { 191 | if raw { 192 | zenoh_util::try_init_log_from_env(); 193 | return Ok(()); 194 | } 195 | import!(py, logging.addLevelName).call1((5, "TRACE"))?; 196 | if basic_config { 197 | import!(py, logging.basicConfig).call((), kwargs)?; 198 | } 199 | let filter = LogFilter::new(py); 200 | let logger = LOGGER.get(py).unwrap().bind(py); 201 | let cache = LoggerCache { 202 | inner: PyDict::new_bound(py).unbind(), 203 | filter: filter.clone(), 204 | }; 205 | logger.setattr("_cache", cache.into_py(py))?; 206 | let (tx, rx) = flume::unbounded(); 207 | zenoh_util::init_log_with_callback( 208 | move |meta| filter.filter(*meta.level()), 209 | move |record| tx.send(record).unwrap(), 210 | ); 211 | let mut loggers = HashMap::new(); 212 | thread::spawn(move || { 213 | for record in rx { 214 | Python::with_gil(|gil| handle_record(gil, &mut loggers, record)).ok(); 215 | } 216 | }); 217 | Ok(()) 218 | } 219 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | macro_rules! py_static { 15 | ($py:expr, $tp:ty, $expr:expr) => {{ 16 | static CELL: pyo3::sync::GILOnceCell> = pyo3::sync::GILOnceCell::new(); 17 | let res: pyo3::PyResult<&pyo3::Bound<$tp>> = 18 | CELL.get_or_try_init($py, $expr).map(|obj| obj.bind($py)); 19 | res 20 | }}; 21 | } 22 | pub(crate) use py_static; 23 | 24 | macro_rules! try_import { 25 | ($py:expr, $module:ident.$attr:ident) => {{ 26 | const MODULE: &str = stringify!($module); 27 | $crate::macros::try_import!($py, MODULE, $attr) 28 | }}; 29 | ($py:expr, $module:expr, $attr:ident) => {{ 30 | $crate::macros::py_static!($py, PyAny, || PyResult::Ok( 31 | $py.import_bound($module)? 32 | .getattr(stringify!($attr))? 33 | .unbind() 34 | )) 35 | }}; 36 | } 37 | pub(crate) use try_import; 38 | 39 | macro_rules! import { 40 | ($py:expr, $module:ident.$attr:ident) => {{ 41 | $crate::macros::try_import!($py, $module.$attr).unwrap() 42 | }}; 43 | ($py:expr, $module:expr, $attr:ident) => {{ 44 | $crate::macros::try_import!($py, $module, $attr).unwrap() 45 | }}; 46 | } 47 | pub(crate) use import; 48 | 49 | macro_rules! into_rust { 50 | ($($ty:ty),* $(,)?) => {$( 51 | impl $crate::utils::IntoRust for $ty { 52 | type Into = $ty; 53 | fn into_rust(self) -> Self::Into { 54 | self 55 | } 56 | } 57 | )*}; 58 | } 59 | pub(crate) use into_rust; 60 | 61 | macro_rules! zerror { 62 | ($($tt:tt)*) => { $crate::ZError::new_err(format!($($tt)*)) }; 63 | } 64 | pub(crate) use zerror; 65 | 66 | macro_rules! downcast_or_new { 67 | ($ty:ty $(=> $new:ty)? $(, $other:expr)?) => { 68 | #[allow(unused)] 69 | impl $ty { 70 | pub(crate) fn from_py(obj: &Bound) -> PyResult { 71 | if let Ok(obj) = ::extract_bound(obj) { 72 | return Ok(obj); 73 | } 74 | Self::new(PyResult::Ok(obj)$(.and_then(<$new>::extract_bound))??.into(), $($other)?) 75 | } 76 | pub(crate) fn from_py_opt(obj: &Bound) -> PyResult> { 77 | if obj.is_none() { 78 | return Ok(None); 79 | } 80 | Self::from_py(obj).map(Some) 81 | } 82 | } 83 | }; 84 | } 85 | pub(crate) use downcast_or_new; 86 | 87 | macro_rules! enum_mapper { 88 | ($($path:ident)::*: $repr:ty { $($variant:ident $(= $discriminator:literal)?),* $(,)? }) => { 89 | $crate::macros::enum_mapper!(@ $($path)::*, $($path)::*: $repr { $($variant $(= $discriminator)?,)* }); 90 | }; 91 | (@ $ty:ident::$($tt:ident)::*, $path:path: $repr:ty { $($variant:ident $(= $discriminator:literal)?,)* }) => { 92 | $crate::macros::enum_mapper!(@ $($tt)::*, $path: $repr { $($variant $(= $discriminator)?,)* }); 93 | }; 94 | (@ $ty:ident, $path:path: $repr:ty { $($variant:ident $(= $discriminator:literal)?,)* }) => {paste::paste!{ 95 | #[pyo3::pyclass] 96 | #[repr($repr)] 97 | #[derive(Copy, Clone)] 98 | pub enum $ty {$( 99 | #[pyo3(name = $variant:snake:upper)] 100 | $variant $(= $discriminator)?, 101 | )*} 102 | 103 | impl $ty { 104 | #[allow(unused)] 105 | fn enum_to_str(&self) -> &'static str { 106 | match self {$( 107 | Self::$variant => stringify!([<$variant:snake:upper>]), 108 | )*} 109 | } 110 | } 111 | 112 | impl From<$ty> for $path { 113 | fn from(value: $ty) -> Self { 114 | match value {$( 115 | $ty::$variant => Self::$variant, 116 | )*} 117 | } 118 | } 119 | 120 | impl From<$path> for $ty { 121 | fn from(value: $path) -> Self { 122 | match value {$( 123 | $path::$variant => Self::$variant, 124 | )*} 125 | } 126 | } 127 | 128 | impl $crate::utils::IntoRust for $ty { 129 | type Into = $path; 130 | fn into_rust(self) -> Self::Into { self.into() } 131 | } 132 | 133 | impl $crate::utils::IntoPython for $path { 134 | type Into = $ty; 135 | fn into_python(self) -> Self::Into { self.into() } 136 | } 137 | }}; 138 | } 139 | pub(crate) use enum_mapper; 140 | 141 | macro_rules! wrapper { 142 | ($($path:ident)::* $(<$arg:lifetime>)? $(:$($derive:ty),*)?) => { 143 | $crate::macros::wrapper!(@ $($path)::*, $($path)::* $(<$arg>)? $(:$($derive),*)?); 144 | }; 145 | (@ $ty:ident::$($tt:ident)::*, $path:path $(:$($derive:ty),*)?) => { 146 | $crate::macros::wrapper!(@ $($tt)::*, $path $(:$($derive),*)?); 147 | }; 148 | (@ $ty:ident, $path:path $(:$($derive:ty),*)?) => { 149 | #[pyo3::pyclass] 150 | #[derive($($($derive),*)?)] 151 | pub(crate) struct $ty(pub(crate) $path); 152 | 153 | impl From<$ty> for $path { 154 | fn from(value: $ty) -> Self { 155 | value.0 156 | } 157 | } 158 | 159 | impl From<$path> for $ty { 160 | fn from(value: $path) -> Self { 161 | Self(value) 162 | } 163 | } 164 | 165 | impl $crate::utils::IntoRust for $ty { 166 | type Into = $path; 167 | fn into_rust(self) -> Self::Into { self.into() } 168 | } 169 | 170 | impl $crate::utils::IntoPython for $path { 171 | type Into = $ty; 172 | fn into_python(self) -> Self::Into { self.into() } 173 | } 174 | 175 | impl $crate::utils::IntoPython for $ty { 176 | type Into = $ty; 177 | fn into_python(self) -> Self::Into { self } 178 | } 179 | }; 180 | } 181 | pub(crate) use wrapper; 182 | 183 | macro_rules! option_wrapper { 184 | ($($path:ident)::* $(<$arg:lifetime>)?, $error:literal) => { 185 | $crate::macros::option_wrapper!(@ $($path)::*, $($path)::* $(<$arg>)?, $error); 186 | }; 187 | ($($path:ident)::* $(<$arg:ty>)?, $error:literal) => { 188 | $crate::macros::option_wrapper!(@ $($path)::*, $($path)::* $(<$arg>)?, $error); 189 | }; 190 | (@ $ty:ident::$($tt:ident)::*, $path:path, $error:literal) => { 191 | $crate::macros::option_wrapper!(@ $($tt)::*, $path, $error); 192 | }; 193 | (@ $ty:ident, $path:path, $error:literal) => { 194 | #[pyclass] 195 | pub(crate) struct $ty(pub(crate) Option<$path>); 196 | 197 | #[allow(unused)] 198 | impl $ty { 199 | fn none() -> PyErr { 200 | $crate::macros::zerror!($error) 201 | } 202 | fn check<'a, 'py>(this: &'a Bound<'py, Self>) -> PyResult<&'a Bound<'py, Self>> { 203 | this.borrow().get_ref()?; 204 | Ok(this) 205 | } 206 | fn get_ref(&self) -> PyResult<&$path> { 207 | self.0.as_ref().ok_or_else(Self::none) 208 | } 209 | fn get_mut(&mut self) -> PyResult<&mut $path> { 210 | self.0.as_mut().ok_or_else(Self::none) 211 | } 212 | fn take(&mut self) -> PyResult<$path> { 213 | self.0.take().ok_or_else(Self::none) 214 | } 215 | } 216 | 217 | impl From<$path> for $ty { 218 | fn from(value: $path) -> Self { 219 | Self(Some(value)) 220 | } 221 | } 222 | 223 | impl $crate::utils::IntoPython for $path { 224 | type Into = $ty; 225 | fn into_python(self) -> Self::Into { self.into() } 226 | } 227 | 228 | impl $crate::utils::IntoPython for $ty { 229 | type Into = $ty; 230 | fn into_python(self) -> Self::Into { self } 231 | } 232 | 233 | impl Drop for $ty { 234 | fn drop(&mut self) { 235 | Python::with_gil(|gil| gil.allow_threads(|| drop(self.0.take()))); 236 | } 237 | } 238 | }; 239 | } 240 | pub(crate) use option_wrapper; 241 | 242 | macro_rules! build { 243 | ($builder:expr, $($value:ident),* $(,)?) => {{ 244 | let mut builder = $builder; 245 | $( 246 | if let Some(value) = $value.map($crate::utils::IntoRust::into_rust) { 247 | builder = builder.$value(value); 248 | } 249 | )* 250 | builder 251 | }}; 252 | } 253 | pub(crate) use build; 254 | -------------------------------------------------------------------------------- /src/pubsub.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use pyo3::{ 15 | prelude::*, 16 | types::{PyDict, PyIterator, PyTuple, PyType}, 17 | }; 18 | 19 | use crate::{ 20 | bytes::{Encoding, ZBytes}, 21 | handlers::HandlerImpl, 22 | key_expr::KeyExpr, 23 | macros::{build, option_wrapper}, 24 | qos::{CongestionControl, Priority, Reliability}, 25 | sample::Sample, 26 | utils::{generic, wait}, 27 | }; 28 | 29 | option_wrapper!(zenoh::pubsub::Publisher<'static>, "Undeclared publisher"); 30 | 31 | #[pymethods] 32 | impl Publisher { 33 | fn __enter__<'a, 'py>(this: &'a Bound<'py, Self>) -> PyResult<&'a Bound<'py, Self>> { 34 | Self::check(this) 35 | } 36 | 37 | #[pyo3(signature = (*_args, **_kwargs))] 38 | fn __exit__( 39 | &mut self, 40 | py: Python, 41 | _args: &Bound, 42 | _kwargs: Option<&Bound>, 43 | ) -> PyResult { 44 | self.undeclare(py)?; 45 | Ok(py.None()) 46 | } 47 | 48 | #[getter] 49 | fn key_expr(&self) -> PyResult { 50 | Ok(self.get_ref()?.key_expr().clone().into()) 51 | } 52 | 53 | #[getter] 54 | fn encoding(&self) -> PyResult { 55 | Ok(self.get_ref()?.encoding().clone().into()) 56 | } 57 | 58 | #[getter] 59 | fn congestion_control(&self) -> PyResult { 60 | Ok(self.get_ref()?.congestion_control().into()) 61 | } 62 | 63 | #[getter] 64 | fn priority(&self) -> PyResult { 65 | Ok(self.get_ref()?.priority().into()) 66 | } 67 | 68 | #[getter] 69 | fn reliability(&self) -> PyResult { 70 | Ok(self.get_ref()?.reliability().into()) 71 | } 72 | 73 | // TODO add timestamp 74 | #[pyo3(signature = (payload, *, encoding = None, attachment = None))] 75 | fn put( 76 | &self, 77 | py: Python, 78 | #[pyo3(from_py_with = "ZBytes::from_py")] payload: ZBytes, 79 | #[pyo3(from_py_with = "Encoding::from_py_opt")] encoding: Option, 80 | #[pyo3(from_py_with = "ZBytes::from_py_opt")] attachment: Option, 81 | ) -> PyResult<()> { 82 | let this = self.get_ref()?; 83 | wait(py, build!(this.put(payload), encoding, attachment)) 84 | } 85 | 86 | #[pyo3(signature = (*, attachment = None))] 87 | fn delete( 88 | &self, 89 | py: Python, 90 | #[pyo3(from_py_with = "ZBytes::from_py_opt")] attachment: Option, 91 | ) -> PyResult<()> { 92 | wait(py, build!(self.get_ref()?.delete(), attachment)) 93 | } 94 | 95 | fn undeclare(&mut self, py: Python) -> PyResult<()> { 96 | wait(py, self.take()?.undeclare()) 97 | } 98 | 99 | fn __repr__(&self) -> PyResult { 100 | Ok(format!("{:?}", self.get_ref()?)) 101 | } 102 | } 103 | 104 | option_wrapper!( 105 | zenoh::pubsub::Subscriber>, 106 | "Undeclared subscriber" 107 | ); 108 | 109 | #[pymethods] 110 | impl Subscriber { 111 | #[classmethod] 112 | fn __class_getitem__(cls: &Bound, args: &Bound) -> PyObject { 113 | generic(cls, args) 114 | } 115 | 116 | fn __enter__<'a, 'py>(this: &'a Bound<'py, Self>) -> &'a Bound<'py, Self> { 117 | this 118 | } 119 | 120 | #[pyo3(signature = (*_args, **_kwargs))] 121 | fn __exit__( 122 | &mut self, 123 | py: Python, 124 | _args: &Bound, 125 | _kwargs: Option<&Bound>, 126 | ) -> PyResult { 127 | self.undeclare(py)?; 128 | Ok(py.None()) 129 | } 130 | 131 | #[getter] 132 | fn key_expr(&self) -> PyResult { 133 | Ok(self.get_ref()?.key_expr().clone().into()) 134 | } 135 | 136 | #[getter] 137 | fn handler(&self, py: Python) -> PyResult { 138 | Ok(self.get_ref()?.handler().to_object(py)) 139 | } 140 | 141 | fn try_recv(&self, py: Python) -> PyResult { 142 | self.get_ref()?.handler().try_recv(py) 143 | } 144 | 145 | fn recv(&self, py: Python) -> PyResult { 146 | self.get_ref()?.handler().recv(py) 147 | } 148 | 149 | fn undeclare(&mut self, py: Python) -> PyResult<()> { 150 | wait(py, self.take()?.undeclare()) 151 | } 152 | 153 | fn __iter__<'py>(&self, py: Python<'py>) -> PyResult> { 154 | self.handler(py)?.bind(py).iter() 155 | } 156 | 157 | fn __repr__(&self) -> PyResult { 158 | Ok(format!("{:?}", self.get_ref()?)) 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/qos.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use pyo3::prelude::*; 15 | 16 | use crate::macros::enum_mapper; 17 | 18 | enum_mapper!(zenoh::qos::Priority: u8 { 19 | RealTime = 1, 20 | InteractiveHigh = 2, 21 | InteractiveLow = 3, 22 | DataHigh = 4, 23 | Data = 5, 24 | DataLow = 6, 25 | Background = 7, 26 | }); 27 | 28 | #[pymethods] 29 | impl Priority { 30 | #[classattr] 31 | const DEFAULT: Self = Self::Data; 32 | #[classattr] 33 | const MIN: Self = Self::Background; 34 | #[classattr] 35 | const MAX: Self = Self::RealTime; 36 | #[classattr] 37 | const NUM: usize = 1 + Self::MIN as usize - Self::MAX as usize; 38 | } 39 | 40 | enum_mapper!(zenoh::qos::CongestionControl: u8 { 41 | Drop = 0, 42 | Block = 1, 43 | }); 44 | 45 | #[pymethods] 46 | impl CongestionControl { 47 | #[classattr] 48 | const DEFAULT: Self = Self::Drop; 49 | } 50 | 51 | enum_mapper!(zenoh::qos::Reliability: u8 { 52 | BestEffort, 53 | Reliable 54 | }); 55 | 56 | #[pymethods] 57 | impl Reliability { 58 | #[classattr] 59 | const DEFAULT: Self = Self::BestEffort; 60 | } 61 | -------------------------------------------------------------------------------- /src/sample.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use pyo3::prelude::*; 15 | 16 | use crate::{ 17 | bytes::{Encoding, ZBytes}, 18 | key_expr::KeyExpr, 19 | macros::{enum_mapper, wrapper}, 20 | qos::{CongestionControl, Priority}, 21 | time::Timestamp, 22 | utils::MapInto, 23 | }; 24 | 25 | enum_mapper!(zenoh::sample::SampleKind: u8 { 26 | Put = 0, 27 | Delete = 1, 28 | }); 29 | 30 | wrapper!(zenoh::sample::Sample); 31 | 32 | #[pymethods] 33 | impl Sample { 34 | #[getter] 35 | fn key_expr(&self) -> KeyExpr { 36 | self.0.key_expr().clone().into() 37 | } 38 | 39 | #[getter] 40 | fn payload(&self) -> ZBytes { 41 | self.0.payload().clone().into() 42 | } 43 | 44 | #[getter] 45 | fn kind(&self) -> SampleKind { 46 | self.0.kind().into() 47 | } 48 | 49 | #[getter] 50 | fn encoding(&self) -> Encoding { 51 | self.0.encoding().clone().into() 52 | } 53 | 54 | #[getter] 55 | fn timestamp(&self) -> Option { 56 | self.0.timestamp().cloned().map_into() 57 | } 58 | 59 | #[getter] 60 | fn congestion_control(&self) -> CongestionControl { 61 | self.0.congestion_control().into() 62 | } 63 | 64 | #[getter] 65 | fn priority(&self) -> Priority { 66 | self.0.priority().into() 67 | } 68 | 69 | #[getter] 70 | fn express(&self) -> bool { 71 | self.0.express() 72 | } 73 | 74 | #[getter] 75 | fn attachment(&self) -> Option { 76 | self.0.attachment().cloned().map_into() 77 | } 78 | 79 | fn __repr__(&self) -> String { 80 | format!("{:?}", self.0) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/scouting.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::ops::Deref; 15 | 16 | use pyo3::{ 17 | prelude::*, 18 | types::{PyDict, PyIterator, PyList, PyTuple, PyType}, 19 | }; 20 | 21 | use crate::{ 22 | config::{Config, WhatAmI, WhatAmIMatcher, ZenohId}, 23 | handlers::{into_handler, HandlerImpl}, 24 | macros::{option_wrapper, wrapper}, 25 | utils::{generic, wait}, 26 | }; 27 | 28 | wrapper!(zenoh::scouting::Hello); 29 | 30 | #[pymethods] 31 | impl Hello { 32 | #[getter] 33 | fn whatami(&self) -> WhatAmI { 34 | self.0.whatami().into() 35 | } 36 | 37 | #[getter] 38 | fn zid(&self) -> ZenohId { 39 | ZenohId(self.0.zid()) 40 | } 41 | 42 | #[getter] 43 | fn locators<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> { 44 | let locators = self 45 | .0 46 | .locators() 47 | .iter() 48 | .map(|loc| loc.as_str().to_object(py)); 49 | PyList::new_bound(py, locators) 50 | } 51 | 52 | fn __repr__(&self) -> String { 53 | format!("{:?}", self.0) 54 | } 55 | 56 | fn __str__(&self) -> String { 57 | format!("{}", self.0) 58 | } 59 | } 60 | 61 | option_wrapper!(zenoh::scouting::Scout>, "Stopped scout"); 62 | 63 | #[pymethods] 64 | impl Scout { 65 | #[classmethod] 66 | fn __class_getitem__(cls: &Bound, args: &Bound) -> PyObject { 67 | generic(cls, args) 68 | } 69 | 70 | fn __enter__<'a, 'py>(this: &'a Bound<'py, Self>) -> PyResult<&'a Bound<'py, Self>> { 71 | Self::check(this) 72 | } 73 | 74 | #[pyo3(signature = (*_args, **_kwargs))] 75 | fn __exit__( 76 | &mut self, 77 | py: Python, 78 | _args: &Bound, 79 | _kwargs: Option<&Bound>, 80 | ) -> PyResult<()> { 81 | self.stop(py) 82 | } 83 | 84 | #[getter] 85 | fn handler(&self, py: Python) -> PyResult { 86 | Ok(self.get_ref()?.deref().to_object(py)) 87 | } 88 | 89 | fn try_recv(&self, py: Python) -> PyResult { 90 | self.get_ref()?.deref().try_recv(py) 91 | } 92 | 93 | fn recv(&self, py: Python) -> PyResult { 94 | self.get_ref()?.deref().recv(py) 95 | } 96 | 97 | fn stop(&mut self, py: Python) -> PyResult<()> { 98 | let this = self.take()?; 99 | py.allow_threads(|| this.stop()); 100 | Ok(()) 101 | } 102 | 103 | fn __iter__<'py>(&self, py: Python<'py>) -> PyResult> { 104 | self.handler(py)?.bind(py).iter() 105 | } 106 | 107 | fn __repr__(&self) -> String { 108 | format!("{:?}", self.0) 109 | } 110 | } 111 | 112 | #[pyfunction] 113 | #[pyo3(signature = (handler = None, what = None, config = None))] 114 | pub(crate) fn scout( 115 | py: Python, 116 | handler: Option<&Bound>, 117 | #[pyo3(from_py_with = "WhatAmIMatcher::from_py_opt")] what: Option, 118 | config: Option, 119 | ) -> PyResult { 120 | let what = what.unwrap_or_default(); 121 | let config = config.unwrap_or_default(); 122 | let (handler, _) = into_handler(py, handler)?; 123 | let builder = zenoh::scout(what, config).with(handler); 124 | Ok(Scout(Some(wait(py, builder)?))) 125 | } 126 | -------------------------------------------------------------------------------- /src/session.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::time::Duration; 15 | 16 | use pyo3::{ 17 | prelude::*, 18 | types::{PyDict, PyList, PyTuple}, 19 | }; 20 | use zenoh::Wait; 21 | 22 | use crate::{ 23 | bytes::{Encoding, ZBytes}, 24 | config::{Config, ZenohId}, 25 | handlers::{into_handler, HandlerImpl}, 26 | key_expr::KeyExpr, 27 | liveliness::Liveliness, 28 | macros::{build, wrapper}, 29 | pubsub::{Publisher, Subscriber}, 30 | qos::{CongestionControl, Priority, Reliability}, 31 | query::{Querier, QueryConsolidation, QueryTarget, Queryable, Reply, Selector}, 32 | time::Timestamp, 33 | utils::{timeout, wait, IntoPython, MapInto}, 34 | }; 35 | 36 | #[pyclass] 37 | pub(crate) struct Session(pub(crate) zenoh::Session); 38 | 39 | #[pymethods] 40 | impl Session { 41 | fn __enter__<'a, 'py>(this: &'a Bound<'py, Self>) -> &'a Bound<'py, Self> { 42 | this 43 | } 44 | 45 | #[pyo3(signature = (*_args, **_kwargs))] 46 | fn __exit__( 47 | &mut self, 48 | py: Python, 49 | _args: &Bound, 50 | _kwargs: Option<&Bound>, 51 | ) -> PyResult { 52 | self.close(py)?; 53 | Ok(py.None()) 54 | } 55 | 56 | fn zid(&self) -> PyResult { 57 | Ok(self.0.zid().into()) 58 | } 59 | 60 | fn close(&self, py: Python) -> PyResult<()> { 61 | wait(py, self.0.close()) 62 | } 63 | 64 | fn is_closed(&self) -> bool { 65 | self.0.is_closed() 66 | } 67 | 68 | fn undeclare(&self, obj: &Bound) -> PyResult<()> { 69 | if let Ok(key_expr) = KeyExpr::from_py(obj) { 70 | return wait(obj.py(), self.0.undeclare(key_expr.0)); 71 | } 72 | obj.call_method0("undeclare")?; 73 | Ok(()) 74 | } 75 | 76 | fn new_timestamp(&self) -> Timestamp { 77 | self.0.new_timestamp().into() 78 | } 79 | 80 | fn declare_keyexpr( 81 | &self, 82 | py: Python, 83 | #[pyo3(from_py_with = "KeyExpr::from_py")] key_expr: KeyExpr, 84 | ) -> PyResult { 85 | wait(py, self.0.declare_keyexpr(key_expr)).map_into() 86 | } 87 | 88 | #[allow(clippy::too_many_arguments)] 89 | #[pyo3(signature = (key_expr, payload, *, encoding = None, congestion_control = None, priority = None, express = None, attachment = None))] 90 | fn put( 91 | &self, 92 | py: Python, 93 | #[pyo3(from_py_with = "KeyExpr::from_py")] key_expr: KeyExpr, 94 | #[pyo3(from_py_with = "ZBytes::from_py")] payload: ZBytes, 95 | #[pyo3(from_py_with = "Encoding::from_py_opt")] encoding: Option, 96 | congestion_control: Option, 97 | priority: Option, 98 | express: Option, 99 | #[pyo3(from_py_with = "ZBytes::from_py_opt")] attachment: Option, 100 | ) -> PyResult<()> { 101 | let build = build!( 102 | self.0.put(key_expr, payload), 103 | encoding, 104 | congestion_control, 105 | priority, 106 | express, 107 | attachment, 108 | ); 109 | wait(py, build) 110 | } 111 | 112 | #[pyo3(signature = (key_expr, *, congestion_control = None, priority = None, express = None, attachment = None))] 113 | fn delete( 114 | &self, 115 | py: Python, 116 | #[pyo3(from_py_with = "KeyExpr::from_py")] key_expr: KeyExpr, 117 | congestion_control: Option, 118 | priority: Option, 119 | express: Option, 120 | #[pyo3(from_py_with = "ZBytes::from_py_opt")] attachment: Option, 121 | ) -> PyResult<()> { 122 | let build = build!( 123 | self.0.delete(key_expr), 124 | congestion_control, 125 | priority, 126 | express, 127 | attachment, 128 | ); 129 | wait(py, build) 130 | } 131 | 132 | #[allow(clippy::too_many_arguments)] 133 | #[pyo3(signature = (selector, handler = None, *, target = None, consolidation = None, timeout = None, congestion_control = None, priority = None, express = None, payload = None, encoding = None, attachment = None))] 134 | fn get( 135 | &self, 136 | py: Python, 137 | #[pyo3(from_py_with = "Selector::from_py")] selector: Selector, 138 | handler: Option<&Bound>, 139 | target: Option, 140 | #[pyo3(from_py_with = "QueryConsolidation::from_py_opt")] consolidation: Option< 141 | QueryConsolidation, 142 | >, 143 | #[pyo3(from_py_with = "timeout")] timeout: Option, 144 | congestion_control: Option, 145 | priority: Option, 146 | express: Option, 147 | #[pyo3(from_py_with = "ZBytes::from_py_opt")] payload: Option, 148 | #[pyo3(from_py_with = "Encoding::from_py_opt")] encoding: Option, 149 | #[pyo3(from_py_with = "ZBytes::from_py_opt")] attachment: Option, 150 | ) -> PyResult> { 151 | let (handler, _) = into_handler(py, handler)?; 152 | let builder = build!( 153 | self.0.get(selector), 154 | target, 155 | consolidation, 156 | timeout, 157 | congestion_control, 158 | priority, 159 | express, 160 | payload, 161 | encoding, 162 | attachment, 163 | ); 164 | wait(py, builder.with(handler)).map_into() 165 | } 166 | 167 | #[getter] 168 | fn info(&self) -> SessionInfo { 169 | self.0.info().into() 170 | } 171 | 172 | #[pyo3(signature = (key_expr, handler = None))] 173 | fn declare_subscriber( 174 | &self, 175 | py: Python, 176 | #[pyo3(from_py_with = "KeyExpr::from_py")] key_expr: KeyExpr, 177 | handler: Option<&Bound>, 178 | ) -> PyResult { 179 | let (handler, background) = into_handler(py, handler)?; 180 | let builder = self.0.declare_subscriber(key_expr); 181 | let mut subscriber = wait(py, builder.with(handler))?; 182 | if background { 183 | subscriber.set_background(true); 184 | } 185 | Ok(subscriber.into()) 186 | } 187 | 188 | #[pyo3(signature = (key_expr, handler = None, *, complete = None))] 189 | fn declare_queryable( 190 | &self, 191 | py: Python, 192 | #[pyo3(from_py_with = "KeyExpr::from_py")] key_expr: KeyExpr, 193 | handler: Option<&Bound>, 194 | complete: Option, 195 | ) -> PyResult { 196 | let (handler, background) = into_handler(py, handler)?; 197 | let builder = build!(self.0.declare_queryable(key_expr), complete); 198 | let mut queryable = wait(py, builder.with(handler))?; 199 | if background { 200 | queryable.set_background(true); 201 | } 202 | Ok(queryable.into()) 203 | } 204 | 205 | #[allow(clippy::too_many_arguments)] 206 | #[pyo3(signature = (key_expr, *, encoding = None, congestion_control = None, priority = None, express = None, reliability = None))] 207 | fn declare_publisher( 208 | &self, 209 | py: Python, 210 | #[pyo3(from_py_with = "KeyExpr::from_py")] key_expr: KeyExpr, 211 | #[pyo3(from_py_with = "Encoding::from_py_opt")] encoding: Option, 212 | congestion_control: Option, 213 | priority: Option, 214 | express: Option, 215 | reliability: Option, 216 | ) -> PyResult { 217 | let builder = build!( 218 | self.0.declare_publisher(key_expr), 219 | encoding, 220 | congestion_control, 221 | priority, 222 | express, 223 | reliability, 224 | ); 225 | wait(py, builder).map_into() 226 | } 227 | 228 | #[allow(clippy::too_many_arguments)] 229 | #[pyo3(signature = (key_expr, *, target = None, consolidation = None, timeout = None, congestion_control = None, priority = None, express = None))] 230 | fn declare_querier( 231 | &self, 232 | py: Python, 233 | #[pyo3(from_py_with = "KeyExpr::from_py")] key_expr: KeyExpr, 234 | target: Option, 235 | #[pyo3(from_py_with = "QueryConsolidation::from_py_opt")] consolidation: Option< 236 | QueryConsolidation, 237 | >, 238 | #[pyo3(from_py_with = "timeout")] timeout: Option, 239 | congestion_control: Option, 240 | priority: Option, 241 | express: Option, 242 | ) -> PyResult { 243 | let builder = build!( 244 | self.0.declare_querier(key_expr), 245 | target, 246 | consolidation, 247 | timeout, 248 | congestion_control, 249 | priority, 250 | express, 251 | ); 252 | wait(py, builder).map_into() 253 | } 254 | 255 | fn liveliness(&self) -> Liveliness { 256 | Liveliness(self.0.clone()) 257 | } 258 | 259 | fn __repr__(&self) -> PyResult { 260 | Ok(format!("{:?}", self.0)) 261 | } 262 | } 263 | 264 | impl Drop for Session { 265 | fn drop(&mut self) { 266 | Python::with_gil(|gil| self.close(gil)).unwrap() 267 | } 268 | } 269 | 270 | #[pyfunction] 271 | pub(crate) fn open(py: Python, config: Config) -> PyResult { 272 | wait(py, zenoh::open(config)).map(Session) 273 | } 274 | 275 | wrapper!(zenoh::session::SessionInfo); 276 | 277 | #[pymethods] 278 | impl SessionInfo { 279 | fn zid(&self, py: Python) -> ZenohId { 280 | py.allow_threads(|| self.0.zid().wait()).into() 281 | } 282 | 283 | fn routers_zid<'py>(&self, py: Python<'py>) -> PyResult> { 284 | let list = PyList::empty_bound(py); 285 | for zid in py.allow_threads(|| self.0.routers_zid().wait()) { 286 | list.append(zid.into_pyobject(py))?; 287 | } 288 | Ok(list) 289 | } 290 | 291 | fn peers_zid<'py>(&self, py: Python<'py>) -> PyResult> { 292 | let list = PyList::empty_bound(py); 293 | for zid in py.allow_threads(|| self.0.peers_zid().wait()) { 294 | list.append(zid.into_pyobject(py))?; 295 | } 296 | Ok(list) 297 | } 298 | 299 | // TODO __repr__ 300 | } 301 | -------------------------------------------------------------------------------- /src/time.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::{ 15 | hash::{Hash, Hasher}, 16 | time::{Duration, SystemTime}, 17 | }; 18 | 19 | use pyo3::{prelude::*, types::PyType}; 20 | 21 | use crate::{macros::wrapper, utils::IntoPyResult}; 22 | 23 | wrapper!(zenoh::time::TimestampId: Copy, Clone, PartialEq, PartialOrd); 24 | 25 | #[pymethods] 26 | impl TimestampId { 27 | fn __richcmp__(&self, other: &Self, op: pyo3::pyclass::CompareOp) -> bool { 28 | match op { 29 | pyo3::pyclass::CompareOp::Lt => self < other, 30 | pyo3::pyclass::CompareOp::Le => self <= other, 31 | pyo3::pyclass::CompareOp::Eq => self == other, 32 | pyo3::pyclass::CompareOp::Ne => self != other, 33 | pyo3::pyclass::CompareOp::Gt => self > other, 34 | pyo3::pyclass::CompareOp::Ge => self >= other, 35 | } 36 | } 37 | 38 | fn __bytes__(&self) -> [u8; zenoh::time::TimestampId::MAX_SIZE] { 39 | self.0.to_le_bytes() 40 | } 41 | 42 | fn __hash__(&self, py: Python) -> PyResult { 43 | self.__bytes__().into_py(py).bind(py).hash() 44 | } 45 | 46 | fn __repr__(&self) -> String { 47 | format!("{:?}", self.0) 48 | } 49 | 50 | fn __str__(&self) -> String { 51 | format!("{}", self.0) 52 | } 53 | } 54 | 55 | wrapper!(zenoh::time::Timestamp: Clone, PartialEq, PartialOrd, Hash); 56 | 57 | #[pymethods] 58 | impl Timestamp { 59 | fn get_time(&self) -> SystemTime { 60 | self.0.get_time().to_system_time() 61 | } 62 | 63 | fn get_id(&self) -> TimestampId { 64 | (*self.0.get_id()).into() 65 | } 66 | 67 | fn get_diff_duration(&self, other: Timestamp) -> Duration { 68 | self.0.get_diff_duration(&other.0) 69 | } 70 | 71 | fn to_string_rfc3339_lossy(&self) -> String { 72 | self.0.to_string_rfc3339_lossy() 73 | } 74 | 75 | #[classmethod] 76 | fn parse_rfc3339(_cls: &Bound, s: &str) -> PyResult { 77 | Ok(Self( 78 | zenoh::time::Timestamp::parse_rfc3339(s) 79 | .map_err(|err| err.cause) 80 | .into_pyres()?, 81 | )) 82 | } 83 | 84 | fn __richcmp__(&self, other: &Self, op: pyo3::pyclass::CompareOp) -> bool { 85 | match op { 86 | pyo3::pyclass::CompareOp::Lt => self < other, 87 | pyo3::pyclass::CompareOp::Le => self <= other, 88 | pyo3::pyclass::CompareOp::Eq => self == other, 89 | pyo3::pyclass::CompareOp::Ne => self != other, 90 | pyo3::pyclass::CompareOp::Gt => self > other, 91 | pyo3::pyclass::CompareOp::Ge => self >= other, 92 | } 93 | } 94 | 95 | fn __hash__(&self) -> u64 { 96 | let mut hasher = std::collections::hash_map::DefaultHasher::new(); 97 | self.0.hash(&mut hasher); 98 | hasher.finish() 99 | } 100 | 101 | fn __repr__(&self) -> String { 102 | format!("{:?}", self.0) 103 | } 104 | 105 | fn __str__(&self) -> String { 106 | format!("{}", self.0) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2024 ZettaScale Technology 3 | // 4 | // This program and the accompanying materials are made available under the 5 | // terms of the Eclipse Public License 2.0 which is available at 6 | // http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | // which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | // 9 | // SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | // 11 | // Contributors: 12 | // ZettaScale Zenoh Team, 13 | // 14 | use std::time::Duration; 15 | 16 | use pyo3::{exceptions::PyValueError, prelude::*, types::PyType}; 17 | 18 | use crate::{ 19 | macros::{import, into_rust}, 20 | ZError, 21 | }; 22 | 23 | pub(crate) trait IntoPyErr { 24 | fn into_pyerr(self) -> PyErr; 25 | } 26 | impl IntoPyErr for E { 27 | fn into_pyerr(self) -> PyErr { 28 | ZError::new_err(self.to_string()) 29 | } 30 | } 31 | pub(crate) trait IntoPyResult { 32 | fn into_pyres(self) -> Result; 33 | } 34 | impl IntoPyResult for Result { 35 | fn into_pyres(self) -> Result { 36 | self.map_err(IntoPyErr::into_pyerr) 37 | } 38 | } 39 | 40 | pub(crate) trait IntoRust: 'static { 41 | type Into; 42 | fn into_rust(self) -> Self::Into; 43 | } 44 | 45 | into_rust!(bool, Duration); 46 | 47 | pub(crate) trait IntoPython: Sized + Send + Sync + 'static { 48 | type Into: IntoPy; 49 | fn into_python(self) -> Self::Into; 50 | fn into_pyobject(self, py: Python) -> PyObject { 51 | self.into_python().into_py(py) 52 | } 53 | } 54 | 55 | impl IntoPython for () { 56 | type Into = (); 57 | fn into_python(self) -> Self::Into { 58 | self 59 | } 60 | } 61 | 62 | impl IntoPython for Option 63 | where 64 | T: IntoPython, 65 | { 66 | type Into = Option; 67 | 68 | fn into_python(self) -> Self::Into { 69 | Some(self?.into_python()) 70 | } 71 | } 72 | 73 | pub(crate) trait MapInto { 74 | fn map_into(self) -> T; 75 | } 76 | 77 | impl, U> MapInto> for Option { 78 | fn map_into(self) -> Option { 79 | self.map(Into::into) 80 | } 81 | } 82 | 83 | impl, U, E> MapInto> for Result { 84 | fn map_into(self) -> Result { 85 | self.map(Into::into) 86 | } 87 | } 88 | 89 | pub(crate) fn generic(cls: &Bound, args: &Bound) -> PyObject { 90 | import!(cls.py(), types.GenericAlias) 91 | .call1((cls, args)) 92 | .unwrap() 93 | .unbind() 94 | } 95 | 96 | pub(crate) fn short_type_name() -> &'static str { 97 | let name = std::any::type_name::(); 98 | name.rsplit_once("::").map_or(name, |(_, name)| name) 99 | } 100 | 101 | pub(crate) fn wait( 102 | py: Python, 103 | resolve: impl zenoh::Resolve>, 104 | ) -> PyResult { 105 | py.allow_threads(|| resolve.wait()).into_pyres() 106 | } 107 | 108 | pub(crate) fn timeout(obj: &Bound) -> PyResult> { 109 | if obj.is_none() { 110 | return Ok(None); 111 | } 112 | Duration::try_from_secs_f64(f64::extract_bound(obj)?) 113 | .map(Some) 114 | .map_err(|_| PyValueError::new_err("negative timeout")) 115 | } 116 | -------------------------------------------------------------------------------- /tests/examples_check.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017, 2022 ZettaScale Technology Inc. 2 | import sys 3 | import time 4 | from os import getpgid, killpg, path 5 | from signal import SIGINT 6 | from subprocess import PIPE, Popen, TimeoutExpired 7 | 8 | import fixtures 9 | 10 | # Contributors: 11 | # ZettaScale Zenoh team, 12 | # 13 | # This program and the accompanying materials are made available under the 14 | # terms of the Eclipse Public License 2.0 which is available at 15 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 16 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 17 | 18 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 19 | 20 | 21 | examples = path.realpath(__file__).split("/tests")[0] + "/examples/" 22 | tab = "\t" 23 | ret = "\r\n" 24 | 25 | 26 | class Pyrun(fixtures.Fixture): 27 | def __init__(self, p, args=None) -> None: 28 | if args is None: 29 | args = [] 30 | self.name = p 31 | print(f"starting {self.name}") 32 | self.process: Popen = Popen( 33 | ["python3", path.join(examples, p), *args], 34 | stdout=PIPE, 35 | stderr=PIPE, 36 | start_new_session=True, 37 | ) 38 | self.start = time.time() 39 | self.end = None 40 | self.errors = [] 41 | self._stdouts = [] 42 | self._stderrs = [] 43 | 44 | def _setUp(self): 45 | self.addCleanup(self.process.send_signal, SIGINT) 46 | 47 | def dbg(self): 48 | self.wait() 49 | print(f"{self.name} stdout:") 50 | print(f"{tab}{tab.join(self.stdout)}") 51 | print(f"{self.name} stderr:") 52 | print(f"{tab}{tab.join(self.stderr)}") 53 | 54 | def status(self, expecting=0): 55 | status = self.wait() 56 | formatted = ( 57 | f"{self.name}: returned {status} (expected {-expecting}) - {self.time:.2}s" 58 | ) 59 | print(formatted) 60 | return formatted if status != -expecting else None 61 | 62 | def wait(self): 63 | try: 64 | code = self.process.wait(timeout=10) 65 | except TimeoutExpired: 66 | self.process.send_signal(SIGINT) 67 | code = self.process.wait(timeout=10) 68 | if self.end is None: 69 | self.end = time.time() 70 | return code 71 | 72 | def interrupt(self): 73 | # send SIGINT to process group 74 | pgid = getpgid(self.process.pid) 75 | killpg(pgid, SIGINT) 76 | return self.status(SIGINT) 77 | 78 | @property 79 | def stdout(self): 80 | self._stdouts.extend( 81 | line.decode("utf8") for line in self.process.stdout.readlines() 82 | ) 83 | return self._stdouts 84 | 85 | @property 86 | def stderr(self): 87 | self._stderrs.extend( 88 | line.decode("utf8") for line in self.process.stderr.readlines() 89 | ) 90 | return self._stderrs 91 | 92 | @property 93 | def time(self): 94 | return None if self.end is None else (self.end - self.start) 95 | 96 | 97 | def test_z_bytes(): 98 | """Test z_bytes.""" 99 | z_bytes = Pyrun("z_bytes.py") 100 | if sys.version_info >= (3, 9): 101 | if error := z_bytes.status(): 102 | z_bytes.dbg() 103 | z_bytes.errors.append(error) 104 | 105 | assert not z_bytes.errors 106 | 107 | 108 | def test_z_info_z_scout(): 109 | z_info = Pyrun("z_info.py") 110 | z_scout = Pyrun("z_scout.py") 111 | if error := z_info.status(): 112 | z_info.dbg() 113 | z_info.errors.append(error) 114 | 115 | if error := z_scout.status(): 116 | z_scout.dbg() 117 | z_scout.errors.append(error) 118 | 119 | assert not z_info.errors 120 | assert not z_scout.errors 121 | 122 | 123 | def test_z_get_z_queryable(): 124 | """Test z_get & z_queryable""" 125 | z_queryable = Pyrun("z_queryable.py", ["-k=demo/example/zenoh-python-queryable"]) 126 | time.sleep(3) 127 | ## z_get: Able to get reply from queryable 128 | z_get = Pyrun("z_get.py", ["-s=demo/example/zenoh-python-queryable"]) 129 | if error := z_get.status(): 130 | z_get.dbg() 131 | z_get.errors.append(error) 132 | 133 | z_queryable.interrupt() 134 | 135 | if not ( 136 | "Received ('demo/example/zenoh-python-queryable': 'Queryable from Python!')" 137 | in "".join(z_get.stdout) 138 | ): 139 | z_get.dbg() 140 | z_queryable.dbg() 141 | z_get.errors.append("z_get didn't get a response from z_queryable") 142 | queryableout = "".join(z_queryable.stdout) 143 | if not ("Received Query 'demo/example/zenoh-python-queryable'" in queryableout): 144 | z_queryable.errors.append("z_queryable didn't catch query") 145 | if any(("z_queryable" in error) for error in z_queryable.errors): 146 | z_queryable.dbg() 147 | 148 | assert not z_get.errors 149 | assert not z_queryable.errors 150 | 151 | 152 | def test_z_querier_z_queryable(): 153 | """Test z_querier & z_queryable""" 154 | z_queryable = Pyrun("z_queryable.py", ["-k=demo/example/zenoh-python-queryable"]) 155 | time.sleep(3) 156 | ## z_querier: Able to get reply from queryable 157 | z_querier = Pyrun( 158 | "z_querier.py", ["-s=demo/example/zenoh-python-queryable", "-p=value"] 159 | ) 160 | time.sleep(5) 161 | z_queryable.interrupt() 162 | z_querier.interrupt() 163 | 164 | if not ( 165 | "Received ('demo/example/zenoh-python-queryable': 'Queryable from Python!')" 166 | in "".join(z_querier.stdout) 167 | ): 168 | z_querier.dbg() 169 | z_queryable.dbg() 170 | z_querier.errors.append("z_querier didn't get a response from z_queryable") 171 | queryableout = "".join(z_queryable.stdout) 172 | if not ( 173 | "Received Query 'demo/example/zenoh-python-queryable' with payload: '[ 0] value'" 174 | in queryableout 175 | ): 176 | z_queryable.errors.append("z_queryable didn't catch query [0]") 177 | elif not ( 178 | "Received Query 'demo/example/zenoh-python-queryable' with payload: '[ 2] value'" 179 | in queryableout 180 | ): 181 | z_queryable.errors.append("z_queryable didn't catch query [2]") 182 | if any(("z_queryable" in error) for error in z_queryable.errors): 183 | z_queryable.dbg() 184 | 185 | assert not z_querier.errors 186 | assert not z_queryable.errors 187 | 188 | 189 | def test_z_storage_z_sub(): 190 | """Test z_storage & z_sub.""" 191 | z_storage = Pyrun("z_storage.py") 192 | z_sub = Pyrun("z_sub.py") 193 | time.sleep(3) 194 | ## z_put: Put one message (to storage & sub) 195 | z_put = Pyrun("z_put.py") 196 | time.sleep(1) 197 | ## z_pub: Put two messages (to storage & sub) 198 | pub = Pyrun("z_pub.py", ["--iter=2"]) 199 | time.sleep(1) 200 | z_get = Pyrun("z_get.py", ["-s=demo/example/zenoh-python-put"]) 201 | if error := z_put.status(): 202 | z_put.dbg() 203 | z_put.errors.append(error) 204 | 205 | if error := z_get.status(): 206 | z_get.dbg() 207 | z_get.errors.append(error) 208 | 209 | if not ( 210 | "Received ('demo/example/zenoh-python-put': 'Put from Python!')" 211 | in "".join(z_get.stdout) 212 | ): 213 | z_get.dbg() 214 | z_get.errors.append("z_get didn't get a response from z_storage about put") 215 | if any(("z_get" in error) for error in z_get.errors): 216 | z_get.dbg() 217 | time.sleep(1) 218 | 219 | z_delete = Pyrun("z_delete.py") 220 | if error := z_delete.status(): 221 | z_delete.dbg() 222 | z_delete.errors.append(error) 223 | time.sleep(1) 224 | 225 | ## z_get: Unable to get put from storage 226 | z_get = Pyrun("z_get.py", ["-s=demo/example/zenoh-python-put"]) 227 | if error := z_get.status(): 228 | z_get.dbg() 229 | z_get.errors.append(error) 230 | if "Received ('demo/example/zenoh-python-put': 'Put from Python!')" in "".join( 231 | z_get.stdout 232 | ): 233 | z_storage.dbg() 234 | z_get.errors.append( 235 | "z_get did get a response from z_storage about put after delete" 236 | ) 237 | if any(("z_get" in error) for error in z_get.errors): 238 | z_get.dbg() 239 | time.sleep(1) 240 | 241 | ## z_sub: Should receive put, pub and delete 242 | if error := z_sub.process.send_signal(SIGINT): 243 | z_sub.dbg() 244 | z_sub.errors.append(error) 245 | subout = "".join(z_sub.stdout) 246 | if not ( 247 | "Received SampleKind.PUT ('demo/example/zenoh-python-put': 'Put from Python!')" 248 | in subout 249 | ): 250 | z_sub.errors.append("z_sub didn't catch put") 251 | if not ( 252 | "Received SampleKind.PUT ('demo/example/zenoh-python-pub': '[ 1] Pub from Python!')" 253 | in subout 254 | ): 255 | z_sub.errors.append("z_sub didn't catch second z_pub") 256 | if not ( 257 | "Received SampleKind.DELETE ('demo/example/zenoh-python-put': '')" in subout 258 | ): 259 | z_sub.errors.append("z_sub didn't catch delete") 260 | if any(("z_sub" in error) for error in z_sub.errors): 261 | z_sub.dbg() 262 | 263 | ## z_storage: Should receive put, pub, delete, and query 264 | if error := z_storage.process.send_signal(SIGINT): 265 | z_storage.dbg() 266 | z_storage.errors.append(error) 267 | storageout = "".join(z_storage.stdout) 268 | if not ( 269 | "Received SampleKind.PUT ('demo/example/zenoh-python-put': 'Put from Python!')" 270 | in storageout 271 | ): 272 | z_storage.errors.append("z_storage didn't catch put") 273 | if not ( 274 | "Received SampleKind.PUT ('demo/example/zenoh-python-pub': '[ 1] Pub from Python!')" 275 | in storageout 276 | ): 277 | z_storage.errors.append("z_storage didn't catch second z_pub") 278 | if not ( 279 | "Received SampleKind.DELETE ('demo/example/zenoh-python-put': '')" in storageout 280 | ): 281 | z_storage.errors.append("z_storage didn't catch delete") 282 | if not ("Received Query 'demo/example/zenoh-python-put'" in storageout): 283 | z_storage.errors.append("z_storage didn't catch query") 284 | if any(("z_storage" in error) for error in z_storage.errors): 285 | z_storage.dbg() 286 | 287 | assert not z_sub.errors 288 | assert not z_storage.errors 289 | assert not z_get.errors 290 | 291 | 292 | def test_z_pull_z_sub_queued(): 293 | """Test z_pull & z_sub_queued.""" 294 | ## Run z_pull and z_sub_queued 295 | sub_queued = Pyrun("z_sub_queued.py") 296 | time.sleep(3) 297 | pull = Pyrun("z_pull.py", ["--size=1", "--interval=1"]) 298 | time.sleep(3) 299 | ## z_pub: Put two messages (to storage & sub) 300 | pub = Pyrun("z_pub.py", ["--iter=2", "--interval=0"]) 301 | if error := pub.status(): 302 | pub.dbg() 303 | pub.errors.append(error) 304 | ## z_sub_queued: Should receive two messages 305 | if error := sub_queued.interrupt(): 306 | sub_queued.dbg() 307 | sub_queued.errors.append(error) 308 | sub_queued_out = "".join(sub_queued.stdout) 309 | if not ( 310 | "Received SampleKind.PUT ('demo/example/zenoh-python-pub': '[ 0] Pub from Python!')" 311 | in sub_queued_out 312 | ): 313 | sub_queued.errors.append("z_sub_queued didn't catch the first z_pub") 314 | if not ( 315 | "Received SampleKind.PUT ('demo/example/zenoh-python-pub': '[ 1] Pub from Python!')" 316 | in sub_queued_out 317 | ): 318 | sub_queued.errors.append("z_sub_queued didn't catch the second z_pub") 319 | if any(("z_sub_queued" in error) for error in sub_queued.errors): 320 | sub_queued.dbg() 321 | ## z_pull: Should only receive the last messages 322 | time.sleep(3) 323 | if error := pull.interrupt(): 324 | pull.dbg() 325 | pull.errors.append(error) 326 | pullout = "".join(pull.stdout) 327 | if ( 328 | "Received SampleKind.PUT ('demo/example/zenoh-python-pub': '[ 0] Pub from Python!')" 329 | in pullout 330 | ): 331 | pull.errors.append("z_pull shouldn't catch the old z_pub") 332 | if not ( 333 | "Received SampleKind.PUT ('demo/example/zenoh-python-pub': '[ 1] Pub from Python!')" 334 | in pullout 335 | ): 336 | pull.errors.append("z_pull didn't catch the last z_pub") 337 | if any(("z_pull" in error) for error in pull.errors): 338 | pull.dbg() 339 | 340 | assert not pub.errors 341 | assert not sub_queued.errors 342 | assert not pull.errors 343 | 344 | 345 | def test_z_sub_thr_z_pub_thr(): 346 | """Test z_sub_thr & z_pub_thr.""" 347 | sub_thr = Pyrun("z_sub_thr.py") 348 | pub_thr = Pyrun("z_pub_thr.py", ["128"]) 349 | time.sleep(5) 350 | if error := sub_thr.interrupt(): 351 | sub_thr.dbg() 352 | sub_thr.errors.append(error) 353 | if error := pub_thr.interrupt(): 354 | pub_thr.dbg() 355 | pub_thr.errors.append(error) 356 | 357 | assert not sub_thr.errors 358 | assert not pub_thr.errors 359 | -------------------------------------------------------------------------------- /tests/stubs_check.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import ast 15 | import importlib 16 | import inspect 17 | from inspect import Parameter 18 | from pathlib import Path 19 | from typing import Any 20 | 21 | PACKAGE = (Path(__file__) / "../../zenoh").resolve() 22 | 23 | 24 | class CheckExported(ast.NodeVisitor): 25 | def __init__(self, module: Any): 26 | self.module = module 27 | self.current_cls = None 28 | 29 | def visit_ClassDef(self, node: ast.ClassDef): 30 | # register the current class for method name disambiguation 31 | self.current_cls = getattr(self.module, node.name) 32 | getattr(self.current_cls, "__repr__") 33 | self.generic_visit(node) 34 | self.current_cls = None 35 | 36 | def visit_FunctionDef(self, node: ast.FunctionDef): 37 | if node.name == "_unstable": 38 | return 39 | func = getattr(self.current_cls or self.module, node.name) 40 | if node.name.startswith("__") or node.name.endswith("serializer"): 41 | pass 42 | elif callable(func): 43 | sig_params = { 44 | p.name: (p.kind, p.default is not Parameter.empty) 45 | for p in inspect.signature(func).parameters.values() 46 | if p.name != "background" 47 | } 48 | node_params = {} 49 | for i, arg in enumerate(node.args.posonlyargs): 50 | node_params[arg.arg] = ( 51 | Parameter.POSITIONAL_ONLY, 52 | len(node.args.defaults) 53 | >= len(node.args.args) + len(node.args.posonlyargs) - i, 54 | ) 55 | for i, arg in enumerate(node.args.args): 56 | node_params[arg.arg] = ( 57 | Parameter.POSITIONAL_OR_KEYWORD, 58 | len(node.args.defaults) >= len(node.args.args) - i, 59 | ) 60 | if arg := node.args.vararg: 61 | node_params[arg.arg] = (Parameter.VAR_POSITIONAL, False) 62 | for arg, default in zip(node.args.kwonlyargs, node.args.kw_defaults): 63 | node_params[arg.arg] = (Parameter.KEYWORD_ONLY, default is not None) 64 | if arg := node.args.kwarg: 65 | node_params[arg.arg] = (Parameter.VAR_KEYWORD, False) 66 | node_params.pop("cls", ...) 67 | if "self" in node_params: 68 | node_params["self"] = (Parameter.POSITIONAL_ONLY, False) 69 | if (param := node_params.get("handler")) and not param[1]: 70 | return 71 | assert ( 72 | sig_params == node_params 73 | ), f"{self.current_cls=}\n{func=}\n{sig_params=}\n{node_params=}" 74 | else: 75 | getattr(func, "__get__") 76 | 77 | def visit_AnnAssign(self, node: ast.AnnAssign): 78 | if self.current_cls is not None: 79 | assert isinstance(node.target, ast.Name) 80 | getattr(self.current_cls, node.target.id) 81 | 82 | 83 | def main(): 84 | for entry in PACKAGE.glob("*.pyi"): 85 | with open(entry) as f: 86 | parts = list(entry.relative_to(PACKAGE.parent).parts) 87 | parts[-1] = parts[-1].rstrip(".pyi") 88 | module_name = ".".join(p for p in parts if p != "__init__") 89 | visitor = CheckExported(importlib.import_module(module_name)) 90 | visitor.visit(ast.parse(f.read())) 91 | 92 | 93 | if __name__ == "__main__": 94 | main() 95 | -------------------------------------------------------------------------------- /tests/test_serializer.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import sys 15 | from dataclasses import dataclass 16 | 17 | import pytest 18 | 19 | from zenoh import ZBytes 20 | from zenoh.ext import ( 21 | Float32, 22 | Float64, 23 | Int8, 24 | Int16, 25 | Int32, 26 | Int64, 27 | Int128, 28 | UInt8, 29 | UInt16, 30 | UInt32, 31 | UInt64, 32 | UInt128, 33 | z_deserialize, 34 | z_serialize, 35 | ) 36 | 37 | default_serializer_tests = [ 38 | (ZBytes, ZBytes(b"foo")), 39 | (bytes, b"foo"), 40 | (bytearray, bytearray(b"foo")), 41 | (str, "foo"), 42 | *( 43 | (tp, tp(i)) 44 | for i in (-42, 42) 45 | for tp in (int, Int8, Int16, Int32, Int64, Int128) 46 | ), 47 | *((tp, tp(42)) for tp in (UInt8, UInt16, UInt32, UInt64, UInt128)), 48 | (float, 0.5), 49 | (Float64, Float64(0.5)), 50 | (Float32, Float32(0.5)), 51 | (bool, True), 52 | (list[int], [0, 1, 2]), 53 | (tuple[int, int], (0, 1)), 54 | (dict[str, str], {"foo": "bar"}), 55 | (set[int], {0, 1, 2}), 56 | (frozenset[int], frozenset([0, 1, 2])), 57 | (list[tuple[float, Float32]], [(0.0, Float32(0.5)), (1.5, Float32(2))]), 58 | ] 59 | 60 | 61 | @pytest.mark.parametrize("tp, value", default_serializer_tests) 62 | def test_default_serializer(tp, value): 63 | zbytes = z_serialize(value) 64 | assert z_deserialize(tp, zbytes) == value 65 | -------------------------------------------------------------------------------- /tests/test_session.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | import json 15 | import time 16 | from typing import List, Tuple 17 | 18 | import zenoh 19 | from zenoh import CongestionControl, Priority, Query, Sample, Session 20 | 21 | SLEEP = 1 22 | MSG_COUNT = 1_000 23 | MSG_SIZE = [1_024, 131_072] 24 | 25 | 26 | def open_session(endpoints: List[str]) -> Tuple[Session, Session]: 27 | # listen peer 28 | conf = zenoh.Config() 29 | conf.insert_json5("listen/endpoints", json.dumps(endpoints)) 30 | conf.insert_json5("scouting/multicast/enabled", "false") 31 | print("[ ][01a] Opening peer01 session") 32 | peer01 = zenoh.open(conf) 33 | 34 | # connect peer 35 | conf = zenoh.Config() 36 | conf.insert_json5("connect/endpoints", json.dumps(endpoints)) 37 | conf.insert_json5("scouting/multicast/enabled", "false") 38 | print("[ ][02a] Opening peer02 session") 39 | peer02 = zenoh.open(conf) 40 | 41 | return (peer01, peer02) 42 | 43 | 44 | def close_session(peer01: Session, peer02: Session): 45 | print("[ ][01e] Closing peer01 session") 46 | peer01.close() 47 | print("[ ][02e] Closing peer02 session") 48 | peer02.close() 49 | 50 | 51 | def run_session_qryrep(peer01: Session, peer02: Session): 52 | keyexpr = "test/session" 53 | 54 | for size in MSG_SIZE: 55 | num_requests = 0 56 | num_replies = 0 57 | num_errors = 0 58 | 59 | def queryable_callback(query: Query): 60 | nonlocal num_requests 61 | query.reply(keyexpr, bytes(size)) 62 | num_requests += 1 63 | 64 | print("[QR][01c] Queryable on peer01 session") 65 | queryable = peer01.declare_queryable( 66 | keyexpr, queryable_callback, complete=False 67 | ) 68 | 69 | time.sleep(SLEEP) 70 | 71 | print(f"[QR][02c] Getting on peer02 session. {MSG_COUNT} msgs.") 72 | for _ in range(MSG_COUNT): 73 | replies = peer02.get(keyexpr) 74 | for reply in replies: 75 | try: 76 | unwraped_reply = reply.ok 77 | except: 78 | unwraped_reply = None 79 | 80 | if unwraped_reply: 81 | assert len(unwraped_reply.payload) == size 82 | num_replies += 1 83 | else: 84 | num_errors += 1 85 | 86 | time.sleep(SLEEP) 87 | print(f"[QR][02c] Got on peer02 session. {num_replies}/{MSG_COUNT} msgs.") 88 | assert num_replies == MSG_COUNT 89 | assert num_requests == MSG_COUNT 90 | assert num_errors == 0 91 | 92 | print("[QR][03c] Unqueryable on peer01 session") 93 | queryable.undeclare() 94 | 95 | 96 | def run_session_qrrrep(peer01: Session, peer02: Session): 97 | keyexpr = "test/querier" 98 | 99 | for size in MSG_SIZE: 100 | num_requests = 0 101 | num_replies = 0 102 | num_errors = 0 103 | 104 | def queryable_callback(query: Query): 105 | nonlocal num_requests 106 | query.reply(keyexpr, bytes(size)) 107 | num_requests += 1 108 | 109 | print("[QR][01c] Queryable on peer01 session") 110 | queryable = peer01.declare_queryable( 111 | keyexpr, queryable_callback, complete=False 112 | ) 113 | 114 | time.sleep(SLEEP) 115 | 116 | print(f"[QR][02c] Declaring querier on peer02 session.") 117 | querier = peer02.declare_querier(keyexpr) 118 | print(f"[QR][03c] Sending {MSG_COUNT} queries.") 119 | for _ in range(MSG_COUNT): 120 | replies = querier.get() 121 | for reply in replies: 122 | try: 123 | unwraped_reply = reply.ok 124 | except: 125 | unwraped_reply = None 126 | 127 | if unwraped_reply: 128 | assert len(unwraped_reply.payload) == size 129 | num_replies += 1 130 | else: 131 | num_errors += 1 132 | 133 | time.sleep(SLEEP) 134 | print(f"[QR][03c] Got on querier {num_replies}/{MSG_COUNT} replies.") 135 | assert num_replies == MSG_COUNT 136 | assert num_requests == MSG_COUNT 137 | assert num_errors == 0 138 | 139 | print("[QR][04c] Undeclare querier on peer02 session") 140 | querier.undeclare() 141 | 142 | print("[QR][05c] Unqueryable on peer01 session") 143 | queryable.undeclare() 144 | 145 | 146 | def run_session_pubsub(peer01: Session, peer02: Session): 147 | keyexpr = "test_pub/session" 148 | msg = "Pub Message".encode() 149 | 150 | num_received = 0 151 | num_errors = 0 152 | 153 | def sub_callback(sample: Sample): 154 | nonlocal num_received 155 | nonlocal num_errors 156 | if ( 157 | sample.key_expr != keyexpr 158 | or sample.priority != Priority.DATA_HIGH 159 | or sample.congestion_control != CongestionControl.BLOCK 160 | or bytes(sample.payload) != msg 161 | ): 162 | num_errors += 1 163 | num_received += 1 164 | 165 | print("[PS][01d] Publisher on peer01 session") 166 | publisher = peer01.declare_publisher( 167 | keyexpr, priority=Priority.DATA_HIGH, congestion_control=CongestionControl.BLOCK 168 | ) 169 | time.sleep(SLEEP) 170 | 171 | print(f"[PS][02d] Subscriber on peer02 session. {MSG_COUNT} msgs.") 172 | subscriber = peer02.declare_subscriber(keyexpr, sub_callback) 173 | time.sleep(SLEEP) 174 | 175 | for _ in range(0, MSG_COUNT): 176 | publisher.put("Pub Message") 177 | 178 | time.sleep(SLEEP) 179 | print(f"[PS][02d] Received on peer02 session. {num_received}/{MSG_COUNT} msgs.") 180 | assert num_received == MSG_COUNT 181 | assert num_errors == 0 182 | 183 | print("[PS][03d] Undeclare publisher on peer01 session") 184 | publisher.undeclare() 185 | print("[PS][04d] Undeclare subscriber on peer02 session") 186 | subscriber.undeclare() 187 | 188 | 189 | def test_session(): 190 | zenoh.try_init_log_from_env() 191 | (peer01, peer02) = open_session(["tcp/127.0.0.1:17447"]) 192 | run_session_qryrep(peer01, peer02) 193 | run_session_pubsub(peer01, peer02) 194 | run_session_qrrrep(peer01, peer02) 195 | close_session(peer01, peer02) 196 | -------------------------------------------------------------------------------- /zenoh-dragon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclipse-zenoh/zenoh-python/8862ae7a07bd23d3fafe344d87034152e9670f4d/zenoh-dragon.png -------------------------------------------------------------------------------- /zenoh/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | from .zenoh import * 15 | 16 | try: 17 | from . import ext 18 | except ImportError: 19 | pass 20 | -------------------------------------------------------------------------------- /zenoh/ext.py: -------------------------------------------------------------------------------- 1 | try: 2 | from zenoh._ext import * 3 | except ImportError: 4 | import warnings 5 | 6 | raise ModuleNotFoundError( 7 | "No module named 'zenoh.ext'.\nzenoh must be built wit zenoh-ext feature to enable it." 8 | ) from None 9 | 10 | _INT8_MIN = -(1 << 7) 11 | _INT16_MIN = -(1 << 15) 12 | _INT32_MIN = -(1 << 31) 13 | _INT64_MIN = -(1 << 63) 14 | _INT128_MIN = -(1 << 127) 15 | 16 | _INT8_MAX = 1 << 7 17 | _INT16_MAX = 1 << 15 18 | _INT32_MAX = 1 << 31 19 | _INT64_MAX = 1 << 63 20 | _INT128_MAX = 1 << 127 21 | 22 | _UINT8_MAX = 1 << 8 23 | _UINT16_MAX = 1 << 16 24 | _UINT32_MAX = 1 << 32 25 | _UINT64_MAX = 1 << 64 26 | _UINT128_MAX = 1 << 128 27 | 28 | 29 | class Int8(int): 30 | """int subclass enabling to (de)serialize 8bit signed integer.""" 31 | 32 | def __new__(cls, i: int): 33 | assert _INT8_MIN <= i < _INT8_MAX, f"{i} too big for Int8" 34 | return int.__new__(cls, i) 35 | 36 | 37 | class Int16(int): 38 | """int subclass enabling to (de)serialize 16bit signed integer.""" 39 | 40 | def __new__(cls, i: int): 41 | assert _INT16_MIN <= i < _INT16_MAX, f"{i} too big for Int16" 42 | return int.__new__(cls, i) 43 | 44 | 45 | class Int32(int): 46 | """int subclass enabling to (de)serialize 32bit signed integer.""" 47 | 48 | def __new__(cls, i: int): 49 | assert _INT32_MIN <= i < _INT32_MAX, f"{i} too big for Int32" 50 | return int.__new__(cls, i) 51 | 52 | 53 | class Int64(int): 54 | """int subclass enabling to (de)serialize 64bit signed integer.""" 55 | 56 | def __new__(cls, i: int): 57 | assert _INT64_MIN <= i < _INT64_MAX, f"{i} too big for Int64" 58 | return int.__new__(cls, i) 59 | 60 | 61 | class Int128(int): 62 | """int subclass enabling to (de)serialize 128bit signed integer.""" 63 | 64 | def __new__(cls, i: int): 65 | assert _INT128_MIN <= i < _INT128_MAX, f"{i} too big for Int128" 66 | return int.__new__(cls, i) 67 | 68 | 69 | class UInt8(int): 70 | """int subclass enabling to (de)serialize 8bit unsigned integer.""" 71 | 72 | def __new__(cls, i: int): 73 | assert 0 <= i < _UINT8_MAX, f"{i} too big for UInt8" 74 | return int.__new__(cls, i) 75 | 76 | 77 | class UInt16(int): 78 | """int subclass enabling to (de)serialize 16bit unsigned integer.""" 79 | 80 | def __new__(cls, i: int): 81 | assert 0 <= i < _UINT16_MAX, f"{i} too big for UInt16" 82 | return int.__new__(cls, i) 83 | 84 | 85 | class UInt32(int): 86 | """int subclass enabling to (de)serialize 32bit unsigned integer.""" 87 | 88 | def __new__(cls, i: int): 89 | assert 0 <= i < _UINT32_MAX, f"{i} too big for UInt32" 90 | return int.__new__(cls, i) 91 | 92 | 93 | class UInt64(int): 94 | """int subclass enabling to (de)serialize 64bit unsigned integer.""" 95 | 96 | def __new__(cls, i: int): 97 | assert 0 <= i < _UINT64_MAX, f"{i} too big for UInt64" 98 | return int.__new__(cls, i) 99 | 100 | 101 | class UInt128(int): 102 | """int subclass enabling to (de)serialize 128bit unsigned integer.""" 103 | 104 | def __new__(cls, i: int): 105 | assert 0 <= i < _UINT128_MAX, f"{i} too big for UInt128" 106 | return int.__new__(cls, i) 107 | 108 | 109 | class Float32(float): 110 | """float subclass enabling to (de)serialize 32bit floating point numbers.""" 111 | 112 | 113 | class Float64(float): 114 | """float subclass enabling to (de)serialize 64bit floating point numbers.""" 115 | -------------------------------------------------------------------------------- /zenoh/ext.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, TypeVar 2 | 3 | from zenoh import ZBytes 4 | 5 | _T = TypeVar("_T") 6 | 7 | class Int8(int): 8 | """int subclass enabling to (de)serialize 8bit signed integer.""" 9 | 10 | class Int16(int): 11 | """int subclass enabling to (de)serialize 16bit signed integer.""" 12 | 13 | class Int32(int): 14 | """int subclass enabling to (de)serialize 32bit signed integer.""" 15 | 16 | class Int64(int): 17 | """int subclass enabling to (de)serialize 64bit signed integer.""" 18 | 19 | class Int128(int): 20 | """int subclass enabling to (de)serialize 128bit signed integer.""" 21 | 22 | class UInt8(int): 23 | """int subclass enabling to (de)serialize 8bit unsigned integer.""" 24 | 25 | class UInt16(int): 26 | """int subclass enabling to (de)serialize 16bit unsigned integer.""" 27 | 28 | class UInt32(int): 29 | """int subclass enabling to (de)serialize 32bit unsigned integer.""" 30 | 31 | class UInt64(int): 32 | """int subclass enabling to (de)serialize 64bit unsigned integer.""" 33 | 34 | class UInt128(int): 35 | """int subclass enabling to (de)serialize 128bit unsigned integer.""" 36 | 37 | class Float32(float): 38 | """float subclass enabling to (de)serialize 32bit floating point numbers.""" 39 | 40 | class Float64(float): 41 | """float subclass enabling to (de)serialize 64bit floating point numbers.""" 42 | 43 | class ZDeserializeError(Exception): 44 | pass 45 | 46 | def z_serialize(obj: Any) -> ZBytes: ... 47 | def z_deserialize(tp: type[_T], zbytes: ZBytes) -> _T: ... 48 | -------------------------------------------------------------------------------- /zenoh/handlers.pyi: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2024 ZettaScale Technology 3 | # 4 | # This program and the accompanying materials are made available under the 5 | # terms of the Eclipse Public License 2.0 which is available at 6 | # http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 7 | # which is available at https://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 10 | # 11 | # Contributors: 12 | # ZettaScale Zenoh Team, 13 | # 14 | from collections.abc import Callable 15 | from typing import Any, Generic, Protocol, Self, TypeVar, final 16 | 17 | _T = TypeVar("_T") 18 | 19 | @final 20 | class Handler(Generic[_T]): 21 | """Handler for `DefaultHandler`/`FifoHandler`/`RingHandler`.""" 22 | 23 | def try_recv(self) -> _T | None: ... 24 | def recv(self) -> _T: ... 25 | def __iter__(self) -> Self: ... 26 | def __next__(self) -> _T: ... 27 | 28 | @final 29 | class DefaultHandler(Generic[_T]): 30 | """The default handler in Zenoh is a FIFO queue.""" 31 | 32 | ... 33 | 34 | @final 35 | class FifoChannel(Generic[_T]): 36 | """The default handler in Zenoh is a FIFO queue.""" 37 | 38 | def __new__(cls, capacity: int) -> Self: ... 39 | 40 | @final 41 | class RingChannel(Generic[_T]): 42 | """A synchrounous ring channel with a limited size that allows users to keep the last N data.""" 43 | 44 | def __new__(cls, capacity: int) -> Self: ... 45 | 46 | @final 47 | class Callback(Generic[_T]): 48 | def __new__( 49 | cls, 50 | callback: Callable[[_T], Any], 51 | drop: Callable[[], Any] | None = None, 52 | *, 53 | indirect: bool = True, 54 | ) -> Self: ... 55 | def __call__(self, arg: _T, /) -> Any: ... 56 | @property 57 | def callback(self) -> Callable[[_T], Any]: ... 58 | @property 59 | def drop(self) -> Callable[[], Any] | None: ... 60 | @property 61 | def indirect(self) -> bool: ... 62 | -------------------------------------------------------------------------------- /zenoh/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eclipse-zenoh/zenoh-python/8862ae7a07bd23d3fafe344d87034152e9670f4d/zenoh/py.typed --------------------------------------------------------------------------------