pax_global_header00006660000000000000000000000064150466420430014516gustar00rootroot0000000000000052 comment=2637127b41f633bc8885bc786f05e3d39b0308a4 Yakifo-amqtt-2637127/000077500000000000000000000000001504664204300142615ustar00rootroot00000000000000Yakifo-amqtt-2637127/.codecov.yml000066400000000000000000000001061504664204300165010ustar00rootroot00000000000000--- coverage: status: patch: default: target: 80% Yakifo-amqtt-2637127/.coveragerc000066400000000000000000000002241504664204300164000ustar00rootroot00000000000000[run] branch = True source = bumper omit = tests/* amqtt/scripts/*.py [report] exclude_lines = pragma: no cover if TYPE_CHECKING: Yakifo-amqtt-2637127/.dockerignore000066400000000000000000000001061504664204300167320ustar00rootroot00000000000000docs/** docs_test/** docs_web/** tests/** htmlcov/** cache/** dist/** Yakifo-amqtt-2637127/.gitattributes000066400000000000000000000013311504664204300171520ustar00rootroot00000000000000# https://docs.github.com/en/repositories/working-with-files/managing-files/customizing-how-changed-files-appear-on-github # Default behavior - Auto detect text files and perform LF normalization * text=auto # https://docs.github.com/en/get-started/getting-started-with-git/configuring-git-to-handle-line-endings # Ensure to read article prior to adding # Scripts should have Unix endings *.py text eol=lf *.sh text eol=lf # Windows Batch or PowerShell scripts should have CRLF endings *.bat text eol=crlf *.ps1 text eol=crlf # adding github settings to show correct language *.sh linguist-detectable=true *.yml linguist-detectable=true *.ps1 linguist-detectable=true *.j2 linguist-detectable=true *.md linguist-documentation Yakifo-amqtt-2637127/.github/000077500000000000000000000000001504664204300156215ustar00rootroot00000000000000Yakifo-amqtt-2637127/.github/discusion_template/000077500000000000000000000000001504664204300215145ustar00rootroot00000000000000Yakifo-amqtt-2637127/.github/discusion_template/feature-requests.yml000066400000000000000000000006421504664204300255450ustar00rootroot00000000000000--- title: "[Feature Request] " labels: ["enhancement"] body: - type: textarea id: description attributes: label: Description description: A clear and concise description of what you would like to see. validations: required: true - type: textarea id: other attributes: label: Other description: Add any other context or information about the feature request here. Yakifo-amqtt-2637127/.github/issue_template/000077500000000000000000000000001504664204300206445ustar00rootroot00000000000000Yakifo-amqtt-2637127/.github/issue_template/documentation_request.md000066400000000000000000000003521504664204300256070ustar00rootroot00000000000000--- name: Documentation request about: Suggest documentation for this project title: "" labels: documentation assignees: "" --- **Describe the documentation you'd like** A clear and concise description of what you'd want documented. Yakifo-amqtt-2637127/.github/issue_template/feature_request.md000066400000000000000000000011331504664204300243670ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: "" labels: enhancement assignees: "" --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. Yakifo-amqtt-2637127/.github/issue_template/question.md000066400000000000000000000007721504664204300230430ustar00rootroot00000000000000--- name: Bug about: Describe this issue template's purpose here. title: "" labels: bug assignees: "" --- When your issue is related to code that isn't working as expected, please enable debug logs: ```python import logging logging.basicConfig(level=logging.DEBUG) ``` More info can be found in the [Debugging section](https://github.com/mobilityhouse/ocpp#debugging) of the README. If these actions didn't help to find the cause of your issue, please provide code samples and logs with your question. Yakifo-amqtt-2637127/.github/pull_request_template.md000066400000000000000000000007611504664204300225660ustar00rootroot00000000000000### Changes included in this PR _(Bug fix, feature, docs update, ...)_ ### Current behavior _Link to an open issue here..._ ### New behavior _If this is a feature change, describe the new behavior_ ### Impact _Describe breaking changes, including changes a users might need to make due to this PR_ ### Checklist 1. [ ] Does your submission pass the existing tests? 2. [ ] Are there new tests that cover these additions/changes? 3. [ ] Have you linted your code locally before submission? Yakifo-amqtt-2637127/.github/release-drafter.yml000066400000000000000000000017671504664204300214240ustar00rootroot00000000000000--- name-template: "$RESOLVED_VERSION" tag-template: "$RESOLVED_VERSION" change-template: "- #$NUMBER $TITLE @$AUTHOR" sort-direction: ascending filter-by-commitish: true categories: - title: ":boom: Breaking changes" label: "pr: Breaking Change" - title: ":sparkles: New features" label: "pr: new-feature" - title: ":zap: Enhancements" label: "pr: enhancement" - title: ":recycle: Refactor" label: "pr: refactor" - title: ":bug: Bug Fixes" label: "pr: bugfix" - title: ":arrow_up: Dependency Updates" labels: - "pr: dependency-update" - "dependencies" include-labels: - "pr: Breaking Change" - "pr: enhancement" - "pr: dependency-update" - "pr: new-feature" - "pr: bugfix" - "pr: refactor" version-resolver: major: labels: - "pr: Breaking Change" minor: labels: - "pr: enhancement" - "pr: dependency-update" - "pr: new-feature" patch: labels: - "pr: bugfix" default: patch template: | $CHANGES Yakifo-amqtt-2637127/.github/renovate.json000066400000000000000000000014061504664204300203400ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "commitMessagePrefix": "πŸš€", "configMigration": true, "dependencyDashboard": true, "labels": ["dependencies", "no-stale"], "lockFileMaintenance": { "enabled": true }, "packageRules": [ { "addLabels": ["python"], "matchManagers": ["pep621"], "groupName": "Python dependencies", "automerge": false }, { "addLabels": ["github_actions"], "matchManagers": ["github-actions"], "rangeStrategy": "pin", "groupName": "GitHub Actions" }, { "addLabels": ["pre-commit"], "matchManagers": ["pre-commit"], "groupName": "Pre-commit hooks", "automerge": false } ], "rebaseWhen": "behind-base-branch" } Yakifo-amqtt-2637127/.github/workflow_release-drafter.yml000066400000000000000000000007061504664204300233460ustar00rootroot00000000000000--- name: Release Drafter on: push: tags: # Push events to matching v*, i.e. v1.0, v20.15.10 - "v*" workflow_dispatch: jobs: update_release_draft: name: Update Release Draft runs-on: ubuntu-latest permissions: contents: write steps: # https://github.com/release-drafter/release-drafter - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} Yakifo-amqtt-2637127/.github/workflows/000077500000000000000000000000001504664204300176565ustar00rootroot00000000000000Yakifo-amqtt-2637127/.github/workflows/ci.yml000066400000000000000000000054011504664204300207740ustar00rootroot00000000000000--- name: CI on: push: branches: - main - dev pull_request: workflow_dispatch: env: UV_CACHE_DIR: /tmp/.uv-cache PROJECT_PATH: "amqtt" jobs: code-quality: name: Check code quality runs-on: ubuntu-latest steps: # https://github.com/actions/checkout - name: ‡️ Checkout repository uses: actions/checkout@v4 # https://github.com/astral-sh/setup-uv - name: πŸ— Install uv and Python uses: astral-sh/setup-uv@v6 with: enable-cache: true cache-dependency-glob: "uv.lock" cache-local-path: ${{ env.UV_CACHE_DIR }} python-version: "3.13" - name: install openldap dependencies run: sudo apt-get install -y libldap2-dev libsasl2-dev - name: πŸ— Install the project run: uv sync --locked --dev --all-extras - name: Run mypy run: uv run --frozen mypy ${{ env.PROJECT_PATH }}/ - name: Pylint review run: uv run --frozen pylint ${{ env.PROJECT_PATH }}/ - name: Ruff check run: uv run --frozen ruff check ${{ env.PROJECT_PATH }}/ tests: name: Run tests runs-on: ubuntu-latest strategy: matrix: python-version: - "3.10" - "3.11" - "3.12" - "3.13" steps: # https://github.com/actions/checkout - name: ‡️ Checkout repository uses: actions/checkout@v4 # https://github.com/astral-sh/setup-uv - name: πŸ— Install uv and Python ${{ matrix.python-version }} uses: astral-sh/setup-uv@v6 with: enable-cache: true cache-dependency-glob: "uv.lock" cache-local-path: ${{ env.UV_CACHE_DIR }} python-version: ${{ matrix.python-version }} - name: install openldap dependencies run: sudo apt-get install -y libldap2-dev libsasl2-dev - name: πŸ— Install the project run: uv sync --locked --dev --all-extras - name: Run pytest run: uv run --frozen pytest tests/ --cov=./ --cov-report=xml --junitxml=pytest-report.xml # https://github.com/actions/upload-artifact - name: Upload test report uses: actions/upload-artifact@v4 with: name: pytest-report-${{ matrix.python-version }} path: pytest-report.xml # https://github.com/actions/upload-artifact - name: Upload coverage results uses: actions/upload-artifact@v4 with: name: coverage-results-${{ matrix.python-version }} path: coverage.xml # # https://github.com/codecov/codecov-action # - name: Upload coverage to Codecov # uses: codecov/codecov-action@v5 # with: # token: ${{ secrets.CODECOV_TOKEN }} # fail_ci_if_error: true Yakifo-amqtt-2637127/.github/workflows/codeql-analysis.yml000066400000000000000000000017571504664204300235030ustar00rootroot00000000000000--- name: CodeQL on: push: branches: - main - dev pull_request: workflow_dispatch: # schedule: # - cron: "20 10 * * 0" jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["python"] steps: # https://github.com/actions/checkout - name: Checkout repository uses: actions/checkout@v4 # https://github.com/github/codeql-action - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # https://github.com/github/codeql-action - name: Autobuild uses: github/codeql-action/autobuild@v3 # https://github.com/github/codeql-action - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 with: category: "/language:${{matrix.language}}" Yakifo-amqtt-2637127/.gitignore000066400000000000000000000007501504664204300162530ustar00rootroot00000000000000#------- Package & Cache Files ------- *.egg-info __pycache__ node_modules .vite *.pem *.crt *.key *.patch #------- Environment Files ------- .python-version .venv #------- Git Files ------- BASE LOCAL *.orig REMOTE #------- OS Files ------- .DS_Store #------ Database Files ------- *.sqlite3 *.db #------ IDE ------ .idea .vscode/ #----- Built Directory & Files------ .coverage dist/ site/ _build/ .hypothesis/ coverage.xml #----- generated files ----- *.log *memray* .coverage* Yakifo-amqtt-2637127/.pre-commit-config.yaml000066400000000000000000000017301504664204300205430ustar00rootroot00000000000000--- # Pre-commit configuration # For details, visit: https://pre-commit.com/hooks.html ci: autofix_prs: false skip: # These steps run in the CI workflow. Keep in sync. - mypy - pylint repos: # Python-specific hooks ###################################################### - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.10 hooks: - id: ruff args: - --line-length=130 - --exit-non-zero-on-fix # Local hooks for mypy and pylint - repo: local hooks: - id: mypy name: Run Mypy in Virtualenv entry: scripts/run-in-env.sh mypy language: script types: [python] require_serial: true exclude: ^tests/.+|^docs/.+|^samples/.+ - id: pylint name: Run Pylint in Virtualenv entry: scripts/run-in-env.sh pylint language: script types: [python] require_serial: true exclude: ^tests/.+|^docs/.+|^samples/.+ Yakifo-amqtt-2637127/.readthedocs.yaml000066400000000000000000000007631504664204300175160ustar00rootroot00000000000000version: 2 build: os: "ubuntu-24.04" tools: python: "3.13" apt_packages: - libldap2-dev - libsasl2-dev jobs: pre_install: - pip install --upgrade pip - pip install uv - uv venv - uv pip install --group dev --group docs ".[contrib]" - uv run pytest --mock-docker=true build: html: - uv run python -m mkdocs build --clean --site-dir $READTHEDOCS_OUTPUT/html --config-file mkdocs.rtd.yml mkdocs: configuration: mkdocs.rtd.yml Yakifo-amqtt-2637127/CODE_OF_CONDUCT.md000066400000000000000000000062231504664204300170630ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at {{ email }}. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org Yakifo-amqtt-2637127/CONTRIBUTING.md000066400000000000000000000030101504664204300165040ustar00rootroot00000000000000# Contributing to aMQTT :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: The following is a set of guidelines for contributing to aMQTT on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. ## Development Setup ### Requirements 1. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) 2. Use `uv python install ` to install [3.10, 3.11, 3.12 or 3.13](https://docs.astral.sh/uv/guides/install-python/) 3. Fork repository (if you intend to open a pull request) 4. Clone repo `git clone git@github.com:/amqtt.git` 5. Add repo to receive latest updates `git add upstream git@github.com:Yakio/amqtt.git` ### Installation Create and start virtual environment with `UV` ```shell uv venv .venv --python 3.13.0 source .venv/bin/activate ``` Install the package with development (and doc) dependencies: ```shell uv pip install -e . --group dev --group doc ``` Add git pre-commit checks (which parallel the CI checks): ```shell pre-commit install ``` ### Run Run CLI commands: ```shell uv run amqtt uv run amqtt_pub uv run amqtt_sub ``` Run the test case suite: ```shell pytest ``` Run the type checker and linters manually: ```shell pre-commit run --all-files ``` ## Testing When adding a new feature, please add corollary tests. The testing coverage should not decrease. If you encounter a bug when using aMQTT which you then resolve, please reproduce the issue in a test as well. Yakifo-amqtt-2637127/Dockerfile000066400000000000000000000011401504664204300162470ustar00rootroot00000000000000 # -- build stage, install dependencies only using `uv` FROM python:3.13-alpine AS build RUN apk add gcc python3-dev musl-dev linux-headers RUN pip install uv WORKDIR /app COPY . /app RUN uv pip install --target=/deps . # -- final image, copy dependencies and amqtt source FROM python:3.13-alpine WORKDIR /app COPY --from=build /deps /usr/local/lib/python3.13/site-packages/ COPY ./amqtt/scripts/default_broker.yaml /app/conf/broker.yaml EXPOSE 1883 ENV PATH="/usr/local/lib/python3.13/site-packages/bin:$PATH" # Run `amqtt` when the container launches CMD ["amqtt", "-c", "/app/conf/broker.yaml"] Yakifo-amqtt-2637127/LICENSE.md000066400000000000000000000022271504664204300156700ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Nicolas JOUANIN Copyright (c) 2021 aMQTT Contributors (https://github.com/Yakifo/amqtt/graphs/contributors) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Yakifo-amqtt-2637127/MANIFEST.in000066400000000000000000000007011504664204300160150ustar00rootroot00000000000000include *.rst include *.txt include license.txt include samples/passwd include tests/plugins/passwd recursive-include docs *.css recursive-include docs *.html recursive-include docs *.py recursive-include docs *.rst recursive-include docs Makefile recursive-include hbmqtt/scripts *.py recursive-include hbmqtt/scripts *.yaml recursive-include samples *.crt recursive-include samples *.py recursive-include tests *.crt recursive-include tests *.py Yakifo-amqtt-2637127/Makefile000066400000000000000000000014201504664204300157160ustar00rootroot00000000000000# Image name and tag IMAGE_NAME := amqtt IMAGE_TAG := latest VERSION_TAG := 0.11.3 REGISTRY := amqtt/$(IMAGE_NAME) # Platforms to build for PLATFORMS := linux/amd64,linux/arm64 # Default target .PHONY: all all: build # Build multi-platform image .PHONY: build build: docker buildx build \ --platform $(PLATFORMS) \ --tag $(REGISTRY):$(IMAGE_TAG) \ --tag $(REGISTRY):$(VERSION_TAG) \ --file Dockerfile \ --push . # Optional: build without pushing (for local testing) .PHONY: build-local build-local: docker buildx build \ --tag $(REGISTRY):$(IMAGE_TAG) \ --tag $(REGISTRY):$(VERSION_TAG) \ --file Dockerfile \ --load . # Create builder if not exists .PHONY: init init: docker buildx create --use --name multi-builder || true docker buildx inspect --bootstrapYakifo-amqtt-2637127/README.md000066400000000000000000000063501504664204300155440ustar00rootroot00000000000000[![MIT licensed](https://img.shields.io/github/license/Yakifo/amqtt?style=plastic)](https://amqtt.readthedocs.io/en/latest/) [![CI](https://github.com/Yakifo/amqtt/actions/workflows/ci.yml/badge.svg?branch=rc)](https://github.com/Yakifo/amqtt/actions/workflows/ci.yml) [![CodeQL](https://github.com/Yakifo/amqtt/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/Yakifo/amqtt/actions/workflows/codeql-analysis.yml) [![Read the Docs](https://img.shields.io/readthedocs/amqtt/v0.11.0?style=plastic&logo=readthedocs)](https://amqtt.readthedocs.io/) [![Discord](https://dcbadge.limes.pink/api/server/https://discord.gg/S3sP6dDaF3?style=plastic)](https://discord.gg/S3sP6dDaF3) ![Python Version](https://img.shields.io/pypi/pyversions/amqtt?style=plastic&logo=python&logoColor=yellow) ![Python Wheel](https://img.shields.io/pypi/wheel/amqtt?style=plastic) [![PyPI](https://img.shields.io/pypi/v/amqtt?style=plastic&logo=python&logoColor=yellow)](https://pypi.org/project/amqtt/) ![docs/assets/amqtt.svg](https://raw.githubusercontent.com/Yakifo/amqtt/refs/tags/v0.11.0/docs/assets/amqtt.svg) `aMQTT` is an open source [MQTT](http://www.mqtt.org) broker and client[^1], natively implemented with Python's [asyncio](https://docs.python.org/3/library/asyncio.html). ## Features - Full set of [MQTT 3.1.1](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html) protocol specifications - Communication over multiple TCP and/or websocket ports, including support for SSL/TLS - Support QoS 0, QoS 1 and QoS 2 messages flow - Client auto-reconnection on network lost - Plugin framework for functionality expansion; included plugins: - `$SYS` topic publishing - AWS IOT-style shadow states - x509 certificate authentication (including cli cert creation) - Secure file-based password authentication - Configuration-based topic authorization - MySQL, Postgres & SQLite user and/or topic auth (including cli manager) - External server (HTTP) user and/or topic auth - LDAP user and/or topic auth - JWT user and/or topic auth - Fail over session persistence ## Installation `amqtt` is available on [PyPI](https://pypi.python.org/pypi/amqtt) ```bash $ pip install amqtt ``` ## Documentation Available on [Read the Docs](http://amqtt.readthedocs.org/). ## Containerization Launch from [DockerHub](https://hub.docker.com/repositories/amqtt) ```shell $ docker run -d -p 1883:1883 amqtt/amqtt:latest ``` ## Testing The `amqtt` project runs a test aMQTT broker/server at [test.amqtt.io](https://test.amqtt.io) which supports: MQTT, MQTT over TLS, websocket, secure websockets. ## Support Bug reports, patches and suggestions welcome! Just [open an issue](https://github.com/Yakifo/amqtt/issues/new) or join the [discord community](https://discord.gg/S3sP6dDaF3). ## Python Version Compatibility | Version | hbmqtt compatibility | Supported Python Versions | | ------- | -------------------- | ------------------------- | | 0.10.x | yes [^2] | 3.7 - 3.9 | | 0.11.x | no [^3] | 3.10 - 3.13 | [^1]: Forked from [HBMQTT](https://github.com/beerfactory/hbmqtt) after it was deprecated by the original author. [^2]: drop-in replacement [^3]: module renamed and small API differences Yakifo-amqtt-2637127/SECURITY.md000066400000000000000000000002241504664204300160500ustar00rootroot00000000000000# Security Policy ## Reporting a Vulnerability Please use the **issues tracker** to report any security vulnerabilities found in this repository. Yakifo-amqtt-2637127/SUPPORT.md000066400000000000000000000025151504664204300157620ustar00rootroot00000000000000# Support This article explains where to get help with this aMQTT project. Please read through the following guidelines. > πŸ‘‰ **Note**: before participating in our community, please read our > [code of conduct](code_of_conduct.md). > By interacting with this repository, organization, or community you agree to > abide by its terms. ## Asking quality questions Questions can go to [GitHub discussions][chat]. Help us help you! Spend time framing questions and add links and resources. Spending the extra time up front can help save everyone time in the long run. Here are some tips: * Search to find out if a similar question has been asked or a similar issue has been reported * Check to see if a PR is already in progress for the issue you want to raise * Try to define what you need help with: * Is there something in particular you want to do? * What problem are you encountering and what steps have you taken to try and fix it? * Is there a concept you don’t understand? * Provide sample code, such as a [CodeSandbox][cs] or video, if possible * Screenshots can help, but if there’s important text such as code or error messages in them, please also provide those as text * The more time you put into asking your question, the better we can help you ## Contributions See [contributing](contributing.md) on how to contribute. Yakifo-amqtt-2637127/amqtt/000077500000000000000000000000001504664204300154075ustar00rootroot00000000000000Yakifo-amqtt-2637127/amqtt/__init__.py000066400000000000000000000000441504664204300175160ustar00rootroot00000000000000"""INIT.""" __version__ = "0.11.3" Yakifo-amqtt-2637127/amqtt/adapters.py000066400000000000000000000165011504664204300175670ustar00rootroot00000000000000from abc import ABC, abstractmethod from asyncio import StreamReader, StreamWriter from contextlib import suppress import io import logging import ssl from typing import cast from websockets import ConnectionClosed from websockets.asyncio.connection import Connection class ReaderAdapter(ABC): """Base class for all network protocol reader adapters. Reader adapters are used to adapt read operations on the network depending on the protocol used. """ @abstractmethod async def read(self, n: int = -1) -> bytes: """Read up to n bytes. If n is not provided, or set to -1, read until EOF and return all read bytes. If the EOF was received and the internal buffer is empty, return an empty bytes object. :return: packet read as bytes data. """ raise NotImplementedError @abstractmethod def feed_eof(self) -> None: """Acknowledge EOF.""" raise NotImplementedError class WriterAdapter(ABC): """Base class for all network protocol writer adapters. Writer adapters are used to adapt write operations on the network depending on the protocol used. """ @abstractmethod def write(self, data: bytes) -> None: """Write some data to the protocol layer.""" raise NotImplementedError @abstractmethod async def drain(self) -> None: """Let the write buffer of the underlying transport a chance to be flushed.""" raise NotImplementedError @abstractmethod def get_peer_info(self) -> tuple[str, int] | None: """Return peer socket info (remote address and remote port as tuple).""" raise NotImplementedError @abstractmethod def get_ssl_info(self) -> ssl.SSLObject | None: """Return peer certificate information (if available) used to establish a TLS session.""" raise NotImplementedError @abstractmethod async def close(self) -> None: """Close the protocol connection.""" raise NotImplementedError class WebSocketsReader(ReaderAdapter): """WebSockets API reader adapter. This adapter relies on Connection to read from a WebSocket. """ def __init__(self, protocol: Connection) -> None: self._protocol = protocol self._stream = io.BytesIO(b"") async def read(self, n: int = -1) -> bytes: await self._feed_buffer(n) return self._stream.read(n) async def _feed_buffer(self, n: int = 1) -> None: """Feed the data buffer by reading a WebSocket message. :param n: Optional; feed buffer until it contains at least n bytes. Defaults to 1. """ buffer = bytearray(self._stream.read()) message: str | bytes | None = None while len(buffer) < n: with suppress(ConnectionClosed): message = await self._protocol.recv() if message is None: break message = message.encode("utf-8") if isinstance(message, str) else message buffer.extend(message) self._stream = io.BytesIO(buffer) def feed_eof(self) -> None: # NOTE: not implemented?! pass class WebSocketsWriter(WriterAdapter): """WebSockets API writer adapter. This adapter relies on Connection to write to a WebSocket. """ def __init__(self, protocol: Connection) -> None: self._protocol = protocol self._stream = io.BytesIO(b"") def write(self, data: bytes) -> None: """Write some data to the protocol layer.""" self._stream.write(data) async def drain(self) -> None: """Let the write buffer of the underlying transport a chance to be flushed.""" data = self._stream.getvalue() if data and len(data): await self._protocol.send(data) self._stream = io.BytesIO(b"") def get_peer_info(self) -> tuple[str, int] | None: # remote_address can be either a 4-tuple or 2-tuple depending on whether # it is an IPv6 or IPv4 address, so we take their shared (host, port) # prefix here to present a uniform return value. remote_address: tuple[str, int] | None = self._protocol.remote_address[:2] return remote_address def get_ssl_info(self) -> ssl.SSLObject | None: return cast("ssl.SSLObject", self._protocol.transport.get_extra_info("ssl_object")) async def close(self) -> None: await self._protocol.close() class StreamReaderAdapter(ReaderAdapter): """Asyncio Streams API protocol adapter. This adapter relies on StreamReader to read from a TCP socket. Because API is very close, this class is trivial. """ def __init__(self, reader: StreamReader) -> None: self._reader = reader async def read(self, n: int = -1) -> bytes: if n == -1: data = await self._reader.read(n) else: data = await self._reader.readexactly(n) return data def feed_eof(self) -> None: self._reader.feed_eof() class StreamWriterAdapter(WriterAdapter): """Asyncio Streams API protocol adapter. This adapter relies on StreamWriter to write to a TCP socket. Because API is very close, this class is trivial. """ def __init__(self, writer: StreamWriter) -> None: self.logger = logging.getLogger(__name__) self._writer = writer self.is_closed = False # StreamWriter has no test for closed...we use our own def write(self, data: bytes) -> None: if not self.is_closed: self._writer.write(data) async def drain(self) -> None: if not self.is_closed: await self._writer.drain() def get_peer_info(self) -> tuple[str, int]: extra_info = self._writer.get_extra_info("peername") return extra_info[0], extra_info[1] def get_ssl_info(self) -> ssl.SSLObject | None: return cast("ssl.SSLObject", self._writer.get_extra_info("ssl_object")) async def close(self) -> None: if not self.is_closed: self.is_closed = True # we first mark this closed so yields below don't cause races with waiting writes await self._writer.drain() if self._writer.can_write_eof(): self._writer.write_eof() self._writer.close() with suppress(AttributeError): await self._writer.wait_closed() class BufferReader(ReaderAdapter): """Byte Buffer reader adapter. This adapter simply adapts reading a byte buffer. """ def __init__(self, buffer: bytes) -> None: self._stream = io.BytesIO(buffer) async def read(self, n: int = -1) -> bytes: return self._stream.read(n) def feed_eof(self) -> None: # NOTE: not implemented?! pass class BufferWriter(WriterAdapter): """ByteBuffer writer adapter. This adapter simply adapts writing to a byte buffer. """ def get_ssl_info(self) -> ssl.SSLObject | None: return None def __init__(self, buffer: bytes = b"") -> None: self._stream = io.BytesIO(buffer) def write(self, data: bytes) -> None: """Write some data to the protocol layer.""" self._stream.write(data) async def drain(self) -> None: pass def get_buffer(self) -> bytes: return self._stream.getvalue() def get_peer_info(self) -> tuple[str, int]: return "BufferWriter", 0 async def close(self) -> None: self._stream.close() Yakifo-amqtt-2637127/amqtt/broker.py000066400000000000000000001511431504664204300172520ustar00rootroot00000000000000import asyncio from asyncio import CancelledError, futures from collections import deque from collections.abc import Generator from functools import partial import logging from math import floor import re import ssl import time from typing import Any, ClassVar, TypeAlias from transitions import Machine, MachineError import websockets.asyncio.server from websockets.asyncio.server import ServerConnection from amqtt.adapters import ( ReaderAdapter, StreamReaderAdapter, StreamWriterAdapter, WebSocketsReader, WebSocketsWriter, WriterAdapter, ) from amqtt.contexts import Action, BaseContext, BrokerConfig, ListenerConfig, ListenerType from amqtt.errors import AMQTTError, BrokerError, MQTTError, NoDataError from amqtt.mqtt.protocol.broker_handler import BrokerProtocolHandler from amqtt.session import ApplicationMessage, OutgoingApplicationMessage, Session from amqtt.utils import format_client_message, gen_client_id from .events import BrokerEvents from .mqtt.constants import QOS_0, QOS_1, QOS_2 from .mqtt.disconnect import DisconnectPacket from .plugins.manager import PluginManager _BROADCAST: TypeAlias = dict[str, Session | str | bytes | bytearray | int | None] # Default port numbers DEFAULT_PORTS = {"tcp": 1883, "ws": 8883} AMQTT_MAGIC_VALUE_RET_SUBSCRIBED = 0x80 class RetainedApplicationMessage(ApplicationMessage): __slots__ = ("data", "qos", "source_session", "topic") def __init__(self, source_session: Session | None, topic: str, data: bytes | bytearray, qos: int | None = None) -> None: super().__init__(None, topic, qos, data, retain=True) self.source_session = source_session self.topic = topic self.data = data self.qos = qos class Server: """Used to encapsulate the server associated with a listener. Allows broker to interact with the connection lifecycle.""" def __init__( self, listener_name: str, server_instance: asyncio.Server | websockets.asyncio.server.Server, max_connections: int = -1, ) -> None: self.logger = logging.getLogger(__name__) self.instance = server_instance self.conn_count = 0 self.listener_name = listener_name self.max_connections = max_connections self.semaphore = asyncio.Semaphore(max_connections) if max_connections > 0 else None async def acquire_connection(self) -> None: if self.semaphore: await self.semaphore.acquire() self.conn_count += 1 self.logger.info( f"Listener '{self.listener_name}': {self.conn_count}/" f"{self.max_connections if self.max_connections > 0 else '∞'} connections acquired", ) def release_connection(self) -> None: if self.semaphore: self.semaphore.release() self.conn_count -= 1 self.logger.info( f"Listener '{self.listener_name}': {self.conn_count}/" f"{self.max_connections if self.max_connections > 0 else '∞'} connections acquired", ) async def close_instance(self) -> None: if self.instance: self.instance.close() await self.instance.wait_closed() class ExternalServer(Server): """For external listeners, the connection lifecycle is handled by that implementation so these are no-ops.""" def __init__(self) -> None: super().__init__("aiohttp", None) # type: ignore[arg-type] async def acquire_connection(self) -> None: pass def release_connection(self) -> None: pass async def close_instance(self) -> None: pass class BrokerContext(BaseContext): """Used to provide the server's context as well as public methods for accessing internal state.""" def __init__(self, broker: "Broker") -> None: super().__init__() self.config: BrokerConfig | None = None self._broker_instance = broker async def broadcast_message(self, topic: str, data: bytes, qos: int | None = None) -> None: """Send message to all client sessions subscribing to `topic`.""" await self._broker_instance.internal_message_broadcast(topic, data, qos) async def retain_message(self, topic_name: str, data: bytes | bytearray, qos: int | None = None) -> None: await self._broker_instance.retain_message(None, topic_name, data, qos) @property def sessions(self) -> Generator[Session]: for session in self._broker_instance.sessions.values(): yield session[0] def get_session(self, client_id: str) -> Session | None: """Return the session associated with `client_id`, if it exists.""" return self._broker_instance.sessions.get(client_id, (None, None))[0] @property def retained_messages(self) -> dict[str, RetainedApplicationMessage]: return self._broker_instance.retained_messages @property def subscriptions(self) -> dict[str, list[tuple[Session, int]]]: return self._broker_instance.subscriptions async def add_subscription(self, client_id: str, topic: str | None, qos: int | None) -> None: """Create a topic subscription for the given `client_id`. If a client session doesn't exist for `client_id`, create a disconnected session. If `topic` and `qos` are both `None`, only create the client session. """ if client_id not in self._broker_instance.sessions: broker_handler, session = self._broker_instance.create_offline_session(client_id) self._broker_instance._sessions[client_id] = (session, broker_handler) # noqa: SLF001 if topic is not None and qos is not None: session, _ = self._broker_instance.sessions[client_id] await self._broker_instance.add_subscription((topic, qos), session) class Broker: """MQTT 3.1.1 compliant broker implementation. Args: config: `BrokerConfig` or dictionary of equivalent structure options (see [broker configuration](broker_config.md)). loop: asyncio loop. defaults to `asyncio.new_event_loop()`. plugin_namespace: plugin namespace to use when loading plugin entry_points. defaults to `amqtt.broker.plugins`. Raises: BrokerError: problem with broker configuration PluginImportError: if importing a plugin from configuration PluginInitError: if initialization plugin fails """ states: ClassVar[list[str]] = [ "new", "starting", "started", "not_started", "stopping", "stopped", "not_stopped", ] def __init__( self, config: BrokerConfig | dict[str, Any] | None = None, loop: asyncio.AbstractEventLoop | None = None, plugin_namespace: str | None = None, ) -> None: """Initialize the broker.""" self.logger = logging.getLogger(__name__) if isinstance(config, dict): self.config = BrokerConfig.from_dict(config) else: self.config = config or BrokerConfig() # listeners are populated from default within BrokerConfig self.listeners_config = self.config.listeners self._loop = loop or asyncio.get_running_loop() self._servers: dict[str, Server] = {} self._init_states() self._sessions: dict[str, tuple[Session, BrokerProtocolHandler]] = {} self._subscriptions: dict[str, list[tuple[Session, int]]] = {} self._retained_messages: dict[str, RetainedApplicationMessage] = {} self._topic_filter_matchers: dict[str, re.Pattern[str]] = {} # Broadcast queue for outgoing messages self._broadcast_queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue() self._broadcast_task: asyncio.Task[Any] | None = None self._broadcast_shutdown_waiter: asyncio.Future[Any] = futures.Future() # Tasks queue for managing broadcasting tasks self._tasks_queue: deque[asyncio.Task[OutgoingApplicationMessage]] = deque() # Task for session monitor self._session_monitor_task: asyncio.Task[Any] | None = None # Initialize plugins manager context = BrokerContext(self) context.config = self.config namespace = plugin_namespace or "amqtt.broker.plugins" self.plugins_manager = PluginManager(namespace, context, self._loop) def _init_states(self) -> None: self.transitions = Machine(states=Broker.states, initial="new") self.transitions.add_transition(trigger="start", source="new", dest="starting", before=self._log_state_change) self.transitions.add_transition(trigger="starting_fail", source="starting", dest="not_started") self.transitions.add_transition(trigger="starting_success", source="starting", dest="started") self.transitions.add_transition(trigger="shutdown", source="started", dest="stopping") self.transitions.add_transition(trigger="stopping_success", source="stopping", dest="stopped") self.transitions.add_transition(trigger="stopping_failure", source="stopping", dest="not_stopped") self.transitions.add_transition(trigger="start", source="stopped", dest="starting") def _log_state_change(self) -> None: self.logger.debug(f"State transition: {self.transitions.state}") async def start(self) -> None: """Start the broker to serve with the given configuration. Start method opens network sockets and will start listening for incoming connections. """ try: self._sessions.clear() self._subscriptions.clear() self._retained_messages.clear() self.transitions.start() self.logger.debug("Broker starting") except (MachineError, ValueError) as exc: # Backwards compat: MachineError is raised by transitions < 0.5.0. self.logger.warning(f"[WARN-0001] Invalid method call at this moment: {exc}") msg = f"Broker instance can't be started: {exc}" raise BrokerError(msg) from exc await self.plugins_manager.fire_event(BrokerEvents.PRE_START) try: await self._start_listeners() self.transitions.starting_success() await self.plugins_manager.fire_event(BrokerEvents.POST_START) self._broadcast_task = asyncio.ensure_future(self._broadcast_loop()) self._session_monitor_task = asyncio.create_task(self._session_monitor()) self.logger.debug("Broker started") except Exception as e: self.logger.exception("Broker startup failed") self.transitions.starting_fail() msg = f"Broker instance can't be started: {e}" raise BrokerError(msg) from e async def _start_listeners(self) -> None: """Start network listeners based on the configuration.""" for listener_name, listener in self.listeners_config.items(): if "bind" not in listener: self.logger.debug(f"Listener configuration '{listener_name}' is not bound") continue max_connections = listener.get("max_connections", -1) ssl_context = self._create_ssl_context(listener) if listener.get("ssl", False) else None # for listeners which are external, don't need to create a server if listener.type == ListenerType.EXTERNAL: # broker still needs to associate a new connection to the listener self.logger.info(f"External listener exists for '{listener_name}' ") self._servers[listener_name] = ExternalServer() else: # for tcp and websockets, start servers to listen for inbound connections try: address, port = self._split_bindaddr_port(listener["bind"], DEFAULT_PORTS[listener["type"]]) except ValueError as e: msg = f"Invalid port value in bind value: {listener['bind']}" raise BrokerError(msg) from e instance = await self._create_server_instance(listener_name, listener.type, address, port, ssl_context) self._servers[listener_name] = Server(listener_name, instance, max_connections) self.logger.info(f"Listener '{listener_name}' bind to {listener['bind']} (max_connections={max_connections})") @staticmethod def _create_ssl_context(listener: ListenerConfig) -> ssl.SSLContext: """Create an SSL context for a listener.""" try: ssl_context = ssl.create_default_context( ssl.Purpose.CLIENT_AUTH, cafile=listener.get("cafile"), capath=listener.get("capath"), cadata=listener.get("cadata"), ) ssl_context.load_cert_chain(listener["certfile"], listener["keyfile"]) ssl_context.verify_mode = ssl.CERT_OPTIONAL except KeyError as ke: msg = f"'certfile' or 'keyfile' configuration parameter missing: {ke}" raise BrokerError(msg) from ke except FileNotFoundError as fnfe: msg = f"Can't read cert files '{listener['certfile']}' or '{listener['keyfile']}' : {fnfe}" raise BrokerError(msg) from fnfe return ssl_context async def _create_server_instance( self, listener_name: str, listener_type: ListenerType, address: str | None, port: int, ssl_context: ssl.SSLContext | None, ) -> asyncio.Server | websockets.asyncio.server.Server: """Create a server instance for a listener.""" match listener_type: case ListenerType.TCP: return await asyncio.start_server( partial(self.stream_connected, listener_name=listener_name), address, port, reuse_address=True, ssl=ssl_context, ) case ListenerType.WS: return await websockets.serve( partial(self.ws_connected, listener_name=listener_name), address, port, ssl=ssl_context, subprotocols=[websockets.Subprotocol("mqtt")], ) case _: msg = f"Unsupported listener type: {listener_type}" raise BrokerError(msg) async def _session_monitor(self) -> None: self.logger.info("Starting session expiration monitor.") while True: session_count_before = len(self._sessions) # clean or anonymous sessions don't retain messages (or subscriptions); the session can be filtered out sessions_to_remove = [client_id for client_id, (session, _) in self._sessions.items() if session.transitions.state == "disconnected" and (session.is_anonymous or session.clean_session)] # if session expiration is enabled, check to see if any of the sessions are disconnected and past expiration if self.config.session_expiry_interval is not None: retain_after = floor(time.time() - self.config.session_expiry_interval) sessions_to_remove += [client_id for client_id, (session, _) in self._sessions.items() if session.transitions.state == "disconnected" and session.last_disconnect_time and session.last_disconnect_time < retain_after] for client_id in sessions_to_remove: await self._cleanup_session(client_id) if session_count_before > (session_count_after := len(self._sessions)): self.logger.debug(f"Expired {session_count_before - session_count_after} sessions") await asyncio.sleep(1) async def shutdown(self) -> None: """Stop broker instance.""" self.logger.info("Shutting down broker...") # Fire broker_shutdown event to plugins await self.plugins_manager.fire_event(BrokerEvents.PRE_SHUTDOWN) # Cleanup all sessions for client_id in list(self._sessions.keys()): await self._cleanup_session(client_id) # Clear retained messages self.logger.debug(f"Clearing {len(self._retained_messages)} retained messages") self._retained_messages.clear() self.transitions.shutdown() await self._shutdown_broadcast_loop() if self._session_monitor_task: self._session_monitor_task.cancel() for server in self._servers.values(): await server.close_instance() if not self._broadcast_queue.empty(): self.logger.warning(f"{self._broadcast_queue.qsize()} messages not broadcasted") # Clear the broadcast queue while not self._broadcast_queue.empty(): self._broadcast_queue.get_nowait() self.logger.info("Broker closed") await self.plugins_manager.fire_event(BrokerEvents.POST_SHUTDOWN) self.transitions.stopping_success() async def _cleanup_session(self, client_id: str) -> None: """Centralized cleanup logic for a session.""" session, handler = self._sessions.pop(client_id, (None, None)) if handler: self.logger.debug(f"Stopping handler for session {client_id}") await self._stop_handler(handler) if session: self.logger.debug(f"Clearing all subscriptions for session {client_id}") await self._del_all_subscriptions(session) session.clear_queues() async def internal_message_broadcast(self, topic: str, data: bytes, qos: int | None = None) -> None: return await self._broadcast_message(None, topic, data, qos) async def ws_connected(self, websocket: ServerConnection, listener_name: str) -> None: await self._client_connected(listener_name, WebSocketsReader(websocket), WebSocketsWriter(websocket)) async def stream_connected(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, listener_name: str) -> None: await self._client_connected(listener_name, StreamReaderAdapter(reader), StreamWriterAdapter(writer)) async def external_connected(self, reader: ReaderAdapter, writer: WriterAdapter, listener_name: str) -> None: """Engage the broker in handling the data stream to/from an established connection.""" await self._client_connected(listener_name, reader, writer) async def _client_connected(self, listener_name: str, reader: ReaderAdapter, writer: WriterAdapter) -> None: """Handle a new client connection.""" server = self._servers.get(listener_name) if not server: msg = f"Invalid listener name '{listener_name}'" raise BrokerError(msg) await server.acquire_connection() remote_info = writer.get_peer_info() if remote_info is None: self.logger.warning("Remote info could not be retrieved from peer info") return remote_address, remote_port = remote_info self.logger.info(f"Connection from {remote_address}:{remote_port} on listener '{listener_name}'") try: handler, client_session = await self._initialize_client_session(reader, writer, remote_address, remote_port) except (AMQTTError, MQTTError, NoDataError) as exc: self.logger.warning(f"Failed to initialize client session: {exc}") server.release_connection() return try: await self._handle_client_session(reader, writer, client_session, handler, server, listener_name) except (AMQTTError, MQTTError, NoDataError) as exc: self.logger.warning(f"Error while handling client session: {exc}") finally: self.logger.debug(f"{client_session.client_id} Client disconnected") server.release_connection() async def _initialize_client_session( self, reader: ReaderAdapter, writer: WriterAdapter, remote_address: str, remote_port: int, ) -> tuple[BrokerProtocolHandler, Session]: """Initialize a client session and protocol handler.""" # Wait for first packet and expect a CONNECT try: handler, client_session = await BrokerProtocolHandler.init_from_connect(reader, writer, self.plugins_manager) except AMQTTError as exc: self.logger.warning( f"[MQTT-3.1.0-1] {format_client_message(address=remote_address, port=remote_port)}:" f" Can't read first packet as CONNECT: {exc}", ) raise AMQTTError(exc) from exc except MQTTError as exc: self.logger.exception( f"Invalid connection from {format_client_message(address=remote_address, port=remote_port)}", ) await writer.close() raise MQTTError(exc) from exc except NoDataError as exc: self.logger.error( # noqa: TRY400 f"No data from {format_client_message(address=remote_address, port=remote_port)} : {exc}", ) raise AMQTTError(exc) from exc if client_session.clean_session: # Delete existing session and create a new one if client_session.client_id is not None and client_session.client_id != "": await self._delete_session(client_session.client_id) else: client_session.client_id = gen_client_id() client_session.parent = 0 # Get session from cache elif client_session.client_id in self._sessions: self.logger.debug(f"Found old session {self._sessions[client_session.client_id]!r}") # even though the session previously existed, the new connection can bring updated configuration and credentials existing_client_session, _ = self._sessions[client_session.client_id] existing_client_session.will_flag = client_session.will_flag existing_client_session.will_message = client_session.will_message existing_client_session.will_topic = client_session.will_topic existing_client_session.will_qos = client_session.will_qos existing_client_session.keep_alive = client_session.keep_alive existing_client_session.username = client_session.username existing_client_session.password = client_session.password client_session = existing_client_session client_session.parent = 1 else: client_session.parent = 0 timeout_disconnect_delay = self.config.get("timeout-disconnect-delay", 0) if client_session.keep_alive > 0 and isinstance(timeout_disconnect_delay, int): client_session.keep_alive += timeout_disconnect_delay self.logger.debug(f"Keep-alive timeout={client_session.keep_alive}") return handler, client_session def create_offline_session(self, client_id: str) -> tuple[BrokerProtocolHandler, Session]: session = Session() session.client_id = client_id bph = BrokerProtocolHandler(self.plugins_manager, session) session.transitions.disconnect() return bph, session async def _handle_client_session( self, reader: ReaderAdapter, writer: WriterAdapter, client_session: Session, handler: BrokerProtocolHandler, server: Server, listener_name: str, ) -> None: """Handle the lifecycle of a client session.""" authenticated = await self._authenticate(client_session, self.listeners_config[listener_name]) if not authenticated: await writer.close() return if client_session.client_id is None: msg = "Client ID was not correctly created/set." raise BrokerError(msg) while True: try: client_session.transitions.connect() break except (MachineError, ValueError): if client_session.transitions.is_connected(): self.logger.warning(f"Client {client_session.client_id} is already connected, performing take-over.") old_session = self._sessions[client_session.client_id] await old_session[1].handle_connection_closed() await old_session[1].stop() break self.logger.warning(f"Client {client_session.client_id} is reconnecting too quickly, make it wait") await asyncio.sleep(1) handler.attach(client_session, reader, writer) self._sessions[client_session.client_id] = (client_session, handler) await handler.mqtt_connack_authorize(authenticated) await self.plugins_manager.fire_event(BrokerEvents.CLIENT_CONNECTED, client_id=client_session.client_id, client_session=client_session) self.logger.debug(f"{client_session.client_id} Start messages handling") await handler.start() # publish messages that were retained because the client session was disconnected self.logger.debug(f"Retained messages queue size: {client_session.retained_messages.qsize()}") await self._publish_session_retained_messages(client_session) # if this is not a new session, there are subscriptions associated with them; publish any topic retained messages self.logger.debug("Publish retained messages to a pre-existing session's subscriptions.") for topic in self._subscriptions: await self._publish_retained_messages_for_subscription((topic, QOS_0), client_session) await self._client_message_loop(client_session, handler) async def _client_message_loop(self, client_session: Session, handler: BrokerProtocolHandler) -> None: """Run the main loop to handle client messages.""" # Init and start loop for handling client messages (publish, subscribe/unsubscribe, disconnect) disconnect_waiter = asyncio.ensure_future(handler.wait_disconnect()) subscribe_waiter = asyncio.ensure_future(handler.get_next_pending_subscription()) unsubscribe_waiter = asyncio.ensure_future(handler.get_next_pending_unsubscription()) wait_deliver = asyncio.ensure_future(handler.mqtt_deliver_next_message()) connected = True while connected: try: done, _ = await asyncio.wait( [ disconnect_waiter, subscribe_waiter, unsubscribe_waiter, wait_deliver, ], return_when=asyncio.FIRST_COMPLETED, ) if disconnect_waiter in done: # handle the disconnection: normal or abnormal result, either way, the client is no longer connected await self._handle_disconnect(client_session, handler, disconnect_waiter) connected = False # no need to reschedule the `disconnect_waiter` since we're exiting the message loop if subscribe_waiter in done: await self._handle_subscription(client_session, handler, subscribe_waiter) subscribe_waiter = asyncio.ensure_future(handler.get_next_pending_subscription()) self.logger.debug(repr(self._subscriptions)) if unsubscribe_waiter in done: await self._handle_unsubscription(client_session, handler, unsubscribe_waiter) unsubscribe_waiter = asyncio.ensure_future(handler.get_next_pending_unsubscription()) if wait_deliver in done: if not await self._handle_message_delivery(client_session, handler, wait_deliver): break wait_deliver = asyncio.ensure_future(handler.mqtt_deliver_next_message()) except asyncio.CancelledError: self.logger.debug("Client loop cancelled") break disconnect_waiter.cancel() subscribe_waiter.cancel() unsubscribe_waiter.cancel() wait_deliver.cancel() async def _handle_disconnect( self, client_session: Session, handler: BrokerProtocolHandler, disconnect_waiter: asyncio.Future[Any], ) -> None: """Handle client disconnection. Args: client_session (Session): client session handler (BrokerProtocolHandler): broker protocol handler disconnect_waiter (asyncio.Future[Any]): future to wait for disconnection """ # check the disconnected waiter result result = disconnect_waiter.result() self.logger.debug(f"{client_session.client_id} Result from wait_disconnect: {result}") # if the client disconnects abruptly by sending no message or the message isn't a disconnect packet if result is None or not isinstance(result, DisconnectPacket): self.logger.debug(f"Will flag: {client_session.will_flag}") if client_session.will_flag: self.logger.debug( f"Client {format_client_message(client_session)} disconnected abnormally, sending will message", ) await self._broadcast_message( client_session, client_session.will_topic, client_session.will_message, client_session.will_qos, ) if client_session.will_retain: await self.retain_message( client_session, client_session.will_topic, client_session.will_message, client_session.will_qos, ) # normal or not, let's end the client's session self.logger.debug(f"{client_session.client_id} Disconnecting session") await self._stop_handler(handler) client_session.transitions.disconnect() await self.plugins_manager.fire_event(BrokerEvents.CLIENT_DISCONNECTED, client_id=client_session.client_id, client_session=client_session) async def _handle_subscription( self, client_session: Session, handler: BrokerProtocolHandler, subscribe_waiter: asyncio.Future[Any], ) -> None: """Handle client subscription.""" self.logger.debug(f"{client_session.client_id} handling subscription") subscriptions = subscribe_waiter.result() return_codes = [await self.add_subscription(subscription, client_session) for subscription in subscriptions.topics] await handler.mqtt_acknowledge_subscription(subscriptions.packet_id, return_codes) for index, subscription in enumerate(subscriptions.topics): if return_codes[index] != AMQTT_MAGIC_VALUE_RET_SUBSCRIBED: await self.plugins_manager.fire_event( BrokerEvents.CLIENT_SUBSCRIBED, client_id=client_session.client_id, topic=subscription[0], qos=subscription[1], ) await self._publish_retained_messages_for_subscription(subscription, client_session) async def _handle_unsubscription( self, client_session: Session, handler: BrokerProtocolHandler, unsubscribe_waiter: asyncio.Future[Any], ) -> None: """Handle client unsubscription.""" self.logger.debug(f"{client_session.client_id} handling unsubscription") unsubscription = unsubscribe_waiter.result() for topic in unsubscription.topics: self._del_subscription(topic, client_session) await self.plugins_manager.fire_event( BrokerEvents.CLIENT_UNSUBSCRIBED, client_id=client_session.client_id, topic=topic, ) await handler.mqtt_acknowledge_unsubscription(unsubscription.packet_id) async def _handle_message_delivery( self, client_session: Session, handler: BrokerProtocolHandler, wait_deliver: asyncio.Future[Any], ) -> bool: """Handle message delivery to the client.""" self.logger.debug(f"{client_session.client_id} handling message delivery") app_message = wait_deliver.result() # notify of a message's receipt, even if a client isn't necessarily allowed to send it await self.plugins_manager.fire_event( BrokerEvents.MESSAGE_RECEIVED, client_id=client_session.client_id, message=app_message, ) if app_message is None: self.logger.debug("app_message was empty!") return True if not app_message.topic: self.logger.warning( f"[MQTT-4.7.3-1] - {client_session.client_id} invalid TOPIC sent in PUBLISH message, closing connection", ) return False if "#" in app_message.topic or "+" in app_message.topic: self.logger.warning( f"[MQTT-3.3.2-2] - {client_session.client_id} invalid TOPIC sent in PUBLISH message, closing connection", ) return False if app_message.topic.startswith("$"): self.logger.warning( f"[MQTT-4.7.2-1] - {client_session.client_id} cannot use a topic with a leading $ character." ) return False permitted = await self._topic_filtering(client_session, topic=app_message.topic, action=Action.PUBLISH) if not permitted: self.logger.info(f"{client_session.client_id} not allowed to publish to TOPIC {app_message.topic}.") else: # notify that a received message is valid and is allowed to be distributed to other clients await self.plugins_manager.fire_event( BrokerEvents.MESSAGE_BROADCAST, client_id=client_session.client_id, message=app_message, ) await self._broadcast_message(client_session, app_message.topic, app_message.data) if app_message.publish_packet and app_message.publish_packet.retain_flag: await self.retain_message(client_session, app_message.topic, app_message.data, app_message.qos) return True async def _init_handler(self, session: Session, reader: ReaderAdapter, writer: WriterAdapter) -> BrokerProtocolHandler: """Create a BrokerProtocolHandler and attach to a session.""" handler = BrokerProtocolHandler(self.plugins_manager, loop=self._loop) handler.attach(session, reader, writer) return handler async def _stop_handler(self, handler: BrokerProtocolHandler) -> None: """Stop a running handler and detach if from the session.""" try: await handler.stop() # a failure in stopping a handler shouldn't cause the broker to fail except asyncio.QueueEmpty: self.logger.exception("Failed to stop handler") async def _authenticate(self, session: Session, _: ListenerConfig) -> bool: """Call the authenticate method on registered plugins to test user authentication. User is considered authenticated if all plugins called returns True. Plugins authenticate() method are supposed to return : - True if user is authentication succeed - False if user authentication fails - None if authentication can't be achieved (then plugin result is then ignored) :param session: :return: """ returns = await self.plugins_manager.map_plugin_auth(session=session) results = [result for _, result in returns.items() if result is not None] if returns else [] if len(results) < 1: self.logger.debug("Authentication failed: no plugin responded with a boolean") return False if all(results): self.logger.debug("Authentication succeeded") return True for plugin, result in returns.items(): self.logger.debug(f"Authentication '{plugin.__class__.__name__}' result: {result}") return False async def retain_message( self, source_session: Session | None, topic_name: str | None, data: bytes | bytearray | None, qos: int | None = None, ) -> None: if data and topic_name is not None: # If retained flag set, store the message for further subscriptions self.logger.debug(f"Retaining message on topic {topic_name}") self._retained_messages[topic_name] = RetainedApplicationMessage(source_session, topic_name, data, qos) await self.plugins_manager.fire_event(BrokerEvents.RETAINED_MESSAGE, client_id=None, retained_message=self._retained_messages[topic_name]) # [MQTT-3.3.1-10] elif topic_name in self._retained_messages: self.logger.debug(f"Clearing retained messages for topic '{topic_name}'") cleared_message = self._retained_messages[topic_name] cleared_message.data = b"" await self.plugins_manager.fire_event(BrokerEvents.RETAINED_MESSAGE, client_id=None, retained_message=cleared_message) del self._retained_messages[topic_name] async def add_subscription(self, subscription: tuple[str, int], session: Session) -> int: topic_filter, qos = subscription if "#" in topic_filter and not topic_filter.endswith("#"): # [MQTT-4.7.1-2] Wildcard character '#' is only allowed as last character in filter return 0x80 if topic_filter != "+" and "+" in topic_filter and ("/+" not in topic_filter and "+/" not in topic_filter): # [MQTT-4.7.1-3] + wildcard character must occupy entire level return 0x80 # Check if the client is authorised to connect to the topic if not await self._topic_filtering(session, topic_filter, Action.SUBSCRIBE): return 0x80 # Ensure "max-qos" is an integer before using it max_qos = self.config.get("max-qos", qos) if not isinstance(max_qos, int): max_qos = qos qos = min(qos, max_qos) if topic_filter not in self._subscriptions: self._subscriptions[topic_filter] = [] if all(s.client_id != session.client_id for s, _ in self._subscriptions[topic_filter]): self._subscriptions[topic_filter].append((session, qos)) else: self.logger.debug(f"Client {format_client_message(session=session)} has already subscribed to {topic_filter}") return qos async def _topic_filtering(self, session: Session, topic: str, action: Action) -> bool: """Call the topic_filtering method on registered plugins to check that the subscription is allowed. User is considered allowed if all plugins called return True. Plugins topic_filtering() method are supposed to return : - True if MQTT client can be subscribed to the topic - False if MQTT client is not allowed to subscribe to the topic - None if topic filtering can't be achieved (then plugin result is then ignored) :param session: :param topic: Topic in which the client wants to subscribe / publish :param action: What is being done with the topic? subscribe or publish :return: """ if not self.plugins_manager.is_topic_filtering_enabled(): return True results = await self.plugins_manager.map_plugin_topic(session=session, topic=topic, action=action) return all(result for result in results.values()) async def _delete_session(self, client_id: str) -> None: """Delete an existing session data, for example due to clean session set in CONNECT.""" session = self._sessions.pop(client_id, (None, None))[0] if session is None: self.logger.debug(f"Delete session : session {client_id} doesn't exist") return self.logger.debug(f"Deleted existing session {session!r}") # Delete subscriptions self.logger.debug(f"Deleting session {session!r} subscriptions") await self._del_all_subscriptions(session) session.clear_queues() async def _del_all_subscriptions(self, session: Session) -> None: """Delete all topic subscriptions for a given session.""" filter_queue: deque[str] = deque() for topic in self._subscriptions: if self._del_subscription(topic, session): filter_queue.append(topic) for topic in filter_queue: if not self._subscriptions[topic]: del self._subscriptions[topic] def _del_subscription(self, a_filter: str, session: Session) -> int: """Delete a session subscription on a given topic. :param a_filter: The topic filter for the subscription. :param session: The session to be unsubscribed. :return: The number of deleted subscriptions (0 or 1). """ deleted = 0 try: subscriptions = self._subscriptions[a_filter] for index, (sub_session, _qos) in enumerate(subscriptions): if sub_session.client_id == session.client_id: self.logger.debug( f"Removing subscription on topic '{a_filter}' for client {format_client_message(session=session)}", ) subscriptions.pop(index) deleted += 1 break except KeyError: self.logger.debug(f"Unsubscription on topic '{a_filter}' for client {format_client_message(session=session)}") return deleted async def _broadcast_loop(self) -> None: """Run the main loop to broadcast messages.""" running_tasks: deque[asyncio.Task[OutgoingApplicationMessage]] = self._tasks_queue try: while True: while running_tasks and running_tasks[0].done(): task = running_tasks.popleft() try: task.result() except CancelledError: self.logger.info(f"Task has been cancelled: {task}") # if a task fails, don't want it to cause the broker to fail except Exception: # pylint: disable=W0718 self.logger.exception(f"Task failed and will be skipped: {task}") run_broadcast_task = asyncio.ensure_future(self._run_broadcast(running_tasks)) completed, _ = await asyncio.wait( [run_broadcast_task, self._broadcast_shutdown_waiter], return_when=asyncio.FIRST_COMPLETED, ) # Shutdown has been triggered by the broker, so stop the loop execution if self._broadcast_shutdown_waiter in completed: run_broadcast_task.cancel() break except BaseException: self.logger.exception("Broadcast loop stopped by exception") raise finally: # Wait until current broadcasting tasks end if running_tasks: await asyncio.gather(*running_tasks) async def _run_broadcast(self, running_tasks: deque[asyncio.Task[OutgoingApplicationMessage]]) -> None: """Process a single broadcast message.""" broadcast = await self._broadcast_queue.get() self.logger.debug(f"Processing broadcast message: {broadcast}") for k_filter, subscriptions in self._subscriptions.items(): # Skip all subscriptions which do not match the topic if not self._matches(broadcast["topic"], k_filter): self.logger.debug(f"Topic '{broadcast['topic']}' does not match filter '{k_filter}'") continue for target_session, sub_qos in subscriptions: qos = broadcast.get("qos", sub_qos) sendable = await self._topic_filtering(target_session, topic=broadcast["topic"], action=Action.RECEIVE) if not sendable: self.logger.info( f"{target_session.client_id} not allowed to receive messages from TOPIC {broadcast['topic']}.") continue # Retain all messages which cannot be broadcasted, due to the session not being connected # but only when clean session is false and qos is 1 or 2 [MQTT 3.1.2.4] # and, if a client used anonymous authentication, there is no expectation that messages should be retained if (target_session.transitions.state != "connected" and not target_session.clean_session and qos in (QOS_1, QOS_2) and not target_session.is_anonymous): self.logger.debug(f"Session {target_session.client_id} is not connected, retaining message.") await self._retain_broadcast_message(broadcast, qos, target_session) continue # Only broadcast the message to connected clients if target_session.transitions.state != "connected": continue self.logger.debug( f"Broadcasting message from {format_client_message(session=broadcast['session'])}" f" on topic '{broadcast['topic']}' to {format_client_message(session=target_session)}", ) handler = self._get_handler(target_session) if handler: task = asyncio.ensure_future( handler.mqtt_publish( broadcast["topic"], broadcast["data"], qos, retain=False, ), ) running_tasks.append(task) async def _retain_broadcast_message(self, broadcast: dict[str, Any], qos: int, target_session: Session) -> None: if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug( f"retaining application message from {format_client_message(session=broadcast['session'])}" f" on topic '{broadcast['topic']}' to client '{format_client_message(session=target_session)}'", ) retained_message = RetainedApplicationMessage(broadcast["session"], broadcast["topic"], broadcast["data"], qos) await target_session.retained_messages.put(retained_message) await self.plugins_manager.fire_event(BrokerEvents.RETAINED_MESSAGE, client_id=target_session.client_id, retained_message=retained_message) if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"target_session.retained_messages={target_session.retained_messages.qsize()}") async def _shutdown_broadcast_loop(self) -> None: if self._broadcast_task and not self._broadcast_shutdown_waiter.done(): self._broadcast_shutdown_waiter.set_result(True) try: await asyncio.wait_for(self._broadcast_task, timeout=30) except TimeoutError as e: self.logger.warning(f"Failed to cleanly shutdown broadcast loop: {e}") if not self._broadcast_queue.empty(): self.logger.warning(f"{self._broadcast_queue.qsize()} messages not broadcasted") self._broadcast_shutdown_waiter = asyncio.Future() async def _broadcast_message( self, session: Session | None, topic: str | None, data: bytes | bytearray | None, force_qos: int | None = None, ) -> None: broadcast: _BROADCAST = {"session": session, "topic": topic, "data": data} if force_qos is not None: broadcast["qos"] = force_qos await self._broadcast_queue.put(broadcast) async def _publish_session_retained_messages(self, session: Session) -> None: self.logger.debug( f"Publishing {session.retained_messages.qsize()}" f" messages retained for session {format_client_message(session=session)}", ) publish_tasks = [] handler = self._get_handler(session) if handler: while not session.retained_messages.empty(): retained = await session.retained_messages.get() publish_tasks.append( asyncio.ensure_future( handler.mqtt_publish(retained.topic, retained.data, retained.qos, retain=True), ), ) if publish_tasks: await asyncio.wait(publish_tasks) async def _publish_retained_messages_for_subscription(self, subscription: tuple[str, int], session: Session) -> None: self.logger.debug( f"Begin broadcasting messages retained due to subscription on '{subscription[0]}'" f" from {format_client_message(session=session)}", ) publish_tasks = [] topic_filter, qos = subscription for topic, retained in self._retained_messages.items(): self.logger.debug(f"matching : {topic} {topic_filter}") if self._matches(topic, topic_filter): self.logger.debug(f"{topic} and {topic_filter} match") handler = self._get_handler(session) if handler: publish_tasks.append( asyncio.Task( handler.mqtt_publish(retained.topic, retained.data, min(qos, retained.qos or qos), retain=True), ), ) if publish_tasks: await asyncio.wait(publish_tasks) self.logger.debug( f"End broadcasting messages retained due to subscription on '{subscription[0]}'" f" from {format_client_message(session=session)}", ) def _matches(self, topic: str, a_filter: str) -> bool: if topic.startswith("$") and (a_filter.startswith(("+", "#"))): self.logger.debug("[MQTT-4.7.2-1] - ignoring broadcasting $ topic to subscriptions starting with + or #") return False if "#" not in a_filter and "+" not in a_filter: # if filter doesn't contain wildcard, return exact match return a_filter == topic # else use regex (re.compile is an expensive operation, store the matcher for future use) if a_filter not in self._topic_filter_matchers: self._topic_filter_matchers[a_filter] = re.compile(re.escape(a_filter) .replace("\\#", "?.*") .replace("\\+", "[^/]*") .lstrip("?")) match_pattern = self._topic_filter_matchers[a_filter] return bool(match_pattern.fullmatch(topic)) def _get_handler(self, session: Session) -> BrokerProtocolHandler | None: client_id = session.client_id if client_id: return self._sessions.get(client_id, (None, None))[1] return None @classmethod def _split_bindaddr_port(cls, port_str: str, default_port: int) -> tuple[str | None, int]: """Split an address:port pair into separate IP address and port. with IPv6 special-case handling. - Address can be specified using one of the following methods: - empty string - all interfaces default port - 1883 - Port number only (listen all interfaces) - :1883 - Port number only (listen all interfaces) - 0.0.0.0:1883 - IPv4 address - [::]:1883 - IPv6 address """ def _parse_port(port_str: str) -> int: port_str = port_str.removeprefix(":") if not port_str: return default_port return int(port_str) if port_str.startswith("["): # IPv6 literal try: addr_end = port_str.index("]") except ValueError as e: msg = "Expecting '[' to be followed by ']'" raise ValueError(msg) from e return (port_str[0 : addr_end + 1], _parse_port(port_str[addr_end + 1 :])) if ":" in port_str: address, port_str = port_str.rsplit(":", 1) return (address or None, _parse_port(port_str)) try: return (None, _parse_port(port_str)) except ValueError: return (port_str, default_port) @property def subscriptions(self) -> dict[str, list[tuple[Session, int]]]: return self._subscriptions @property def retained_messages(self) -> dict[str, RetainedApplicationMessage]: return self._retained_messages @property def sessions(self) -> dict[str, tuple[Session, BrokerProtocolHandler]]: return self._sessions Yakifo-amqtt-2637127/amqtt/client.py000066400000000000000000000600601504664204300172410ustar00rootroot00000000000000import asyncio from collections import deque from collections.abc import Callable, Coroutine import contextlib from functools import wraps import logging import ssl from typing import TYPE_CHECKING, Any, TypeAlias, cast from urllib.parse import urlparse, urlunparse import websockets from websockets import HeadersLike, InvalidHandshake, InvalidURI from amqtt.adapters import ( StreamReaderAdapter, StreamWriterAdapter, WebSocketsReader, WebSocketsWriter, ) from amqtt.contexts import BaseContext, ClientConfig from amqtt.errors import ClientError, ConnectError, ProtocolHandlerError from amqtt.mqtt.connack import CONNECTION_ACCEPTED from amqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 from amqtt.mqtt.protocol.client_handler import ClientProtocolHandler from amqtt.plugins.manager import PluginManager from amqtt.session import ApplicationMessage, OutgoingApplicationMessage, Session from amqtt.utils import gen_client_id if TYPE_CHECKING: from websockets.asyncio.client import ClientConnection class ClientContext(BaseContext): """ClientContext is used as the context passed to plugins interacting with the client. It acts as an adapter to client services from plugins. """ def __init__(self) -> None: super().__init__() self.config: ClientConfig | None = None base_logger = logging.getLogger(__name__) _F: TypeAlias = Callable[..., Coroutine[Any, Any, Any]] def mqtt_connected(func: _F) -> _F: """MQTTClient coroutines decorator which will wait until connection before calling the decorated method. :param func: coroutine to be called once connected :return: coroutine result. """ @wraps(func) async def wrapper(self: "MQTTClient", *args: Any, **kwargs: Any) -> Any: if not self._connected_state.is_set(): base_logger.warning("Client not connected, waiting for it") _, pending = await asyncio.wait( [ asyncio.create_task(self._connected_state.wait()), asyncio.create_task(self._no_more_connections.wait()), ], return_when=asyncio.FIRST_COMPLETED, ) for t in pending: t.cancel() if self._no_more_connections.is_set(): msg = "Will not reconnect" raise ClientError(msg) return await func(self, *args, **kwargs) return cast("_F", wrapper) class MQTTClient: """MQTT client implementation, providing an API for connecting to a broker and send/receive messages using the MQTT protocol. Args: client_id: MQTT client ID to use when connecting to the broker. If none, it will be generated randomly by `amqtt.utils.gen_client_id` config: `ClientConfig` or dictionary of equivalent structure options (see [client configuration](client_config.md)). Raises: PluginImportError: if importing a plugin from configuration fails PluginInitError: if initialization plugin fails """ def __init__(self, client_id: str | None = None, config: ClientConfig | dict[str, Any] | None = None) -> None: self.logger = logging.getLogger(__name__) if isinstance(config, dict): self.config = ClientConfig.from_dict(config) else: self.config = config or ClientConfig() self.client_id = client_id if client_id is not None else gen_client_id() self.session: Session | None = None self._handler: ClientProtocolHandler | None = None self._disconnect_task: asyncio.Task[Any] | None = None self._connected_state = asyncio.Event() self._no_more_connections = asyncio.Event() self.additional_headers: dict[str, Any] | HeadersLike = {} # Init plugins manager context = ClientContext() context.config = self.config self.plugins_manager: PluginManager[ClientContext] = PluginManager("amqtt.client.plugins", context) self.client_tasks: deque[asyncio.Task[Any]] = deque() async def connect( self, uri: str | None = None, cleansession: bool | None = None, cafile: str | None = None, capath: str | None = None, cadata: str | None = None, additional_headers: dict[str, Any] | HeadersLike | None = None, ) -> int: """Connect to a remote broker. At first, a network connection is established with the server using the given protocol (``mqtt``, ``mqtts``, ``ws`` or ``wss``). Once the socket is connected, a [CONNECT](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028>) message is sent with the requested information. Args: uri: Broker URI connection, conforming to [MQTT URI scheme](https://github.com/mqtt/mqtt.github.io/wiki/URI-Scheme). default, will be taken from the ``uri`` config attribute. cleansession: MQTT CONNECT clean session flag cafile: server certificate authority file (optional, used for secured connection) capath: server certificate authority path (optional, used for secured connection) cadata: server certificate authority data (optional, used for secured connection) additional_headers: a dictionary with additional http headers that should be sent on the initial connection (optional, used only with websocket connections) Returns: [CONNACK](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718033)'s return code Raises: ConnectError: could not connect to broker """ additional_headers = additional_headers if additional_headers is not None else {} self.session = self._init_session(uri, cleansession, cafile, capath, cadata) self.additional_headers = additional_headers self.logger.debug(f"Connecting to: {self.session.broker_uri}") try: return await self._do_connect() except asyncio.CancelledError as e: msg = "Future or Task was cancelled" raise ConnectError(msg) from e # no matter the failure mode, still try to reconnect except Exception as e: # pylint: disable=W0718 self.logger.warning(f"Connection failed: {e!r}") if not self.config.get("auto_reconnect", False): raise return await self.reconnect() async def disconnect(self) -> None: """Disconnect from the connected broker. This method sends a [DISCONNECT](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718090) message and closes the network socket. """ await self.cancel_tasks() if not (self.session and self._handler): self.logger.warning("Session or handler not initialized, ignoring disconnect.") return if not self.session.transitions.is_connected(): self.logger.warning("Client session not connected, ignoring call.") return if self._disconnect_task and not self._disconnect_task.done(): self._disconnect_task.cancel() await self._handler.mqtt_disconnect() self._connected_state.clear() await self._handler.stop() self.session.transitions.disconnect() async def cancel_tasks(self) -> None: """Cancel all pending tasks.""" while self.client_tasks: task = self.client_tasks.pop() task.cancel() async def reconnect(self, cleansession: bool | None = None) -> int: """Reconnect a previously connected broker. Reconnection tries to establish a network connection and send a [CONNECT](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028) message. Retries interval and attempts can be controlled with the ``reconnect_max_interval`` and ``reconnect_retries`` configuration parameters. Args: cleansession: clean session flag used in MQTT CONNECT messages sent for reconnections. Returns: [CONNACK](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718033) return code Raises: amqtt.client.ConnectException: if re-connection fails after max retries. """ if self.session and self.session.transitions.is_connected(): self.logger.warning("Client already connected") return CONNECTION_ACCEPTED if self.session and cleansession: self.session.clean_session = cleansession self.logger.debug(f"Reconnecting with session parameters: {self.session}") reconnect_max_interval = self.config.get("reconnect_max_interval", 10) reconnect_retries = self.config.get("reconnect_retries", 2) nb_attempt = 1 while True: try: self.logger.debug(f"Reconnect attempt {nb_attempt}...") return await self._do_connect() except asyncio.CancelledError as e: msg = "Future or Task was cancelled" raise ConnectError(msg) from e # no matter the failure mode, still try to reconnect except Exception as e: # pylint: disable=W0718 self.logger.warning(f"Reconnection attempt failed: {e!r}") self.logger.debug("", exc_info=True) if 0 <= reconnect_retries < nb_attempt: self.logger.exception("Maximum connection attempts reached. Reconnection aborted.") self.logger.debug("", exc_info=True) msg = "Too many failed attempts" raise ConnectError(msg) from e delay = min(reconnect_max_interval, 2**nb_attempt) self.logger.debug(f"Waiting {delay} seconds before next attempt") await asyncio.sleep(delay) nb_attempt += 1 async def _do_connect(self) -> int: return_code = await self._connect_coro() self._disconnect_task = asyncio.create_task(self.handle_connection_close()) return return_code @mqtt_connected async def ping(self) -> None: """Ping the broker. Send a MQTT [PINGREQ](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718081) message for response. """ if self.session and self._handler and self.session.transitions.is_connected(): await self._handler.mqtt_ping() elif not self.session: self.logger.warning("Session is not initialized.") elif not self._handler: self.logger.warning("Handler is not initialized.") else: self.logger.warning(f"PING incompatible with state '{self.session.transitions.state}'") @mqtt_connected async def publish( self, topic: str, message: bytes, qos: int | None = None, retain: bool | None = None, ack_timeout: int | None = None, ) -> OutgoingApplicationMessage: """Publish a message to the broker. Send a MQTT [PUBLISH](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718037) message and wait for acknowledgment depending on Quality Of Service Args: topic: topic name to which message data is published message: payload message (as bytes) to send. qos: requested publish quality of service : QOS_0, QOS_1 or QOS_2. Defaults to `default_qos` config parameter or QOS_0. retain: retain flag. Defaults to ``default_retain`` config parameter or False. ack_timeout: duration to wait for connection acknowledgment from broker. Returns: the message that was sent """ if self._handler is None: msg = "Handler is not initialized." raise ClientError(msg) def get_retain_and_qos() -> tuple[int, bool]: if qos is not None: if qos not in (QOS_0, QOS_1, QOS_2): msg = f"QOS '{qos}' is not one of QOS_0, QOS_1, QOS_2." raise ClientError(msg) _qos = qos else: _qos = self.config["default_qos"] with contextlib.suppress(KeyError): _qos = self.config["topics"][topic]["qos"] if retain: _retain = retain else: _retain = self.config["default_retain"] with contextlib.suppress(KeyError): _retain = self.config["topics"][topic]["retain"] return _qos, _retain (app_qos, app_retain) = get_retain_and_qos() return await self._handler.mqtt_publish( topic, message, app_qos, app_retain, ack_timeout, ) @mqtt_connected async def subscribe(self, topics: list[tuple[str, int]]) -> list[int]: """Subscribe to topics. Send a MQTT [SUBSCRIBE](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718063) message and wait for broker acknowledgment. Args: topics: array of tuples containing topic pattern and QOS from `amqtt.mqtt.constants` to subscribe. For example: ```python [ ("$SYS/broker/uptime", QOS_1), ("$SYS/broker/load/#", QOS_2), ] ``` Returns: [SUBACK](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718068) message return code. """ if self._handler and self.session: return await self._handler.mqtt_subscribe(topics, self.session.next_packet_id) return [0x80] @mqtt_connected async def unsubscribe(self, topics: list[str]) -> None: """Unsubscribe from topics. Send a MQTT [UNSUBSCRIBE](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718072) message and wait for broker [UNSUBACK](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718077) message. Args: topics: array of topics to unsubscribe from. ``` ["$SYS/broker/uptime", "$SYS/broker/load/#"] ``` """ if self._handler and self.session: await self._handler.mqtt_unsubscribe(topics, self.session.next_packet_id) async def deliver_message(self, timeout_duration: float | None = None) -> ApplicationMessage | None: """Deliver the next received message. Deliver next message received from the broker. If no message is available, this methods waits until next message arrives or ``timeout_duration`` occurs. Args: timeout_duration: maximum number of seconds to wait before returning. If not specified or None, there is no limit. Returns: instance of `amqtt.session.ApplicationMessage` containing received message information flow. Raises: asyncio.TimeoutError: if timeout occurs before a message is delivered ClientError: if client is not connected """ if self._handler is None: msg = "Handler is not initialized." raise ClientError(msg) deliver_task = asyncio.create_task(self._handler.mqtt_deliver_next_message()) self.client_tasks.append(deliver_task) self.logger.debug("Waiting for message delivery") done, _ = await asyncio.wait( [deliver_task], return_when=asyncio.FIRST_EXCEPTION, timeout=timeout_duration, ) if self.client_tasks: self.client_tasks.pop() if deliver_task in done: exception = deliver_task.exception() if exception is not None: # deliver_task raised an exception, pass it on to our caller raise exception return deliver_task.result() # timeout occurred before message received deliver_task.cancel() msg = "Timeout waiting for message" raise asyncio.TimeoutError(msg) async def _connect_coro(self) -> int: """Perform the core connection logic.""" if self.session is None: msg = "Session is not initialized." raise ClientError(msg) kwargs: dict[str, Any] = {} # Decode URI attributes uri_attributes = urlparse(self.session.broker_uri) scheme = uri_attributes.scheme secure = scheme in ("mqtts", "wss") self.session.username = ( self.session.username or (str(uri_attributes.username) if uri_attributes.username else None) ) self.session.password = ( self.session.password or (str(uri_attributes.password) if uri_attributes.password else None) ) self.session.remote_address = str(uri_attributes.hostname) if uri_attributes.hostname else None self.session.remote_port = uri_attributes.port if scheme in ("mqtt", "mqtts") and not self.session.remote_port: self.session.remote_port = 8883 if scheme == "mqtts" else 1883 if scheme in ("ws", "wss") and not self.session.remote_port: self.session.remote_port = 443 if scheme == "wss" else 80 if scheme in ("ws", "wss"): # Rewrite URI to conform to https://tools.ietf.org/html/rfc6455#section-3 uri = ( str(scheme), f"{self.session.remote_address}:{self.session.remote_port}", str(uri_attributes.path), str(uri_attributes.params), str(uri_attributes.query), str(uri_attributes.fragment), ) self.session.broker_uri = str(urlunparse(uri)) # Init protocol handler # if not self._handler: self._handler = ClientProtocolHandler(self.plugins_manager) connection_timeout = self.config.get("connection_timeout", None) if secure: sc = ssl.create_default_context( ssl.Purpose.SERVER_AUTH, cafile=self.session.cafile ) if self.config.connection.certfile and self.config.connection.keyfile: sc.load_cert_chain(certfile=self.config.connection.certfile, keyfile=self.config.connection.keyfile) if self.config.connection.cafile: sc.load_verify_locations(cafile=self.config.connection.cafile) if self.config.check_hostname is not None: sc.check_hostname = self.config.check_hostname sc.verify_mode = ssl.CERT_REQUIRED kwargs["ssl"] = sc try: reader: StreamReaderAdapter | WebSocketsReader | None = None writer: StreamWriterAdapter | WebSocketsWriter | None = None self._connected_state.clear() # Open connection if scheme in ("mqtt", "mqtts"): conn_reader, conn_writer = await asyncio.wait_for( asyncio.open_connection( self.session.remote_address, self.session.remote_port, **kwargs, ), timeout=connection_timeout) reader = StreamReaderAdapter(conn_reader) writer = StreamWriterAdapter(conn_writer) elif scheme in ("ws", "wss") and self.session.broker_uri: websocket: ClientConnection = await asyncio.wait_for( websockets.connect( self.session.broker_uri, subprotocols=[websockets.Subprotocol("mqtt")], additional_headers=self.additional_headers, **kwargs, ), timeout=connection_timeout) reader = WebSocketsReader(websocket) writer = WebSocketsWriter(websocket) elif not self.session.broker_uri: msg = "missing broker uri" raise ClientError(msg) else: msg = f"incorrect scheme defined in uri: '{scheme!r}'" raise ClientError(msg) # Start MQTT protocol self._handler.attach(self.session, reader, writer) return_code: int | None = await self._handler.mqtt_connect() if return_code is not CONNECTION_ACCEPTED: self.session.transitions.disconnect() self.logger.warning(f"Connection rejected with code '{return_code}'") msg = "Connection rejected by broker" exc = ConnectError(msg) exc.return_code = return_code raise exc # Handle MQTT protocol await self._handler.start() self.session.transitions.connect() self._connected_state.set() self.logger.debug(f"Connected to {self.session.remote_address}:{self.session.remote_port}") except (InvalidURI, InvalidHandshake, ProtocolHandlerError, ConnectionError, OSError, asyncio.TimeoutError) as e: self.logger.debug(f"Connection failed : {self.session.broker_uri} [{e!r}]") self.session.transitions.disconnect() raise ConnectError(e) from e return return_code async def handle_connection_close(self) -> None: """Handle disconnection from the broker.""" if self.session is None: msg = "Session is not initialized." raise ClientError(msg) if self._handler is None: msg = "Handler is not initialized." raise ClientError(msg) def cancel_tasks() -> None: self._no_more_connections.set() while self.client_tasks: task = self.client_tasks.popleft() if not task.done(): task.cancel(msg="Connection closed.") self.logger.debug("Monitoring broker disconnection") # Wait for disconnection from broker (like connection lost) await self._handler.wait_disconnect() self.logger.warning("Disconnected from broker") # Block client API self._connected_state.clear() # stop an clean handler await self._handler.stop() self._handler.detach() self.session.transitions.disconnect() if self.config.get("auto_reconnect", False): # Try reconnection self.logger.debug("Auto-reconnecting") try: await self.reconnect() except ConnectError: # Cancel client pending tasks cancel_tasks() else: # Cancel client pending tasks cancel_tasks() def _init_session( self, uri: str | None = None, cleansession: bool | None = None, cafile: str | None = None, capath: str | None = None, cadata: str | None = None, ) -> Session: """Initialize the MQTT session.""" broker_conf = self.config.get("connection", {}).copy() if uri is not None: broker_conf.uri = uri if cleansession is not None: self.config.cleansession = cleansession if cafile is not None: broker_conf.cafile = cafile if capath is not None: broker_conf.capath = capath if cadata is not None: broker_conf.cadata = cadata if not broker_conf.get("uri"): msg = "Missing connection parameter 'uri'" raise ClientError(msg) session = Session() session.broker_uri = broker_conf["uri"] session.client_id = self.client_id session.cafile = broker_conf.get("cafile") session.capath = broker_conf.get("capath") session.cadata = broker_conf.get("cadata") session.clean_session = self.config.get("cleansession", True) session.keep_alive = self.config["keep_alive"] - self.config["ping_delay"] if "will" in self.config: session.will_flag = True session.will_retain = self.config["will"]["retain"] session.will_topic = self.config["will"]["topic"] session.will_message = self.config["will"]["message"].encode() session.will_qos = self.config["will"]["qos"] return session Yakifo-amqtt-2637127/amqtt/codecs_amqtt.py000066400000000000000000000111061504664204300204260ustar00rootroot00000000000000import asyncio from decimal import ROUND_HALF_UP, Decimal from struct import pack, unpack from amqtt.adapters import ReaderAdapter from amqtt.errors import NoDataError, ZeroLengthReadError def bytes_to_hex_str(data: bytes | bytearray) -> str: """Convert a sequence of bytes into its displayable hex representation, ie: 0x??????. :param data: byte sequence :return: Hexadecimal displayable representation. """ return "0x" + "".join(format(b, "02x") for b in data) def bytes_to_int(data: bytes | int) -> int: """Convert a sequence of bytes to an integer using big endian byte ordering. :param data: byte sequence :return: integer value. """ if isinstance(data, int): return data return int.from_bytes(data, byteorder="big") def int_to_bytes(int_value: int, length: int) -> bytes: """Convert an integer to a sequence of bytes using big endian byte ordering. :param int_value: integer value to convert :param length: byte length (must be 1 or 2) :return: byte sequence :raises ValueError: if the length is unsupported """ # Map length to the appropriate format string fmt_mapping = { 1: "!B", # 1 byte, unsigned char 2: "!H", # 2 bytes, unsigned short } fmt = fmt_mapping.get(length) if not fmt: msg = "Unsupported length for int to bytes conversion. Only lengths 1 or 2 are allowed." raise ValueError(msg) return pack(fmt, int_value) async def read_or_raise(reader: ReaderAdapter | asyncio.StreamReader, n: int = -1) -> bytes: """Read a given byte number from Stream. NoDataException is raised if read gives no data. :param reader: reader adapter :param n: number of bytes to read :return: bytes read. """ try: data = await reader.read(n) except (asyncio.IncompleteReadError, ConnectionResetError, BrokenPipeError): data = None if data is None: msg = "No more data" raise NoDataError(msg) return data async def decode_string(reader: ReaderAdapter | asyncio.StreamReader) -> str: """Read a string from a reader and decode it according to MQTT string specification. :param reader: Stream reader :return: string read from stream. """ length_bytes = await read_or_raise(reader, 2) if len(length_bytes) < 1: raise ZeroLengthReadError str_length = unpack("!H", length_bytes)[0] if str_length: byte_str = await read_or_raise(reader, str_length) try: return byte_str.decode(encoding="utf-8") except UnicodeDecodeError: return str(byte_str) else: return "" async def decode_data_with_length(reader: ReaderAdapter | asyncio.StreamReader) -> bytes: """Read data from a reader. Data is prefixed with 2 bytes length. :param reader: Stream reader :return: bytes read from stream (without length). """ length_bytes = await read_or_raise(reader, 2) if len(length_bytes) < 1: raise ZeroLengthReadError bytes_length = unpack("!H", length_bytes)[0] return await read_or_raise(reader, bytes_length) def encode_string(string: str) -> bytes: """Encode a string with its length as prefix. :param string: string to encode :return: string with length prefix. """ data = string.encode(encoding="utf-8") data_length = len(data) return int_to_bytes(data_length, 2) + data def encode_data_with_length(data: bytes | bytearray) -> bytes: """Encode data with its length as prefix. :param data: data to encode :return: data with length prefix. """ data_length = len(data) return int_to_bytes(data_length, 2) + data async def decode_packet_id(reader: ReaderAdapter | asyncio.StreamReader) -> int: """Read a packet ID as 2-bytes int from stream according to MQTT specification (2.3.1). :param reader: Stream reader :return: Packet ID. """ packet_id_bytes = await read_or_raise(reader, 2) packet_id = unpack("!H", packet_id_bytes) packet: int = packet_id[0] return packet def int_to_bytes_str(value: int) -> bytes: """Convert an int value to a bytes array containing the numeric character. Ex: 123 -> b'123' :param value: int value to convert :return: bytes array. """ return str(value).encode("utf-8") def float_to_bytes_str(value: float, places: int = 3) -> bytes: """Convert an float value to a bytes array containing the numeric character.""" quant = Decimal(f"0.{''.join(['0' for i in range(places - 1)])}1") rounded = Decimal(value).quantize(quant, rounding=ROUND_HALF_UP) return str(rounded).encode("utf-8") Yakifo-amqtt-2637127/amqtt/contexts.py000066400000000000000000000410021504664204300176250ustar00rootroot00000000000000from dataclasses import dataclass, field, fields, replace import logging import warnings try: from enum import Enum, StrEnum except ImportError: # support for python 3.10 from enum import Enum class StrEnum(str, Enum): # type: ignore[no-redef] pass from collections.abc import Iterator from pathlib import Path from typing import TYPE_CHECKING, Any, Literal from dacite import Config as DaciteConfig, from_dict as dict_to_dataclass from amqtt.mqtt.constants import QOS_0, QOS_2 if TYPE_CHECKING: import asyncio logger = logging.getLogger(__name__) class BaseContext: def __init__(self) -> None: self.loop: asyncio.AbstractEventLoop | None = None self.logger: logging.Logger = logging.getLogger(__name__) # cleanup with a `Generic` type self.config: ClientConfig | BrokerConfig | dict[str, Any] | None = None class Action(StrEnum): """Actions issued by the broker.""" SUBSCRIBE = "subscribe" PUBLISH = "publish" RECEIVE = "receive" class ListenerType(StrEnum): """Types of mqtt listeners.""" TCP = "tcp" WS = "ws" EXTERNAL = "external" def __repr__(self) -> str: """Display the string value, instead of the enum member.""" return f'"{self.value!s}"' class Dictable: """Add dictionary methods to a dataclass.""" def __getitem__(self, key: str) -> Any: """Allow dict-style `[]` access to a dataclass.""" return self.get(key) def get(self, name: str, default: Any = None) -> Any: """Allow dict-style access to a dataclass.""" name = name.replace("-", "_") if hasattr(self, name): return getattr(self, name) if default is not None: return default msg = f"'{name}' is not defined" raise ValueError(msg) def __contains__(self, name: str) -> bool: """Provide dict-style 'in' check.""" return getattr(self, name.replace("-", "_"), None) is not None def __iter__(self) -> Iterator[Any]: """Provide dict-style iteration.""" for f in fields(self): # type: ignore[arg-type] yield getattr(self, f.name) def copy(self) -> dataclass: # type: ignore[valid-type] """Return a copy of the dataclass.""" return replace(self) # type: ignore[type-var] @staticmethod def _coerce_lists(value: list[Any] | dict[str, Any] | Any) -> list[dict[str, Any]]: if isinstance(value, list): return value # It's already a list of dicts if isinstance(value, dict): return [value] # Promote single dict to a list msg = "Could not convert 'list' to 'list[dict[str, Any]]'" raise ValueError(msg) @dataclass class ListenerConfig(Dictable): """Structured configuration for a broker's listeners.""" type: ListenerType = ListenerType.TCP """Type of listener: `tcp` for 'mqtt' or `ws` for 'websocket' when specified in dictionary or yaml.'""" bind: str | None = "0.0.0.0:1883" """address and port for the listener to bind to""" max_connections: int = 0 """max number of connections allowed for this listener""" ssl: bool = False """secured by ssl""" cafile: str | Path | None = None """Path to a file of concatenated CA certificates in PEM format. See [Certificates](https://docs.python.org/3/library/ssl.html#ssl-certificates) for more info.""" capath: str | Path | None = None """Path to a directory containing one or more CA certificates in PEM format, following the [OpenSSL-specific layout](https://docs.openssl.org/master/man3/SSL_CTX_load_verify_locations/).""" cadata: str | Path | None = None """Either an ASCII string of one or more PEM-encoded certificates or a bytes-like object of DER-encoded certificates.""" certfile: str | Path | None = None """Full path to file in PEM format containing the server's certificate (as well as any number of CA certificates needed to establish the certificate's authenticity.)""" keyfile: str | Path | None = None """Full path to file in PEM format containing the server's private key.""" reader: str | None = None writer: str | None = None def __post_init__(self) -> None: """Check config for errors and transform fields for easier use.""" if (self.certfile is None) ^ (self.keyfile is None): msg = "If specifying the 'certfile' or 'keyfile', both are required." raise ValueError(msg) for fn in ("cafile", "capath", "certfile", "keyfile"): if isinstance(getattr(self, fn), str): setattr(self, fn, Path(getattr(self, fn))) if getattr(self, fn) and not getattr(self, fn).exists(): msg = f"'{fn}' does not exist : {getattr(self, fn)}" raise FileNotFoundError(msg) def apply(self, other: "ListenerConfig") -> None: """Apply the field from 'other', if 'self' field is default.""" for f in fields(self): if getattr(self, f.name) == f.default: setattr(self, f.name, other[f.name]) def default_listeners() -> dict[str, Any]: """Create defaults for BrokerConfig.listeners.""" return { "default": ListenerConfig() } def default_broker_plugins() -> dict[str, Any]: """Create defaults for BrokerConfig.plugins.""" return { "amqtt.plugins.logging_amqtt.EventLoggerPlugin": {}, "amqtt.plugins.logging_amqtt.PacketLoggerPlugin": {}, "amqtt.plugins.authentication.AnonymousAuthPlugin": {"allow_anonymous": True}, "amqtt.plugins.sys.broker.BrokerSysPlugin": {"sys_interval": 20} } @dataclass class BrokerConfig(Dictable): """Structured configuration for a broker. Can be passed directly to `amqtt.broker.Broker` or created from a dictionary.""" listeners: dict[Literal["default"] | str, ListenerConfig] = field(default_factory=default_listeners) # noqa: PYI051 """Network of listeners used by the services. a 'default' named listener is required; if another listener does not set a value, the 'default' settings are applied. See [`ListenerConfig`](broker_config.md#amqtt.contexts.ListenerConfig) for more information.""" sys_interval: int | None = None """*Deprecated field to configure the `BrokerSysPlugin`. See [`BrokerSysPlugin`](../plugins/packaged_plugins.md#sys-topics) for recommended configuration.*""" timeout_disconnect_delay: int | None = 0 """Client disconnect timeout without a keep-alive.""" session_expiry_interval: int | None = None """Seconds for an inactive session to be retained.""" auth: dict[str, Any] | None = None """*Deprecated field used to config EntryPoint-loaded plugins. See [`AnonymousAuthPlugin`](../plugins/packaged_plugins.md#anonymous-auth-plugin) and [`FileAuthPlugin`](../plugins/packaged_plugins.md#password-file-auth-plugin) for recommended configuration.*""" topic_check: dict[str, Any] | None = None """*Deprecated field used to config EntryPoint-loaded plugins. See [`TopicTabooPlugin`](../plugins/packaged_plugins.md#taboo-topic-plugin) and [`TopicACLPlugin`](../plugins/packaged_plugins.md#acl-topic-plugin) for recommended configuration method.*""" plugins: dict[str, Any] | list[str | dict[str, Any]] | None = field(default_factory=default_broker_plugins) """The dictionary has a key of the dotted-module path of a class derived from `BasePlugin`, `BaseAuthPlugin` or `BaseTopicPlugin`; the value is a dictionary of configuration options for that plugin. See [custom plugins](../plugins/custom_plugins.md) for more information. `list[str | dict[str,Any]]` is deprecated but available to support legacy use cases.""" def __post_init__(self) -> None: """Check config for errors and transform fields for easier use.""" if self.sys_interval is not None: logger.warning("sys_interval is deprecated, use 'plugins' to define configuration") if self.auth is not None or self.topic_check is not None: logger.warning("'auth' and 'topic-check' are deprecated, use 'plugins' to define configuration") default_listener = self.listeners["default"] for listener_name, listener in self.listeners.items(): if listener_name == "default": continue listener.apply(default_listener) if isinstance(self.plugins, list): _plugins: dict[str, Any] = {} for plugin in self.plugins: # in case a plugin in a yaml file is listed without config map if isinstance(plugin, str): _plugins |= {plugin: {}} continue _plugins |= plugin self.plugins = _plugins @classmethod def from_dict(cls, d: dict[str, Any] | None) -> "BrokerConfig": """Create a broker config from a dictionary.""" if d is None: return BrokerConfig() # patch the incoming dictionary so it can be loaded correctly if "topic-check" in d: d["topic_check"] = d["topic-check"] del d["topic-check"] # identify EntryPoint plugin loading and prevent 'plugins' from getting defaults if ("auth" in d or "topic-check" in d) and "plugins" not in d: d["plugins"] = None return dict_to_dataclass(data_class=BrokerConfig, data=d, config=DaciteConfig( cast=[StrEnum, ListenerType], strict=True, type_hooks={list[dict[str, Any]]: cls._coerce_lists} )) @dataclass class ConnectionConfig(Dictable): """Properties for connecting to the broker.""" uri: str | None = "mqtt://127.0.0.1:1883" """URI of the broker""" cafile: str | Path | None = None """Path to a file of concatenated CA certificates in PEM format to verify broker's authenticity. See [Certificates](https://docs.python.org/3/library/ssl.html#ssl-certificates) for more info.""" capath: str | Path | None = None """Path to a directory containing one or more CA certificates in PEM format, following the [OpenSSL-specific layout](https://docs.openssl.org/master/man3/SSL_CTX_load_verify_locations/).""" cadata: str | None = None """The certificate to verify the broker's authenticity in an ASCII string format of one or more PEM-encoded certificates or a bytes-like object of DER-encoded certificates.""" certfile: str | Path | None = None """Full path to file in PEM format containing the client's certificate (as well as any number of CA certificates needed to establish the certificate's authenticity.)""" keyfile: str | Path | None = None """Full path to file in PEM format containing the client's private key associated with the certfile.""" def __post__init__(self) -> None: """Check config for errors and transform fields for easier use.""" if (self.certfile is None) ^ (self.keyfile is None): msg = "If specifying the 'certfile' or 'keyfile', both are required." raise ValueError(msg) for fn in ("cafile", "capath", "certfile", "keyfile"): if isinstance(getattr(self, fn), str): setattr(self, fn, Path(getattr(self, fn))) @dataclass class TopicConfig(Dictable): """Configuration of how messages to specific topics are published. The topic name is specified as the key in the dictionary of the `ClientConfig.topics. """ qos: int = 0 """The quality of service associated with the publishing to this topic.""" retain: bool = False """Determines if the message should be retained by the topic it was published.""" def __post__init__(self) -> None: """Check config for errors and transform fields for easier use.""" if self.qos is not None and (self.qos < QOS_0 or self.qos > QOS_2): msg = "Topic config: default QoS must be 0, 1 or 2." raise ValueError(msg) @dataclass class WillConfig(Dictable): """Configuration of the 'last will & testament' of the client upon improper disconnection.""" topic: str """The will message will be published to this topic.""" message: str """The contents of the message to be published.""" qos: int | None = QOS_0 """The quality of service associated with sending this message.""" retain: bool | None = False """Determines if the message should be retained by the topic it was published.""" def __post__init__(self) -> None: """Check config for errors and transform fields for easier use.""" if self.qos is not None and (self.qos < QOS_0 or self.qos > QOS_2): msg = "Will config: default QoS must be 0, 1 or 2." raise ValueError(msg) def default_client_plugins() -> dict[str, Any]: """Create defaults for `ClientConfig.plugins`.""" return { "amqtt.plugins.logging_amqtt.PacketLoggerPlugin": {} } @dataclass class ClientConfig(Dictable): """Structured configuration for a broker. Can be passed directly to `amqtt.broker.Broker` or created from a dictionary.""" keep_alive: int | None = 10 """Keep-alive timeout sent to the broker.""" ping_delay: int | None = 1 """Auto-ping delay before keep-alive timeout. Setting to 0 will disable which may lead to broker disconnection.""" default_qos: int | None = QOS_0 """Default QoS for messages published.""" default_retain: bool | None = False """Default retain value to messages published.""" auto_reconnect: bool | None = True """Enable or disable auto-reconnect if connection with the broker is interrupted.""" connection_timeout: int | None = 60 """The number of seconds before a connection times out""" reconnect_retries: int | None = 2 """Number of reconnection retry attempts. Negative value will cause client to reconnect indefinitely.""" reconnect_max_interval: int | None = 10 """Maximum seconds to wait before retrying a connection.""" cleansession: bool | None = True """Upon reconnect, should subscriptions be cleared. Can be overridden by `MQTTClient.connect`""" topics: dict[str, TopicConfig] | None = field(default_factory=dict) """Specify the topics and what flags should be set for messages published to them.""" broker: ConnectionConfig | None = None """*Deprecated* Configuration for connecting to the broker. Use `connection` field instead.""" connection: ConnectionConfig = field(default_factory=ConnectionConfig) """Configuration for connecting to the broker. See [`ConnectionConfig`](client_config.md#amqtt.contexts.ConnectionConfig) for more information.""" plugins: dict[str, Any] | list[dict[str, Any]] | None = field(default_factory=default_client_plugins) """The dictionary has a key of the dotted-module path of a class derived from `BasePlugin`; the value is a dictionary of configuration options for that plugin. See [custom plugins](../plugins/custom_plugins.md) for more information. `list[str | dict[str,Any]]` is deprecated but available to support legacy use cases.""" check_hostname: bool | None = True """If establishing a secure connection, should the hostname of the certificate be verified.""" will: WillConfig | None = None """Message, topic and flags that should be sent to if the client disconnects. See [`WillConfig`](client_config.md#amqtt.contexts.WillConfig) for more information.""" def __post_init__(self) -> None: """Check config for errors and transform fields for easier use.""" if self.default_qos is not None and (self.default_qos < QOS_0 or self.default_qos > QOS_2): msg = "Client config: default QoS must be 0, 1 or 2." raise ValueError(msg) if self.broker is not None: warnings.warn("The 'broker' option is deprecated, please use 'connection' instead.", stacklevel=2) self.connection = self.broker if bool(not self.connection.keyfile) ^ bool(not self.connection.certfile): msg = "Connection key and certificate files are _both_ required." raise ValueError(msg) @classmethod def from_dict(cls, d: dict[str, Any] | None) -> "ClientConfig": """Create a client config from a dictionary.""" if d is None: return ClientConfig() return dict_to_dataclass(data_class=ClientConfig, data=d, config=DaciteConfig( cast=[StrEnum], strict=True) ) Yakifo-amqtt-2637127/amqtt/contrib/000077500000000000000000000000001504664204300170475ustar00rootroot00000000000000Yakifo-amqtt-2637127/amqtt/contrib/__init__.py000066400000000000000000000025651504664204300211700ustar00rootroot00000000000000"""Module for contributed plugins.""" from dataclasses import asdict, is_dataclass from typing import Any, TypeVar from sqlalchemy import JSON, TypeDecorator T = TypeVar("T") class DataClassListJSON(TypeDecorator[list[dict[str, Any]]]): impl = JSON cache_ok = True def __init__(self, dataclass_type: type[T]) -> None: if not is_dataclass(dataclass_type): msg = f"{dataclass_type} must be a dataclass type" raise TypeError(msg) self.dataclass_type = dataclass_type super().__init__() def process_bind_param( self, value: list[Any] | None, # Python -> DB dialect: Any ) -> list[dict[str, Any]] | None: if value is None: return None return [asdict(item) for item in value] def process_result_value( self, value: list[dict[str, Any]] | None, # DB -> Python dialect: Any ) -> list[Any] | None: if value is None: return None return [self.dataclass_type(**item) for item in value] def process_literal_param(self, value: Any, dialect: Any) -> Any: # Required by SQLAlchemy, typically used for literal SQL rendering. return value @property def python_type(self) -> type: # Required by TypeEngine to indicate the expected Python type. return list Yakifo-amqtt-2637127/amqtt/contrib/auth_db/000077500000000000000000000000001504664204300204555ustar00rootroot00000000000000Yakifo-amqtt-2637127/amqtt/contrib/auth_db/__init__.py000066400000000000000000000027471504664204300226000ustar00rootroot00000000000000"""Plugin to determine authentication of clients with DB storage.""" from dataclasses import dataclass import click try: from enum import StrEnum except ImportError: # support for python 3.10 from enum import Enum class StrEnum(str, Enum): # type: ignore[no-redef] pass from .plugin import TopicAuthDBPlugin, UserAuthDBPlugin class DBType(StrEnum): """Enumeration for supported relational databases.""" MARIA = "mariadb" MYSQL = "mysql" POSTGRESQL = "postgresql" SQLITE = "sqlite" @dataclass class DBInfo: """SQLAlchemy database information.""" connect_str: str connect_port: int | None _db_map = { DBType.MARIA: DBInfo("mysql+aiomysql", 3306), DBType.MYSQL: DBInfo("mysql+aiomysql", 3306), DBType.POSTGRESQL: DBInfo("postgresql+asyncpg", 5432), DBType.SQLITE: DBInfo("sqlite+aiosqlite", None) } def db_connection_str(db_type: DBType, db_username: str, db_host: str, db_port: int | None, db_filename: str) -> str: """Create sqlalchemy database connection string.""" db_info = _db_map[db_type] if db_type == DBType.SQLITE: return f"{db_info.connect_str}:///{db_filename}" db_password = click.prompt("Enter the db password (press enter for none)", hide_input=True) pwd = f":{db_password}" if db_password else "" return f"{db_info.connect_str}://{db_username}:{pwd}@{db_host}:{db_port or db_info.connect_port}" __all__ = ["DBType", "TopicAuthDBPlugin", "UserAuthDBPlugin", "db_connection_str"] Yakifo-amqtt-2637127/amqtt/contrib/auth_db/managers.py000066400000000000000000000170261504664204300226320ustar00rootroot00000000000000from collections.abc import Iterator import logging from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from amqtt.contexts import Action from amqtt.contrib.auth_db.models import AllowedTopic, Base, TopicAuth, UserAuth from amqtt.errors import MQTTError logger = logging.getLogger(__name__) class UserManager: def __init__(self, connection: str) -> None: self._engine = create_async_engine(connection) self._db_session_maker = async_sessionmaker(self._engine, expire_on_commit=False) async def db_sync(self) -> None: """Sync the database schema.""" async with self._engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) @staticmethod async def _get_auth_or_raise(db_session: AsyncSession, username: str) -> UserAuth: stmt = select(UserAuth).filter(UserAuth.username == username) user_auth = await db_session.scalar(stmt) if not user_auth: msg = f"Username '{username}' doesn't exist." logger.debug(msg) raise MQTTError(msg) return user_auth async def get_user_auth(self, username: str) -> UserAuth | None: """Retrieve a user by username.""" async with self._db_session_maker() as db_session, db_session.begin(): try: return await self._get_auth_or_raise(db_session, username) except MQTTError: return None async def list_user_auths(self) -> Iterator[UserAuth]: """Return list of all clients.""" async with self._db_session_maker() as db_session, db_session.begin(): stmt = select(UserAuth).order_by(UserAuth.username) users = await db_session.scalars(stmt) if not users: msg = "No users exist." logger.info(msg) raise MQTTError(msg) return users async def create_user_auth(self, username: str, plain_password: str) -> UserAuth | None: """Create a new user.""" async with self._db_session_maker() as db_session, db_session.begin(): stmt = select(UserAuth).filter(UserAuth.username == username) user_auth = await db_session.scalar(stmt) if user_auth: msg = f"Username '{username}' already exists." logger.info(msg) raise MQTTError(msg) user_auth = UserAuth(username=username) user_auth.password = plain_password db_session.add(user_auth) await db_session.commit() await db_session.flush() return user_auth async def delete_user_auth(self, username: str) -> UserAuth | None: """Delete a user.""" async with self._db_session_maker() as db_session, db_session.begin(): try: user_auth = await self._get_auth_or_raise(db_session, username) except MQTTError: return None await db_session.delete(user_auth) await db_session.commit() await db_session.flush() return user_auth async def update_user_auth_password(self, username: str, plain_password: str) -> UserAuth | None: """Change a user's password.""" async with self._db_session_maker() as db_session, db_session.begin(): user_auth = await self._get_auth_or_raise(db_session, username) user_auth.password = plain_password await db_session.commit() await db_session.flush() return user_auth class TopicManager: def __init__(self, connection: str) -> None: self._engine = create_async_engine(connection) self._db_session_maker = async_sessionmaker(self._engine, expire_on_commit=False) async def db_sync(self) -> None: """Sync the database schema.""" async with self._engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) @staticmethod async def _get_auth_or_raise(db_session: AsyncSession, username: str) -> TopicAuth: stmt = select(TopicAuth).filter(TopicAuth.username == username) topic_auth = await db_session.scalar(stmt) if not topic_auth: msg = f"Username '{username}' doesn't exist." logger.debug(msg) raise MQTTError(msg) return topic_auth @staticmethod def _field_name(action: Action) -> str: return f"{action}_acl" async def create_topic_auth(self, username: str) -> TopicAuth | None: """Create a new user.""" async with self._db_session_maker() as db_session, db_session.begin(): stmt = select(TopicAuth).filter(TopicAuth.username == username) topic_auth = await db_session.scalar(stmt) if topic_auth: msg = f"Username '{username}' already exists." raise MQTTError(msg) topic_auth = TopicAuth(username=username) db_session.add(topic_auth) await db_session.commit() await db_session.flush() return topic_auth async def get_topic_auth(self, username: str) -> TopicAuth | None: """Retrieve a allowed topics by username.""" async with self._db_session_maker() as db_session, db_session.begin(): try: return await self._get_auth_or_raise(db_session, username) except MQTTError: return None async def list_topic_auths(self) -> Iterator[TopicAuth]: """Return list of all authorized clients.""" async with self._db_session_maker() as db_session, db_session.begin(): stmt = select(TopicAuth).order_by(TopicAuth.username) topics = await db_session.scalars(stmt) if not topics: msg = "No topics exist." logger.info(msg) raise MQTTError(msg) return topics async def add_allowed_topic(self, username: str, topic: str, action: Action) -> list[AllowedTopic] | None: """Add allowed topic from action for user.""" if action == Action.PUBLISH and topic.startswith("$"): msg = "MQTT does not allow clients to publish to $ topics." raise MQTTError(msg) async with self._db_session_maker() as db_session, db_session.begin(): user_auth = await self._get_auth_or_raise(db_session, username) topic_list = getattr(user_auth, self._field_name(action)) updated_list = [*topic_list, AllowedTopic(topic)] setattr(user_auth, self._field_name(action), updated_list) await db_session.commit() await db_session.flush() return updated_list async def remove_allowed_topic(self, username: str, topic: str, action: Action) -> list[AllowedTopic] | None: """Remove topic from action for user.""" async with self._db_session_maker() as db_session, db_session.begin(): topic_auth = await self._get_auth_or_raise(db_session, username) topic_list = topic_auth.get_topic_list(action) if AllowedTopic(topic) not in topic_list: msg = f"Client '{username}' doesn't have topic '{topic}' for action '{action}'." logger.debug(msg) raise MQTTError(msg) updated_list = [allowed_topic for allowed_topic in topic_list if allowed_topic != AllowedTopic(topic)] setattr(topic_auth, f"{action}_acl", updated_list) await db_session.commit() await db_session.flush() return updated_list Yakifo-amqtt-2637127/amqtt/contrib/auth_db/models.py000066400000000000000000000101701504664204300223110ustar00rootroot00000000000000from dataclasses import dataclass import logging from typing import TYPE_CHECKING, Any, Optional, Union, cast from sqlalchemy import String from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from amqtt.contexts import Action from amqtt.contrib import DataClassListJSON from amqtt.plugins import TopicMatcher if TYPE_CHECKING: from passlib.context import CryptContext logger = logging.getLogger(__name__) matcher = TopicMatcher() @dataclass class AllowedTopic: topic: str def __contains__(self, item: Union[str, "AllowedTopic"]) -> bool: """Determine `in`.""" return self.__eq__(item) def __eq__(self, item: object) -> bool: """Determine `==` or `!=`.""" if isinstance(item, str): return matcher.is_topic_allowed(item, self.topic) if isinstance(item, AllowedTopic): return item.topic == self.topic msg = "AllowedTopic can only be compared to another AllowedTopic or string." raise AttributeError(msg) def __str__(self) -> str: """Display topic.""" return self.topic def __repr__(self) -> str: """Display topic.""" return self.topic class PasswordHasher: """singleton to initialize the CryptContext and then use it elsewhere in the code.""" _instance: Optional["PasswordHasher"] = None def __init__(self) -> None: if not hasattr(self, "_crypt_context"): self._crypt_context: CryptContext | None = None def __new__(cls, *args: list[Any], **kwargs: dict[str, Any]) -> "PasswordHasher": if cls._instance is None: cls._instance = super().__new__(cls, *args, **kwargs) return cls._instance @property def crypt_context(self) -> "CryptContext": if not self._crypt_context: msg = "CryptContext is empty" raise ValueError(msg) return self._crypt_context @crypt_context.setter def crypt_context(self, value: "CryptContext") -> None: self._crypt_context = value class Base(DeclarativeBase): pass class UserAuth(Base): __tablename__ = "user_auth" id: Mapped[int] = mapped_column(primary_key=True) username: Mapped[str] = mapped_column(String, unique=True) _password_hash: Mapped[str] = mapped_column("password_hash", String(128)) publish_acl: Mapped[list[AllowedTopic]] = mapped_column(DataClassListJSON(AllowedTopic), default=list) subscribe_acl: Mapped[list[AllowedTopic]] = mapped_column(DataClassListJSON(AllowedTopic), default=list) receive_acl: Mapped[list[AllowedTopic]] = mapped_column(DataClassListJSON(AllowedTopic), default=list) @hybrid_property def password(self) -> None: msg = "Password is write-only" raise AttributeError(msg) @password.inplace.setter # type: ignore[arg-type] def _password_setter(self, plain_password: str) -> None: self._password_hash = PasswordHasher().crypt_context.hash(plain_password) def verify_password(self, plain_password: str) -> bool: return bool(PasswordHasher().crypt_context.verify(plain_password, self._password_hash)) def __str__(self) -> str: """Display client id and password hash.""" return f"'{self.username}' with password hash: {self._password_hash}" class TopicAuth(Base): __tablename__ = "topic_auth" id: Mapped[int] = mapped_column(primary_key=True) username: Mapped[str] = mapped_column(String, unique=True) publish_acl: Mapped[list[AllowedTopic]] = mapped_column(DataClassListJSON(AllowedTopic), default=list) subscribe_acl: Mapped[list[AllowedTopic]] = mapped_column(DataClassListJSON(AllowedTopic), default=list) receive_acl: Mapped[list[AllowedTopic]] = mapped_column(DataClassListJSON(AllowedTopic), default=list) def get_topic_list(self, action: Action) -> list[AllowedTopic]: return cast("list[AllowedTopic]", getattr(self, f"{action}_acl")) def __str__(self) -> str: """Display client id and password hash.""" return f"""'{self.username}': \tpublish: {self.publish_acl}, subscribe: {self.subscribe_acl}, receive: {self.receive_acl} """ Yakifo-amqtt-2637127/amqtt/contrib/auth_db/plugin.py000066400000000000000000000075731504664204300223410ustar00rootroot00000000000000from dataclasses import dataclass, field import logging from passlib.context import CryptContext from sqlalchemy.ext.asyncio import create_async_engine from amqtt.broker import BrokerContext from amqtt.contexts import Action from amqtt.contrib.auth_db.managers import TopicManager, UserManager from amqtt.contrib.auth_db.models import Base, PasswordHasher from amqtt.errors import MQTTError from amqtt.plugins.base import BaseAuthPlugin, BaseTopicPlugin from amqtt.session import Session logger = logging.getLogger(__name__) def default_hash_scheme() -> list[str]: """Create config dataclass defaults.""" return ["argon2", "bcrypt", "pbkdf2_sha256", "scrypt"] class UserAuthDBPlugin(BaseAuthPlugin): def __init__(self, context: BrokerContext) -> None: super().__init__(context) # access the singleton and set the proper crypt context pwd_hasher = PasswordHasher() pwd_hasher.crypt_context = CryptContext(schemes=self.config.hash_schemes, deprecated="auto") self._user_manager = UserManager(self.config.connection) self._engine = create_async_engine(f"{self.config.connection}") async def on_broker_pre_start(self) -> None: """Sync the schema (if configured).""" if not self.config.sync_schema: return async with self._engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async def authenticate(self, *, session: Session) -> bool | None: """Authenticate a client's session.""" if not session.username or not session.password: return False user_auth = await self._user_manager.get_user_auth(session.username) if not user_auth: return False return bool(session.password) and user_auth.verify_password(session.password) @dataclass class Config: """Configuration for DB authentication.""" connection: str """SQLAlchemy connection string for the asyncio version of the database connector: - `mysql+aiomysql://user:password@host:port/dbname` - `postgresql+asyncpg://user:password@host:port/dbname` - `sqlite+aiosqlite:///dbfilename.db` """ sync_schema: bool = False """Use SQLAlchemy to create / update the database schema.""" hash_schemes: list[str] = field(default_factory=default_hash_scheme) """list of hash schemes to use for passwords""" class TopicAuthDBPlugin(BaseTopicPlugin): def __init__(self, context: BrokerContext) -> None: super().__init__(context) self._topic_manager = TopicManager(self.config.connection) self._engine = create_async_engine(f"{self.config.connection}") async def on_broker_pre_start(self) -> None: """Sync the schema (if configured).""" if not self.config.sync_schema: return async with self._engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async def topic_filtering( self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None ) -> bool | None: if not session or not session.username or not topic: return None try: topic_auth = await self._topic_manager.get_topic_auth(session.username) topic_list = getattr(topic_auth, f"{action}_acl") except MQTTError: return False return topic in topic_list @dataclass class Config: """Configuration for DB topic filtering.""" connection: str """SQLAlchemy connection string for the asyncio version of the database connector: - `mysql+aiomysql://user:password@host:port/dbname` - `postgresql+asyncpg://user:password@host:port/dbname` - `sqlite+aiosqlite:///dbfilename.db` """ sync_schema: bool = False """Use SQLAlchemy to create / update the database schema.""" Yakifo-amqtt-2637127/amqtt/contrib/auth_db/topic_mgr_cli.py000066400000000000000000000143261504664204300236470ustar00rootroot00000000000000import asyncio import contextlib import logging from pathlib import Path from typing import Annotated import typer from amqtt.contexts import Action from amqtt.contrib.auth_db import DBType, db_connection_str from amqtt.contrib.auth_db.managers import TopicManager, UserManager from amqtt.errors import MQTTError logging.basicConfig(level=logging.INFO, format="%(message)s") logger = logging.getLogger(__name__) topic_app = typer.Typer(no_args_is_help=True) @topic_app.callback() def main( ctx: typer.Context, db_type: Annotated[DBType, typer.Option("--db", "-d", help="db type", count=False)], db_username: Annotated[str, typer.Option("--username", "-u", help="db username", show_default=False)] = "", db_port: Annotated[int, typer.Option("--port", "-p", help="database port (defaults to db type)", show_default=False)] = 0, db_host: Annotated[str, typer.Option("--host", "-h", help="database host")] = "localhost", db_filename: Annotated[str, typer.Option("--file", "-f", help="database file name (sqlite only)")] = "auth.db", ) -> None: """Command line interface to add / remove topic authorization. Passwords are not allowed to be passed via the command line for security reasons. You will be prompted for database password (if applicable). If you need to create users programmatically, see `amqtt.contrib.auth_db.managers.TopicManager` which provides the underlying functionality to this command line interface. """ if db_type == DBType.SQLITE and ctx.invoked_subcommand == "sync" and not Path(db_filename).exists(): pass elif db_type == DBType.SQLITE and not Path(db_filename).exists(): logger.error(f"SQLite option could not find '{db_filename}'") raise typer.Exit(code=1) elif db_type != DBType.SQLITE and not db_username: logger.error("DB access requires a username be provided.") raise typer.Exit(code=1) ctx.obj = {"type": db_type, "username": db_username, "host": db_host, "port": db_port, "filename": db_filename} @topic_app.command(name="sync") def db_sync(ctx: typer.Context) -> None: """Create the table and schema for username and topic lists for subscribe, publish or receive. Non-destructive if run multiple times. To clear the whole table, need to drop it manually. """ async def run_sync() -> None: connect = db_connection_str(ctx.obj["type"], ctx.obj["username"], ctx.obj["host"], ctx.obj["port"], ctx.obj["filename"]) mgr = UserManager(connect) try: await mgr.db_sync() except MQTTError as me: logger.critical("Could not sync schema on db.") raise typer.Exit(code=1) from me asyncio.run(run_sync()) logger.info("Success: database synced.") @topic_app.command(name="list") def list_clients(ctx: typer.Context) -> None: """List all Client IDs (in alphabetical order). Will also display the hashed passwords.""" async def run_list() -> None: connect = db_connection_str(ctx.obj["type"], ctx.obj["username"], ctx.obj["host"], ctx.obj["port"], ctx.obj["filename"]) mgr = TopicManager(connect) user_count = 0 for user in await mgr.list_topic_auths(): user_count += 1 logger.info(user) if not user_count: logger.info("No client authorizations exist.") asyncio.run(run_list()) @topic_app.command(name="add") def add_topic_allowance( ctx: typer.Context, topic: Annotated[str, typer.Argument(help="list of topics", show_default=False)], client_id: Annotated[str, typer.Option("--client-id", "-c", help="id for the client", show_default=False)], action: Annotated[Action, typer.Option("--action", "-a", help="action for topic to allow", show_default=False)] ) -> None: """Create a new user with a client id and password (prompted).""" async def run_add() -> None: connect = db_connection_str(ctx.obj["type"], ctx.obj["username"], ctx.obj["host"], ctx.obj["port"], ctx.obj["filename"]) mgr = TopicManager(connect) with contextlib.suppress(MQTTError): await mgr.create_topic_auth(client_id) topic_auth = await mgr.get_topic_auth(client_id) if not topic_auth: logger.info(f"Topic auth doesn't exist for '{client_id}'") raise typer.Exit(code=1) if topic in [allowed_topic.topic for allowed_topic in topic_auth.get_topic_list(action)]: logger.info(f"Topic '{topic}' already exists for '{action}'.") raise typer.Exit(1) await mgr.add_allowed_topic(client_id, topic, action) logger.info(f"Success: topic '{topic}' added to {action} for '{client_id}'") asyncio.run(run_add()) @topic_app.command(name="rm") def remove_topic_allowance(ctx: typer.Context, client_id: Annotated[str, typer.Option("--client-id", "-c", help="id for the client to remove")], action: Annotated[Action, typer.Option("--action", "-a", help="action for topic to allow")], topic: Annotated[str, typer.Argument(help="list of topics")] ) -> None: """Remove a client from the authentication database.""" async def run_remove() -> None: connect = db_connection_str(ctx.obj["type"], ctx.obj["username"], ctx.obj["host"], ctx.obj["port"], ctx.obj["filename"]) mgr = TopicManager(connect) topic_auth = await mgr.get_topic_auth(client_id) if not topic_auth: logger.info(f"client '{client_id}' doesn't exist.") raise typer.Exit(1) if topic not in getattr(topic_auth, f"{action}_acl"): logger.info(f"Error: topic '{topic}' not in the {action} allow list for {client_id}.") raise typer.Exit(1) try: await mgr.remove_allowed_topic(client_id, topic, action) except MQTTError as me: logger.info(f"'Error: could not remove '{topic}' for client '{client_id}'.") raise typer.Exit(1) from me logger.info(f"Success: removed topic '{topic}' from {action} for '{client_id}'") asyncio.run(run_remove()) if __name__ == "__main__": topic_app() Yakifo-amqtt-2637127/amqtt/contrib/auth_db/user_mgr_cli.py000066400000000000000000000146761504664204300235170ustar00rootroot00000000000000import asyncio import logging from pathlib import Path from typing import Annotated import click import passlib import typer from amqtt.contrib.auth_db import DBType, db_connection_str from amqtt.contrib.auth_db.managers import UserManager from amqtt.errors import MQTTError logging.basicConfig(level=logging.INFO, format="%(message)s") logger = logging.getLogger(__name__) user_app = typer.Typer(no_args_is_help=True) @user_app.callback() def main( ctx: typer.Context, db_type: Annotated[DBType, typer.Option(..., "--db", "-d", help="db type", show_default=False)], db_username: Annotated[str, typer.Option("--username", "-u", help="db username", show_default=False)] = "", db_port: Annotated[int, typer.Option("--port", "-p", help="database port (defaults to db type)", show_default=False)] = 0, db_host: Annotated[str, typer.Option("--host", "-h", help="database host")] = "localhost", db_filename: Annotated[str, typer.Option("--file", "-f", help="database file name (sqlite only)")] = "auth.db", ) -> None: """Command line interface to list, create, remove and add clients. Passwords are not allowed to be passed via the command line for security reasons. You will be prompted for database password (if applicable) and the client id's password. If you need to create users programmatically, see `amqtt.contrib.auth_db.managers.UserManager` which provides the underlying functionality to this command line interface. """ if db_type == DBType.SQLITE and ctx.invoked_subcommand == "sync" and not Path(db_filename).exists(): pass elif db_type == DBType.SQLITE and not Path(db_filename).exists(): logger.error(f"SQLite option could not find '{db_filename}'") raise typer.Exit(code=1) elif db_type != DBType.SQLITE and not db_username: logger.error("DB access requires a username be provided.") raise typer.Exit(code=1) ctx.obj = {"type": db_type, "username": db_username, "host": db_host, "port": db_port, "filename": db_filename} @user_app.command(name="sync") def db_sync(ctx: typer.Context) -> None: """Create the table and schema for username and hashed password. Non-destructive if run multiple times. To clear the whole table, need to drop it manually. """ async def run_sync() -> None: connect = db_connection_str(ctx.obj["type"], ctx.obj["username"], ctx.obj["host"], ctx.obj["port"], ctx.obj["filename"]) mgr = UserManager(connect) try: await mgr.db_sync() except MQTTError as me: logger.critical("Could not sync schema on db.") raise typer.Exit(code=1) from me asyncio.run(run_sync()) logger.info("Success: database synced.") @user_app.command(name="list") def list_user_auths(ctx: typer.Context) -> None: """List all Client IDs (in alphabetical order). Will also display the hashed passwords.""" async def run_list() -> None: connect = db_connection_str(ctx.obj["type"], ctx.obj["username"], ctx.obj["host"], ctx.obj["port"], ctx.obj["filename"]) mgr = UserManager(connect) user_count = 0 for user in await mgr.list_user_auths(): user_count += 1 logger.info(user) if not user_count: logger.info("No client authentications exist.") asyncio.run(run_list()) @user_app.command(name="add") def create_user_auth( ctx: typer.Context, client_id: Annotated[str, typer.Option("--client-id", "-c", help="id for the new client")], ) -> None: """Create a new user with a client id and password (prompted).""" async def run_create() -> None: connect = db_connection_str(ctx.obj["type"], ctx.obj["username"], ctx.obj["host"], ctx.obj["port"], ctx.obj["filename"]) mgr = UserManager(connect) client_password = click.prompt("Enter the client's password", hide_input=True) if not client_password.strip(): logger.info("Error: client password cannot be empty.") raise typer.Exit(1) try: user = await mgr.create_user_auth(client_id, client_password.strip()) except passlib.exc.MissingBackendError as mbe: logger.info(f"Please install backend: {mbe}") raise typer.Exit(code=1) from mbe if not user: logger.info(f"Error: could not create user: {client_id}") raise typer.Exit(code=1) logger.info(f"Success: created {user}") asyncio.run(run_create()) @user_app.command(name="rm") def remove_user_auth(ctx: typer.Context, client_id: Annotated[str, typer.Option("--client-id", "-c", help="id for the client to remove")]) -> None: """Remove a client from the authentication database.""" async def run_remove() -> None: connect = db_connection_str(ctx.obj["type"], ctx.obj["username"], ctx.obj["host"], ctx.obj["port"], ctx.obj["filename"]) mgr = UserManager(connect) user = await mgr.get_user_auth(client_id) if not user: logger.info(f"Error: client '{client_id}' does not exist.") raise typer.Exit(1) if not click.confirm(f"Please confirm the removal of '{client_id}'?"): raise typer.Exit(0) user = await mgr.delete_user_auth(client_id) if not user: logger.info(f"Error: client '{client_id}' does not exist.") raise typer.Exit(1) logger.info(f"Success: '{user.username}' was removed.") asyncio.run(run_remove()) @user_app.command(name="pwd") def change_password( ctx: typer.Context, client_id: Annotated[str, typer.Option("--client-id", "-c", help="id for the new client")], ) -> None: """Update a user's password (prompted).""" async def run_password() -> None: client_password = click.prompt("Enter the client's new password", hide_input=True) if not client_password.strip(): logger.error("Error: client password cannot be empty.") raise typer.Exit(1) connect = db_connection_str(ctx.obj["type"], ctx.obj["username"], ctx.obj["host"], ctx.obj["port"], ctx.obj["filename"]) mgr = UserManager(connect) await mgr.update_user_auth_password(client_id, client_password.strip()) logger.info(f"Success: client '{client_id}' password updated.") asyncio.run(run_password()) if __name__ == "__main__": user_app() Yakifo-amqtt-2637127/amqtt/contrib/cert.py000066400000000000000000000224131504664204300203600ustar00rootroot00000000000000from dataclasses import dataclass from datetime import datetime, timedelta try: from datetime import UTC except ImportError: # support for python 3.10 from datetime import timezone UTC = timezone.utc from ipaddress import IPv4Address import logging from pathlib import Path import re from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509 import Certificate, CertificateSigningRequest from cryptography.x509.oid import NameOID from amqtt.plugins.base import BaseAuthPlugin from amqtt.session import Session logger = logging.getLogger(__name__) class UserAuthCertPlugin(BaseAuthPlugin): """Used a *signed* x509 certificate's `Subject AlternativeName` or `SAN` to verify client authentication. Often used for IoT devices, this method provides the most secure form of identification. A root certificate, often referenced as a CA certificate -- either issued by a known authority (such as LetsEncrypt) or a self-signed certificate) is used to sign a private key and certificate for the server. Each device/client also gets a unique private key and certificate signed by the same CA certificate; also included in the device certificate is a 'SAN' or SubjectAlternativeName which is the device's unique identifier. Since both server and device certificates are signed by the same CA certificate, the client can verify the server's authenticity; and the server can verify the client's authenticity. And since the device's certificate contains a x509 SAN, the server (with this plugin) can identify the device securely. !!! note "URI and Client ID configuration" `uri_domain` configuration must be set to the same uri used to generate the device credentials when a device is connecting with private key and certificate, the `client_id` must match the device id used to generate the device credentials. Available ore three scripts to help with the key generation and certificate signing: `ca_creds`, `server_creds` and `device_creds`. !!! note "Configuring broker & client for using Self-signed root CA" If using self-signed root credentials, the `cafile` configuration for both broker and client need to be configured with `cafile` set to the `ca.crt`. """ async def authenticate(self, *, session: Session) -> bool | None: """Verify the client's session using the provided client's x509 certificate.""" if not session.ssl_object: return False der_cert = session.ssl_object.getpeercert(binary_form=True) if der_cert: cert = x509.load_der_x509_certificate(der_cert, backend=default_backend()) try: san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) uris = san.value.get_values_for_type(x509.UniformResourceIdentifier) if self.config.uri_domain not in uris[0]: return False pattern = rf"^spiffe://{re.escape(self.config.uri_domain)}/device/([^/]+)$" match = re.match(pattern, uris[0]) if not match: return False return match.group(1) == session.client_id except x509.ExtensionNotFound: logger.warning("No SAN extension found.") return False @dataclass class Config: """Configuration for the CertificateAuthPlugin.""" uri_domain: str """The domain that is expected as part of the device certificate's spiffe (e.g. test.amqtt.io)""" def generate_root_creds(country: str, state: str, locality: str, org_name: str, cn: str) -> tuple[rsa.RSAPrivateKey, Certificate]: """Generate CA key and certificate.""" # generate private key for the server ca_key = rsa.generate_private_key( public_exponent=65537, key_size=4096, ) # Create certificate subject and issuer (self-signed) subject = issuer = x509.Name([ x509.NameAttribute(NameOID.COUNTRY_NAME, country), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, state), x509.NameAttribute(NameOID.LOCALITY_NAME, locality), x509.NameAttribute(NameOID.ORGANIZATION_NAME, org_name), x509.NameAttribute(NameOID.COMMON_NAME, cn), ]) # 3. Build self-signed certificate cert = ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(issuer) .public_key(ca_key.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(datetime.now(UTC)) .not_valid_after(datetime.now(UTC) + timedelta(days=3650)) # 10 years .add_extension( x509.BasicConstraints(ca=True, path_length=None), critical=True, ) .add_extension( x509.SubjectKeyIdentifier.from_public_key(ca_key.public_key()), critical=False, ) .add_extension( x509.KeyUsage( key_cert_sign=True, crl_sign=True, digital_signature=False, key_encipherment=False, content_commitment=False, data_encipherment=False, key_agreement=False, encipher_only=False, decipher_only=False, ), critical=True, ) .sign(ca_key, hashes.SHA256()) ) return ca_key, cert def generate_server_csr(country: str, org_name: str, cn: str) -> tuple[rsa.RSAPrivateKey, CertificateSigningRequest]: """Generate server private key and server certificate-signing-request.""" key = rsa.generate_private_key(public_exponent=65537, key_size=2048) csr = ( x509.CertificateSigningRequestBuilder() .subject_name(x509.Name([ x509.NameAttribute(NameOID.COUNTRY_NAME, country), x509.NameAttribute(NameOID.ORGANIZATION_NAME, org_name), x509.NameAttribute(NameOID.COMMON_NAME, cn), ])) .add_extension( x509.SubjectAlternativeName([ x509.DNSName(cn), x509.IPAddress(IPv4Address("127.0.0.1")), ]), critical=False, ) .sign(key, hashes.SHA256()) ) return key, csr def generate_device_csr(country: str, org_name: str, common_name: str, uri_san: str, dns_san: str ) -> tuple[rsa.RSAPrivateKey, CertificateSigningRequest]: """Generate a device key and a csr.""" key = rsa.generate_private_key(public_exponent=65537, key_size=2048) csr = ( x509.CertificateSigningRequestBuilder() .subject_name(x509.Name([ x509.NameAttribute(NameOID.COUNTRY_NAME, country), x509.NameAttribute(NameOID.ORGANIZATION_NAME, org_name ), x509.NameAttribute(NameOID.COMMON_NAME, common_name), ])) .add_extension( x509.SubjectAlternativeName([ x509.UniformResourceIdentifier(uri_san), x509.DNSName(dns_san), ]), critical=False, ) .sign(key, hashes.SHA256()) ) return key, csr def sign_csr(csr: CertificateSigningRequest, ca_key: rsa.RSAPrivateKey, ca_cert: Certificate, validity_days: int = 365) -> Certificate: """Sign a csr with CA credentials.""" return ( x509.CertificateBuilder() .subject_name(csr.subject) .issuer_name(ca_cert.subject) .public_key(csr.public_key()) .serial_number(x509.random_serial_number()) .not_valid_before(datetime.now(UTC)) .not_valid_after(datetime.now(UTC) + timedelta(days=validity_days)) .add_extension( x509.BasicConstraints(ca=False, path_length=None), critical=True, ) .add_extension( csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value, critical=False, ) .add_extension( x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_cert.public_key()), # type: ignore[arg-type] critical=False, ) .sign(ca_key, hashes.SHA256()) ) def load_ca(ca_key_fn: str, ca_crt_fn: str) -> tuple[rsa.RSAPrivateKey, Certificate]: """Load server key and certificate.""" with Path(ca_key_fn).open("rb") as f: ca_key: rsa.RSAPrivateKey = serialization.load_pem_private_key(f.read(), password=None) # type: ignore[assignment] with Path(ca_crt_fn).open("rb") as f: ca_cert = x509.load_pem_x509_certificate(f.read()) return ca_key, ca_cert def write_key_and_crt(key: rsa.RSAPrivateKey, crt: Certificate, prefix: str, path: Path | None = None) -> None: """Create pem-encoded files for key and certificate.""" path = path or Path() crt_fn = path / f"{prefix}.crt" key_fn = path / f"{prefix}.key" with crt_fn.open("wb") as f: f.write(crt.public_bytes(serialization.Encoding.PEM)) with key_fn.open("wb") as f: f.write(key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption() )) Yakifo-amqtt-2637127/amqtt/contrib/http.py000066400000000000000000000133211504664204300204000ustar00rootroot00000000000000from dataclasses import dataclass try: from enum import StrEnum except ImportError: # support for python 3.10 from enum import Enum class StrEnum(str, Enum): # type: ignore[no-redef] pass import logging from typing import Any from aiohttp import ClientResponse, ClientSession, FormData from amqtt.broker import BrokerContext from amqtt.contexts import Action from amqtt.plugins.base import BaseAuthPlugin, BasePlugin, BaseTopicPlugin from amqtt.session import Session logger = logging.getLogger(__name__) class ResponseMode(StrEnum): STATUS = "status" JSON = "json" TEXT = "text" class RequestMethod(StrEnum): GET = "get" POST = "post" PUT = "put" class ParamsMode(StrEnum): JSON = "json" FORM = "form" class ACLError(Exception): pass HTTP_2xx_MIN = 200 HTTP_2xx_MAX = 299 HTTP_4xx_MIN = 400 HTTP_4xx_MAX = 499 @dataclass class HttpConfig: """Configuration for the HTTP Auth & ACL Plugin.""" host: str """hostname of the server for the auth & acl check""" port: int """port of the server for the auth & acl check""" request_method: RequestMethod = RequestMethod.GET """send the request as a GET, POST or PUT""" params_mode: ParamsMode = ParamsMode.JSON # see docs/plugins/http.md for additional details """send the request with `JSON` or `FORM` data. *additional details below*""" response_mode: ResponseMode = ResponseMode.JSON # see docs/plugins/http.md for additional details """expected response from the auth/acl server. `STATUS` (code), `JSON`, or `TEXT`. *additional details below*""" with_tls: bool = False """http or https""" user_agent: str = "amqtt" """the 'User-Agent' header sent along with the request""" superuser_uri: str | None = None """URI to verify if the user is a superuser (e.g. '/superuser'), `None` if superuser is not supported""" timeout: int = 5 """duration, in seconds, to wait for the HTTP server to respond""" class AuthHttpPlugin(BasePlugin[BrokerContext]): def __init__(self, context: BrokerContext) -> None: super().__init__(context) self.http = ClientSession(headers={"User-Agent": self.config.user_agent}) match self.config.request_method: case RequestMethod.GET: self.method = self.http.get case RequestMethod.PUT: self.method = self.http.put case _: self.method = self.http.post async def on_broker_pre_shutdown(self) -> None: await self.http.close() @staticmethod def _is_2xx(r: ClientResponse) -> bool: return HTTP_2xx_MIN <= r.status <= HTTP_2xx_MAX @staticmethod def _is_4xx(r: ClientResponse) -> bool: return HTTP_4xx_MIN <= r.status <= HTTP_4xx_MAX def _get_params(self, payload: dict[str, Any]) -> dict[str, Any]: match self.config.params_mode: case ParamsMode.FORM: match self.config.request_method: case RequestMethod.GET: kwargs = {"params": payload} case _: # POST, PUT d: Any = FormData(payload) kwargs = {"data": d} case _: # JSON kwargs = {"json": payload} return kwargs async def _send_request(self, url: str, payload: dict[str, Any]) -> bool | None: # pylint: disable=R0911 kwargs = self._get_params(payload) async with self.method(url, **kwargs) as r: logger.debug(f"http request returned {r.status}") match self.config.response_mode: case ResponseMode.TEXT: return self._is_2xx(r) and (await r.text()).lower() == "ok" case ResponseMode.STATUS: if self._is_2xx(r): return True if self._is_4xx(r): return False # any other code return None case _: if not self._is_2xx(r): return False data: dict[str, Any] = await r.json() data = {k.lower(): v for k, v in data.items()} return data.get("ok", None) def get_url(self, uri: str) -> str: return f"{'https' if self.config.with_tls else 'http'}://{self.config.host}:{self.config.port}{uri}" class UserAuthHttpPlugin(AuthHttpPlugin, BaseAuthPlugin): async def authenticate(self, *, session: Session) -> bool | None: d = {"username": session.username, "password": session.password, "client_id": session.client_id} return await self._send_request(self.get_url(self.config.user_uri), d) @dataclass class Config(HttpConfig): """Configuration for the HTTP Auth Plugin.""" user_uri: str = "/user" """URI of the auth check.""" class TopicAuthHttpPlugin(AuthHttpPlugin, BaseTopicPlugin): async def topic_filtering(self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None) -> bool | None: if not session: return None acc = 0 match action: case Action.PUBLISH: acc = 2 case Action.SUBSCRIBE: acc = 4 case Action.RECEIVE: acc = 1 d = {"username": session.username, "client_id": session.client_id, "topic": topic, "acc": acc} return await self._send_request(self.get_url(self.config.topic_uri), d) @dataclass class Config(HttpConfig): """Configuration for the HTTP Topic Plugin.""" topic_uri: str = "/acl" """URI of the topic check.""" Yakifo-amqtt-2637127/amqtt/contrib/jwt.py000066400000000000000000000075411504664204300202340ustar00rootroot00000000000000from dataclasses import dataclass import logging from typing import ClassVar import jwt try: from enum import StrEnum except ImportError: # support for python 3.10 from enum import Enum class StrEnum(str, Enum): # type: ignore[no-redef] pass from amqtt.broker import BrokerContext from amqtt.contexts import Action from amqtt.plugins import TopicMatcher from amqtt.plugins.base import BaseAuthPlugin, BaseTopicPlugin from amqtt.session import Session logger = logging.getLogger(__name__) class Algorithms(StrEnum): ES256 = "ES256" ES256K = "ES256K" ES384 = "ES384" ES512 = "ES512" ES521 = "ES521" EdDSA = "EdDSA" HS256 = "HS256" HS384 = "HS384" HS512 = "HS512" PS256 = "PS256" PS384 = "PS384" PS512 = "PS512" RS256 = "RS256" RS384 = "RS384" RS512 = "RS512" class UserAuthJwtPlugin(BaseAuthPlugin): async def authenticate(self, *, session: Session) -> bool | None: if not session.username or not session.password: return None try: decoded_payload = jwt.decode(session.password, self.config.secret_key, algorithms=["HS256"]) return bool(decoded_payload.get(self.config.user_claim, None) == session.username) except jwt.ExpiredSignatureError: logger.debug(f"jwt for '{session.username}' is expired") return False except jwt.InvalidTokenError: logger.debug(f"jwt for '{session.username}' is invalid") return False @dataclass class Config: """Configuration for the JWT user authentication.""" secret_key: str """Secret key to decrypt the token.""" user_claim: str """Payload key for user name.""" algorithm: str = "HS256" """Algorithm to use for token encryption: 'ES256', 'ES256K', 'ES384', 'ES512', 'ES521', 'EdDSA', 'HS256', 'HS384', 'HS512', 'PS256', 'PS384', 'PS512', 'RS256', 'RS384', 'RS512'""" class TopicAuthJwtPlugin(BaseTopicPlugin): _topic_jwt_claims: ClassVar = { Action.PUBLISH: "publish_claim", Action.SUBSCRIBE: "subscribe_claim", Action.RECEIVE: "receive_claim", } def __init__(self, context: BrokerContext) -> None: super().__init__(context) self.topic_matcher = TopicMatcher() async def topic_filtering( self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None ) -> bool | None: if not session or not topic or not action: return None if not session.password: return None try: decoded_payload = jwt.decode(session.password.encode(), self.config.secret_key, algorithms=["HS256"]) claim = getattr(self.config, self._topic_jwt_claims[action]) return any(self.topic_matcher.is_topic_allowed(topic, a_filter) for a_filter in decoded_payload.get(claim, [])) except jwt.ExpiredSignatureError: logger.debug(f"jwt for '{session.username}' is expired") return False except jwt.InvalidTokenError: logger.debug(f"jwt for '{session.username}' is invalid") return False @dataclass class Config: """Configuration for the JWT topic authorization.""" secret_key: str """Secret key to decrypt the token.""" publish_claim: str """Payload key for contains a list of permissible publish topics.""" subscribe_claim: str """Payload key for contains a list of permissible subscribe topics.""" receive_claim: str """Payload key for contains a list of permissible receive topics.""" algorithm: str = "HS256" """Algorithm to use for token encryption: 'ES256', 'ES256K', 'ES384', 'ES512', 'ES521', 'EdDSA', 'HS256', 'HS384', 'HS512', 'PS256', 'PS384', 'PS512', 'RS256', 'RS384', 'RS512'""" Yakifo-amqtt-2637127/amqtt/contrib/ldap.py000066400000000000000000000116131504664204300203430ustar00rootroot00000000000000from dataclasses import dataclass import logging from typing import ClassVar import ldap from amqtt.broker import BrokerContext from amqtt.contexts import Action from amqtt.errors import PluginInitError from amqtt.plugins import TopicMatcher from amqtt.plugins.base import BaseAuthPlugin, BasePlugin, BaseTopicPlugin from amqtt.session import Session logger = logging.getLogger(__name__) @dataclass class LdapConfig: """Configuration for the LDAP Plugins.""" server: str """uri formatted server location. e.g `ldap://localhost:389`""" base_dn: str """distinguished name (dn) of the ldap server. e.g. `dc=amqtt,dc=io`""" user_attribute: str """attribute in ldap entry to match the username against""" bind_dn: str """distinguished name (dn) of known, preferably read-only, user. e.g. `cn=admin,dc=amqtt,dc=io`""" bind_password: str """password for known, preferably read-only, user""" class AuthLdapPlugin(BasePlugin[BrokerContext]): def __init__(self, context: BrokerContext) -> None: super().__init__(context) self.conn = ldap.initialize(self.config.server) self.conn.protocol_version = ldap.VERSION3 # pylint: disable=E1101 try: self.conn.simple_bind_s(self.config.bind_dn, self.config.bind_password) except ldap.INVALID_CREDENTIALS as e: # pylint: disable=E1101 raise PluginInitError(self.__class__) from e class UserAuthLdapPlugin(AuthLdapPlugin, BaseAuthPlugin): """Plugin to authenticate a user with an LDAP directory server.""" async def authenticate(self, *, session: Session) -> bool | None: # use our initial creds to see if the user exists search_filter = f"({self.config.user_attribute}={session.username})" result = self.conn.search_s(self.config.base_dn, ldap.SCOPE_SUBTREE, search_filter, ["dn"]) # pylint: disable=E1101 if not result: logger.debug(f"user not found: {session.username}") return False try: # `search_s` responds with list of tuples: (dn, entry); first in list is our match user_dn = result[0][0] except IndexError: return False try: user_conn = ldap.initialize(self.config.server) user_conn.simple_bind_s(user_dn, session.password) except ldap.INVALID_CREDENTIALS: # pylint: disable=E1101 logger.debug(f"invalid credentials for '{session.username}'") return False except ldap.LDAPError as e: # pylint: disable=E1101 logger.debug(f"LDAP error during user bind: {e}") return False return True @dataclass class Config(LdapConfig): """Configuration for the User Auth LDAP Plugin.""" class TopicAuthLdapPlugin(AuthLdapPlugin, BaseTopicPlugin): """Plugin to authenticate a user with an LDAP directory server.""" _action_attr_map: ClassVar = { Action.PUBLISH: "publish_attribute", Action.SUBSCRIBE: "subscribe_attribute", Action.RECEIVE: "receive_attribute" } def __init__(self, context: BrokerContext) -> None: super().__init__(context) self.topic_matcher = TopicMatcher() async def topic_filtering( self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None ) -> bool | None: # if not provided needed criteria, can't properly evaluate topic filtering if not session or not action or not topic: return None search_filter = f"({self.config.user_attribute}={session.username})" attrs = [ "cn", self.config.publish_attribute, self.config.subscribe_attribute, self.config.receive_attribute ] results = self.conn.search_s(self.config.base_dn, ldap.SCOPE_SUBTREE, search_filter, attrs) # pylint: disable=E1101 if not results: logger.debug(f"user not found: {session.username}") return False if len(results) > 1: found_users = [dn for dn, _ in results] logger.debug(f"multiple users found: {', '.join(found_users)}") return False dn, entry = results[0] ldap_attribute = getattr(self.config, self._action_attr_map[action]) topic_filters = [t.decode("utf-8") for t in entry.get(ldap_attribute, [])] logger.debug(f"DN: {dn} - {ldap_attribute}={topic_filters}") return self.topic_matcher.are_topics_allowed(topic, topic_filters) @dataclass class Config(LdapConfig): """Configuration for the LDAPAuthPlugin.""" publish_attribute: str """LDAP attribute which contains a list of permissible publish topics.""" subscribe_attribute: str """LDAP attribute which contains a list of permissible subscribe topics.""" receive_attribute: str """LDAP attribute which contains a list of permissible receive topics.""" Yakifo-amqtt-2637127/amqtt/contrib/persistence.py000066400000000000000000000270311504664204300217500ustar00rootroot00000000000000from dataclasses import dataclass import logging from pathlib import Path from sqlalchemy import Boolean, Integer, LargeBinary, Result, String, select from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from amqtt.broker import BrokerContext, RetainedApplicationMessage from amqtt.contrib import DataClassListJSON from amqtt.errors import PluginError from amqtt.plugins.base import BasePlugin from amqtt.session import Session logger = logging.getLogger(__name__) class Base(DeclarativeBase): pass @dataclass class RetainedMessage: topic: str data: str qos: int @dataclass class Subscription: topic: str qos: int class StoredSession(Base): __tablename__ = "stored_sessions" id: Mapped[int] = mapped_column(primary_key=True) client_id: Mapped[str] = mapped_column(String) clean_session: Mapped[bool | None] = mapped_column(Boolean, nullable=True) will_flag: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") will_message: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True, default=None) will_qos: Mapped[int | None] = mapped_column(Integer, nullable=True, default=None) will_retain: Mapped[bool | None] = mapped_column(Boolean, nullable=True, default=None) will_topic: Mapped[str | None] = mapped_column(String, nullable=True, default=None) keep_alive: Mapped[int] = mapped_column(Integer, default=0) retained: Mapped[list[RetainedMessage]] = mapped_column(DataClassListJSON(RetainedMessage), default=list) subscriptions: Mapped[list[Subscription]] = mapped_column(DataClassListJSON(Subscription), default=list) class StoredMessage(Base): __tablename__ = "stored_messages" id: Mapped[int] = mapped_column(primary_key=True) topic: Mapped[str] = mapped_column(String) data: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True, default=None) qos: Mapped[int] = mapped_column(Integer, default=0) class SessionDBPlugin(BasePlugin[BrokerContext]): """Plugin to store session information and retained topic messages in the event that the broker terminates abnormally. Configuration: - file *(string)* path & filename to store the session db. default: `amqtt.db` - clear_on_shutdown *(bool)* if the broker shutdowns down normally, don't retain any information. default: `True` """ def __init__(self, context: BrokerContext) -> None: super().__init__(context) # bypass the `test_plugins_correct_has_attr` until it can be updated if not hasattr(self.config, "file"): logger.warning("`Config` is missing a `file` attribute") return self._engine = create_async_engine(f"sqlite+aiosqlite:///{self.config.file}") self._db_session_maker = async_sessionmaker(self._engine, expire_on_commit=False) @staticmethod async def _get_or_create_session(db_session: AsyncSession, client_id: str) -> StoredSession: stmt = select(StoredSession).filter(StoredSession.client_id == client_id) stored_session = await db_session.scalar(stmt) if stored_session is None: stored_session = StoredSession(client_id=client_id) db_session.add(stored_session) await db_session.flush() return stored_session @staticmethod async def _get_or_create_message(db_session: AsyncSession, topic: str) -> StoredMessage: stmt = select(StoredMessage).filter(StoredMessage.topic == topic) stored_message = await db_session.scalar(stmt) if stored_message is None: stored_message = StoredMessage(topic=topic) db_session.add(stored_message) await db_session.flush() return stored_message async def on_broker_client_connected(self, client_id: str, client_session: Session) -> None: """Search to see if session already exists.""" # if client id doesn't exist, create (can ignore if session is anonymous) # update session information (will, clean_session, etc) # don't store session information for clean or anonymous sessions if client_session.clean_session in (None, True) or client_session.is_anonymous: return async with self._db_session_maker() as db_session, db_session.begin(): stored_session = await self._get_or_create_session(db_session, client_id) stored_session.clean_session = client_session.clean_session stored_session.will_flag = client_session.will_flag stored_session.will_message = client_session.will_message # type: ignore[assignment] stored_session.will_qos = client_session.will_qos stored_session.will_retain = client_session.will_retain stored_session.will_topic = client_session.will_topic stored_session.keep_alive = client_session.keep_alive await db_session.flush() async def on_broker_client_subscribed(self, client_id: str, topic: str, qos: int) -> None: """Create/update subscription if clean session = false.""" session = self.context.get_session(client_id) if not session: logger.warning(f"'{client_id}' is subscribing but doesn't have a session") return if session.clean_session: return async with self._db_session_maker() as db_session, db_session.begin(): # stored sessions shouldn't need to be created here, but we'll use the same helper... stored_session = await self._get_or_create_session(db_session, client_id) stored_session.subscriptions = [*stored_session.subscriptions, Subscription(topic, qos)] await db_session.flush() async def on_broker_client_unsubscribed(self, client_id: str, topic: str) -> None: """Remove subscription if clean session = false.""" async def on_broker_retained_message(self, *, client_id: str | None, retained_message: RetainedApplicationMessage) -> None: """Update to retained messages. if retained_message.data is None or '', the message is being cleared """ # if client_id is valid, the retained message is for a disconnected client if client_id is not None: async with self._db_session_maker() as db_session, db_session.begin(): # stored sessions shouldn't need to be created here, but we'll use the same helper... stored_session = await self._get_or_create_session(db_session, client_id) stored_session.retained = [*stored_session.retained, RetainedMessage(retained_message.topic, retained_message.data.decode(), retained_message.qos or 0)] await db_session.flush() return async with self._db_session_maker() as db_session, db_session.begin(): # if the retained message has data, we need to store/update for the topic if retained_message.data: client_message = await self._get_or_create_message(db_session, retained_message.topic) client_message.data = retained_message.data # type: ignore[assignment] client_message.qos = retained_message.qos or 0 await db_session.flush() return # if there is no data, clear the stored message (if exists) for the topic stmt = select(StoredMessage).filter(StoredMessage.topic == retained_message.topic) topic_message = await db_session.scalar(stmt) if topic_message is not None: await db_session.delete(topic_message) await db_session.flush() return async def on_broker_pre_start(self) -> None: """Initialize the database and db connection.""" async with self._engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async def on_broker_post_start(self) -> None: """Load subscriptions.""" if len(self.context.subscriptions) > 0: msg = "SessionDBPlugin : broker shouldn't have any subscriptions yet" raise PluginError(msg) if len(list(self.context.sessions)) > 0: msg = "SessionDBPlugin : broker shouldn't have any sessions yet" raise PluginError(msg) async with self._db_session_maker() as db_session, db_session.begin(): stmt = select(StoredSession) stored_sessions = await db_session.execute(stmt) restored_sessions = 0 for stored_session in stored_sessions.scalars(): await self.context.add_subscription(stored_session.client_id, None, None) for subscription in stored_session.subscriptions: await self.context.add_subscription(stored_session.client_id, subscription.topic, subscription.qos) session = self.context.get_session(stored_session.client_id) if not session: continue session.clean_session = stored_session.clean_session session.will_flag = stored_session.will_flag session.will_message = stored_session.will_message session.will_qos = stored_session.will_qos session.will_retain = stored_session.will_retain session.will_topic = stored_session.will_topic session.keep_alive = stored_session.keep_alive for message in stored_session.retained: retained_message = RetainedApplicationMessage( source_session=None, topic=message.topic, data=message.data.encode(), qos=message.qos ) await session.retained_messages.put(retained_message) restored_sessions += 1 stmt = select(StoredMessage) stored_messages: Result[tuple[StoredMessage]] = await db_session.execute(stmt) restored_messages = 0 retained_messages = self.context.retained_messages for stored_message in stored_messages.scalars(): retained_messages[stored_message.topic] = (RetainedApplicationMessage( source_session=None, topic=stored_message.topic, data=stored_message.data or b"", qos=stored_message.qos )) restored_messages += 1 logger.info(f"Retained messages restored: {restored_messages}") logger.info(f"Restored {restored_sessions} sessions.") async def on_broker_pre_shutdown(self) -> None: """Clean up the db connection.""" await self._engine.dispose() async def on_broker_post_shutdown(self) -> None: if self.config.clear_on_shutdown and self.config.file.exists(): self.config.file.unlink() @dataclass class Config: """Configuration variables.""" file: str | Path = "amqtt.db" """path & filename to store the sqlite session db.""" clear_on_shutdown: bool = True """if the broker shutdowns down normally, don't retain any information.""" def __post_init__(self) -> None: """Create `Path` from string path.""" if isinstance(self.file, str): self.file = Path(self.file) Yakifo-amqtt-2637127/amqtt/contrib/shadows/000077500000000000000000000000001504664204300205175ustar00rootroot00000000000000Yakifo-amqtt-2637127/amqtt/contrib/shadows/__init__.py000066400000000000000000000003171504664204300226310ustar00rootroot00000000000000"""Module for the shadow state plugin.""" from .plugin import ShadowPlugin, ShadowTopicAuthPlugin from .states import ShadowOperation __all__ = ["ShadowOperation", "ShadowPlugin", "ShadowTopicAuthPlugin"] Yakifo-amqtt-2637127/amqtt/contrib/shadows/messages.py000066400000000000000000000063121504664204300227020ustar00rootroot00000000000000from collections.abc import MutableMapping from dataclasses import dataclass, fields, is_dataclass import json from typing import Any from amqtt.contrib.shadows.states import MetaTimestamp, ShadowOperation, State, StateDocument def asdict_no_none(obj: Any) -> Any: """Create dictionary from dataclass, but eliminate any key set to `None`.""" if is_dataclass(obj): result = {} for f in fields(obj): value = getattr(obj, f.name) if value is not None: result[f.name] = asdict_no_none(value) return result if isinstance(obj, list): return [asdict_no_none(item) for item in obj if item is not None] if isinstance(obj, dict): return { key: asdict_no_none(value) for key, value in obj.items() if value is not None } return obj def create_shadow_topic(device_id: str, shadow_name: str, message_op: "ShadowOperation") -> str: """Create a shadow topic for message type.""" return f"$shadow/{device_id}/{shadow_name}/{message_op}" class ShadowMessage: def to_message(self) -> bytes: return json.dumps(asdict_no_none(self)).encode("utf-8") @dataclass class GetAcceptedMessage(ShadowMessage): state: State[dict[str, Any]] metadata: State[MetaTimestamp] timestamp: int version: int @staticmethod def topic(device_id: str, shadow_name: str) -> str: return create_shadow_topic(device_id, shadow_name, ShadowOperation.GET_ACCEPT) @dataclass class GetRejectedMessage(ShadowMessage): code: int message: str timestamp: int | None = None @staticmethod def topic(device_id: str, shadow_name: str) -> str: return create_shadow_topic(device_id, shadow_name, ShadowOperation.GET_REJECT) @dataclass class UpdateAcceptedMessage(ShadowMessage): state: State[dict[str, Any]] metadata: State[MetaTimestamp] timestamp: int version: int @staticmethod def topic(device_id: str, shadow_name: str) -> str: return create_shadow_topic(device_id, shadow_name, ShadowOperation.UPDATE_ACCEPT) @dataclass class UpdateRejectedMessage(ShadowMessage): code: int message: str timestamp: int @staticmethod def topic(device_id: str, shadow_name: str) -> str: return create_shadow_topic(device_id, shadow_name, ShadowOperation.UPDATE_REJECT) @dataclass class UpdateDeltaMessage(ShadowMessage): state: MutableMapping[str, Any] metadata: MutableMapping[str, Any] timestamp: int version: int @staticmethod def topic(device_id: str, shadow_name: str) -> str: return create_shadow_topic(device_id, shadow_name, ShadowOperation.UPDATE_DELTA) class UpdateIotaMessage(UpdateDeltaMessage): """Same format, corollary name.""" @staticmethod def topic(device_id: str, shadow_name: str) -> str: return create_shadow_topic(device_id, shadow_name, ShadowOperation.UPDATE_IOTA) @dataclass class UpdateDocumentMessage(ShadowMessage): previous: StateDocument current: StateDocument timestamp: int @staticmethod def topic(device_id: str, shadow_name: str) -> str: return create_shadow_topic(device_id, shadow_name, ShadowOperation.UPDATE_DOCUMENTS) Yakifo-amqtt-2637127/amqtt/contrib/shadows/models.py000066400000000000000000000117701504664204300223620ustar00rootroot00000000000000from collections.abc import Sequence from dataclasses import asdict import logging import time from typing import Any, Optional import uuid from sqlalchemy import JSON, CheckConstraint, Integer, String, UniqueConstraint, desc, event, func, select from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession from sqlalchemy.orm import DeclarativeBase, Mapped, Mapper, Session, make_transient, mapped_column from amqtt.contrib.shadows.states import StateDocument logger = logging.getLogger(__name__) class ShadowUpdateError(Exception): def __init__(self, message: str = "updating an existing Shadow is not allowed") -> None: super().__init__(message) class ShadowBase(DeclarativeBase): pass async def sync_shadow_base(connection: AsyncConnection) -> None: """Create tables and table schemas.""" await connection.run_sync(ShadowBase.metadata.create_all) def default_state_document() -> dict[str, Any]: """Create a default (empty) state document, factory for model field.""" return asdict(StateDocument()) class Shadow(ShadowBase): __tablename__ = "shadows_shadow" id: Mapped[str | None] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) device_id: Mapped[str] = mapped_column(String(128), nullable=False) name: Mapped[str] = mapped_column(String(128), nullable=False) version: Mapped[int] = mapped_column(Integer, nullable=False) _state: Mapped[dict[str, Any]] = mapped_column("state", JSON, nullable=False, default=dict) created_at: Mapped[int] = mapped_column(Integer, default=lambda: int(time.time()), nullable=False) __table_args__ = ( CheckConstraint("version > 0", name="check_quantity_positive"), UniqueConstraint("device_id", "name", "version", name="uq_device_id_name_version"), ) @property def state(self) -> StateDocument: if not self._state: return StateDocument() return StateDocument.from_dict(self._state) @state.setter def state(self, value: StateDocument) -> None: self._state = asdict(value) @classmethod async def latest_version(cls, session: AsyncSession, device_id: str, name: str) -> Optional["Shadow"]: """Get the latest version of the shadow associated with the device and name.""" stmt = ( select(cls).where( cls.device_id == device_id, cls.name == name ).order_by(desc(cls.version)).limit(1) ) result = await session.execute(stmt) return result.scalar_one_or_none() @classmethod async def all(cls, session: AsyncSession, device_id: str, name: str) -> Sequence["Shadow"]: """Return a list of all shadows associated with the device and name.""" stmt = ( select(cls).where( cls.device_id == device_id, cls.name == name ).order_by(desc(cls.version))) result = await session.execute(stmt) return result.scalars().all() @event.listens_for(Shadow, "before_insert") def assign_incremental_version(_: Mapper[Any], connection: Session, target: "Shadow") -> None: """Get the latest version of the state document.""" stmt = ( select(func.max(Shadow.version)) .where( Shadow.device_id == target.device_id, Shadow.name == target.name ) ) result = connection.execute(stmt).scalar_one_or_none() target.version = (result or 0) + 1 @event.listens_for(Shadow, "before_update") def prevent_update(_mapper: Mapper[Any], _session: Session, _instance: "Shadow") -> None: """Prevent existing shadow from being updated.""" raise ShadowUpdateError @event.listens_for(Session, "before_flush") def convert_update_to_insert(session: Session, _flush_context: object, _instances: object | None) -> None: """Force a shadow to insert a new version, instead of updating an existing.""" # Make a copy of the dirty set so we can safely mutate the session dirty = list(session.dirty) for obj in dirty: if not session.is_modified(obj, include_collections=False): continue # skip unchanged # You can scope this to a particular class if not isinstance(obj, Shadow): continue # Clone logic: convert update into insert session.expunge(obj) # remove from session make_transient(obj) # remove identity and history obj.id = "" # clear primary key obj.version += 1 # bump version or modify fields session.add(obj) # re-add as new object _listener_example = '''# # @event.listens_for(Shadow, "before_insert") # def convert_state_document_to_json(_1: Mapper[Any], _2: Session, target: "Shadow") -> None: # """Listen for insertion and convert state document to json.""" # if not isinstance(target.state, StateDocument): # msg = "'state' field needs to be a StateDocument" # raise TypeError(msg) # # target.state = target.state.to_dict() ''' Yakifo-amqtt-2637127/amqtt/contrib/shadows/plugin.py000066400000000000000000000166701504664204300224010ustar00rootroot00000000000000from collections import defaultdict from dataclasses import dataclass, field import json import re from typing import Any from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from amqtt.broker import BrokerContext from amqtt.contexts import Action from amqtt.contrib.shadows.messages import ( GetAcceptedMessage, GetRejectedMessage, UpdateAcceptedMessage, UpdateDeltaMessage, UpdateDocumentMessage, UpdateIotaMessage, ) from amqtt.contrib.shadows.models import Shadow, sync_shadow_base from amqtt.contrib.shadows.states import ( ShadowOperation, StateDocument, calculate_delta_update, calculate_iota_update, ) from amqtt.plugins.base import BasePlugin, BaseTopicPlugin from amqtt.session import ApplicationMessage, Session shadow_topic_re = re.compile(r"^\$shadow/(?P[a-zA-Z0-9_-]+?)/(?P[a-zA-Z0-9_-]+?)/(?Pget|update)") DeviceID = str ShadowName = str @dataclass class ShadowTopic: device_id: DeviceID name: ShadowName message_op: ShadowOperation def shadow_dict() -> dict[DeviceID, dict[ShadowName, StateDocument]]: """Nested defaultdict for shadow cache.""" return defaultdict(shadow_dict) # type: ignore[arg-type] class ShadowPlugin(BasePlugin[BrokerContext]): def __init__(self, context: BrokerContext) -> None: super().__init__(context) self._shadows: dict[DeviceID, dict[ShadowName, StateDocument]] = defaultdict(dict) self._engine = create_async_engine(self.config.connection) self._db_session_maker = async_sessionmaker(self._engine, expire_on_commit=False) async def on_broker_pre_start(self) -> None: """Sync the schema.""" async with self._engine.begin() as conn: await sync_shadow_base(conn) @staticmethod def shadow_topic_match(topic: str) -> ShadowTopic | None: """Check if topic matches the shadow topic format.""" # pattern is "$shadow///get, update, etc match = shadow_topic_re.search(topic) if match: groups = match.groupdict() return ShadowTopic(groups["client_id"], groups["shadow_name"], ShadowOperation(groups["request"])) return None async def _handle_get(self, st: ShadowTopic) -> None: """Send 'accepted.""" async with self._db_session_maker() as db_session, db_session.begin(): shadow = await Shadow.latest_version(db_session, st.device_id, st.name) if not shadow: reject_msg = GetRejectedMessage( code=404, message="shadow not found", ) await self.context.broadcast_message(reject_msg.topic(st.device_id, st.name), reject_msg.to_message()) return accept_msg = GetAcceptedMessage( state=shadow.state.state, metadata=shadow.state.metadata, timestamp=shadow.created_at, version=shadow.version ) await self.context.broadcast_message(accept_msg.topic(st.device_id, st.name), accept_msg.to_message()) async def _handle_update(self, st: ShadowTopic, update: dict[str, Any]) -> None: async with self._db_session_maker() as db_session, db_session.begin(): shadow = await Shadow.latest_version(db_session, st.device_id, st.name) if not shadow: shadow = Shadow(device_id=st.device_id, name=st.name) state_update = StateDocument.from_dict(update) prev_state = shadow.state or StateDocument() prev_state.version = shadow.version or 0 # only required when generating shadow messages prev_state.timestamp = shadow.created_at or 0 # only required when generating shadow messages next_state = prev_state + state_update shadow.state = next_state db_session.add(shadow) await db_session.commit() next_state.version = shadow.version next_state.timestamp = shadow.created_at accept_msg = UpdateAcceptedMessage( state=next_state.state, metadata=next_state.metadata, timestamp=123, version=1 ) await self.context.broadcast_message(accept_msg.topic(st.device_id, st.name), accept_msg.to_message()) delta_msg = UpdateDeltaMessage( state=calculate_delta_update(next_state.state.desired, next_state.state.reported), metadata=calculate_delta_update(next_state.metadata.desired, next_state.metadata.reported), version=shadow.version, timestamp=shadow.created_at ) await self.context.broadcast_message(delta_msg.topic(st.device_id, st.name), delta_msg.to_message()) iota_msg = UpdateIotaMessage( state=calculate_iota_update(next_state.state.desired, next_state.state.reported), metadata=calculate_delta_update(next_state.metadata.desired, next_state.metadata.reported), version=shadow.version, timestamp=shadow.created_at ) await self.context.broadcast_message(iota_msg.topic(st.device_id, st.name), iota_msg.to_message()) doc_msg = UpdateDocumentMessage( previous=prev_state, current=next_state, timestamp=shadow.created_at ) await self.context.broadcast_message(doc_msg.topic(st.device_id, st.name), doc_msg.to_message()) async def on_broker_message_received(self, *, client_id: str, message: ApplicationMessage) -> None: """Process a message that was received from a client.""" topic = message.topic if not topic.startswith("$shadow"): # this is less overhead than do the full regular expression match return if not (shadow_topic := self.shadow_topic_match(topic)): return match shadow_topic.message_op: case ShadowOperation.GET: await self._handle_get(shadow_topic) case ShadowOperation.UPDATE: await self._handle_update(shadow_topic, json.loads(message.data.decode("utf-8"))) @dataclass class Config: """Configuration for shadow plugin.""" connection: str """SQLAlchemy connection string for the asyncio version of the database connector: - `mysql+aiomysql://user:password@host:port/dbname` - `postgresql+asyncpg://user:password@host:port/dbname` - `sqlite+aiosqlite:///dbfilename.db` """ class ShadowTopicAuthPlugin(BaseTopicPlugin): async def topic_filtering(self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None) -> bool | None: session = session or Session() if not topic: return False shadow_topic = ShadowPlugin.shadow_topic_match(topic) if not shadow_topic: return False return shadow_topic.device_id == session.username or session.username in self.config.superusers @dataclass class Config: """Configuration for only allowing devices access to their own shadow topics.""" superusers: list[str] = field(default_factory=list) """A list of one or more usernames that can write to any device topic, primarily for the central app sending updates to devices.""" Yakifo-amqtt-2637127/amqtt/contrib/shadows/states.py000066400000000000000000000151101504664204300223720ustar00rootroot00000000000000from collections import Counter from collections.abc import MutableMapping from dataclasses import dataclass, field try: from enum import StrEnum except ImportError: # support for python 3.10 from enum import Enum class StrEnum(str, Enum): # type: ignore[no-redef] pass import time from typing import Any, Generic, TypeVar from mergedeep import merge C = TypeVar("C", bound=Any) class StateError(Exception): def __init__(self, msg: str = "'state' field is required") -> None: super().__init__(msg) @dataclass class MetaTimestamp: timestamp: int = 0 def __eq__(self, other: object) -> bool: """Compare timestamps.""" if isinstance(other, int): return self.timestamp == other if isinstance(other, self.__class__): return self.timestamp == other.timestamp msg = "needs to be int or MetaTimestamp" raise ValueError(msg) # numeric operations to make this dataclass transparent def __abs__(self) -> int: """Absolute timestamp.""" return self.timestamp def __add__(self, other: int) -> int: """Add to a timestamp.""" return self.timestamp + other def __sub__(self, other: int) -> int: """Subtract from a timestamp.""" return self.timestamp - other def __mul__(self, other: int) -> int: """Multiply a timestamp.""" return self.timestamp * other def __float__(self) -> float: """Convert timestamp to float.""" return float(self.timestamp) def __int__(self) -> int: """Convert timestamp to int.""" return int(self.timestamp) def __lt__(self, other: int) -> bool: """Compare timestamp.""" return self.timestamp < other def __le__(self, other: int) -> bool: """Compare timestamp.""" return self.timestamp <= other def __gt__(self, other: int) -> bool: """Compare timestamp.""" return self.timestamp > other def __ge__(self, other: int) -> bool: """Compare timestamp.""" return self.timestamp >= other def create_metadata(state: MutableMapping[str, Any], timestamp: int) -> dict[str, Any]: """Create metadata (timestamps) for each of the keys in 'state'.""" metadata: dict[str, Any] = {} for key, value in state.items(): if isinstance(value, dict): metadata[key] = create_metadata(value, timestamp) elif value is None: metadata[key] = None else: metadata[key] = MetaTimestamp(timestamp) return metadata def calculate_delta_update(desired: MutableMapping[str, Any], reported: MutableMapping[str, Any], depth: bool = True, exclude_nones: bool = True, ordered_lists: bool = True) -> dict[str, Any]: """Calculate state differences between desired and reported.""" diff_dict = {} for key, value in desired.items(): if value is None and exclude_nones: continue # if the desired has an element that the reported does not... if key not in reported: diff_dict[key] = value # if the desired has an element that's a list, but the list is elif isinstance(value, list) and not ordered_lists: if Counter(value) != Counter(reported[key]): diff_dict[key] = value elif isinstance(value, dict) and depth: # recurse, report when there is a difference obj_diff = calculate_delta_update(value, reported[key]) if obj_diff: diff_dict[key] = obj_diff elif value != reported[key]: diff_dict[key] = value return diff_dict def calculate_iota_update(desired: MutableMapping[str, Any], reported: MutableMapping[str, Any]) -> MutableMapping[str, Any]: """Calculate state differences between desired and reported (including missing keys).""" delta = calculate_delta_update(desired, reported, depth=False, exclude_nones=False) for key in reported: if key not in desired: delta[key] = None return delta @dataclass class State(Generic[C]): desired: MutableMapping[str, C] = field(default_factory=dict) reported: MutableMapping[str, C] = field(default_factory=dict) @classmethod def from_dict(cls, data: dict[str, C]) -> "State[C]": """Create state from dictionary.""" return cls( desired=data.get("desired", {}), reported=data.get("reported", {}) ) def __bool__(self) -> bool: """Determine if state is empty.""" return bool(self.desired) or bool(self.reported) def __add__(self, other: "State[C]") -> "State[C]": """Merge states together.""" return State( desired=merge({}, self.desired, other.desired), reported=merge({}, self.reported, other.reported) ) @dataclass class StateDocument: state: State[dict[str, Any]] = field(default_factory=State) metadata: State[MetaTimestamp] = field(default_factory=State) version: int | None = None # only required when generating shadow messages timestamp: int | None = None # only required when generating shadow messages @classmethod def from_dict(cls, data: dict[str, Any]) -> "StateDocument": """Create state document from dictionary.""" now = int(time.time()) if data and "state" not in data: raise StateError state = State.from_dict(data.get("state", {})) metadata = State( desired=create_metadata(state.desired, now), reported=create_metadata(state.reported, now) ) return cls(state=state, metadata=metadata) def __post_init__(self) -> None: """Initialize meta data if not provided.""" now = int(time.time()) if not self.metadata: self.metadata = State( desired=create_metadata(self.state.desired, now), reported=create_metadata(self.state.reported, now), ) def __add__(self, other: "StateDocument") -> "StateDocument": """Merge two state documents together.""" return StateDocument( state=self.state + other.state, metadata=self.metadata + other.metadata ) class ShadowOperation(StrEnum): GET = "get" UPDATE = "update" GET_ACCEPT = "get/accepted" GET_REJECT = "get/rejected" UPDATE_ACCEPT = "update/accepted" UPDATE_REJECT = "update/rejected" UPDATE_DOCUMENTS = "update/documents" UPDATE_DELTA = "update/delta" UPDATE_IOTA = "update/iota" Yakifo-amqtt-2637127/amqtt/errors.py000066400000000000000000000026521504664204300173020ustar00rootroot00000000000000from typing import Any class AMQTTError(Exception): """aMQTT base exception.""" class MQTTError(Exception): """Base class for all errors referring to MQTT specifications.""" class CodecError(Exception): """Exceptions thrown by packet encode/decode functions.""" class NoDataError(Exception): """Exceptions thrown by packet encode/decode functions.""" class ZeroLengthReadError(NoDataError): def __init__(self) -> None: super().__init__("Decoding a string of length zero.") class BrokerError(Exception): """Exceptions thrown by broker.""" class PluginError(Exception): """Exceptions thrown when loading or initializing a plugin.""" class PluginImportError(PluginError): """Exceptions thrown when loading plugin.""" class PluginCoroError(PluginError): """Exceptions thrown when loading a plugin with a non-async call method.""" class PluginInitError(PluginError): """Exceptions thrown when initializing plugin.""" def __init__(self, plugin: Any) -> None: super().__init__(f"Plugin init failed: {plugin!r}") class ClientError(Exception): """Exceptions thrown by client.""" class ConnectError(ClientError): """Exceptions thrown by client connect.""" return_code: int | None = None class ProtocolHandlerError(Exception): """Exceptions thrown by protocol handle.""" class PluginLoadError(Exception): """Exception thrown when loading a plugin.""" Yakifo-amqtt-2637127/amqtt/events.py000066400000000000000000000017501504664204300172700ustar00rootroot00000000000000try: from enum import StrEnum except ImportError: # support for python 3.10 from enum import Enum class StrEnum(str, Enum): # type: ignore[no-redef] pass class Events(StrEnum): """Class for all events.""" class ClientEvents(Events): """Events issued by the client.""" class MQTTEvents(Events): PACKET_SENT = "mqtt_packet_sent" PACKET_RECEIVED = "mqtt_packet_received" class BrokerEvents(Events): """Events issued by the broker.""" PRE_START = "broker_pre_start" POST_START = "broker_post_start" PRE_SHUTDOWN = "broker_pre_shutdown" POST_SHUTDOWN = "broker_post_shutdown" CLIENT_CONNECTED = "broker_client_connected" CLIENT_DISCONNECTED = "broker_client_disconnected" CLIENT_SUBSCRIBED = "broker_client_subscribed" CLIENT_UNSUBSCRIBED = "broker_client_unsubscribed" RETAINED_MESSAGE = "broker_retained_message" MESSAGE_RECEIVED = "broker_message_received" MESSAGE_BROADCAST = "broker_message_broadcast" Yakifo-amqtt-2637127/amqtt/mqtt/000077500000000000000000000000001504664204300163745ustar00rootroot00000000000000Yakifo-amqtt-2637127/amqtt/mqtt/__init__.py000066400000000000000000000042671504664204300205160ustar00rootroot00000000000000"""INIT.""" __all__ = ["MQTTPacket"] from typing import Any, TypeAlias from amqtt.errors import AMQTTError from amqtt.mqtt.connack import ConnackPacket from amqtt.mqtt.connect import ConnectPacket from amqtt.mqtt.disconnect import DisconnectPacket from amqtt.mqtt.packet import ( CONNACK, CONNECT, DISCONNECT, PINGREQ, PINGRESP, PUBACK, PUBCOMP, PUBLISH, PUBREC, PUBREL, SUBACK, SUBSCRIBE, UNSUBACK, UNSUBSCRIBE, MQTTFixedHeader, MQTTPacket, ) from amqtt.mqtt.pingreq import PingReqPacket from amqtt.mqtt.pingresp import PingRespPacket from amqtt.mqtt.puback import PubackPacket from amqtt.mqtt.pubcomp import PubcompPacket from amqtt.mqtt.publish import PublishPacket from amqtt.mqtt.pubrec import PubrecPacket from amqtt.mqtt.pubrel import PubrelPacket from amqtt.mqtt.suback import SubackPacket from amqtt.mqtt.subscribe import SubscribePacket from amqtt.mqtt.unsuback import UnsubackPacket from amqtt.mqtt.unsubscribe import UnsubscribePacket _P: TypeAlias = MQTTPacket[Any, Any, Any] packet_dict: dict[int, type[_P]] = { CONNECT: ConnectPacket, CONNACK: ConnackPacket, PUBLISH: PublishPacket, PUBACK: PubackPacket, PUBREC: PubrecPacket, PUBREL: PubrelPacket, PUBCOMP: PubcompPacket, SUBSCRIBE: SubscribePacket, SUBACK: SubackPacket, UNSUBSCRIBE: UnsubscribePacket, UNSUBACK: UnsubackPacket, PINGREQ: PingReqPacket, PINGRESP: PingRespPacket, DISCONNECT: DisconnectPacket, } def packet_class(fixed_header: MQTTFixedHeader) -> type[_P]: """Return the packet class for a given fixed header. :param fixed_header: The fixed header of the packet. :type fixed_header: MQTTFixedHeader :return: The packet class for the given fixed header. :rtype: type[MQTTPacket] :raises AMQTTError: If the packet type is not recognized. """ if fixed_header.packet_type not in packet_dict: msg = f"Unexpected packet Type '{fixed_header.packet_type}'" raise AMQTTError(msg) try: return packet_dict[fixed_header.packet_type] except KeyError as e: msg = f"Unexpected packet Type '{fixed_header.packet_type}'" raise AMQTTError(msg) from e Yakifo-amqtt-2637127/amqtt/mqtt/connack.py000066400000000000000000000067001504664204300203650ustar00rootroot00000000000000from typing_extensions import Self from amqtt.adapters import ReaderAdapter from amqtt.codecs_amqtt import bytes_to_int, read_or_raise from amqtt.errors import AMQTTError from amqtt.mqtt.packet import CONNACK, MQTTFixedHeader, MQTTPacket, MQTTPayload, MQTTVariableHeader CONNECTION_ACCEPTED = 0x00 UNACCEPTABLE_PROTOCOL_VERSION = 0x01 IDENTIFIER_REJECTED = 0x02 SERVER_UNAVAILABLE = 0x03 BAD_USERNAME_PASSWORD = 0x04 NOT_AUTHORIZED = 0x05 class ConnackVariableHeader(MQTTVariableHeader): __slots__ = ("return_code", "session_parent") def __init__(self, session_parent: int | None = None, return_code: int | None = None) -> None: super().__init__() self.session_parent = session_parent self.return_code = return_code @classmethod async def from_stream(cls, reader: ReaderAdapter, _: MQTTFixedHeader | None) -> Self: data = await read_or_raise(reader, 2) session_parent = data[0] & 0x01 return_code = bytes_to_int(data[1]) return cls(session_parent, return_code) def to_bytes(self) -> bytes | bytearray: out = bytearray(2) # Connect acknowledge flags out[0] = 1 if self.session_parent else 0 # Return code out[1] = self.return_code or 0 return out def __repr__(self) -> str: """Return a string representation of the ConnackVariableHeader object.""" return f"{type(self).__name__}(session_parent={hex(self.session_parent or 0)}, return_code={hex(self.return_code or 0)})" class ConnackPacket(MQTTPacket[ConnackVariableHeader, MQTTPayload[MQTTVariableHeader], MQTTFixedHeader]): VARIABLE_HEADER = ConnackVariableHeader PAYLOAD = MQTTPayload[MQTTVariableHeader] def __init__( self, fixed: MQTTFixedHeader | None = None, variable_header: ConnackVariableHeader | None = None, payload: MQTTPayload[MQTTVariableHeader] | None = None, ) -> None: if fixed is None: header = MQTTFixedHeader(CONNACK, 0x00) elif fixed.packet_type != CONNACK: msg = f"Invalid fixed packet type {fixed.packet_type} for ConnackPacket init" raise AMQTTError(msg) from None else: header = fixed super().__init__(header, variable_header, payload) @classmethod def build(cls, session_parent: int | None = None, return_code: int | None = None) -> Self: v_header = ConnackVariableHeader(session_parent, return_code) return cls(variable_header=v_header) @property def return_code(self) -> int | None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.return_code @return_code.setter def return_code(self, return_code: int | None) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.return_code = return_code @property def session_parent(self) -> int | None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.session_parent @session_parent.setter def session_parent(self, session_parent: int | None) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.session_parent = session_parent Yakifo-amqtt-2637127/amqtt/mqtt/connect.py000066400000000000000000000372371504664204300204130ustar00rootroot00000000000000from asyncio import StreamReader from typing_extensions import Self from amqtt.adapters import ReaderAdapter from amqtt.codecs_amqtt import ( bytes_to_int, decode_data_with_length, decode_string, encode_data_with_length, encode_string, int_to_bytes, read_or_raise, ) from amqtt.errors import AMQTTError, NoDataError from amqtt.mqtt.packet import CONNECT, MQTTFixedHeader, MQTTPacket, MQTTPayload, MQTTVariableHeader from amqtt.utils import gen_client_id class ConnectVariableHeader(MQTTVariableHeader): __slots__ = ("flags", "keep_alive", "proto_level", "proto_name") USERNAME_FLAG = 0x80 PASSWORD_FLAG = 0x40 WILL_RETAIN_FLAG = 0x20 WILL_FLAG = 0x04 WILL_QOS_MASK = 0x18 CLEAN_SESSION_FLAG = 0x02 RESERVED_FLAG = 0x01 def __init__(self, connect_flags: int = 0x00, keep_alive: int = 0, proto_name: str = "MQTT", proto_level: int = 0x04) -> None: super().__init__() self.proto_name = proto_name self.proto_level = proto_level self.flags = connect_flags self.keep_alive = keep_alive def __repr__(self) -> str: """Return a string representation of the ConnectVariableHeader object.""" return ( f"ConnectVariableHeader(proto_name={self.proto_name}, proto_level={self.proto_level}," f" flags={hex(self.flags)}, keepalive={self.keep_alive})" ) def _set_flag(self, val: bool, mask: int) -> None: if val: self.flags |= mask else: self.flags &= ~mask def _get_flag(self, mask: int) -> bool: return bool(self.flags & mask) @classmethod async def from_stream(cls, reader: ReaderAdapter, _: MQTTFixedHeader) -> Self: # protocol name protocol_name = await decode_string(reader) # protocol level protocol_level_byte = await read_or_raise(reader, 1) protocol_level = bytes_to_int(protocol_level_byte) # flags flags_byte = await read_or_raise(reader, 1) flags = bytes_to_int(flags_byte) # keep-alive keep_alive_byte = await read_or_raise(reader, 2) keep_alive = bytes_to_int(keep_alive_byte) return cls(flags, keep_alive, protocol_name, protocol_level) def to_bytes(self) -> bytes | bytearray: out = bytearray() # Protocol name out.extend(encode_string(self.proto_name)) # Protocol level out.append(self.proto_level) # flags out.append(self.flags) # keep alive out.extend(int_to_bytes(self.keep_alive, 2)) return out @property def username_flag(self) -> bool: return self._get_flag(self.USERNAME_FLAG) @username_flag.setter def username_flag(self, val: bool) -> None: self._set_flag(val, self.USERNAME_FLAG) @property def password_flag(self) -> bool: return self._get_flag(self.PASSWORD_FLAG) @password_flag.setter def password_flag(self, val: bool) -> None: self._set_flag(val, self.PASSWORD_FLAG) @property def will_retain_flag(self) -> bool: return self._get_flag(self.WILL_RETAIN_FLAG) @will_retain_flag.setter def will_retain_flag(self, val: bool) -> None: self._set_flag(val, self.WILL_RETAIN_FLAG) @property def will_flag(self) -> bool: return self._get_flag(self.WILL_FLAG) @will_flag.setter def will_flag(self, val: bool) -> None: self._set_flag(val, self.WILL_FLAG) @property def clean_session_flag(self) -> bool: return self._get_flag(self.CLEAN_SESSION_FLAG) @clean_session_flag.setter def clean_session_flag(self, val: bool) -> None: self._set_flag(val, self.CLEAN_SESSION_FLAG) @property def reserved_flag(self) -> bool: return self._get_flag(self.RESERVED_FLAG) @reserved_flag.setter def reserved_flag(self, val: bool) -> None: self._set_flag(val, self.RESERVED_FLAG) @property def will_qos(self) -> int: return (self.flags & 0x18) >> 3 @will_qos.setter def will_qos(self, val: int) -> None: self.flags &= 0xE7 # Reset QOS flags self.flags |= val << 3 class ConnectPayload(MQTTPayload[ConnectVariableHeader]): __slots__ = ( "client_id", "client_id_is_random", "password", "username", "will_message", "will_topic", ) def __init__( self, client_id: str | None = None, will_topic: str | None = None, will_message: bytes | bytearray | None = None, username: str | None = None, password: str | None = None, ) -> None: super().__init__() self.client_id_is_random = False self.client_id = client_id self.will_topic = will_topic self.will_message = will_message self.username = username self.password = password def __repr__(self) -> str: """Return a string representation of the ConnectPayload object.""" return ( f"ConnectVariableHeader(client_id={self.client_id}, will_topic={self.will_topic}," f"will_message={self.will_message!r}, username={self.username}, password={self.password})" ) @classmethod async def from_stream( cls, reader: StreamReader | ReaderAdapter, _: MQTTFixedHeader | None, variable_header: ConnectVariableHeader | None, ) -> Self: payload = cls() # Client identifier try: payload.client_id = await decode_string(reader) except NoDataError: payload.client_id = None if payload.client_id is None or payload.client_id == "": # A Server MAY allow a Client to supply a ClientId that has a length of zero bytes # [MQTT-3.1.3-6] payload.client_id = gen_client_id() # indicator to throw exception in case CLEAN_SESSION_FLAG is set to False payload.client_id_is_random = True # Read will topic, username and password if variable_header is not None and variable_header.will_flag: try: payload.will_topic = await decode_string(reader) payload.will_message = await decode_data_with_length(reader) except NoDataError: payload.will_topic = None payload.will_message = None if variable_header is not None and variable_header.username_flag: try: payload.username = await decode_string(reader) except NoDataError: payload.username = None if variable_header is not None and variable_header.password_flag: try: payload.password = await decode_string(reader) except NoDataError: payload.password = None return payload def to_bytes( self, fixed_header: MQTTFixedHeader | None = None, variable_header: ConnectVariableHeader | None = None, ) -> bytes | bytearray: out = bytearray() # Client identifier if self.client_id is not None: out.extend(encode_string(self.client_id)) # Will topic / message if variable_header is not None and variable_header.will_flag: if self.will_topic is not None: out.extend(encode_string(self.will_topic)) if self.will_message is not None: out.extend(encode_data_with_length(self.will_message)) # username if variable_header is not None and variable_header.username_flag and self.username is not None: out.extend(encode_string(self.username)) # password if variable_header is not None and variable_header.password_flag and self.password is not None: out.extend(encode_string(self.password)) return out class ConnectPacket(MQTTPacket[ConnectVariableHeader, ConnectPayload, MQTTFixedHeader]): # type: ignore [type-var] VARIABLE_HEADER = ConnectVariableHeader PAYLOAD = ConnectPayload def __init__( self, fixed: MQTTFixedHeader | None = None, variable_header: ConnectVariableHeader | None = None, payload: ConnectPayload | None = None, ) -> None: if fixed is None: header = MQTTFixedHeader(CONNECT, 0x00) else: if fixed.packet_type is not CONNECT: msg = f"Invalid fixed packet type {fixed.packet_type} for ConnectPacket init" raise AMQTTError(msg) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @property def proto_name(self) -> str: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.proto_name @proto_name.setter def proto_name(self, name: str) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.proto_name = name @property def proto_level(self) -> int: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.proto_level @proto_level.setter def proto_level(self, level: int) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.proto_level = level @property def username_flag(self) -> bool: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.username_flag @username_flag.setter def username_flag(self, flag: bool) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.username_flag = flag @property def password_flag(self) -> bool: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.password_flag @password_flag.setter def password_flag(self, flag: bool) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.password_flag = flag @property def clean_session_flag(self) -> bool: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.clean_session_flag @clean_session_flag.setter def clean_session_flag(self, flag: bool) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.clean_session_flag = flag @property def will_retain_flag(self) -> bool: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.will_retain_flag @will_retain_flag.setter def will_retain_flag(self, flag: bool) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.will_retain_flag = flag @property def will_qos(self) -> int: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.will_qos @will_qos.setter def will_qos(self, flag: int) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.will_qos = flag @property def will_flag(self) -> bool: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.will_flag @will_flag.setter def will_flag(self, flag: bool) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.will_flag = flag @property def reserved_flag(self) -> bool: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.reserved_flag @reserved_flag.setter def reserved_flag(self, flag: bool) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.reserved_flag = flag @property def client_id(self) -> str | None: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) return self.payload.client_id @client_id.setter def client_id(self, client_id: str) -> None: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) self.payload.client_id = client_id @property def client_id_is_random(self) -> bool: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) return self.payload.client_id_is_random @client_id_is_random.setter def client_id_is_random(self, client_id_is_random: bool) -> None: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) self.payload.client_id_is_random = client_id_is_random @property def will_topic(self) -> str | None: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) return self.payload.will_topic @will_topic.setter def will_topic(self, will_topic: str) -> None: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) self.payload.will_topic = will_topic @property def will_message(self) -> bytes | bytearray | None: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) return self.payload.will_message @will_message.setter def will_message(self, will_message: bytes | bytearray) -> None: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) self.payload.will_message = will_message @property def username(self) -> str | None: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) return self.payload.username @username.setter def username(self, username: str) -> None: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) self.payload.username = username @property def password(self) -> str | None: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) return self.payload.password @password.setter def password(self, password: str) -> None: if self.payload is None: msg = "Payload is not set" raise ValueError(msg) self.payload.password = password @property def keep_alive(self) -> int: if self.variable_header is None: msg = "Payload is not set" raise ValueError(msg) return self.variable_header.keep_alive @keep_alive.setter def keep_alive(self, keep_alive: int) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.keep_alive = keep_alive Yakifo-amqtt-2637127/amqtt/mqtt/constants.py000066400000000000000000000000471504664204300207630ustar00rootroot00000000000000QOS_0 = 0x00 QOS_1 = 0x01 QOS_2 = 0x02 Yakifo-amqtt-2637127/amqtt/mqtt/disconnect.py000066400000000000000000000012701504664204300210770ustar00rootroot00000000000000from amqtt.errors import AMQTTError from amqtt.mqtt.packet import DISCONNECT, MQTTFixedHeader, MQTTPacket class DisconnectPacket(MQTTPacket[None, None, MQTTFixedHeader]): VARIABLE_HEADER = None PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader | None = None) -> None: if fixed is None: header = MQTTFixedHeader(DISCONNECT, 0x00) else: if fixed.packet_type is not DISCONNECT: msg = f"Invalid fixed packet type {fixed.packet_type} for DisconnectPacket init" raise AMQTTError(msg) header = fixed super().__init__(header) self.variable_header = None self.payload = None Yakifo-amqtt-2637127/amqtt/mqtt/packet.py000066400000000000000000000211161504664204300202160ustar00rootroot00000000000000from abc import ABC, abstractmethod import asyncio try: from datetime import UTC, datetime except ImportError: from datetime import datetime, timezone UTC = timezone.utc from struct import unpack from typing import Generic from typing_extensions import Self, TypeVar from amqtt.adapters import ReaderAdapter, WriterAdapter from amqtt.codecs_amqtt import bytes_to_hex_str, decode_packet_id, int_to_bytes, read_or_raise from amqtt.errors import CodecError, MQTTError, NoDataError RESERVED_0 = 0x00 CONNECT = 0x01 CONNACK = 0x02 PUBLISH = 0x03 PUBACK = 0x04 PUBREC = 0x05 PUBREL = 0x06 PUBCOMP = 0x07 SUBSCRIBE = 0x08 SUBACK = 0x09 UNSUBSCRIBE = 0x0A UNSUBACK = 0x0B PINGREQ = 0x0C PINGRESP = 0x0D DISCONNECT = 0x0E RESERVED_15 = 0x0F class MQTTFixedHeader: """Represents the fixed header of an MQTT packet.""" __slots__ = ("flags", "packet_type", "remaining_length") def __init__(self, packet_type: int, flags: int = 0, length: int = 0) -> None: self.packet_type = packet_type self.flags = flags self.remaining_length = length def to_bytes(self) -> bytes: """Encode the fixed header to bytes.""" def encode_remaining_length(length: int) -> bytes: """Encode the remaining length as per MQTT protocol.""" encoded = bytearray() while True: length_byte = length % 0x80 length //= 0x80 if length > 0: length_byte |= 0x80 encoded.append(length_byte) if length <= 0: break return bytes(encoded) try: packet_type_flags = (self.packet_type << 4) | self.flags encoded_length = encode_remaining_length(self.remaining_length) return bytes([packet_type_flags]) + encoded_length except OverflowError as exc: msg = f"Fixed header encoding failed: {exc}" raise CodecError(msg) from exc async def to_stream(self, writer: WriterAdapter) -> None: """Write the fixed header to the stream.""" writer.write(self.to_bytes()) @property def bytes_length(self) -> int: return len(self.to_bytes()) @classmethod async def from_stream(cls: type[Self], reader: ReaderAdapter) -> "Self | None": """Decode a fixed header from the stream.""" async def decode_remaining_length() -> int: """Decode the remaining length from the stream.""" multiplier: int value: int multiplier, value = 1, 0 buffer = bytearray() while True: encoded_byte = await reader.read(1) byte_value = unpack("!B", encoded_byte)[0] buffer.append(byte_value) value += (byte_value & 0x7F) * multiplier if (byte_value & 0x80) == 0: break multiplier *= 128 if multiplier > 128**3: msg = f"Invalid remaining length bytes:{bytes_to_hex_str(buffer)}, packet_type={packet_type}" raise MQTTError(msg) return value try: byte1 = await read_or_raise(reader, 1) int1 = unpack("!B", byte1)[0] packet_type = (int1 & 0xF0) >> 4 flags = int1 & 0x0F remaining_length = await decode_remaining_length() return cls(packet_type, flags, remaining_length) except NoDataError: return None def __repr__(self) -> str: """Return a string representation of the MQTTFixedHeader object.""" return f"{self.__class__.__name__}(packet_type={self.packet_type}, flags={self.flags}, length={self.remaining_length})" _FH = TypeVar("_FH", bound=MQTTFixedHeader) class MQTTVariableHeader(ABC): """Abstract base class for MQTT variable headers.""" async def to_stream(self, writer: asyncio.StreamWriter) -> None: writer.write(self.to_bytes()) await writer.drain() @abstractmethod def to_bytes(self) -> bytes | bytearray: """Serialize the variable header to bytes.""" @property def bytes_length(self) -> int: return len(self.to_bytes()) @classmethod @abstractmethod async def from_stream(cls: type[Self], reader: ReaderAdapter, fixed_header: MQTTFixedHeader) -> Self: pass class PacketIdVariableHeader(MQTTVariableHeader): """Represents a variable header containing a packet ID.""" __slots__ = ("packet_id",) def __init__(self, packet_id: int) -> None: super().__init__() self.packet_id = packet_id def to_bytes(self) -> bytes: return int_to_bytes(self.packet_id, 2) @classmethod async def from_stream( cls: type[Self], reader: ReaderAdapter, _: MQTTFixedHeader | None = None, ) -> Self: packet_id = await decode_packet_id(reader) return cls(packet_id) def __repr__(self) -> str: """Return a string representation of the PacketIdVariableHeader object.""" return f"{self.__class__.__name__}(packet_id={self.packet_id})" _VH = TypeVar("_VH", bound=MQTTVariableHeader | None) class MQTTPayload(ABC, Generic[_VH]): """Abstract base class for MQTT payloads.""" async def to_stream(self, writer: asyncio.StreamWriter) -> None: writer.write(self.to_bytes()) await writer.drain() @abstractmethod def to_bytes(self, fixed_header: MQTTFixedHeader | None = None, variable_header: _VH | None = None) -> bytes | bytearray: pass @classmethod @abstractmethod async def from_stream( cls: type[Self], reader: asyncio.StreamReader | ReaderAdapter, fixed_header: MQTTFixedHeader | None, variable_header: _VH | None, ) -> Self: pass _P = TypeVar("_P", bound=MQTTPayload[MQTTVariableHeader] | None) class MQTTPacket(Generic[_VH, _P, _FH]): """Represents an MQTT packet.""" __slots__ = ("fixed_header", "payload", "protocol_ts", "variable_header") VARIABLE_HEADER: type[_VH] | None = None PAYLOAD: type[_P] | None = None FIXED_HEADER: type[_FH] = MQTTFixedHeader # type: ignore [assignment] def __init__(self, fixed: _FH, variable_header: _VH | None = None, payload: _P | None = None) -> None: self.fixed_header = fixed self.variable_header = variable_header self.payload = payload self.protocol_ts: datetime | None = None async def to_stream(self, writer: WriterAdapter) -> None: """Write the entire packet to the stream.""" writer.write(self.to_bytes()) await writer.drain() self.protocol_ts = datetime.now(UTC) def to_bytes(self) -> bytes: """Serialize the packet into bytes.""" variable_header_bytes = self.variable_header.to_bytes() if self.variable_header is not None else b"" payload_bytes = self.payload.to_bytes(self.fixed_header, self.variable_header) if self.payload is not None else b"" fixed_header_bytes = b"" if self.fixed_header: self.fixed_header.remaining_length = len(variable_header_bytes) + len(payload_bytes) fixed_header_bytes = self.fixed_header.to_bytes() return fixed_header_bytes + variable_header_bytes + payload_bytes @classmethod async def from_stream( cls: type[Self], reader: ReaderAdapter, fixed_header: _FH | None = None, variable_header: _VH | None = None, ) -> Self: """Decode an MQTT packet from the stream.""" if fixed_header is None: fixed_header = await cls.FIXED_HEADER.from_stream(reader) if cls.VARIABLE_HEADER and variable_header is None: variable_header = await cls.VARIABLE_HEADER.from_stream(reader, fixed_header) if cls.PAYLOAD and fixed_header: payload = await cls.PAYLOAD.from_stream(reader, fixed_header, variable_header) else: payload = None if fixed_header and not variable_header and not payload: instance = cls(fixed_header) elif fixed_header and not payload: instance = cls(fixed_header, variable_header) else: instance = cls(fixed_header, variable_header, payload) instance.protocol_ts = datetime.now(UTC) return instance @property def bytes_length(self) -> int: return len(self.to_bytes()) def __repr__(self) -> str: """Return a string representation of the packet.""" return ( f"{self.__class__.__name__}(ts={self.protocol_ts}, " f"fixed={self.fixed_header}, variable={self.variable_header}, payload={self.payload})" ) Yakifo-amqtt-2637127/amqtt/mqtt/pingreq.py000066400000000000000000000012511504664204300204120ustar00rootroot00000000000000from amqtt.errors import AMQTTError from amqtt.mqtt.packet import PINGREQ, MQTTFixedHeader, MQTTPacket class PingReqPacket(MQTTPacket[None, None, MQTTFixedHeader]): VARIABLE_HEADER = None PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader | None = None) -> None: if fixed is None: header = MQTTFixedHeader(PINGREQ, 0x00) else: if fixed.packet_type is not PINGREQ: msg = f"Invalid fixed packet type {fixed.packet_type} for PingReqPacket init" raise AMQTTError(msg) header = fixed super().__init__(header) self.variable_header = None self.payload = None Yakifo-amqtt-2637127/amqtt/mqtt/pingresp.py000066400000000000000000000014251504664204300205770ustar00rootroot00000000000000from typing_extensions import Self from amqtt.errors import AMQTTError from amqtt.mqtt.packet import PINGRESP, MQTTFixedHeader, MQTTPacket class PingRespPacket(MQTTPacket[None, None, MQTTFixedHeader]): VARIABLE_HEADER = None PAYLOAD = None def __init__(self, fixed: MQTTFixedHeader | None = None) -> None: if fixed is None: header = MQTTFixedHeader(PINGRESP, 0x00) else: if fixed.packet_type is not PINGRESP: msg = f"Invalid fixed packet type {fixed.packet_type} for PingRespPacket init" raise AMQTTError(msg) header = fixed super().__init__(header) self.variable_header = None self.payload = None @classmethod def build(cls) -> Self: return cls() Yakifo-amqtt-2637127/amqtt/mqtt/protocol/000077500000000000000000000000001504664204300202355ustar00rootroot00000000000000Yakifo-amqtt-2637127/amqtt/mqtt/protocol/__init__.py000066400000000000000000000000141504664204300223410ustar00rootroot00000000000000"""INIT.""" Yakifo-amqtt-2637127/amqtt/mqtt/protocol/broker_handler.py000066400000000000000000000261141504664204300235740ustar00rootroot00000000000000import asyncio from asyncio import AbstractEventLoop, Queue from typing import TYPE_CHECKING from amqtt.adapters import ReaderAdapter, WriterAdapter from amqtt.errors import MQTTError from amqtt.events import MQTTEvents from amqtt.mqtt.connack import ( BAD_USERNAME_PASSWORD, CONNECTION_ACCEPTED, IDENTIFIER_REJECTED, NOT_AUTHORIZED, UNACCEPTABLE_PROTOCOL_VERSION, ConnackPacket, ) from amqtt.mqtt.connect import ConnectPacket from amqtt.mqtt.disconnect import DisconnectPacket from amqtt.mqtt.pingreq import PingReqPacket from amqtt.mqtt.pingresp import PingRespPacket from amqtt.mqtt.protocol.handler import ProtocolHandler from amqtt.mqtt.suback import SubackPacket from amqtt.mqtt.subscribe import SubscribePacket from amqtt.mqtt.unsuback import UnsubackPacket from amqtt.mqtt.unsubscribe import UnsubscribePacket from amqtt.plugins.manager import PluginManager from amqtt.session import Session from amqtt.utils import format_client_message _MQTT_PROTOCOL_LEVEL_SUPPORTED = 4 if TYPE_CHECKING: from amqtt.broker import BrokerContext class Subscription: def __init__(self, packet_id: int, topics: list[tuple[str, int]]) -> None: self.packet_id = packet_id self.topics = topics class UnSubscription: def __init__(self, packet_id: int, topics: list[str]) -> None: self.packet_id = packet_id self.topics = topics class BrokerProtocolHandler(ProtocolHandler["BrokerContext"]): def __init__( self, plugins_manager: PluginManager["BrokerContext"], session: Session | None = None, loop: AbstractEventLoop | None = None, ) -> None: super().__init__(plugins_manager, session, loop) self._disconnect_waiter: asyncio.Future[DisconnectPacket | None] | None = None self._pending_subscriptions: Queue[Subscription] = Queue() self._pending_unsubscriptions: Queue[UnSubscription] = Queue() async def start(self) -> None: await super().start() # Ensure the disconnect waiter is reset if self._disconnect_waiter is None or self._disconnect_waiter.done(): self._disconnect_waiter = asyncio.Future() async def stop(self) -> None: """Stop the protocol handler and reset the disconnect waiter.""" await super().stop() if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): self._disconnect_waiter.set_result(None) self._disconnect_waiter = None # Reset the disconnect waiter # Clear pending subscriptions and unsubscriptions while not self._pending_subscriptions.empty(): self._pending_subscriptions.get_nowait() while not self._pending_unsubscriptions.empty(): self._pending_unsubscriptions.get_nowait() async def wait_disconnect(self) -> DisconnectPacket | None: """Wait for a disconnect packet or connection closure.""" if self._disconnect_waiter is not None: return await self._disconnect_waiter return None def handle_write_timeout(self) -> None: pass def handle_read_timeout(self) -> None: pass async def handle_disconnect(self, disconnect: DisconnectPacket | None) -> None: """Handle a disconnect packet and notify the disconnect waiter.""" self.logger.debug("Client disconnecting") if self._disconnect_waiter and not self._disconnect_waiter.done(): self.logger.debug(f"Setting disconnect waiter result to {disconnect!r}") self._disconnect_waiter.set_result(disconnect) self._disconnect_waiter = None # Reset the disconnect waiter to avoid reuse async def handle_connection_closed(self) -> None: """Handle connection closure and notify the disconnect waiter.""" await self.handle_disconnect(None) async def handle_connect(self, connect: ConnectPacket) -> None: # Broker handler shouldn't receive CONNECT message during messages handling # as CONNECT messages are managed by the broker on client connection self.logger.error( f"{self.session.client_id if self.session else None} [MQTT-3.1.0-2] {format_client_message(self.session)} :" f" CONNECT message received during messages handling", ) if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): self._disconnect_waiter.set_result(None) async def handle_pingreq(self, pingreq: PingReqPacket) -> None: await self._send_packet(PingRespPacket.build()) async def handle_subscribe(self, subscribe: SubscribePacket) -> None: if subscribe.variable_header is None: msg = "SUBSCRIBE packet: variable header not initialized." raise MQTTError(msg) if subscribe.payload is None: msg = "SUBSCRIBE packet: payload not initialized." raise MQTTError(msg) subscription: Subscription = Subscription(subscribe.variable_header.packet_id, subscribe.payload.topics) await self._pending_subscriptions.put(subscription) async def handle_unsubscribe(self, unsubscribe: UnsubscribePacket) -> None: if unsubscribe.variable_header is None: msg = "UNSUBSCRIBE packet: variable header not initialized." raise MQTTError(msg) if unsubscribe.payload is None: msg = "UNSUBSCRIBE packet: payload not initialized." raise MQTTError(msg) unsubscription: UnSubscription = UnSubscription(unsubscribe.variable_header.packet_id, unsubscribe.payload.topics) await self._pending_unsubscriptions.put(unsubscription) async def get_next_pending_subscription(self) -> Subscription: return await self._pending_subscriptions.get() async def get_next_pending_unsubscription(self) -> UnSubscription: return await self._pending_unsubscriptions.get() async def mqtt_acknowledge_subscription(self, packet_id: int, return_codes: list[int]) -> None: suback = SubackPacket.build(packet_id, return_codes) await self._send_packet(suback) async def mqtt_acknowledge_unsubscription(self, packet_id: int) -> None: unsuback = UnsubackPacket.build(packet_id) await self._send_packet(unsuback) async def mqtt_connack_authorize(self, authorize: bool) -> None: if self.session is None: msg = "Session is not initialized!" raise MQTTError(msg) connack = ConnackPacket.build(self.session.parent, CONNECTION_ACCEPTED if authorize else NOT_AUTHORIZED) await self._send_packet(connack) @classmethod async def init_from_connect( cls, reader: ReaderAdapter, writer: WriterAdapter, plugins_manager: PluginManager["BrokerContext"], loop: asyncio.AbstractEventLoop | None = None, ) -> tuple["BrokerProtocolHandler", Session]: """Initialize from a CONNECT packet and validates the connection.""" connect = await ConnectPacket.from_stream(reader) await plugins_manager.fire_event(MQTTEvents.PACKET_RECEIVED, packet=connect) if connect.variable_header is None: msg = "CONNECT packet: variable header not initialized." raise MQTTError(msg) if connect.payload is None: msg = "CONNECT packet: payload not initialized." raise MQTTError(msg) # this shouldn't be required anymore since broker generates for each client a random client_id if not provided # [MQTT-3.1.3-6] if connect.payload.client_id is None: msg = "[[MQTT-3.1.3-3]] : Client identifier must be present" raise MQTTError(msg) if connect.variable_header.will_flag and (connect.payload.will_topic is None or connect.payload.will_message is None): msg = "Will flag set, but will topic/message not present in payload" raise MQTTError(msg) if connect.variable_header.reserved_flag: msg = "[MQTT-3.1.2-3] CONNECT reserved flag must be set to 0" raise MQTTError(msg) if connect.proto_name != "MQTT": msg = f'[MQTT-3.1.2-1] Incorrect protocol name: "{connect.proto_name}"' raise MQTTError(msg) remote_info = writer.get_peer_info() if remote_info is not None: remote_address, remote_port = remote_info connack = None error_msg = None if connect.proto_level != _MQTT_PROTOCOL_LEVEL_SUPPORTED: # only MQTT 3.1.1 supported error_msg = ( f"Invalid protocol from {format_client_message(address=remote_address, port=remote_port)}:" f" {connect.proto_level}" ) connack = ConnackPacket.build(0, UNACCEPTABLE_PROTOCOL_VERSION) # [MQTT-3.2.2-4] session_parent=0 elif not connect.username_flag and connect.password_flag: connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.1.2-22] elif connect.username_flag and connect.username is None: error_msg = f"Invalid username from {format_client_message(address=remote_address, port=remote_port)}" connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.2.2-4] session_parent=0 elif connect.password_flag and connect.password is None: error_msg = f"Invalid password from {format_client_message(address=remote_address, port=remote_port)}" connack = ConnackPacket.build(0, BAD_USERNAME_PASSWORD) # [MQTT-3.2.2-4] session_parent=0 elif connect.clean_session_flag is False and connect.payload.client_id_is_random: error_msg = ( f"[MQTT-3.1.3-8] [MQTT-3.1.3-9] {format_client_message(address=remote_address, port=remote_port)}:" " No client Id provided (cleansession=0)" ) connack = ConnackPacket.build(0, IDENTIFIER_REJECTED) if connack is not None: await plugins_manager.fire_event(MQTTEvents.PACKET_SENT, packet=connack) await connack.to_stream(writer) await writer.close() raise MQTTError(error_msg) from None incoming_session = Session() incoming_session.client_id = connect.client_id incoming_session.clean_session = connect.clean_session_flag incoming_session.will_flag = connect.will_flag incoming_session.will_retain = connect.will_retain_flag incoming_session.will_qos = connect.will_qos incoming_session.will_topic = connect.will_topic incoming_session.will_message = connect.will_message incoming_session.username = connect.username incoming_session.password = connect.password incoming_session.remote_address = remote_address incoming_session.remote_port = remote_port incoming_session.ssl_object = writer.get_ssl_info() incoming_session.keep_alive = max(connect.keep_alive, 0) if connect.keep_alive > 0: incoming_session.keep_alive = connect.keep_alive else: incoming_session.keep_alive = 0 handler = cls(plugins_manager, loop=loop) return handler, incoming_session Yakifo-amqtt-2637127/amqtt/mqtt/protocol/client_handler.py000066400000000000000000000200031504664204300235550ustar00rootroot00000000000000import asyncio from typing import TYPE_CHECKING, Any from amqtt.errors import AMQTTError, NoDataError from amqtt.events import MQTTEvents from amqtt.mqtt.connack import ConnackPacket from amqtt.mqtt.connect import ConnectPacket, ConnectPayload, ConnectVariableHeader from amqtt.mqtt.disconnect import DisconnectPacket from amqtt.mqtt.pingreq import PingReqPacket from amqtt.mqtt.pingresp import PingRespPacket from amqtt.mqtt.protocol.handler import ProtocolHandler from amqtt.mqtt.suback import SubackPacket from amqtt.mqtt.subscribe import SubscribePacket from amqtt.mqtt.unsuback import UnsubackPacket from amqtt.mqtt.unsubscribe import UnsubscribePacket from amqtt.plugins.manager import PluginManager from amqtt.session import Session if TYPE_CHECKING: from amqtt.client import ClientContext class ClientProtocolHandler(ProtocolHandler["ClientContext"]): def __init__( self, plugins_manager: PluginManager["ClientContext"], session: Session | None = None, loop: asyncio.AbstractEventLoop | None = None, ) -> None: super().__init__(plugins_manager, session, loop=loop) self._ping_task: asyncio.Task[Any] | None = None self._pingresp_queue: asyncio.Queue[PingRespPacket] = asyncio.Queue() self._subscriptions_waiter: dict[int, asyncio.Future[list[int]]] = {} self._unsubscriptions_waiter: dict[int, asyncio.Future[Any]] = {} self._disconnect_waiter: asyncio.Future[Any] | None = asyncio.Future() async def start(self) -> None: await super().start() if self._disconnect_waiter and self._disconnect_waiter.cancelled(): self._disconnect_waiter = asyncio.Future() async def stop(self) -> None: await super().stop() if self._ping_task and not self._ping_task.cancelled(): self.logger.debug("Cancel ping task") self._ping_task.cancel() if self._disconnect_waiter and not self._disconnect_waiter.done(): self._disconnect_waiter.cancel() def _build_connect_packet(self) -> ConnectPacket: vh = ConnectVariableHeader() payload = ConnectPayload() if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) vh.keep_alive = self.session.keep_alive vh.clean_session_flag = self.session.clean_session if self.session.clean_session is not None else False vh.will_retain_flag = self.session.will_retain if self.session.will_retain is not None else False payload.client_id = self.session.client_id if self.session.username: vh.username_flag = True payload.username = self.session.username else: vh.username_flag = False if self.session.password: vh.password_flag = True payload.password = self.session.password else: vh.password_flag = False if self.session.will_flag: vh.will_flag = True if self.session.will_qos is not None: vh.will_qos = self.session.will_qos payload.will_message = self.session.will_message payload.will_topic = self.session.will_topic else: vh.will_flag = False return ConnectPacket(variable_header=vh, payload=payload) async def mqtt_connect(self) -> int | None: connect_packet = self._build_connect_packet() await self._send_packet(connect_packet) if self.reader is None: msg = "Reader is not initialized." raise AMQTTError(msg) try: connack = await ConnackPacket.from_stream(self.reader) except NoDataError as e: raise ConnectionError from e await self.plugins_manager.fire_event(MQTTEvents.PACKET_RECEIVED, packet=connack, session=self.session) return connack.return_code def handle_write_timeout(self) -> None: try: if not self._ping_task: self.logger.debug("Scheduling Ping") self._ping_task = asyncio.create_task(self.mqtt_ping()) except asyncio.InvalidStateError as e: self.logger.warning(f"Invalid state while scheduling ping task: {e!r}") except asyncio.CancelledError as e: self.logger.info(f"Ping task was cancelled: {e!r}") def handle_read_timeout(self) -> None: pass async def mqtt_subscribe(self, topics: list[tuple[str, int]], packet_id: int) -> list[int]: """Subscribe to the given topics. :param topics: List of tuples, e.g. [('filter', '/a/b', 'qos': 0x00)]. :return: Return codes for the subscription. """ subscribe = SubscribePacket.build(topics, packet_id) await self._send_packet(subscribe) if subscribe.variable_header is None: msg = f"Invalid variable header in SUBSCRIBE packet: {subscribe.variable_header}" raise AMQTTError(msg) waiter: asyncio.Future[list[int]] = asyncio.Future() self._subscriptions_waiter[subscribe.variable_header.packet_id] = waiter try: return_codes = await waiter finally: del self._subscriptions_waiter[subscribe.variable_header.packet_id] return return_codes async def handle_suback(self, suback: SubackPacket) -> None: if suback.variable_header is None: msg = "SUBACK packet: variable header not initialized." raise AMQTTError(msg) if suback.payload is None: msg = "SUBACK packet: payload not initialized." raise AMQTTError(msg) packet_id = suback.variable_header.packet_id waiter = self._subscriptions_waiter.get(packet_id) if waiter is not None: waiter.set_result(suback.payload.return_codes) else: self.logger.warning(f"Received SUBACK for unknown pending subscription with Id: {packet_id}") async def mqtt_unsubscribe(self, topics: list[str], packet_id: int) -> None: """Unsubscribe from the given topics. :param topics: List of topics ['/a/b', ...]. """ unsubscribe = UnsubscribePacket.build(topics, packet_id) if unsubscribe.variable_header is None: msg = "UNSUBSCRIBE packet: variable header not initialized." raise AMQTTError(msg) await self._send_packet(unsubscribe) waiter: asyncio.Future[Any] = asyncio.Future() self._unsubscriptions_waiter[unsubscribe.variable_header.packet_id] = waiter try: await waiter finally: del self._unsubscriptions_waiter[unsubscribe.variable_header.packet_id] async def handle_unsuback(self, unsuback: UnsubackPacket) -> None: if unsuback.variable_header is None: msg = "UNSUBACK packet: variable header not initialized." raise AMQTTError(msg) packet_id = unsuback.variable_header.packet_id waiter = self._unsubscriptions_waiter.get(packet_id) if waiter is not None: waiter.set_result(None) else: self.logger.warning(f"Received UNSUBACK for unknown pending unsubscription with Id: {packet_id}") async def mqtt_disconnect(self) -> None: disconnect_packet = DisconnectPacket() await self._send_packet(disconnect_packet) async def mqtt_ping(self) -> PingRespPacket: ping_packet = PingReqPacket() try: await self._send_packet(ping_packet) resp = await self._pingresp_queue.get() finally: self._ping_task = None # Ensure the task is cleaned up return resp async def handle_pingresp(self, pingresp: PingRespPacket) -> None: await self._pingresp_queue.put(pingresp) async def handle_connection_closed(self) -> None: self.logger.debug("Broker closed connection") if self._disconnect_waiter is not None and not self._disconnect_waiter.done(): self._disconnect_waiter.set_result(None) async def wait_disconnect(self) -> None: if self._disconnect_waiter is not None: await self._disconnect_waiter Yakifo-amqtt-2637127/amqtt/mqtt/protocol/handler.py000066400000000000000000001036171504664204300222340ustar00rootroot00000000000000import asyncio try: from asyncio import InvalidStateError, QueueFull, QueueShutDown except ImportError: # Fallback for Python < 3.12 class InvalidStateError(Exception): # type: ignore[no-redef] pass class QueueFull(Exception): # type: ignore[no-redef] # noqa : N818 pass class QueueShutDown(Exception): # type: ignore[no-redef] # noqa : N818 pass import collections import itertools import logging from typing import Generic, TypeVar, cast from amqtt.adapters import ReaderAdapter, WriterAdapter from amqtt.contexts import BaseContext from amqtt.errors import AMQTTError, MQTTError, NoDataError, ProtocolHandlerError from amqtt.events import MQTTEvents from amqtt.mqtt import packet_class from amqtt.mqtt.connack import ConnackPacket from amqtt.mqtt.connect import ConnectPacket from amqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 from amqtt.mqtt.disconnect import DisconnectPacket from amqtt.mqtt.packet import ( CONNACK, CONNECT, DISCONNECT, PINGREQ, PINGRESP, PUBACK, PUBCOMP, PUBLISH, PUBREC, PUBREL, RESERVED_0, RESERVED_15, SUBACK, SUBSCRIBE, UNSUBACK, UNSUBSCRIBE, MQTTFixedHeader, ) from amqtt.mqtt.pingreq import PingReqPacket from amqtt.mqtt.pingresp import PingRespPacket from amqtt.mqtt.puback import PubackPacket from amqtt.mqtt.pubcomp import PubcompPacket from amqtt.mqtt.publish import PublishPacket from amqtt.mqtt.pubrec import PubrecPacket from amqtt.mqtt.pubrel import PubrelPacket from amqtt.mqtt.suback import SubackPacket from amqtt.mqtt.subscribe import SubscribePacket from amqtt.mqtt.unsuback import UnsubackPacket from amqtt.mqtt.unsubscribe import UnsubscribePacket from amqtt.plugins.manager import PluginManager from amqtt.session import INCOMING, OUTGOING, ApplicationMessage, IncomingApplicationMessage, OutgoingApplicationMessage, Session C = TypeVar("C", bound=BaseContext) class ProtocolHandler(Generic[C]): """Class implementing the MQTT communication protocol using asyncio features.""" def __init__( self, plugins_manager: PluginManager[C], session: Session | None = None, loop: asyncio.AbstractEventLoop | None = None, ) -> None: self.logger: logging.Logger | logging.LoggerAdapter[logging.Logger] = logging.getLogger(__name__) if session is not None: self._init_session(session) else: self.session: Session | None = None self.reader: ReaderAdapter | None = None self.writer: WriterAdapter | None = None self.plugins_manager: PluginManager[C] = plugins_manager try: self._loop = loop if loop is not None else asyncio.get_running_loop() except RuntimeError: self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) self._reader_task: asyncio.Task[None] | None = None self._keepalive_task: asyncio.TimerHandle | None = None self._reader_ready: asyncio.Event | None = None self._reader_stopped = asyncio.Event() self._puback_waiters: dict[int, asyncio.Future[PubackPacket]] = {} self._pubrec_waiters: dict[int, asyncio.Future[PubrecPacket]] = {} self._pubrel_waiters: dict[int, asyncio.Future[PubrelPacket]] = {} self._pubcomp_waiters: dict[int, asyncio.Future[PubcompPacket]] = {} self._write_lock = asyncio.Lock() def _init_session(self, session: Session) -> None: if not session: msg = "Session cannot be None" raise AMQTTError(msg) log = logging.getLogger(__name__) self.session = session self.logger = logging.LoggerAdapter(log, {"client_id": self.session.client_id}) self.keepalive_timeout: int | None = self.session.keep_alive if self.keepalive_timeout <= 0: self.keepalive_timeout = None def attach(self, session: Session, reader: ReaderAdapter, writer: WriterAdapter) -> None: if self.session: msg = "Handler is already attached to a session" raise ProtocolHandlerError(msg) self._init_session(session) self.reader = reader self.writer = writer def detach(self) -> None: self.session = None self.reader = None self.writer = None def _is_attached(self) -> bool: return bool(self.session) async def start(self) -> None: if not self._is_attached(): msg = "Handler is not attached to a stream" raise ProtocolHandlerError(msg) self._reader_ready = asyncio.Event() self._reader_stopped = asyncio.Event() self._reader_task = asyncio.create_task(self._reader_loop()) await self._reader_ready.wait() if self._loop is not None and self.keepalive_timeout is not None: self._keepalive_task = self._loop.call_later(self.keepalive_timeout, self.handle_write_timeout) self.logger.debug("Handler tasks started") await self._retry_deliveries() self.logger.debug("Handler ready") async def stop(self) -> None: # Stop messages flow waiter self._stop_waiters() if self._keepalive_task: self._keepalive_task.cancel() self.logger.debug("Waiting for tasks to be stopped") if self._reader_task and not self._reader_task.done(): self._reader_task.cancel() await self._reader_stopped.wait() self.logger.debug("Closing writer") try: if self.writer is not None: await self.writer.close() except asyncio.CancelledError: # canceling the task is the expected result self.logger.debug("Writer close was cancelled.") except asyncio.TimeoutError: self.logger.debug("Writer close operation timed out.", exc_info=True) except OSError: self.logger.debug("Writer close failed due to I/O error.", exc_info=True) def _stop_waiters(self) -> None: self.logger.debug(f"Stopping {len(self._puback_waiters)} puback waiters") self.logger.debug(f"Stopping {len(self._pubcomp_waiters)} pucomp waiters") self.logger.debug(f"Stopping {len(self._pubrec_waiters)} purec waiters") self.logger.debug(f"Stopping {len(self._pubrel_waiters)} purel waiters") for waiter in itertools.chain( self._puback_waiters.values(), self._pubcomp_waiters.values(), self._pubrec_waiters.values(), self._pubrel_waiters.values(), ): if not isinstance(waiter, asyncio.Future): msg = "Waiter is not a asyncio.Future" raise AMQTTError(msg) waiter.cancel() async def _retry_deliveries(self) -> None: """Handle [MQTT-4.4.0-1] by resending PUBLISH and PUBREL messages for pending out messages.""" self.logger.debug("Begin messages delivery retries") if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) tasks = [ asyncio.create_task( asyncio.wait_for( self._handle_message_flow(cast("IncomingApplicationMessage | OutgoingApplicationMessage", message)), 10, ), ) for message in itertools.chain(self.session.inflight_in.values(), self.session.inflight_out.values()) ] if tasks: done, pending = await asyncio.wait(tasks) self.logger.debug(f"{len(done)} messages redelivered") self.logger.debug(f"{len(pending)} messages not redelivered due to timeout") self.logger.debug("End messages delivery retries") async def mqtt_publish( self, topic: str, data: bytes | bytearray, qos: int | None, retain: bool, ack_timeout: int | None = None, ) -> OutgoingApplicationMessage: """Send a MQTT publish message and manage messages flows. This method doesn't return until the message has been acknowledged by receiver or timeout occurs. :param topic: MQTT topic to publish :param data: data to send on topic :param qos: quality of service to use for message flow. Can be QOS_0, QOS_1 or QOS_2 :param retain: retain message flag :param ack_timeout: acknowledge timeout. If set, this method will return a TimeOut error if the acknowledgment is not completed before ack_timeout second :return: ApplicationMessage used during inflight operations. """ if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) if qos in (QOS_1, QOS_2): packet_id = self.session.next_packet_id if packet_id in self.session.inflight_out: msg = f"A message with the same packet ID '{packet_id}' is already in flight" raise AMQTTError(msg) else: packet_id = None message: OutgoingApplicationMessage = OutgoingApplicationMessage(packet_id, topic, qos, data, retain) # Handle message flow if ack_timeout is not None and ack_timeout > 0: await asyncio.wait_for(self._handle_message_flow(message), ack_timeout) else: await self._handle_message_flow(message) return message async def _handle_message_flow(self, app_message: IncomingApplicationMessage | OutgoingApplicationMessage) -> None: """Handle protocol flow for incoming and outgoing messages. Depending on service level and according to MQTT spec. paragraph 4.3-Quality of Service levels and protocol flows. :param app_message: PublishMessage to handle """ if app_message.qos not in (QOS_0, QOS_1, QOS_2): msg = f"Unexpected QOS value '{app_message.qos}' for message: {app_message}" raise AMQTTError(msg) if app_message.qos == QOS_0: await self._handle_qos0_message_flow(app_message) elif app_message.qos == QOS_1: await self._handle_qos1_message_flow(app_message) elif app_message.qos == QOS_2: await self._handle_qos2_message_flow(app_message) else: msg = f"Unexpected QOS value '{app_message.qos}'" raise AMQTTError(msg) async def _handle_qos0_message_flow(self, app_message: IncomingApplicationMessage | OutgoingApplicationMessage) -> None: """Handle QOS_0 application message acknowledgment. For incoming messages, this method stores the message. For outgoing messages, this methods sends PUBLISH. :param app_message: Application message to handle """ if app_message.qos != QOS_0: msg = f"Expected QOS_0 message, got QOS_{app_message.qos}" raise ValueError(msg) if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) if app_message.direction == OUTGOING: packet = app_message.build_publish_packet() # Send PUBLISH packet await self._send_packet(packet) app_message.publish_packet = packet elif app_message.direction == INCOMING: if app_message.publish_packet is not None and app_message.publish_packet.dup_flag: self.logger.warning( "[MQTT-3.3.1-2] DUP flag must set to 0 for QOS 0 message. Message ignored: %r", app_message.publish_packet, ) else: try: self.session.delivered_message_queue.put_nowait(app_message) self.logger.debug(f"Message added to delivery queue: {app_message}") except QueueShutDown as e: self.logger.warning(f"Delivered messages queue is shut down. QOS_0 message discarded: {e}") except QueueFull as e: self.logger.warning(f"Delivered messages queue is full. QOS_0 message discarded: {e}") async def _handle_qos1_message_flow(self, app_message: OutgoingApplicationMessage | IncomingApplicationMessage) -> None: """Handle QOS_1 application message acknowledgment. For incoming messages, this method stores the message and reply with PUBACK. For outgoing messages, this methods sends PUBLISH and waits for the corresponding PUBACK. :param app_message: Application message to handle """ if app_message.qos != QOS_1: msg = f"Expected QOS_1 message, got QOS_{app_message.qos}" raise ValueError(msg) if app_message.packet_id is None: msg = "Packet ID is not set" raise ValueError(msg) if app_message.puback_packet: msg = f"Message '{app_message.packet_id}' has already been acknowledged" raise AMQTTError(msg) if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) if app_message.direction == OUTGOING: if app_message.packet_id not in self.session.inflight_out and isinstance(app_message, OutgoingApplicationMessage): # Store message in session self.session.inflight_out[app_message.packet_id] = app_message if app_message.publish_packet is not None: # A Publish packet has already been sent, this is a retry publish_packet = app_message.build_publish_packet(dup=True) else: publish_packet = app_message.build_publish_packet() # Send PUBLISH packet await self._send_packet(publish_packet) app_message.publish_packet = publish_packet # Wait for puback waiter: asyncio.Future[PubackPacket] = asyncio.Future() self._puback_waiters[app_message.packet_id] = waiter try: app_message.puback_packet = await asyncio.wait_for(waiter, timeout=5) except asyncio.TimeoutError: msg = f"Timeout waiting for PUBACK for packet ID {app_message.packet_id}" self.logger.warning(msg) raise TimeoutError(msg) from None finally: self._puback_waiters.pop(app_message.packet_id, None) # Discard inflight message self.session.inflight_out.pop(app_message.packet_id, None) elif app_message.direction == INCOMING: # Initiate delivery self.logger.debug("Add message to delivery") await self.session.delivered_message_queue.put(app_message) # Send PUBACK puback = PubackPacket.build(app_message.packet_id) await self._send_packet(puback) app_message.puback_packet = puback async def _handle_qos2_message_flow(self, app_message: OutgoingApplicationMessage | IncomingApplicationMessage) -> None: """Handle QOS_2 application message acknowledgment. For incoming messages, this method stores the message, sends PUBREC, waits for PUBREL, initiate delivery and send PUBCOMP. For outgoing messages, this methods sends PUBLISH, waits for PUBREC, discards messages and wait for PUBCOMP. :param app_message: Application message to handle """ if app_message.qos != QOS_2: msg = f"Expected QOS_2 message, got QOS_{app_message.qos}" raise ValueError(msg) if app_message.packet_id is None: msg = "Packet ID is not set" raise ValueError(msg) if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) if app_message.direction == OUTGOING: if app_message.pubrel_packet and app_message.pubcomp_packet: msg = f"Message '{app_message.packet_id}' has already been acknowledged" raise AMQTTError(msg) if not app_message.pubrel_packet: # Store message publish_packet: PublishPacket if app_message.publish_packet is not None: # This is a retry flow, no need to store just check the message exists in session if app_message.packet_id not in self.session.inflight_out: msg = f"Unknown inflight message '{app_message.packet_id}' in session" raise AMQTTError(msg) publish_packet = app_message.build_publish_packet(dup=True) elif isinstance(app_message, OutgoingApplicationMessage): # Store message in session self.session.inflight_out[app_message.packet_id] = app_message publish_packet = app_message.build_publish_packet() else: self.logger.debug("Message can not be stored, to be checked!") # Send PUBLISH packet await self._send_packet(publish_packet) app_message.publish_packet = publish_packet # Wait PUBREC if app_message.packet_id in self._pubrec_waiters: # PUBREC waiter already exists for this packet ID message = f"Can't add PUBREC waiter, a waiter already exists for message Id '{app_message.packet_id}'" self.logger.warning(message) raise AMQTTError(message) waiter_pub_rec: asyncio.Future[PubrecPacket] = asyncio.Future() self._pubrec_waiters[app_message.packet_id] = waiter_pub_rec try: app_message.pubrec_packet = await waiter_pub_rec finally: self._pubrec_waiters.pop(app_message.packet_id, None) self.session.inflight_out.pop(app_message.packet_id, None) if not app_message.pubcomp_packet: # Send pubrel app_message.pubrel_packet = PubrelPacket.build(app_message.packet_id) await self._send_packet(app_message.pubrel_packet) # Wait for PUBCOMP waiter_pub_comp: asyncio.Future[PubcompPacket] = asyncio.Future() self._pubcomp_waiters[app_message.packet_id] = waiter_pub_comp try: app_message.pubcomp_packet = await waiter_pub_comp finally: self._pubcomp_waiters.pop(app_message.packet_id, None) self.session.inflight_out.pop(app_message.packet_id, None) elif app_message.direction == INCOMING and isinstance(app_message, IncomingApplicationMessage): self.session.inflight_in[app_message.packet_id] = app_message # Send pubrec pubrec_packet = PubrecPacket.build(app_message.packet_id) await self._send_packet(pubrec_packet) app_message.pubrec_packet = pubrec_packet # Wait PUBREL if app_message.packet_id in self._pubrel_waiters and not self._pubrel_waiters[app_message.packet_id].done(): # PUBREL waiter already exists for this packet ID message = f"A waiter already exists for message Id '{app_message.packet_id}', canceling it" self.logger.warning(message) self._pubrel_waiters[app_message.packet_id].cancel() try: waiter_pub_rel: asyncio.Future[PubrelPacket] = asyncio.Future() self._pubrel_waiters[app_message.packet_id] = waiter_pub_rel await waiter_pub_rel del self._pubrel_waiters[app_message.packet_id] app_message.pubrel_packet = waiter_pub_rel.result() # Initiate delivery and discard message await self.session.delivered_message_queue.put(app_message) del self.session.inflight_in[app_message.packet_id] # Send pubcomp pubcomp_packet = PubcompPacket.build(app_message.packet_id) await self._send_packet(pubcomp_packet) app_message.pubcomp_packet = pubcomp_packet except asyncio.CancelledError: self.logger.debug("Message flow cancelled") else: self.logger.debug("Unknown direction!") async def _reader_loop(self) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) if not self._reader_ready: msg = "Reader ready is not initialized." raise ProtocolHandlerError(msg) self.logger.debug(f"{self.session.client_id} Starting reader coro") running_tasks: collections.deque[asyncio.Task[None]] = collections.deque() keepalive_timeout: int | None = self.session.keep_alive if keepalive_timeout is not None and keepalive_timeout <= 0: keepalive_timeout = None while True: try: self._reader_ready.set() while running_tasks and running_tasks[0].done(): running_tasks.popleft() if len(running_tasks) > 1: self.logger.debug(f"Handler running tasks: {len(running_tasks)}") if self.reader is None: self.logger.warning("Reader is not initialized!") break fixed_header = await asyncio.wait_for(MQTTFixedHeader.from_stream(self.reader), timeout=keepalive_timeout) if not fixed_header: self.logger.debug(f"{self.session.client_id} No more data (EOF received), stopping reader coro") break if fixed_header.packet_type in (RESERVED_0, RESERVED_15): self.logger.warning( f"{self.session.client_id} Received reserved packet, which is forbidden: closing connection", ) await self.handle_connection_closed() continue cls = packet_class(fixed_header) packet = await cls.from_stream(self.reader, fixed_header=fixed_header) await self.plugins_manager.fire_event(MQTTEvents.PACKET_RECEIVED, packet=packet, session=self.session) if packet.fixed_header is None or packet.fixed_header.packet_type not in ( CONNACK, SUBSCRIBE, UNSUBSCRIBE, SUBACK, UNSUBACK, PUBACK, PUBREC, PUBREL, PUBCOMP, PINGREQ, PINGRESP, PUBLISH, DISCONNECT, CONNECT, ): self.logger.warning(f"{self.session.client_id} Unhandled packet type: {packet.fixed_header.packet_type}") continue task: asyncio.Task[None] | None = None if packet.fixed_header.packet_type == CONNACK and isinstance(packet, ConnackPacket): task = asyncio.create_task(self.handle_connack(packet)) elif packet.fixed_header.packet_type == SUBSCRIBE and isinstance(packet, SubscribePacket): task = asyncio.create_task(self.handle_subscribe(packet)) elif packet.fixed_header.packet_type == UNSUBSCRIBE and isinstance(packet, UnsubscribePacket): task = asyncio.create_task(self.handle_unsubscribe(packet)) elif packet.fixed_header.packet_type == SUBACK and isinstance(packet, SubackPacket): task = asyncio.create_task(self.handle_suback(packet)) elif packet.fixed_header.packet_type == UNSUBACK and isinstance(packet, UnsubackPacket): task = asyncio.create_task(self.handle_unsuback(packet)) elif packet.fixed_header.packet_type == PUBACK and isinstance(packet, PubackPacket): task = asyncio.create_task(self.handle_puback(packet)) elif packet.fixed_header.packet_type == PUBREC and isinstance(packet, PubrecPacket): task = asyncio.create_task(self.handle_pubrec(packet)) elif packet.fixed_header.packet_type == PUBREL and isinstance(packet, PubrelPacket): task = asyncio.create_task(self.handle_pubrel(packet)) elif packet.fixed_header.packet_type == PUBCOMP and isinstance(packet, PubcompPacket): task = asyncio.create_task(self.handle_pubcomp(packet)) elif packet.fixed_header.packet_type == PINGREQ and isinstance(packet, PingReqPacket): task = asyncio.create_task(self.handle_pingreq(packet)) elif packet.fixed_header.packet_type == PINGRESP and isinstance(packet, PingRespPacket): task = asyncio.create_task(self.handle_pingresp(packet)) elif packet.fixed_header.packet_type == PUBLISH and isinstance(packet, PublishPacket): task = asyncio.create_task(self.handle_publish(packet)) elif packet.fixed_header.packet_type == DISCONNECT and isinstance(packet, DisconnectPacket): task = asyncio.create_task(self.handle_disconnect(packet)) elif packet.fixed_header.packet_type == CONNECT and isinstance(packet, ConnectPacket): # q: why is this not like all other inside a create_task? # a: the connection needs to be established before any other packet tasks for this new session are scheduled await self.handle_connect(packet) if task: running_tasks.append(task) except MQTTError: self.logger.debug("Message discarded") except asyncio.CancelledError: self.logger.debug("Task cancelled, reader loop ending") break except asyncio.TimeoutError: self.logger.debug(f"{self.session.client_id} Input stream read timeout") self.handle_read_timeout() except NoDataError: self.logger.debug(f"{self.session.client_id} No data available") except Exception as e: # noqa: BLE001, pylint: disable=W0718 self.logger.warning(f"{type(self).__name__} Unhandled exception in reader coro: {e!r}") break while running_tasks: running_tasks.popleft().cancel() await self.handle_connection_closed() self._reader_stopped.set() self.logger.debug("Reader coro stopped") await self.stop() async def _send_packet( self, packet: PublishPacket | PubackPacket | ConnackPacket | SubackPacket | ConnectPacket | SubscribePacket | UnsubscribePacket | DisconnectPacket | PingReqPacket | PubrelPacket | PubrecPacket | PubcompPacket | PingRespPacket | UnsubackPacket, ) -> None: try: if self.writer: async with self._write_lock: await packet.to_stream(self.writer) if self._keepalive_task: self._keepalive_task.cancel() if self.keepalive_timeout is not None: self._keepalive_task = self._loop.call_later(self.keepalive_timeout, self.handle_write_timeout) await self.plugins_manager.fire_event(MQTTEvents.PACKET_SENT, packet=packet, session=self.session) except (ConnectionResetError, BrokenPipeError): await self.handle_connection_closed() except asyncio.CancelledError as e: msg = "Packet handling was cancelled" raise ProtocolHandlerError(msg) from e except Exception as e: self.logger.warning(f"Unhandled exception: {e}") raise async def mqtt_deliver_next_message(self) -> ApplicationMessage | None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) if not self._is_attached(): return None if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"{self.session.delivered_message_queue.qsize()} message(s) available for delivery") message: ApplicationMessage | None = None try: message = await self.session.delivered_message_queue.get() except (asyncio.CancelledError, RuntimeError): message = None if self.logger.isEnabledFor(logging.DEBUG): self.logger.debug(f"Delivering message {message}") return message def handle_write_timeout(self) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} write timeout unhandled") def handle_read_timeout(self) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} read timeout unhandled") async def handle_connack(self, connack: ConnackPacket) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} CONNACK unhandled") async def handle_connect(self, connect: ConnectPacket) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} CONNECT unhandled") async def handle_subscribe(self, subscribe: SubscribePacket) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} SUBSCRIBE unhandled") async def handle_unsubscribe(self, unsubscribe: UnsubscribePacket) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} UNSUBSCRIBE unhandled") async def handle_suback(self, suback: SubackPacket) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} SUBACK unhandled") async def handle_unsuback(self, unsuback: UnsubackPacket) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} UNSUBACK unhandled") async def handle_pingresp(self, pingresp: PingRespPacket) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} PINGRESP unhandled") async def handle_pingreq(self, pingreq: PingReqPacket) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} PINGREQ unhandled") async def handle_disconnect(self, disconnect: DisconnectPacket) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} DISCONNECT unhandled") async def handle_connection_closed(self) -> None: if self.session is None: msg = "Session is not initialized." raise AMQTTError(msg) self.logger.debug(f"{self.session.client_id} Connection closed unhandled") async def handle_puback(self, puback: PubackPacket) -> None: if puback.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) packet_id = puback.variable_header.packet_id try: waiter = self._puback_waiters[packet_id] waiter.set_result(puback) except KeyError: self.logger.warning(f"Received PUBACK for unknown pending message Id: '{packet_id}'") except InvalidStateError: self.logger.warning(f"PUBACK waiter with Id '{packet_id}' already done") async def handle_pubrec(self, pubrec: PubrecPacket) -> None: packet_id = pubrec.packet_id try: waiter = self._pubrec_waiters[packet_id] waiter.set_result(pubrec) except KeyError: self.logger.warning(f"Received PUBREC for unknown pending message with Id: {packet_id}") except InvalidStateError: self.logger.warning(f"PUBREC waiter with Id '{packet_id}' already done") async def handle_pubcomp(self, pubcomp: PubcompPacket) -> None: packet_id = pubcomp.packet_id try: waiter = self._pubcomp_waiters[packet_id] waiter.set_result(pubcomp) except KeyError: self.logger.warning(f"Received PUBCOMP for unknown pending message with Id: {packet_id}") except InvalidStateError: self.logger.warning(f"PUBCOMP waiter with Id '{packet_id}' already done") async def handle_pubrel(self, pubrel: PubrelPacket) -> None: packet_id = pubrel.packet_id try: waiter = self._pubrel_waiters[packet_id] waiter.set_result(pubrel) except KeyError: self.logger.warning(f"Received PUBREL for unknown pending message with Id: {packet_id}") except InvalidStateError: self.logger.warning(f"PUBREL waiter with Id '{packet_id}' already done") async def handle_publish(self, publish_packet: PublishPacket) -> None: packet_id = publish_packet.variable_header.packet_id if publish_packet.variable_header else None qos = publish_packet.qos if publish_packet.topic_name is None or publish_packet.data is None: return incoming_message = IncomingApplicationMessage( packet_id, publish_packet.topic_name, qos, publish_packet.data, publish_packet.retain_flag, ) incoming_message.publish_packet = publish_packet await self._handle_message_flow(incoming_message) self.logger.debug(f"Message queue size: {self.session.delivered_message_queue.qsize() if self.session else None}") Yakifo-amqtt-2637127/amqtt/mqtt/puback.py000066400000000000000000000026221504664204300202150ustar00rootroot00000000000000from typing_extensions import Self from amqtt.errors import AMQTTError from amqtt.mqtt.packet import PUBACK, MQTTFixedHeader, MQTTPacket, PacketIdVariableHeader class PubackPacket(MQTTPacket[PacketIdVariableHeader, None, MQTTFixedHeader]): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None @property def packet_id(self) -> int: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.packet_id = val def __init__( self, fixed: MQTTFixedHeader | None = None, variable_header: PacketIdVariableHeader | None = None, ) -> None: if fixed is None: header = MQTTFixedHeader(PUBACK, 0x00) else: if fixed.packet_type is not PUBACK: msg = f"Invalid fixed packet type {fixed.packet_type} for PubackPacket init" raise AMQTTError(msg) header = fixed super().__init__(header, variable_header, None) @classmethod def build(cls, packet_id: int) -> Self: v_header = PacketIdVariableHeader(packet_id) return cls(variable_header=v_header) Yakifo-amqtt-2637127/amqtt/mqtt/pubcomp.py000066400000000000000000000027611504664204300204210ustar00rootroot00000000000000from typing_extensions import Self from amqtt.errors import AMQTTError from amqtt.mqtt.packet import PUBCOMP, MQTTFixedHeader, MQTTPacket, PacketIdVariableHeader class PubcompPacket(MQTTPacket[PacketIdVariableHeader, None, MQTTFixedHeader]): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None def __init__( self, fixed: MQTTFixedHeader | None = None, variable_header: PacketIdVariableHeader | None = None, ) -> None: if fixed is None: header = MQTTFixedHeader(PUBCOMP, 0x00) else: if fixed.packet_type is not PUBCOMP: msg = f"Invalid fixed packet type {fixed.packet_type} for PubcompPacket init" raise AMQTTError( msg, ) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, packet_id: int) -> Self: v_header = PacketIdVariableHeader(packet_id) return cls(variable_header=v_header) @property def packet_id(self) -> int: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.packet_id = val Yakifo-amqtt-2637127/amqtt/mqtt/publish.py000066400000000000000000000150231504664204300204150ustar00rootroot00000000000000import asyncio from typing_extensions import Self from amqtt.adapters import ReaderAdapter from amqtt.codecs_amqtt import decode_packet_id, decode_string, encode_string, int_to_bytes from amqtt.errors import AMQTTError, MQTTError from amqtt.mqtt.packet import PUBLISH, MQTTFixedHeader, MQTTPacket, MQTTPayload, MQTTVariableHeader class PublishVariableHeader(MQTTVariableHeader): __slots__ = ("packet_id", "topic_name") def __init__(self, topic_name: str, packet_id: int | None = None) -> None: super().__init__() if "#" in topic_name or "+" in topic_name: msg = "[MQTT-3.3.2-2] Topic name in the PUBLISH Packet MUST NOT contain wildcard characters." raise MQTTError(msg) self.topic_name = topic_name self.packet_id = packet_id def __repr__(self) -> str: """Return a string representation of the PublishVariableHeader object.""" return f"{type(self).__name__}(topic={self.topic_name}, packet_id={self.packet_id})" def to_bytes(self) -> bytes | bytearray: out = bytearray() out.extend(encode_string(self.topic_name)) if self.packet_id is not None: out.extend(int_to_bytes(self.packet_id, 2)) return out @classmethod async def from_stream(cls, reader: ReaderAdapter | asyncio.StreamReader, fixed_header: MQTTFixedHeader) -> Self: topic_name = await decode_string(reader) has_qos = (fixed_header.flags >> 1) & 0x03 packet_id = await decode_packet_id(reader) if has_qos else None return cls(topic_name, packet_id) class PublishPayload(MQTTPayload[MQTTVariableHeader]): __slots__ = ("data",) def __init__(self, data: bytes | None = None) -> None: super().__init__() self.data = data def to_bytes( self, fixed_header: MQTTFixedHeader | None = None, variable_header: MQTTVariableHeader | None = None, ) -> bytes: return self.data if self.data is not None else b"" @classmethod async def from_stream( cls, reader: asyncio.StreamReader | ReaderAdapter, fixed_header: MQTTFixedHeader | None, variable_header: MQTTVariableHeader | None, ) -> Self: data = bytearray() if fixed_header is None or variable_header is None: msg = "Fixed header or variable header cannot be None" raise ValueError(msg) data_length = fixed_header.remaining_length - variable_header.bytes_length length_read = 0 while length_read < data_length: buffer = await reader.read(data_length - length_read) data.extend(buffer) length_read = len(data) return cls(bytes(data)) def __repr__(self) -> str: """Return a string representation of the PublishPayload object.""" return f"{type(self).__name__}(data={repr(self.data)!r})" class PublishPacket(MQTTPacket[PublishVariableHeader, PublishPayload, MQTTFixedHeader]): VARIABLE_HEADER = PublishVariableHeader PAYLOAD = PublishPayload DUP_FLAG = 0x08 RETAIN_FLAG = 0x01 QOS_FLAG = 0x06 def __init__( self, fixed: MQTTFixedHeader | None = None, variable_header: PublishVariableHeader | None = None, payload: PublishPayload | None = None, ) -> None: if fixed is None: header = MQTTFixedHeader(PUBLISH, 0x00) elif fixed.packet_type != PUBLISH: msg = f"Invalid fixed packet type {fixed.packet_type} for PublishPacket init" raise AMQTTError(msg) from None else: header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, topic_name: str, message: bytes, packet_id: int | None, dup_flag: bool, qos: int | None, retain: bool) -> Self: v_header = PublishVariableHeader(topic_name, packet_id) payload = PublishPayload(message) packet = cls(variable_header=v_header, payload=payload) packet.dup_flag = dup_flag packet.retain_flag = retain packet.qos = qos or 0 return packet def set_flags(self, dup_flag: bool = False, qos: int = 0, retain_flag: bool = False) -> None: self.dup_flag = dup_flag self.retain_flag = retain_flag self.qos = qos def _set_header_flag(self, val: bool, mask: int) -> None: if val: self.fixed_header.flags |= mask else: self.fixed_header.flags &= ~mask def _get_header_flag(self, mask: int) -> bool: return bool(self.fixed_header.flags & mask) @property def dup_flag(self) -> bool: return self._get_header_flag(self.DUP_FLAG) @dup_flag.setter def dup_flag(self, val: bool) -> None: self._set_header_flag(val, self.DUP_FLAG) @property def retain_flag(self) -> bool: return self._get_header_flag(self.RETAIN_FLAG) @retain_flag.setter def retain_flag(self, val: bool) -> None: self._set_header_flag(val, self.RETAIN_FLAG) @property def qos(self) -> int | None: return (self.fixed_header.flags & self.QOS_FLAG) >> 1 @qos.setter def qos(self, val: int) -> None: self.fixed_header.flags &= 0xF9 self.fixed_header.flags |= val << 1 @property def packet_id(self) -> int | None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.packet_id = val @property def data(self) -> bytes | None: if self.payload is None: msg = "Payload header is not set" raise ValueError(msg) return self.payload.data @data.setter def data(self, data: bytes) -> None: if self.payload is None: msg = "Payload header is not set" raise ValueError(msg) self.payload.data = data @property def topic_name(self) -> str | None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.topic_name @topic_name.setter def topic_name(self, name: str) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.topic_name = name Yakifo-amqtt-2637127/amqtt/mqtt/pubrec.py000066400000000000000000000027051504664204300202320ustar00rootroot00000000000000from typing_extensions import Self from amqtt.errors import AMQTTError from amqtt.mqtt.packet import PUBREC, MQTTFixedHeader, MQTTPacket, PacketIdVariableHeader class PubrecPacket(MQTTPacket[PacketIdVariableHeader, None, MQTTFixedHeader]): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None def __init__( self, fixed: MQTTFixedHeader | None = None, variable_header: PacketIdVariableHeader | None = None, ) -> None: if fixed is None: header = MQTTFixedHeader(PUBREC, 0x00) else: if fixed.packet_type is not PUBREC: msg = f"Invalid fixed packet type {fixed.packet_type} for PubrecPacket init" raise AMQTTError(msg) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, packet_id: int) -> Self: v_header = PacketIdVariableHeader(packet_id) return cls(variable_header=v_header) @property def packet_id(self) -> int: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.packet_id = val Yakifo-amqtt-2637127/amqtt/mqtt/pubrel.py000066400000000000000000000027451504664204300202470ustar00rootroot00000000000000from typing_extensions import Self from amqtt.errors import AMQTTError from amqtt.mqtt.packet import PUBREL, MQTTFixedHeader, MQTTPacket, PacketIdVariableHeader class PubrelPacket(MQTTPacket[PacketIdVariableHeader, None, MQTTFixedHeader]): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None def __init__( self, fixed: MQTTFixedHeader | None = None, variable_header: PacketIdVariableHeader | None = None, ) -> None: if fixed is None: header = MQTTFixedHeader(PUBREL, 0x02) # [MQTT-3.6.1-1] else: if fixed.packet_type is not PUBREL: msg = f"Invalid fixed packet type {fixed.packet_type} for PubrelPacket init" raise AMQTTError(msg) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = None @classmethod def build(cls, packet_id: int) -> Self: variable_header = PacketIdVariableHeader(packet_id) return cls(variable_header=variable_header) @property def packet_id(self) -> int: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) return self.variable_header.packet_id @packet_id.setter def packet_id(self, val: int) -> None: if self.variable_header is None: msg = "Variable header is not set" raise ValueError(msg) self.variable_header.packet_id = val Yakifo-amqtt-2637127/amqtt/mqtt/suback.py000066400000000000000000000056711504664204300202270ustar00rootroot00000000000000import asyncio from typing_extensions import Self from amqtt.adapters import ReaderAdapter from amqtt.codecs_amqtt import bytes_to_int, int_to_bytes, read_or_raise from amqtt.errors import AMQTTError, NoDataError from amqtt.mqtt.packet import SUBACK, MQTTFixedHeader, MQTTPacket, MQTTPayload, MQTTVariableHeader, PacketIdVariableHeader class SubackPayload(MQTTPayload[MQTTVariableHeader]): __slots__ = ("return_codes",) RETURN_CODE_00 = 0x00 RETURN_CODE_01 = 0x01 RETURN_CODE_02 = 0x02 RETURN_CODE_80 = 0x80 def __init__(self, return_codes: list[int] | None = None) -> None: super().__init__() self.return_codes = return_codes or [] def __repr__(self) -> str: """Return a string representation of the SubackPayload object.""" return f"{type(self).__name__}(return_codes={self.return_codes!r})" def to_bytes( self, fixed_header: MQTTFixedHeader | None = None, variable_header: MQTTVariableHeader | None = None, ) -> bytes: out = b"" for return_code in self.return_codes: out += int_to_bytes(return_code, 1) return out @classmethod async def from_stream( cls, reader: asyncio.StreamReader | ReaderAdapter, fixed_header: MQTTFixedHeader | None, variable_header: MQTTVariableHeader | None, ) -> Self: return_codes = [] if fixed_header is None or variable_header is None: msg = "Fixed header or variable header cannot be None" raise AMQTTError(msg) bytes_to_read = fixed_header.remaining_length - variable_header.bytes_length for _ in range(bytes_to_read): try: return_code_byte = await read_or_raise(reader, 1) return_code = bytes_to_int(return_code_byte) return_codes.append(return_code) except NoDataError: break return cls(return_codes) class SubackPacket(MQTTPacket[PacketIdVariableHeader, SubackPayload, MQTTFixedHeader]): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = SubackPayload def __init__( self, fixed: MQTTFixedHeader | None = None, variable_header: PacketIdVariableHeader | None = None, payload: SubackPayload | None = None, ) -> None: if fixed is None: header = MQTTFixedHeader(SUBACK, 0x00) else: if fixed.packet_type is not SUBACK: msg = f"Invalid fixed packet type {fixed.packet_type} for SubackPacket init" raise AMQTTError(msg) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, packet_id: int, return_codes: list[int]) -> Self: variable_header = cls.VARIABLE_HEADER(packet_id) payload = cls.PAYLOAD(return_codes) return cls(variable_header=variable_header, payload=payload) Yakifo-amqtt-2637127/amqtt/mqtt/subscribe.py000066400000000000000000000060051504664204300207300ustar00rootroot00000000000000import asyncio from typing_extensions import Self from amqtt.adapters import ReaderAdapter from amqtt.codecs_amqtt import bytes_to_int, decode_string, encode_string, int_to_bytes, read_or_raise from amqtt.errors import AMQTTError, NoDataError from amqtt.mqtt.packet import SUBSCRIBE, MQTTFixedHeader, MQTTPacket, MQTTPayload, MQTTVariableHeader, PacketIdVariableHeader class SubscribePayload(MQTTPayload[MQTTVariableHeader]): __slots__ = ("topics",) def __init__(self, topics: list[tuple[str, int]] | None = None) -> None: super().__init__() self.topics = topics or [] def to_bytes( self, fixed_header: MQTTFixedHeader | None = None, variable_header: MQTTVariableHeader | None = None, ) -> bytes: out = b"" for topic in self.topics: out += encode_string(topic[0]) out += int_to_bytes(topic[1], 1) return out @classmethod async def from_stream( cls, reader: asyncio.StreamReader | ReaderAdapter, fixed_header: MQTTFixedHeader | None, variable_header: MQTTVariableHeader | None, ) -> Self: topics = [] if fixed_header is None or variable_header is None: msg = "Fixed header or variable header cannot be None" raise ValueError(msg) payload_length = fixed_header.remaining_length - variable_header.bytes_length read_bytes = 0 while read_bytes < payload_length: try: topic = await decode_string(reader) qos_byte = await read_or_raise(reader, 1) qos = bytes_to_int(qos_byte) topics.append((topic, qos)) read_bytes += 2 + len(topic.encode("utf-8")) + 1 except NoDataError: break return cls(topics) def __repr__(self) -> str: """Return a string representation of the SubscribePayload object.""" return type(self).__name__ + f"(topics={self.topics!r})" class SubscribePacket(MQTTPacket[PacketIdVariableHeader, SubscribePayload, MQTTFixedHeader]): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = SubscribePayload def __init__( self, fixed: MQTTFixedHeader | None = None, variable_header: PacketIdVariableHeader | None = None, payload: SubscribePayload | None = None, ) -> None: if fixed is None: header = MQTTFixedHeader(SUBSCRIBE, 0x02) # [MQTT-3.8.1-1] else: if fixed.packet_type is not SUBSCRIBE: msg = f"Invalid fixed packet type {fixed.packet_type} for SubscribePacket init" raise AMQTTError(msg) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, topics: list[tuple[str, int]], packet_id: int) -> Self: v_header = PacketIdVariableHeader(packet_id) payload = SubscribePayload(topics) return cls(variable_header=v_header, payload=payload) Yakifo-amqtt-2637127/amqtt/mqtt/unsuback.py000066400000000000000000000021461504664204300205640ustar00rootroot00000000000000from typing_extensions import Self from amqtt.errors import AMQTTError from amqtt.mqtt.packet import UNSUBACK, MQTTFixedHeader, MQTTPacket, PacketIdVariableHeader class UnsubackPacket(MQTTPacket[PacketIdVariableHeader, None, MQTTFixedHeader]): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = None def __init__( self, fixed: MQTTFixedHeader | None = None, variable_header: PacketIdVariableHeader | None = None, payload: None = None, ) -> None: if fixed is None: header = MQTTFixedHeader(UNSUBACK, 0x00) else: if fixed.packet_type is not UNSUBACK: msg = f"Invalid fixed packet type {fixed.packet_type} for UnsubackPacket init" raise AMQTTError( msg, ) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, packet_id: int) -> Self: variable_header = PacketIdVariableHeader(packet_id) return cls(variable_header=variable_header) Yakifo-amqtt-2637127/amqtt/mqtt/unsubscribe.py000066400000000000000000000052201504664204300212710ustar00rootroot00000000000000from asyncio import StreamReader from typing_extensions import Self from amqtt.adapters import ReaderAdapter from amqtt.codecs_amqtt import decode_string, encode_string from amqtt.errors import AMQTTError, NoDataError from amqtt.mqtt.packet import UNSUBSCRIBE, MQTTFixedHeader, MQTTPacket, MQTTPayload, MQTTVariableHeader, PacketIdVariableHeader class UnubscribePayload(MQTTPayload[MQTTVariableHeader]): __slots__ = ("topics",) def __init__(self, topics: list[str] | None = None) -> None: super().__init__() self.topics = topics or [] def to_bytes(self, fixed_header: MQTTFixedHeader | None = None, variable_header: MQTTVariableHeader | None = None) -> bytes: out = b"" for topic in self.topics: out += encode_string(topic) return out @classmethod async def from_stream( cls: type[Self], reader: StreamReader | ReaderAdapter, fixed_header: MQTTFixedHeader | None, variable_header: MQTTVariableHeader | None, ) -> Self: if fixed_header is None or variable_header is None: msg = "Fixed header or Value header is not set." raise ValueError(msg) topics = [] payload_length = fixed_header.remaining_length - variable_header.bytes_length read_bytes = 0 while read_bytes < payload_length: try: topic = await decode_string(reader) topics.append(topic) read_bytes += 2 + len(topic.encode("utf-8")) except NoDataError: break return cls(topics) class UnsubscribePacket(MQTTPacket[PacketIdVariableHeader, UnubscribePayload, MQTTFixedHeader]): VARIABLE_HEADER = PacketIdVariableHeader PAYLOAD = UnubscribePayload def __init__( self, fixed: MQTTFixedHeader | None = None, variable_header: PacketIdVariableHeader | None = None, payload: UnubscribePayload | None = None, ) -> None: if fixed is None: header = MQTTFixedHeader(UNSUBSCRIBE, 0x02) # [MQTT-3.10.1-1] else: if fixed.packet_type is not UNSUBSCRIBE: msg = f"Invalid fixed packet type {fixed.packet_type} for UnsubscribePacket init" raise AMQTTError(msg) header = fixed super().__init__(header) self.variable_header = variable_header self.payload = payload @classmethod def build(cls, topics: list[str], packet_id: int) -> "UnsubscribePacket": v_header = PacketIdVariableHeader(packet_id) payload = UnubscribePayload(topics) return UnsubscribePacket(variable_header=v_header, payload=payload) Yakifo-amqtt-2637127/amqtt/plugins/000077500000000000000000000000001504664204300170705ustar00rootroot00000000000000Yakifo-amqtt-2637127/amqtt/plugins/__init__.py000066400000000000000000000031011504664204300211740ustar00rootroot00000000000000"""INIT.""" import re from typing import Any, Optional class TopicMatcher: _instance: Optional["TopicMatcher"] = None def __init__(self) -> None: if not hasattr(self, "_topic_filter_matchers"): self._topic_filter_matchers: dict[str, re.Pattern[str]] = {} def __new__(cls, *args: list[Any], **kwargs: dict[str, Any]) -> "TopicMatcher": if cls._instance is None: cls._instance = super().__new__(cls, *args, **kwargs) return cls._instance def is_topic_allowed(self, topic: str, a_filter: str) -> bool: if topic.startswith("$") and (a_filter.startswith(("+", "#"))): return False if "#" not in a_filter and "+" not in a_filter: # if filter doesn't contain wildcard, return exact match return a_filter == topic # else use regex (re.compile is an expensive operation, store the matcher for future use) if a_filter not in self._topic_filter_matchers: self._topic_filter_matchers[a_filter] = re.compile(re.escape(a_filter) .replace("\\#", "?.*") .replace("\\+", "[^/]*") .lstrip("?")) match_pattern = self._topic_filter_matchers[a_filter] return bool(match_pattern.fullmatch(topic)) def are_topics_allowed(self, topic: str, many_filters: list[str]) -> bool: return any(self.is_topic_allowed(topic, a_filter) for a_filter in many_filters) Yakifo-amqtt-2637127/amqtt/plugins/authentication.py000066400000000000000000000114571504664204300224710ustar00rootroot00000000000000from dataclasses import dataclass, field from pathlib import Path from passlib.apps import custom_app_context as pwd_context from amqtt.broker import BrokerContext from amqtt.contexts import BaseContext from amqtt.plugins.base import BaseAuthPlugin from amqtt.session import Session _PARTS_EXPECTED_LENGTH = 2 # Expected number of parts in a valid line class AnonymousAuthPlugin(BaseAuthPlugin): """Authentication plugin allowing anonymous access.""" def __init__(self, context: BaseContext) -> None: super().__init__(context) # Default to allowing anonymous self._allow_anonymous = self._get_config_option("allow-anonymous", True) # noqa: FBT003 async def authenticate(self, *, session: Session) -> bool: authenticated = await super().authenticate(session=session) if authenticated: if self._allow_anonymous: self.context.logger.debug("Authentication success: config allows anonymous") session.is_anonymous = True return True if session and session.username: self.context.logger.debug(f"Authentication success: session has username '{session.username}'") return True self.context.logger.debug("Authentication failure: session has no username") return False @dataclass class Config: """Configuration for AnonymousAuthPlugin.""" allow_anonymous: bool = field(default=True) """Allow all anonymous authentication (even with _no_ username).""" class FileAuthPlugin(BaseAuthPlugin): """Authentication plugin based on a file-stored user database.""" def __init__(self, context: BrokerContext) -> None: super().__init__(context) self._users: dict[str, str] = {} self._read_password_file() def _read_password_file(self) -> None: """Read the password file and populates the user dictionary.""" password_file = self._get_config_option("password-file", None) if not password_file: self.context.logger.warning("Configuration parameter 'password-file' not found") return try: file = password_file if isinstance(file, str): file = Path(file) with file.open(mode="r", encoding="utf-8") as file: self.context.logger.debug(f"Reading user database from {password_file}") for _line in file: line = _line.strip() if line and not line.startswith("#"): # Skip empty lines and comments parts = line.split(":", maxsplit=1) if len(parts) == _PARTS_EXPECTED_LENGTH: username, pwd_hash = parts self._users[username] = pwd_hash self.context.logger.debug(f"User '{username}' loaded") else: self.context.logger.warning(f"Malformed line in password file: {line}") self.context.logger.info(f"{len(self._users)} user(s) loaded from {password_file}") except FileNotFoundError: self.context.logger.warning(f"Password file '{password_file}' not found") except ValueError: self.context.logger.exception(f"Malformed password file '{password_file}'") except OSError: self.context.logger.exception(f"Unexpected error reading password file '{password_file}'") async def authenticate(self, *, session: Session) -> bool | None: """Authenticate users based on the file-stored user database.""" authenticated = await super().authenticate(session=session) if authenticated: if not session: self.context.logger.debug("Authentication failure: no session provided") return False if not session.username: self.context.logger.debug("Authentication failure: no username provided in session") return None hash_session_username = self._users.get(session.username) if not hash_session_username: self.context.logger.debug(f"Authentication failure: no hash found for user '{session.username}'") return False if pwd_context.verify(session.password, hash_session_username): self.context.logger.debug(f"Authentication success for user '{session.username}'") return True self.context.logger.debug(f"Authentication failure: password mismatch for user '{session.username}'") return False @dataclass class Config: """Configuration for FileAuthPlugin.""" password_file: str | Path | None = None """Path to file with `username:password` pairs, one per line. All passwords are encoded using sha-512.""" Yakifo-amqtt-2637127/amqtt/plugins/base.py000066400000000000000000000132731504664204300203620ustar00rootroot00000000000000from dataclasses import dataclass, is_dataclass from typing import Any, Generic, TypeVar, cast from amqtt.contexts import Action, BaseContext, BrokerConfig from amqtt.session import Session C = TypeVar("C", bound=BaseContext) class BasePlugin(Generic[C]): """The base from which all plugins should inherit. Type Parameters --------------- C: A BaseContext: either BrokerContext or ClientContext, depending on plugin usage Attributes ---------- context (C): Information about the environment in which this plugin is executed. Modifying the broker or client state should happen through methods available here. config (self.Config): An instance of the Config dataclass defined by the plugin (or an empty dataclass, if not defined). If using entrypoint- or mixed-style configuration, use `_get_config_option()` to access the variable. """ def __init__(self, context: C) -> None: self.context: C = context # since the PluginManager will hydrate the config from a plugin's `Config` class, this is a safe cast self.config = cast("self.Config", context.config) # type: ignore[name-defined] # Deprecated: included to support entrypoint-style configs. Replaced by dataclass Config class. def _get_config_section(self, name: str) -> dict[str, Any] | None: if not self.context.config or not hasattr(self.context.config, "get") or not self.context.config.get(name, None): return None section_config: int | dict[str, Any] | None = self.context.config.get(name, None) # mypy has difficulty excluding int from `config`'s type, unless there's an explicit check if isinstance(section_config, int): return None return section_config # Deprecated : supports entrypoint-style configs as well as dataclass configuration. def _get_config_option(self, option_name: str, default: Any = None) -> Any: if not self.context.config: return default if is_dataclass(self.context.config): # overloaded context.config for BasePlugin `Config` class, so ignoring static type check return getattr(self.context.config, option_name.replace("-", "_"), default) if option_name in self.context.config: return self.context.config[option_name] return default @dataclass class Config: """Override to define the configuration and defaults for plugin.""" async def close(self) -> None: """Override if plugin needs to clean up resources upon shutdown.""" class BaseTopicPlugin(BasePlugin[BaseContext]): """Base class for topic plugins.""" def __init__(self, context: BaseContext) -> None: super().__init__(context) self.topic_config: dict[str, Any] | None = self._get_config_section("topic-check") if not bool(self.topic_config) and not is_dataclass(self.context.config): self.context.logger.warning("'topic-check' section not found in context configuration") def _get_config_option(self, option_name: str, default: Any = None) -> Any: if not self.context.config: return default # overloaded context.config with either BrokerConfig or plugin's Config if is_dataclass(self.context.config) and not isinstance(self.context.config, BrokerConfig): # overloaded context.config for BasePlugin `Config` class, so ignoring static type check return getattr(self.context.config, option_name.replace("-", "_"), default) if self.topic_config and option_name in self.topic_config: return self.topic_config[option_name] return default async def topic_filtering( self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None ) -> bool | None: """Logic for filtering out topics. Args: session: amqtt.session.Session topic: str action: amqtt.broker.Action Returns: bool: `True` if topic is allowed, `False` otherwise. `None` if it can't be determined """ return bool(self.topic_config) or is_dataclass(self.context.config) class BaseAuthPlugin(BasePlugin[BaseContext]): """Base class for authentication plugins.""" def _get_config_option(self, option_name: str, default: Any = None) -> Any: if not self.context.config: return default if is_dataclass(self.context.config) and not isinstance(self.context.config, BrokerConfig): # overloaded context.config for BasePlugin `Config` class, so ignoring static type check return getattr(self.context.config, option_name.replace("-", "_"), default) if self.auth_config and option_name in self.auth_config: return self.auth_config[option_name] return default def __init__(self, context: BaseContext) -> None: super().__init__(context) self.auth_config: dict[str, Any] | None = self._get_config_section("auth") if not bool(self.auth_config) and not is_dataclass(self.context.config): # auth config section not found and Config dataclass not provided self.context.logger.warning("'auth' section not found in context configuration") async def authenticate(self, *, session: Session) -> bool | None: """Logic for session authentication. Args: session: amqtt.session.Session Returns: - `True` if user is authentication succeed, `False` if user authentication fails - `None` if authentication can't be achieved (then plugin result is then ignored) """ return bool(self.auth_config) or is_dataclass(self.context.config) Yakifo-amqtt-2637127/amqtt/plugins/logging_amqtt.py000066400000000000000000000044761504664204300223110ustar00rootroot00000000000000from collections.abc import Callable, Coroutine from functools import partial import logging from typing import Any, TypeAlias from amqtt.contexts import BaseContext from amqtt.events import BrokerEvents from amqtt.mqtt import MQTTPacket from amqtt.mqtt.packet import MQTTFixedHeader, MQTTPayload, MQTTVariableHeader from amqtt.plugins.base import BasePlugin from amqtt.session import Session PACKET: TypeAlias = MQTTPacket[MQTTVariableHeader, MQTTPayload[MQTTVariableHeader], MQTTFixedHeader] class EventLoggerPlugin(BasePlugin[BaseContext]): """A plugin to log events dynamically based on method names.""" async def log_event(self, *args: Any, **kwargs: Any) -> None: """Log the occurrence of an event.""" event_name = kwargs["event_name"].replace("old", "") if event_name.replace("on_", "") in (BrokerEvents.CLIENT_CONNECTED, BrokerEvents.CLIENT_DISCONNECTED): self.context.logger.info(f"### '{event_name}' EVENT FIRED ###") else: self.context.logger.debug(f"### '{event_name}' EVENT FIRED ###") def __getattr__(self, name: str) -> Callable[..., Coroutine[Any, Any, None]]: """Dynamically handle calls to methods starting with 'on_'.""" if name.startswith("on_"): return partial(self.log_event, event_name=name) msg = f"'EventLoggerPlugin' object has no attribute {name!r}" raise AttributeError(msg) class PacketLoggerPlugin(BasePlugin[BaseContext]): """A plugin to log MQTT packets sent and received.""" async def on_mqtt_packet_received(self, *, packet: PACKET, session: Session | None = None) -> None: """Log an MQTT packet when it is received.""" if self.context.logger.isEnabledFor(logging.DEBUG): if session is not None: self.context.logger.debug(f"{session.client_id} <-in-- {packet!r}") else: self.context.logger.debug(f"<-in-- {packet!r}") async def on_mqtt_packet_sent(self, *, packet: PACKET, session: Session | None = None) -> None: """Log an MQTT packet when it is sent.""" if self.context.logger.isEnabledFor(logging.DEBUG): if session is not None: self.context.logger.debug(f"{session.client_id} -out-> {packet!r}") else: self.context.logger.debug(f"-out-> {packet!r}") Yakifo-amqtt-2637127/amqtt/plugins/manager.py000066400000000000000000000421051504664204300210560ustar00rootroot00000000000000__all__ = ["PluginManager", "get_plugin_manager"] import asyncio from collections import defaultdict from collections.abc import Awaitable, Callable, Coroutine import contextlib import copy from importlib.metadata import EntryPoint, EntryPoints, entry_points from inspect import iscoroutinefunction import logging import sys import traceback from typing import Any, Generic, NamedTuple, Optional, TypeAlias, TypeVar, cast import warnings from dacite import Config as DaciteConfig, DaciteError, from_dict from amqtt.contexts import Action, BaseContext from amqtt.errors import PluginCoroError, PluginImportError, PluginInitError, PluginLoadError from amqtt.events import BrokerEvents, Events, MQTTEvents from amqtt.plugins.base import BaseAuthPlugin, BasePlugin, BaseTopicPlugin from amqtt.session import Session from amqtt.utils import import_string class Plugin(NamedTuple): name: str ep: EntryPoint object: Any plugins_manager: dict[str, "PluginManager[Any]"] = {} def get_plugin_manager(namespace: str) -> "PluginManager[Any] | None": """Get the plugin manager for a given namespace. :param namespace: The namespace of the plugin manager to retrieve. :return: The plugin manager for the given namespace, or None if it doesn't exist. """ return plugins_manager.get(namespace) def safe_issubclass(sub_class: Any, super_class: Any) -> bool: try: return issubclass(sub_class, super_class) except TypeError: return False AsyncFunc: TypeAlias = Callable[..., Coroutine[Any, Any, None]] C = TypeVar("C", bound=BaseContext) class PluginManager(Generic[C]): """Wraps contextlib Entry point mechanism to provide a basic plugin system. Plugins are loaded for a given namespace (group). This plugin manager uses coroutines to run plugin calls asynchronously in an event queue. """ def __init__(self, namespace: str, context: C | None, loop: asyncio.AbstractEventLoop | None = None) -> None: try: self._loop = loop if loop is not None else asyncio.get_running_loop() except RuntimeError: self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) self.logger = logging.getLogger(namespace) self.context = context if context is not None else BaseContext() self.context.loop = self._loop self._plugins: list[BasePlugin[C]] = [] self._auth_plugins: list[BaseAuthPlugin] = [] self._topic_plugins: list[BaseTopicPlugin] = [] self._event_plugin_callbacks: dict[str, list[AsyncFunc]] = defaultdict(list) self._is_topic_filtering_enabled = False self._is_auth_filtering_enabled = False self._load_plugins(namespace) self._fired_events: list[asyncio.Future[Any]] = [] plugins_manager[namespace] = self @property def app_context(self) -> BaseContext: return self.context def _load_plugins(self, namespace: str | None = None) -> None: """Load plugins from entrypoint or config dictionary. config style is now recommended; entrypoint has been deprecated Example: config = { 'listeners':..., 'plugins': { 'myproject.myfile.MyPlugin': {} } """ if self.app_context.config and self.app_context.config.get("plugins", None) is not None: # plugins loaded directly from config dictionary if "auth" in self.app_context.config and self.app_context.config["auth"] is not None: self.logger.warning("Loading plugins from config will ignore 'auth' section of config") if "topic-check" in self.app_context.config and self.app_context.config["topic-check"] is not None: self.logger.warning("Loading plugins from config will ignore 'topic-check' section of config") plugins_config: list[Any] | dict[str, Any] = self.app_context.config.get("plugins", []) # if the config was generated from yaml, the plugins maybe a list instead of a dictionary; transform before loading # # plugins: # - myproject.myfile.MyPlugin: if isinstance(plugins_config, list): plugins_info: dict[str, Any] = {} for plugin_config in plugins_config: if isinstance(plugin_config, str): plugins_info.update({plugin_config: {}}) elif not isinstance(plugin_config, dict): msg = "malformed 'plugins' configuration" raise PluginLoadError(msg) else: plugins_info.update(plugin_config) self._load_str_plugins(plugins_info) elif isinstance(plugins_config, dict): self._load_str_plugins(plugins_config) else: if not namespace: msg = "Namespace needs to be provided for EntryPoint plugin definitions" raise PluginLoadError(msg) warnings.warn( "Loading plugins from EntryPoints is deprecated and will be removed in a future version." " Use `plugins` section of config instead.", DeprecationWarning, stacklevel=4 ) self._load_ep_plugins(namespace) # for all the loaded plugins, find all event callbacks for plugin in self._plugins: for event in list(BrokerEvents) + list(MQTTEvents): if awaitable := getattr(plugin, f"on_{event}", None): if not iscoroutinefunction(awaitable): msg = f"'on_{event}' for '{plugin.__class__.__name__}' is not a coroutine'" raise PluginImportError(msg) self.logger.debug(f"'{event}' handler found for '{plugin.__class__.__name__}'") self._event_plugin_callbacks[event].append(awaitable) def _load_ep_plugins(self, namespace: str) -> None: """Load plugins from `pyproject.toml` entrypoints. Deprecated.""" self.logger.debug(f"Loading plugins for namespace {namespace}") auth_filter_list = [] topic_filter_list = [] if self.app_context.config and "auth" in self.app_context.config: auth_filter_list = self.app_context.config["auth"].get("plugins", None) if self.app_context.config and "topic-check" in self.app_context.config: topic_filter_list = self.app_context.config["topic-check"].get("plugins", None) ep: EntryPoints | list[EntryPoint] = [] if hasattr(entry_points(), "select"): ep = entry_points().select(group=namespace) elif namespace in entry_points(): ep = [entry_points()[namespace]] for item in ep: ep_plugin = self._load_ep_plugin(item) if ep_plugin is not None: self._plugins.append(ep_plugin.object) # maintain legacy behavior that if there is no list, use all auth plugins if ((auth_filter_list is None or ep_plugin.name in auth_filter_list) and hasattr(ep_plugin.object, "authenticate")): self._auth_plugins.append(ep_plugin.object) # maintain legacy behavior that if there is no list, use all topic plugins if ((topic_filter_list is None or ep_plugin.name in topic_filter_list) and hasattr(ep_plugin.object, "topic_filtering")): self._topic_plugins.append(ep_plugin.object) self.logger.debug(f" Plugin {item.name} ready") def _load_ep_plugin(self, ep: EntryPoint) -> Plugin | None: """Load plugins from `pyproject.toml` entrypoints. Deprecated.""" try: self.logger.debug(f" Loading plugin {ep!s}") plugin = ep.load() except ImportError as e: self.logger.debug(f"Plugin import failed: {ep!r}", exc_info=True) raise PluginImportError(ep) from e self.logger.debug(f" Initializing plugin {ep!s}") plugin_context = copy.copy(self.app_context) plugin_context.logger = self.logger.getChild(ep.name) try: obj = plugin(plugin_context) return Plugin(ep.name, ep, obj) except Exception as e: self.logger.debug(f"Plugin init failed: {ep!r}", exc_info=True) raise PluginInitError(ep) from e def _load_str_plugins(self, plugins_info: dict[str, Any]) -> None: self.logger.info("Loading plugins from config") # legacy had a filtering 'enabled' flag, even if plugins were loaded/listed self._is_topic_filtering_enabled = True self._is_auth_filtering_enabled = True for plugin_path, plugin_config in plugins_info.items(): plugin = self._load_str_plugin(plugin_path, plugin_config) self._plugins.append(plugin) # make sure that authenticate and topic filtering plugins have the appropriate async signature if isinstance(plugin, BaseAuthPlugin): if not iscoroutinefunction(plugin.authenticate): msg = f"Auth plugin {plugin_path} has non-async authenticate method." raise PluginCoroError(msg) self._auth_plugins.append(plugin) if isinstance(plugin, BaseTopicPlugin): if not iscoroutinefunction(plugin.topic_filtering): msg = f"Topic plugin {plugin_path} has non-async topic_filtering method." raise PluginCoroError(msg) self._topic_plugins.append(plugin) def _load_str_plugin(self, plugin_path: str, plugin_cfg: dict[str, Any] | None = None) -> "BasePlugin[C]": """Load plugin from string dotted path: mymodule.myfile.MyPlugin.""" try: plugin_class: Any = import_string(plugin_path) except ImportError as ep: msg = f"Plugin import failed: {plugin_path}" raise PluginImportError(msg) from ep if not safe_issubclass(plugin_class, BasePlugin): msg = f"Plugin {plugin_path} is not a subclass of 'BasePlugin'" raise PluginLoadError(msg) plugin_context = copy.copy(self.app_context) plugin_context.logger = self.logger.getChild(plugin_class.__name__) try: # populate the config based on the inner dataclass called `Config` # use `dacite` package to type check plugin_context.config = from_dict(data_class=plugin_class.Config, data=plugin_cfg or {}, config=DaciteConfig(strict=True)) except DaciteError as e: raise PluginLoadError from e except TypeError as e: msg = f"Could not marshall 'Config' of {plugin_path}; should be a dataclass." raise PluginLoadError(msg) from e try: pc = plugin_class(plugin_context) self.logger.debug(f"Loading plugin {plugin_path}") return cast("BasePlugin[C]", pc) except Exception as e: self.logger.debug(f"Plugin init failed: {plugin_class.__name__}", exc_info=True) raise PluginInitError(plugin_class) from e def get_plugin(self, name: str) -> Optional["BasePlugin[C]"]: """Get a plugin by its name from the plugins loaded for the current namespace. Only used for testing purposes to verify plugin loading correctly. :param name: :return: """ for p in self._plugins: if p.__class__.__name__ == name: return p return None def is_topic_filtering_enabled(self) -> bool: topic_config = self.app_context.config.get("topic-check", {}) if self.app_context.config else {} if isinstance(topic_config, dict): return topic_config.get("enabled", False) or self._is_topic_filtering_enabled return False or self._is_topic_filtering_enabled async def close(self) -> None: """Free PluginManager resources and cancel pending event methods.""" await self.map_plugin_close() for task in self._fired_events: task.cancel() self._fired_events.clear() @property def plugins(self) -> list["BasePlugin[C]"]: """Get the loaded plugins list. :return: """ return self._plugins def _schedule_coro(self, coro: Awaitable[str | bool | None]) -> asyncio.Future[str | bool | None]: return asyncio.ensure_future(coro) def _clean_fired_events(self, future: asyncio.Future[Any]) -> None: if self.logger.getEffectiveLevel() <= logging.DEBUG: try: future.result() except asyncio.CancelledError: self.logger.warning("fired event was cancelled") # display plugin fault; don't allow it to cause a broker failure except Exception as exc: # noqa: BLE001, pylint: disable=W0718 traceback.print_exception(type(exc), exc, exc.__traceback__, file=sys.stderr) with contextlib.suppress(KeyError, ValueError): self._fired_events.remove(future) async def fire_event(self, event_name: Events, *, wait: bool = False, **method_kwargs: Any) -> None: """Fire an event to plugins. PluginManager schedules async calls for each plugin on method called "on_" + event_name. For example, on_connect will be called on event 'connect'. Method calls are scheduled in the async loop. wait parameter must be set to true to wait until all methods are completed. :param event_name: :param method_kwargs: :param wait: indicates if fire_event should wait for plugin calls completion (True), or not :return: """ tasks: list[asyncio.Future[Any]] = [] # check if any plugin has defined a callback for this event, skip if none if event_name not in self._event_plugin_callbacks: return for event_awaitable in self._event_plugin_callbacks[event_name]: async def call_method(method: AsyncFunc, kwargs: dict[str, Any]) -> Any: return await method(**kwargs) coro_instance: Awaitable[Any] = call_method(event_awaitable, method_kwargs) tasks.append(asyncio.ensure_future(coro_instance)) tasks[-1].add_done_callback(self._clean_fired_events) self._fired_events.extend(tasks) if wait and tasks: await asyncio.wait(tasks) self.logger.debug(f"Plugins len(_fired_events)={len(self._fired_events)}") @staticmethod async def _map_plugin_method( plugins: list["BasePlugin[C]"], method_name: str, method_kwargs: dict[str, Any], ) -> dict["BasePlugin[C]", str | bool | None]: """Call plugin coroutines. :param plugins: List of plugins to execute the method on :param method_name: Name of the method to call on each plugin :param method_kwargs: Keyword arguments to pass to the method :return: dict containing return from coro call for each plugin. """ tasks: list[asyncio.Future[Any]] = [] for plugin in plugins: if not hasattr(plugin, method_name): continue async def call_method(p: "BasePlugin[C]", kwargs: dict[str, Any]) -> Any: method = getattr(p, method_name) return await method(**kwargs) coro_instance: Awaitable[Any] = call_method(plugin, method_kwargs) tasks.append(asyncio.ensure_future(coro_instance)) ret_dict: dict[BasePlugin[C], str | bool | None] = {} if tasks: ret_list = await asyncio.gather(*tasks) ret_dict = dict(zip(plugins, ret_list, strict=False)) return ret_dict async def map_plugin_auth(self, *, session: Session) -> dict["BasePlugin[C]", str | bool | None]: """Schedule a coroutine for plugin 'authenticate' calls. :param session: the client session associated with the authentication check :return: dict containing return from coro call for each plugin. """ return await self._map_plugin_method( self._auth_plugins, "authenticate", {"session": session}) # type: ignore[arg-type] async def map_plugin_topic( self, *, session: Session, topic: str, action: "Action" ) -> dict["BasePlugin[C]", str | bool | None]: """Schedule a coroutine for plugin 'topic_filtering' calls. :param session: the client session associated with the topic_filtering check :param topic: the topic that needs to be filtered :param action: the action being executed :return: dict containing return from coro call for each plugin. """ return await self._map_plugin_method( self._topic_plugins, "topic_filtering", # type: ignore[arg-type] {"session": session, "topic": topic, "action": action} ) async def map_plugin_close(self) -> None: """Schedule a coroutine for plugin 'close' calls. :return: dict containing return from coro call for each plugin. """ await self._map_plugin_method(self._plugins, "close", {}) Yakifo-amqtt-2637127/amqtt/plugins/persistence.py000066400000000000000000000005371504664204300217730ustar00rootroot00000000000000import warnings from amqtt.broker import BrokerContext from amqtt.plugins.base import BasePlugin class SQLitePlugin(BasePlugin[BrokerContext]): def __init__(self, context: BrokerContext) -> None: super().__init__(context) warnings.warn("SQLitePlugin is deprecated, use amqtt.contrib.persistence.SessionDBPlugin", stacklevel=1) Yakifo-amqtt-2637127/amqtt/plugins/sys/000077500000000000000000000000001504664204300177065ustar00rootroot00000000000000Yakifo-amqtt-2637127/amqtt/plugins/sys/__init__.py000066400000000000000000000000141504664204300220120ustar00rootroot00000000000000"""INIT.""" Yakifo-amqtt-2637127/amqtt/plugins/sys/broker.py000066400000000000000000000233241504664204300215500ustar00rootroot00000000000000import asyncio from collections import deque # pylint: disable=C0412 from dataclasses import dataclass from typing import Any, SupportsIndex, SupportsInt, TypeAlias # pylint: disable=C0412 import psutil from amqtt.plugins.base import BasePlugin from amqtt.session import Session try: from collections.abc import Buffer except ImportError: from typing import Protocol, runtime_checkable @runtime_checkable class Buffer(Protocol): # type: ignore[no-redef] def __buffer__(self, flags: int = ...) -> memoryview: """Mimic the behavior of `collections.abc.Buffer` for python 3.10-3.12.""" try: from datetime import UTC, datetime except ImportError: from datetime import datetime, timezone UTC = timezone.utc import amqtt from amqtt.broker import BrokerContext from amqtt.codecs_amqtt import float_to_bytes_str, int_to_bytes_str from amqtt.mqtt.packet import PUBLISH, MQTTFixedHeader, MQTTPacket, MQTTPayload, MQTTVariableHeader DOLLAR_SYS_ROOT = "$SYS/broker/" STAT_BYTES_SENT = "bytes_sent" STAT_BYTES_RECEIVED = "bytes_received" STAT_MSG_SENT = "messages_sent" STAT_MSG_RECEIVED = "messages_received" STAT_PUBLISH_SENT = "publish_sent" STAT_PUBLISH_RECEIVED = "publish_received" STAT_START_TIME = "start_time" STAT_CLIENTS_MAXIMUM = "clients_maximum" STAT_CLIENTS_CONNECTED = "clients_connected" STAT_CLIENTS_DISCONNECTED = "clients_disconnected" MEMORY_USAGE_MAXIMUM = "memory_maximum" CPU_USAGE_MAXIMUM = "cpu_usage_maximum" CPU_USAGE_LAST = "cpu_usage_last" PACKET: TypeAlias = MQTTPacket[MQTTVariableHeader, MQTTPayload[MQTTVariableHeader], MQTTFixedHeader] def val_to_bytes_str(value: Any) -> bytes: """Convert an int, float or string to byte string.""" match value: case int(): return int_to_bytes_str(value) case float(): return float_to_bytes_str(value) case str(): return value.encode("utf-8") case _: msg = f"Unsupported type {type(value)}" raise NotImplementedError(msg) class BrokerSysPlugin(BasePlugin[BrokerContext]): def __init__(self, context: BrokerContext) -> None: super().__init__(context) # Broker statistics initialization self._stats: dict[str, int] = {} self._sys_handle: asyncio.Handle | None = None self._sys_interval: int = 0 self._current_process = psutil.Process() def _clear_stats(self) -> None: """Initialize broker statistics data structures.""" for stat in ( STAT_BYTES_RECEIVED, STAT_BYTES_SENT, STAT_MSG_RECEIVED, STAT_MSG_SENT, STAT_CLIENTS_MAXIMUM, STAT_CLIENTS_CONNECTED, STAT_CLIENTS_DISCONNECTED, STAT_PUBLISH_RECEIVED, STAT_PUBLISH_SENT, MEMORY_USAGE_MAXIMUM, CPU_USAGE_MAXIMUM ): self._stats[stat] = 0 async def _broadcast_sys_topic(self, topic_basename: str, data: bytes) -> None: """Broadcast a system topic.""" await self.context.broadcast_message(topic_basename, data) def schedule_broadcast_sys_topic(self, topic_basename: str, data: bytes) -> asyncio.Task[None]: """Schedule broadcasting of system topics.""" return asyncio.ensure_future( self._broadcast_sys_topic(DOLLAR_SYS_ROOT + topic_basename, data), loop=self.context.loop, ) async def on_broker_pre_start(self) -> None: """Clear statistics before broker start.""" self._clear_stats() async def on_broker_post_start(self) -> None: """Initialize statistics and start $SYS broadcasting.""" self._stats[STAT_START_TIME] = int(datetime.now(tz=UTC).timestamp()) version = f"aMQTT version {amqtt.__version__}" await self.context.retain_message(DOLLAR_SYS_ROOT + "version", version.encode()) # Start $SYS topics management try: self._sys_interval = self._get_config_option("sys_interval", None) if isinstance(self._sys_interval, str | Buffer | SupportsInt | SupportsIndex): self._sys_interval = int(self._sys_interval) if self._sys_interval > 0: self.context.logger.debug(f"Setup $SYS broadcasting every {self._sys_interval} seconds") self._sys_handle = ( self.context.loop.call_later(self._sys_interval, self.broadcast_dollar_sys_topics) if self.context.loop is not None else None ) else: self.context.logger.debug("$SYS disabled") except KeyError: self.context.logger.debug("could not find 'sys_interval' key: {e!r}") # 'sys_interval' config parameter not found async def on_broker_pre_shutdown(self) -> None: """Stop $SYS topics broadcasting.""" if self._sys_handle: self._sys_handle.cancel() def broadcast_dollar_sys_topics(self) -> None: """Broadcast dynamic $SYS topics updates and reschedule next execution.""" # Update stats uptime = int(datetime.now(tz=UTC).timestamp()) - self._stats[STAT_START_TIME] client_connected = self._stats[STAT_CLIENTS_CONNECTED] client_disconnected = self._stats[STAT_CLIENTS_DISCONNECTED] inflight_in = 0 inflight_out = 0 messages_stored = 0 for session in self.context.sessions: inflight_in += session.inflight_in_count inflight_out += session.inflight_out_count messages_stored += session.retained_messages_count messages_stored += len(self.context.retained_messages) subscriptions_count = sum(len(sub) for sub in self.context.subscriptions.values()) self._stats[STAT_CLIENTS_MAXIMUM] = client_connected cpu_usage = self._current_process.cpu_percent(interval=0) self._stats[CPU_USAGE_MAXIMUM] = max(self._stats[CPU_USAGE_MAXIMUM], cpu_usage) mem_info_usage = self._current_process.memory_full_info() mem_size = mem_info_usage.rss / (1024 ** 2) self._stats[MEMORY_USAGE_MAXIMUM] = max(self._stats[MEMORY_USAGE_MAXIMUM], mem_size) # Broadcast updates tasks: deque[asyncio.Task[None]] = deque() stats: dict[str, int | str] = { "load/bytes/received": self._stats[STAT_BYTES_RECEIVED], "load/bytes/sent": self._stats[STAT_BYTES_SENT], "messages/received": self._stats[STAT_MSG_RECEIVED], "messages/sent": self._stats[STAT_MSG_SENT], "time": int(datetime.now(tz=UTC).timestamp()), "uptime": str(uptime), "uptime/formatted": str(datetime.fromtimestamp(self._stats[STAT_START_TIME], UTC)), "clients/connected": client_connected, "clients/disconnected": client_disconnected, "clients/maximum": self._stats[STAT_CLIENTS_MAXIMUM], "clients/total": client_connected + client_disconnected, "messages/inflight": inflight_in + inflight_out, "messages/inflight/in": inflight_in, "messages/inflight/out": inflight_out, "messages/inflight/stored": messages_stored, "messages/publish/received": self._stats[STAT_PUBLISH_RECEIVED], "messages/publish/sent": self._stats[STAT_PUBLISH_SENT], "messages/retained/count": len(self.context.retained_messages), "messages/subscriptions/count": subscriptions_count, "heap/size": mem_size, "heap/maximum": self._stats[MEMORY_USAGE_MAXIMUM], "cpu/percent": cpu_usage, "cpu/maximum": self._stats[CPU_USAGE_MAXIMUM], } for stat_name, stat_value in stats.items(): data: bytes = val_to_bytes_str(stat_value) tasks.append(self.schedule_broadcast_sys_topic(stat_name, data)) # Wait until broadcasting tasks end while tasks and tasks[0].done(): tasks.popleft() # Reschedule self.context.logger.debug(f"Broadcast $SYS topics again in {self._sys_interval} seconds.") self._sys_handle = ( self.context.loop.call_later(self._sys_interval, self.broadcast_dollar_sys_topics) if self.context.loop is not None else None ) async def on_mqtt_packet_received(self, *, packet: PACKET, session: Session | None = None) -> None: """Handle incoming MQTT packets.""" if packet: packet_size = packet.bytes_length self._stats[STAT_BYTES_RECEIVED] += packet_size self._stats[STAT_MSG_RECEIVED] += 1 if packet.fixed_header.packet_type == PUBLISH: self._stats[STAT_PUBLISH_RECEIVED] += 1 async def on_mqtt_packet_sent(self, *, packet: PACKET, session: Session | None = None) -> None: """Handle sent MQTT packets.""" if packet: packet_size = packet.bytes_length self._stats[STAT_BYTES_SENT] += packet_size self._stats[STAT_MSG_SENT] += 1 if packet.fixed_header.packet_type == PUBLISH: self._stats[STAT_PUBLISH_SENT] += 1 async def on_broker_client_connected(self, client_id: str, client_session: Session) -> None: """Handle broker client connection.""" self._stats[STAT_CLIENTS_CONNECTED] += 1 self._stats[STAT_CLIENTS_MAXIMUM] = max( self._stats[STAT_CLIENTS_MAXIMUM], self._stats[STAT_CLIENTS_CONNECTED], ) async def on_broker_client_disconnected(self, client_id: str, client_session: Session) -> None: """Handle broker client disconnection.""" self._stats[STAT_CLIENTS_CONNECTED] -= 1 self._stats[STAT_CLIENTS_DISCONNECTED] += 1 @dataclass class Config: """Configuration struct for plugin.""" sys_interval: int = 20 Yakifo-amqtt-2637127/amqtt/plugins/topic_checking.py000066400000000000000000000073661504664204300224270ustar00rootroot00000000000000from dataclasses import dataclass, field from typing import Any import warnings from amqtt.contexts import Action, BaseContext from amqtt.errors import PluginInitError from amqtt.plugins.base import BaseTopicPlugin from amqtt.session import Session class TopicTabooPlugin(BaseTopicPlugin): def __init__(self, context: BaseContext) -> None: super().__init__(context) self._taboo: list[str] = ["prohibited", "top-secret", "data/classified"] async def topic_filtering( self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None ) -> bool | None: filter_result = await super().topic_filtering(session=session, topic=topic, action=action) if filter_result: if session and session.username == "admin": return True return not (topic and topic in self._taboo) return bool(filter_result) class TopicAccessControlListPlugin(BaseTopicPlugin): def __init__(self, context: BaseContext) -> None: super().__init__(context) if self._get_config_option("acl", None): warnings.warn("The 'acl' option is deprecated, please use 'subscribe-acl' instead.", stacklevel=1) if self._get_config_option("acl", None) and self._get_config_option("subscribe-acl", None): msg = "'acl' has been replaced with 'subscribe-acl'; only one may be included" raise PluginInitError(msg) @staticmethod def topic_ac(topic_requested: str, topic_allowed: str) -> bool: req_split = topic_requested.split("/") allowed_split = topic_allowed.split("/") ret = True for i in range(max(len(req_split), len(allowed_split))): try: a_aux = req_split[i] b_aux = allowed_split[i] except IndexError: ret = False break if b_aux == "#": break if b_aux in ("+", a_aux): continue ret = False break return ret async def topic_filtering( self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None ) -> bool | None: filter_result = await super().topic_filtering(session=session, topic=topic, action=action) if not filter_result: return False # hbmqtt and older amqtt do not support publish filtering if action == Action.PUBLISH and not self._get_config_option("publish-acl", {}): # maintain backward compatibility, assume permitted return True req_topic = topic if not req_topic: return False username = session.username if session else None if username is None: username = "anonymous" acl: dict[str, Any] | None = None match action: case Action.PUBLISH: acl = self._get_config_option("publish-acl", None) case Action.SUBSCRIBE: acl = self._get_config_option("subscribe-acl", self._get_config_option("acl", None)) case Action.RECEIVE: acl = self._get_config_option("receive-acl", None) case _: msg = "Received an invalid action type." raise ValueError(msg) if acl is None: return True allowed_topics = acl.get(username, []) if not allowed_topics: return False return any(self.topic_ac(req_topic, allowed_topic) for allowed_topic in allowed_topics) @dataclass class Config: """Mappings of username and list of approved topics.""" publish_acl: dict[str, list[str]] = field(default_factory=dict) acl: dict[str, list[str]] = field(default_factory=dict) Yakifo-amqtt-2637127/amqtt/scripts/000077500000000000000000000000001504664204300170765ustar00rootroot00000000000000Yakifo-amqtt-2637127/amqtt/scripts/__init__.py000066400000000000000000000000141504664204300212020ustar00rootroot00000000000000"""INIT.""" Yakifo-amqtt-2637127/amqtt/scripts/broker_script.py000066400000000000000000000046031504664204300223230ustar00rootroot00000000000000import asyncio import logging from pathlib import Path import typer from yaml.parser import ParserError from amqtt import __version__ as amqtt_version from amqtt.broker import Broker from amqtt.errors import BrokerError, PluginError from amqtt.utils import read_yaml_config logger = logging.getLogger(__name__) app = typer.Typer(add_completion=False, rich_markup_mode=None) def main() -> None: """Run the MQTT broker.""" app() def _version(v: bool) -> None: if v: typer.echo(f"{amqtt_version}") raise typer.Exit(code=0) @app.command() def broker_main( config_file: str | None = typer.Option(None, "-c", help="broker configuration file"), debug: bool = typer.Option(False, "-d", help="Enable debug messages"), version: bool = typer.Option( # noqa : ARG001 False, "--version", callback=_version, is_eager=True, help="Show version and exit", ), ) -> None: """Command-line script for running a MQTT 3.1.1 broker.""" formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" if debug: formatter = "[%(asctime)s] %(name)s:%(lineno)d :: %(levelname)s - %(message)s" level = logging.DEBUG if debug else logging.INFO logging.basicConfig(level=level, format=formatter) logging.getLogger("transitions").setLevel(logging.WARNING) try: if config_file: config = read_yaml_config(config_file) else: config = read_yaml_config(Path(__file__).parent / "default_broker.yaml") logger.debug("Using default configuration") except FileNotFoundError as exc: typer.echo(f"❌ Config file error: {exc}", err=True) raise typer.Exit(code=1) from exc loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: broker = Broker(config, loop=loop) except (BrokerError, ParserError, PluginError) as exc: typer.echo(f"❌ Broker failed to start: {exc}", err=True) raise typer.Exit(code=1) from exc _ = loop.create_task(broker.start()) # noqa : RUF006 try: loop.run_forever() except KeyboardInterrupt: loop.run_until_complete(broker.shutdown()) except Exception as exc: typer.echo("❌ Broker execution halted", err=True) raise typer.Exit(code=1) from exc finally: loop.close() if __name__ == "__main__": main() Yakifo-amqtt-2637127/amqtt/scripts/ca_creds.py000066400000000000000000000031401504664204300212110ustar00rootroot00000000000000import logging from pathlib import Path import sys import typer logger = logging.getLogger(__name__) app = typer.Typer(add_completion=False, rich_markup_mode=None) def main() -> None: """Run the cli for `ca_creds`.""" app() @app.command() def ca_creds( country: str = typer.Option(..., "--country", help="x509 'country_name' attribute"), state: str = typer.Option(..., "--state", help="x509 'state_or_province_name' attribute"), locality: str = typer.Option(..., "--locality", help="x509 'locality_name' attribute"), org_name: str = typer.Option(..., "--org-name", help="x509 'organization_name' attribute"), cn: str = typer.Option(..., "--cn", help="x509 'common_name' attribute"), output_dir: str = typer.Option(Path.cwd().absolute(), "--output-dir", help="output directory"), ) -> None: """Generate a self-signed key and certificate to be used as the root CA, with a key size of 2048 and a 1-year expiration.""" formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) try: from amqtt.contrib.cert import generate_root_creds, write_key_and_crt # pylint: disable=import-outside-toplevel except ImportError: msg = "Requires installation of the optional 'contrib' package: `pip install amqtt[contrib]`" logger.critical(msg) sys.exit(1) ca_key, ca_crt = generate_root_creds(country=country, state=state, locality=locality, org_name=org_name, cn=cn) write_key_and_crt(ca_key, ca_crt, "ca", Path(output_dir)) if __name__ == "__main__": main() Yakifo-amqtt-2637127/amqtt/scripts/default_broker.yaml000066400000000000000000000004711504664204300227540ustar00rootroot00000000000000--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: amqtt.plugins.logging_amqtt.EventLoggerPlugin: amqtt.plugins.logging_amqtt.PacketLoggerPlugin: amqtt.plugins.authentication.AnonymousAuthPlugin: allow_anonymous: true amqtt.plugins.sys.broker.BrokerSysPlugin: sys_interval: 20Yakifo-amqtt-2637127/amqtt/scripts/default_client.yaml000066400000000000000000000003771504664204300227530ustar00rootroot00000000000000--- keep_alive: 10 ping_delay: 1 default_qos: 0 default_retain: false auto_reconnect: true cleansession: true reconnect_max_interval: 10 reconnect_retries: 2 connection: uri: "mqtt://127.0.0.1" plugins: amqtt.plugins.logging_amqtt.PacketLoggerPlugin: Yakifo-amqtt-2637127/amqtt/scripts/device_creds.py000066400000000000000000000043131504664204300220700ustar00rootroot00000000000000import logging from pathlib import Path import sys import typer logger = logging.getLogger(__name__) app = typer.Typer(add_completion=False, rich_markup_mode=None) def main() -> None: """Run the `device_creds` cli.""" app() @app.command() def device_creds( # pylint: disable=too-many-locals country: str = typer.Option(..., "--country", help="x509 'country_name' attribute"), org_name: str = typer.Option(..., "--org-name", help="x509 'organization_name' attribute"), device_id: str = typer.Option(..., "--device-id", help="device id for the SAN"), uri: str = typer.Option(..., "--uri", help="domain name for device SAN"), output_dir: str = typer.Option(Path.cwd().absolute(), "--output-dir", help="output directory"), ca_key_fn: str = typer.Option("ca.key", "--ca-key", help="root key filename used for signing."), ca_crt_fn: str = typer.Option("ca.crt", "--ca-crt", help="root cert filename used for signing."), ) -> None: """Generate a key and certificate for each device in pem format, signed by the provided CA credentials. With a key size of 2048 and a 1-year expiration.""" # noqa: E501 formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) try: from amqtt.contrib.cert import ( # pylint: disable=import-outside-toplevel generate_device_csr, load_ca, sign_csr, write_key_and_crt, ) except ImportError: msg = "Requires installation of the optional 'contrib' package: `pip install amqtt[contrib]`" logger.critical(msg) sys.exit(1) ca_key, ca_crt = load_ca(ca_key_fn, ca_crt_fn) uri_san = f"spiffe://{uri}/device/{device_id}" dns_san = f"{device_id}.local" device_key, device_csr = generate_device_csr( country=country, org_name=org_name, common_name=device_id, uri_san=uri_san, dns_san=dns_san ) device_crt = sign_csr(device_csr, ca_key, ca_crt) write_key_and_crt(device_key, device_crt, device_id, Path(output_dir)) logger.info(f"βœ… Created: {device_id}.crt and {device_id}.key") if __name__ == "__main__": main() Yakifo-amqtt-2637127/amqtt/scripts/manage_topics.py000066400000000000000000000021111504664204300222540ustar00rootroot00000000000000import logging import sys from amqtt.errors import MQTTError logger = logging.getLogger(__name__) def main() -> None: """Run the auth db cli.""" try: from amqtt.contrib.auth_db.topic_mgr_cli import topic_app # pylint: disable=import-outside-toplevel except ImportError: logger.critical("optional 'contrib' library is missing, please install: `pip install amqtt[contrib]`") sys.exit(1) from amqtt.contrib.auth_db.topic_mgr_cli import topic_app # pylint: disable=import-outside-toplevel try: topic_app() except ModuleNotFoundError as mnfe: logger.critical(f"Please install database-specific dependencies: {mnfe}") sys.exit(1) except ValueError as ve: if "greenlet" in f"{ve}": logger.critical("Please install database-specific dependencies: 'greenlet'") sys.exit(1) logger.critical(f"Unknown error: {ve}") sys.exit(1) except MQTTError as me: logger.critical(f"could not execute command: {me}") sys.exit(1) if __name__ == "__main__": main() Yakifo-amqtt-2637127/amqtt/scripts/manage_users.py000066400000000000000000000021031504664204300221150ustar00rootroot00000000000000import logging import sys from amqtt.errors import MQTTError logger = logging.getLogger(__name__) def main() -> None: """Run the auth db cli.""" try: from amqtt.contrib.auth_db.user_mgr_cli import user_app # pylint: disable=import-outside-toplevel except ImportError: logger.critical("optional 'contrib' library is missing, please install: `pip install amqtt[contrib]`") sys.exit(1) from amqtt.contrib.auth_db.user_mgr_cli import user_app # pylint: disable=import-outside-toplevel try: user_app() except ModuleNotFoundError as mnfe: logger.critical(f"Please install database-specific dependencies: {mnfe}") sys.exit(1) except ValueError as ve: if "greenlet" in f"{ve}": logger.critical("Please install database-specific dependencies: 'greenlet'") sys.exit(1) logger.critical(f"Unknown error: {ve}") sys.exit(1) except MQTTError as me: logger.critical(f"could not execute command: {me}") sys.exit(1) if __name__ == "__main__": main() Yakifo-amqtt-2637127/amqtt/scripts/pub_script.py000066400000000000000000000230141504664204300216220ustar00rootroot00000000000000import asyncio from collections.abc import Generator import contextlib from dataclasses import dataclass import json import logging import os from pathlib import Path import socket import sys from typing import Any import typer from amqtt import __version__ as amqtt_version from amqtt.client import MQTTClient from amqtt.errors import ClientError, ConnectError from amqtt.utils import read_yaml_config logger = logging.getLogger(__name__) def _gen_client_id() -> str: pid = os.getpid() hostname = socket.gethostname() return f"amqtt_pub/{pid}-{hostname}" def _get_extra_headers(extra_headers_json: str | None = None) -> dict[str, Any]: try: extra_headers: dict[str, Any] = json.loads(extra_headers_json or "{}") except (json.JSONDecodeError, TypeError): return {} return extra_headers @dataclass class MessageInput: message_str: str | None = None file: str | None = None stdin: bool | None = False lines: bool | None = False no_message: bool | None = False def get_message(self) -> Generator[bytes | bytearray]: if self.no_message: yield b"" if self.message_str: yield self.message_str.encode(encoding="utf-8") if self.file: try: with Path(self.file).open(encoding="utf-8") as f: for line in f: yield line.encode(encoding="utf-8") except (FileNotFoundError, OSError): logger.exception(f"Failed to read file '{self.file}'") if self.lines: for line in sys.stdin: if line: yield line.encode(encoding="utf-8") if self.stdin: messages = bytearray() for line in sys.stdin: messages.extend(line.encode(encoding="utf-8")) yield messages @dataclass class CAInfo: ca_file: str | None = None ca_path: str | None = None ca_data: str | None = None async def do_pub( client: MQTTClient, topic: str, message_input: MessageInput, ca_info: CAInfo, url: str | None = None, clean_session: bool = False, retain: bool = False, extra_headers_json: str | None = None, qos: int | None = None, ) -> None: """Publish the message.""" running_tasks = [] try: logger.info(f"{client.client_id} Connecting to broker") await client.connect( uri=url, cleansession=clean_session, cafile=ca_info.ca_file, capath=ca_info.ca_path, cadata=ca_info.ca_data, additional_headers=_get_extra_headers(extra_headers_json), ) for message in message_input.get_message(): logger.info(f"{client.client_id} Publishing to '{topic}'") task = asyncio.ensure_future(client.publish(topic, message, qos, retain)) running_tasks.append(task) if running_tasks: await asyncio.wait(running_tasks) await client.disconnect() logger.info(f"{client.client_id} Disconnected from broker") except KeyboardInterrupt: await client.disconnect() logger.info(f"{client.client_id} Disconnected from broker") except ConnectError as ce: logger.fatal(f"Connection to '{client.session.broker_uri if client.session else url}' failed") raise ConnectError from ce except asyncio.CancelledError as ce: logger.fatal("Publish canceled due to previous error") raise asyncio.CancelledError from ce app = typer.Typer(add_completion=False, rich_markup_mode=None) def main() -> None: """Entry point for the amqtt publisher.""" app() def _version(v: bool) -> None: if v: typer.echo(f"{amqtt_version}") raise typer.Exit(code=0) @app.command() def publisher_main( # pylint: disable=R0914,R0917 url: str | None = typer.Option(None, "--url", help="Broker connection URL, *must conform to MQTT or URI scheme: `[mqtt(s)|ws(s)]://@HOST:port`*"), config_file: str | None = typer.Option(None, "-c", "--config-file", help="Client configuration file"), client_id: str | None = typer.Option(None, "-i", "--client-id", help="client identification for mqtt connection. *default: process id and the hostname of the client*"), qos: int = typer.Option(0, "--qos", "-q", help="Quality of service (0, 1, or 2)"), retain: bool = typer.Option(False, "-r", help="Set retain flag on connect"), topic: str = typer.Option(..., "-t", "--topic", help="Message topic"), message: str | None = typer.Option(None, "-m", "--message", help="Message data to send"), file: str | None = typer.Option(None, "-f", "--file", help="Path to file, will publish each line as a separate message."), stdin: bool = typer.Option(False, "-s", "--stdin", help="Read from standard input, all content read is sent as a single message."), lines: bool = typer.Option(False, "-l", "--lines", help="Read from stdin, will publish message for each line."), no_message: bool = typer.Option(False, "-n", "--no-message", help="Publish an empty (null, zero length) message"), keep_alive: int | None = typer.Option(None, "-k", help="Keep alive timeout, in seconds."), clean_session: bool = typer.Option(False, "--clean-session", help="Clean session on connect. *default: False*"), ca_file: str | None = typer.Option(None, "--ca-file", help="Define the path to a file containing PEM encoded CA certificates that are trusted. Used to enable SSL communication."), ca_path: str | None = typer.Option(None, "--ca-path", help="Define the path to a directory containing PEM encoded CA certificates that are trusted. Used to enable SSL communication."), ca_data: str | None = typer.Option(None, "--ca-data", help="Set the PEM encoded CA certificates that are trusted. Used to enable SSL communication."), will_topic: str | None = typer.Option(None, "--will-topic", help="The topic on which to send a Will, in the event that the client disconnects unexpectedly."), will_message: str | None = typer.Option(None, "--will-message", help="Specify a message that will be stored by the broker and sent out if this client disconnects unexpectedly. *required if `--will-topic` is specified*."), will_qos: int = typer.Option(0, "--will-qos", help="The QoS to use for the Will. *default: 0, only valid if `--will-topic` is specified*"), will_retain: bool = typer.Option(False, "--will-retain", help="If the client disconnects unexpectedly the message sent out will be treated as a retained message. *only valid, if `--will-topic` is specified*"), extra_headers_json: str | None = typer.Option(None, "--extra-headers", help="Specify a JSON object string with key-value pairs representing additional headers that are transmitted on the initial connection. *websocket connections only*."), debug: bool = typer.Option(False, "-d", help="Enable debug messages"), version: bool = typer.Option(False, "--version", callback=_version, is_eager=True, help="Show version and exit"), # noqa : ARG001 ) -> None: """Command-line MQTT client for publishing simple messages.""" provided = [bool(message), bool(file), stdin, lines, no_message] if sum(provided) != 1: typer.echo("❌ You must provide exactly one of --message, --file, --stdin, --lines or --no-message", err=True) raise typer.Exit(code=1) if bool(will_message) != bool(will_topic): typer.echo("❌ must specify both 'will_message' and 'will_topic' ") raise typer.Exit(code=1) if will_retain and not (will_message and will_topic): typer.echo("❌ 'will-retain' only valid if 'will_message' and 'will_topic' are specified.", err=True) raise typer.Exit(code=1) formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" level = logging.DEBUG if debug else logging.INFO logging.basicConfig(level=level, format=formatter) if config_file: config = read_yaml_config(config_file) else: default_config_path = Path(__file__).parent / "default_client.yaml" logger.debug(f"Using default configuration from {default_config_path}") config = read_yaml_config(default_config_path) if not client_id: client_id = _gen_client_id() if not isinstance(config, dict): logger.debug("Failed to correctly initialize config") return if keep_alive: config["keep_alive"] = int(keep_alive) if will_topic and will_message: config["will"] = { "topic": will_topic, "message": will_message, "qos": int(will_qos), "retain": will_retain, } client = MQTTClient(client_id=client_id, config=config) message_input = MessageInput( message_str=message, file=file, stdin=stdin, no_message=no_message, lines=lines, ) ca_info = CAInfo( ca_file=ca_file, ca_path=ca_path, ca_data=ca_data, ) with contextlib.suppress(KeyboardInterrupt): try: asyncio.run( do_pub( client=client, message_input=message_input, url=url, topic=topic, retain=retain, clean_session=clean_session, ca_info=ca_info, extra_headers_json=extra_headers_json, qos=qos, ) ) except (ClientError, ConnectError) as exc: typer.echo("❌ Connection failed", err=True) raise typer.Exit(code=1) from exc if __name__ == "__main__": typer.run(main) Yakifo-amqtt-2637127/amqtt/scripts/server_creds.py000066400000000000000000000034771504664204300221510ustar00rootroot00000000000000import logging from pathlib import Path import sys import typer logger = logging.getLogger(__name__) app = typer.Typer(add_completion=False, rich_markup_mode=None) def main() -> None: """Run the `server_creds` cli.""" app() @app.command() def server_creds( country: str = typer.Option(..., "--country", help="x509 'country_name' attribute"), org_name: str = typer.Option(..., "--org-name", help="x509 'organization_name' attribute"), cn: str = typer.Option(..., "--cn", help="x509 'common_name' attribute"), output_dir: str = typer.Option(Path.cwd().absolute(), "--output-dir", help="output directory"), ca_key_fn: str = typer.Option("ca.key", "--ca-key", help="server key output filename."), ca_crt_fn: str = typer.Option("ca.crt", "--ca-crt", help="server cert output filename."), ) -> None: """Generate a key and certificate for the broker in pem format, signed by the provided CA credentials. With a key size of 2048 and a 1-year expiration.""" # noqa : E501 formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) try: from amqtt.contrib.cert import ( # pylint: disable=import-outside-toplevel generate_server_csr, load_ca, sign_csr, write_key_and_crt, ) except ImportError: msg = "Requires installation of the optional 'contrib' package: `pip install amqtt[contrib]`" logger.critical(msg) sys.exit(1) ca_key, ca_crt = load_ca(ca_key_fn, ca_crt_fn) server_key, server_csr = generate_server_csr(country=country, org_name=org_name, cn=cn) server_crt = sign_csr(server_csr, ca_key, ca_crt) write_key_and_crt(server_key, server_crt, "server", Path(output_dir)) if __name__ == "__main__": main() Yakifo-amqtt-2637127/amqtt/scripts/sub_script.py000066400000000000000000000200211504664204300216200ustar00rootroot00000000000000import asyncio import contextlib from dataclasses import dataclass import json import logging import os from pathlib import Path import socket import sys from typing import Any import typer from amqtt import __version__ as amqtt_version from amqtt.client import MQTTClient from amqtt.errors import ClientError, ConnectError, MQTTError from amqtt.mqtt.constants import QOS_0 from amqtt.utils import read_yaml_config logger = logging.getLogger(__name__) def _gen_client_id() -> str: pid = os.getpid() hostname = socket.gethostname() return f"amqtt_sub/{pid}-{hostname}" def _get_extra_headers(extra_headers_json: str | None = None) -> dict[str, Any]: try: extra_headers: dict[str, Any] = json.loads(extra_headers_json or "{}") except (json.JSONDecodeError, TypeError): return {} return extra_headers @dataclass class CAInfo: ca_file: str | None = None ca_path: str | None = None ca_data: str | None = None async def do_sub(client: MQTTClient, url: str, topics: list[str], ca_info: CAInfo, max_count: int | None = None, clean_session: bool = False, extra_headers_json: str | None = None, qos: int | None = None, ) -> None: """Perform the subscription.""" try: logger.info(f"{client.client_id} Connecting to broker") await client.connect( uri=url, cleansession=clean_session, cafile=ca_info.ca_file, capath=ca_info.ca_path, cadata=ca_info.ca_data, additional_headers=_get_extra_headers(extra_headers_json), ) filters = [(topic, qos) for topic in topics] await client.subscribe(filters) count = 0 while True: if max_count and count >= max_count: break try: message = await client.deliver_message() if message and message.publish_packet and message.publish_packet.data: count += 1 sys.stdout.buffer.write(message.publish_packet.data) sys.stdout.write("\n") except MQTTError: logger.debug("Error reading packet") await client.disconnect() logger.info(f"{client.client_id} Disconnected from broker") except KeyboardInterrupt: await client.disconnect() logger.info(f"{client.client_id} Disconnected from broker") except ConnectError as exc: logger.fatal(f"Connection to '{url}' failed: {exc!r}") raise ConnectError from exc except asyncio.CancelledError as exc: logger.fatal("Publish canceled due to previous error") raise asyncio.CancelledError from exc app = typer.Typer(add_completion=False, rich_markup_mode=None) def main() -> None: """Entry point for the amqtt subscriber.""" app() def _version(v: bool) -> None: if v: typer.echo(f"{amqtt_version}") raise typer.Exit(code=0) @app.command() def subscribe_main( # pylint: disable=R0914,R0917 url: str = typer.Option(None, help="Broker connection URL, *must conform to MQTT or URI scheme: `[mqtt(s)|ws(s)]://@HOST:port`*", show_default=False), config_file: str | None = typer.Option(None, "-c", help="Client configuration file"), client_id: str | None = typer.Option(None, "-i", "--client-id", help="client identification for mqtt connection. *default: process id and the hostname of the client*"), max_count: int | None = typer.Option(None, "-n", help="Number of messages to read before ending *default: read indefinitely*"), qos: int = typer.Option(0, "--qos", "-q", help="Quality of service (0, 1, or 2)"), topics: list[str] = typer.Option(..., "-t", help="Topic filter to subscribe, can be used multiple times."), # noqa: B008 keep_alive: int | None = typer.Option(None, "-k", help="Keep alive timeout in seconds"), clean_session: bool = typer.Option(False, "--clean-session", help="Clean session on connect. *default: False*"), ca_file: str | None = typer.Option(None, "--ca-file", help="Define the path to a file containing PEM encoded CA certificates that are trusted. Used to enable SSL communication."), ca_path: str | None = typer.Option(None, "--ca-path", help="Define the path to a directory containing PEM encoded CA certificates that are trusted. Used to enable SSL communication."), ca_data: str | None = typer.Option(None, "--ca-data", help="Set the PEM encoded CA certificates that are trusted. Used to enable SSL communication."), will_topic: str | None = typer.Option(None, "--will-topic", help="The topic on which to send a Will, in the event that the client disconnects unexpectedly."), will_message: str | None = typer.Option(None, "--will-message", help="Specify a message that will be stored by the broker and sent out if this client disconnects unexpectedly. *required if `--will-topic` is specified*."), will_qos: int = typer.Option(0, "--will-qos", help="The QoS to use for the Will. *default: 0, only valid if `--will-topic` is specified*"), will_retain: bool = typer.Option(False, "--will-retain", help="If the client disconnects unexpectedly the message sent out will be treated as a retained message. *only valid, if `--will-topic` is specified*"), extra_headers_json: str | None = typer.Option(None, "--extra-headers", help="Specify a JSON object string with key-value pairs representing additional headers that are transmitted on the initial connection. *websocket connections only*."), debug: bool = typer.Option(False, "-d", help="Enable debug messages"), version: bool = typer.Option(False, "--version", callback=_version, is_eager=True, help="Show version and exit"), # noqa : ARG001 ) -> None: """Command line MQTT client to subscribe to one or more topics and display any messages received.""" if bool(will_message) != bool(will_topic): typer.echo("❌ must specify both 'will_message' and 'will_topic' ") raise typer.Exit(code=1) if will_retain and not (will_message and will_topic): typer.echo("❌ 'will-retain' only valid if 'will_message' and 'will_topic' are specified.", err=True) raise typer.Exit(code=1) formatter = "[%(asctime)s] :: %(levelname)s - %(message)s" level = logging.DEBUG if debug else logging.INFO logging.basicConfig(level=level, format=formatter) if config_file: config = read_yaml_config(config_file) else: default_config_path = Path(__file__).parent / "default_client.yaml" logger.debug(f"Using default configuration from {default_config_path}") config = read_yaml_config(default_config_path) if not client_id: client_id = _gen_client_id() if not isinstance(config, dict): logger.debug("Failed to correctly initialize config") return if keep_alive: config["keep_alive"] = keep_alive if will_topic and will_message: config["will"] = { "topic": will_topic, "message": will_message, "qos": int(will_qos), "retain": will_retain, } client = MQTTClient(client_id=client_id, config=config) ca_info = CAInfo( ca_file=ca_file, ca_path=ca_path, ca_data=ca_data, ) with contextlib.suppress(KeyboardInterrupt): try: asyncio.run(do_sub(client, url=url, topics=topics, ca_info=ca_info, extra_headers_json=extra_headers_json, qos=qos or QOS_0, max_count=max_count, clean_session=clean_session, )) except (ClientError, ConnectError) as exc: typer.echo("❌ Connection failed", err=True) raise typer.Exit(code=1) from exc if __name__ == "__main__": main() Yakifo-amqtt-2637127/amqtt/session.py000066400000000000000000000253661504664204300174600ustar00rootroot00000000000000from asyncio import Queue from collections import OrderedDict import logging from math import floor import time from typing import TYPE_CHECKING, Any, ClassVar from transitions import Machine from amqtt.errors import AMQTTError from amqtt.mqtt.publish import PublishPacket OUTGOING = 0 INCOMING = 1 if TYPE_CHECKING: import ssl logger = logging.getLogger(__name__) class ApplicationMessage: """ApplicationMessage and subclasses are used to store published message information flow. These objects can contain different information depending on the way they were created (incoming or outgoing) and the quality of service used between peers. """ __slots__ = ( "data", "packet_id", "puback_packet", "pubcomp_packet", "publish_packet", "pubrec_packet", "pubrel_packet", "qos", "retain", "topic", ) def __init__(self, packet_id: int | None, topic: str, qos: int | None, data: bytes | bytearray, retain: bool) -> None: self.packet_id: int | None = packet_id """ Publish message packet identifier _ """ self.topic: str = topic """ Publish message topic""" self.qos: int | None = qos """ Publish message Quality of Service""" self.data: bytes | bytearray = data """ Publish message payload data""" self.retain: bool = retain """ Publish message retain flag""" self.publish_packet: PublishPacket | None = None """ :class:`amqtt.mqtt.publish.PublishPacket` instance corresponding to the `PUBLISH `_ packet in the messages flow. ``None`` if the PUBLISH packet has not already been received or sent.""" self.puback_packet: Any | None = None """ :class:`amqtt.mqtt.puback.PubackPacket` instance corresponding to the `PUBACK `_ packet in the messages flow. ``None`` if QoS != QOS_1 or if the PUBACK packet has not already been received or sent.""" self.pubrec_packet: Any | None = None """ :class:`amqtt.mqtt.puback.PubrecPacket` instance corresponding to the `PUBREC `_ packet in the messages flow. ``None`` if QoS != QOS_2 or if the PUBREC packet has not already been received or sent.""" self.pubrel_packet: Any | None = None """ :class:`amqtt.mqtt.puback.PubrelPacket` instance corresponding to the `PUBREL `_ packet in the messages flow. ``None`` if QoS != QOS_2 or if the PUBREL packet has not already been received or sent.""" self.pubcomp_packet: Any | None = None """ :class:`amqtt.mqtt.puback.PubrelPacket` instance corresponding to the `PUBCOMP `_ packet in the messages flow. ``None`` if QoS != QOS_2 or if the PUBCOMP packet has not already been received or sent.""" def build_publish_packet(self, dup: bool = False) -> PublishPacket: """Build :class:`amqtt.mqtt.publish.PublishPacket` from attributes. :param dup: force dup flag :return: :class:`amqtt.mqtt.publish.PublishPacket` built from ApplicationMessage instance attributes """ return PublishPacket.build(self.topic, bytes(self.data), self.packet_id, dup, self.qos, self.retain) def __eq__(self, other: object) -> bool: """Compare two ApplicationMessage instances based on their packet_id. This method is used to check if two messages are the same based on their packet_id. :param other: The other ApplicationMessage instance to compare with. :return: True if the packet_id of both messages are equal, False otherwise. """ if not isinstance(other, ApplicationMessage): return False return self.packet_id == other.packet_id class IncomingApplicationMessage(ApplicationMessage): """Incoming :class:~amqtt.session.ApplicationMessage.""" __slots__ = ("direction",) def __init__(self, packet_id: int | None, topic: str, qos: int | None, data: bytes, retain: bool) -> None: super().__init__(packet_id, topic, qos, data, retain) self.direction: int = INCOMING class OutgoingApplicationMessage(ApplicationMessage): """Outgoing :class:~amqtt.session.ApplicationMessage.""" __slots__ = ("direction",) def __init__(self, packet_id: int | None, topic: str, qos: int | None, data: bytes | bytearray, retain: bool) -> None: super().__init__(packet_id, topic, qos, data, retain) self.direction: int = OUTGOING class Session: states: ClassVar[list[str]] = ["new", "connected", "disconnected"] def __init__(self) -> None: self._init_states() self.remote_address: str | None = None self.remote_port: int | None = None self.client_id: str | None = None self.clean_session: bool | None = None self.will_flag: bool = False self.will_message: bytes | bytearray | None = None self.will_qos: int | None = None self.will_retain: bool | None = None self.will_topic: str | None = None self.keep_alive: int = 0 self.publish_retry_delay: int = 0 self.broker_uri: str | None = None self.username: str | None = None self.password: str | None = None self.cafile: str | None = None self.capath: str | None = None self.cadata: bytes | None = None self._packet_id: int = 0 self.parent: int = 0 self.last_connect_time: int | None = None self.ssl_object: ssl.SSLObject | None = None self.last_disconnect_time: int | None = None # Used to store outgoing ApplicationMessage while publish protocol flows self.inflight_out: OrderedDict[int, OutgoingApplicationMessage] = OrderedDict() # Used to store incoming ApplicationMessage while publish protocol flows self.inflight_in: OrderedDict[int, IncomingApplicationMessage] = OrderedDict() # Stores messages retained for this session (specifically when the client is disconnected) self.retained_messages: Queue[ApplicationMessage] = Queue() # Stores PUBLISH messages ID received in order and ready for application process self.delivered_message_queue: Queue[ApplicationMessage] = Queue() # identify anonymous client sessions or clients which didn't identify themselves self.is_anonymous: bool = False def _init_states(self) -> None: self.transitions = Machine(states=Session.states, initial="new") self.transitions.add_transition( trigger="connect", source="new", dest="connected", ) self.transitions.on_enter_connected(self._on_enter_connected) self.transitions.add_transition( trigger="connect", source="disconnected", dest="connected", ) self.transitions.add_transition( trigger="disconnect", source="connected", dest="disconnected", ) self.transitions.on_enter_disconnected(self._on_enter_disconnected) self.transitions.add_transition( trigger="disconnect", source="new", dest="disconnected", ) self.transitions.add_transition( trigger="disconnect", source="disconnected", dest="disconnected", ) def _on_enter_connected(self) -> None: cur_time = floor(time.time()) if self.last_disconnect_time is not None: logger.debug(f"Session reconnected after {cur_time - self.last_disconnect_time} seconds.") self.last_connect_time = cur_time self.last_disconnect_time = None def _on_enter_disconnected(self) -> None: cur_time = floor(time.time()) if self.last_connect_time is not None: logger.debug(f"Session disconnected after {cur_time - self.last_connect_time} seconds.") self.last_disconnect_time = cur_time @property def next_packet_id(self) -> int: self._packet_id = (self._packet_id % 65535) + 1 limit = self._packet_id while self._packet_id in self.inflight_in or self._packet_id in self.inflight_out: self._packet_id = (self._packet_id % 65535) + 1 if self._packet_id == limit: msg = "More than 65535 messages pending. No free packet ID" raise AMQTTError(msg) return self._packet_id @property def inflight_in_count(self) -> int: return len(self.inflight_in) @property def inflight_out_count(self) -> int: return len(self.inflight_out) @property def retained_messages_count(self) -> int: return self.retained_messages.qsize() def __repr__(self) -> str: """Return a string representation of the session. This method is used for debugging and logging purposes. It includes the client ID and the current state of the session. """ return type(self).__name__ + f"(clientId={self.client_id}, state={self.transitions.state})" def __getstate__(self) -> dict[str, Any]: """Return the state of the session for pickling. This method is called when pickling the session object. It returns a dictionary containing the session's state, excluding unpicklable entries. """ state = self.__dict__.copy() # Remove the unpicklable entries. del state["retained_messages"] del state["delivered_message_queue"] return state def __setstate__(self, state: dict[str, Any]) -> None: """Restore the session from its state. This method is called when unpickling the session object. It restores the session's state and reinitializes the queues. """ self.__dict__.update(state) self.retained_messages = Queue() self.delivered_message_queue = Queue() def clear_queues(self) -> None: """Clear all message queues associated with the session.""" while not self.retained_messages.empty(): self.retained_messages.get_nowait() while not self.delivered_message_queue.empty(): self.delivered_message_queue.get_nowait() def __eq__(self, other: object) -> bool: """Compare two Session instances based on their client_id.""" if not isinstance(other, Session): return False return self.client_id == other.client_id Yakifo-amqtt-2637127/amqtt/utils.py000066400000000000000000000051101504664204300171160ustar00rootroot00000000000000from __future__ import annotations from importlib import import_module import logging from pathlib import Path import secrets import string import sys import typing from typing import Any import yaml if typing.TYPE_CHECKING: from amqtt.session import Session logger = logging.getLogger(__name__) def format_client_message( session: Session | None = None, address: str | None = None, port: int | None = None, ) -> str: """Format a client message for logging.""" if session: return f"(client id={session.client_id})" if address is not None and port is not None: return f"(client @={address}:{port})" return "(unknown client)" def gen_client_id() -> str: """Generate a random client ID.""" gen_id = "amqtt/" # Use secrets to generate a secure random client ID # Defining a valid set of characters for client ID generation valid_chars = string.ascii_letters + string.digits gen_id += "".join(secrets.choice(valid_chars) for _ in range(16)) return gen_id def read_yaml_config(config_file: str | Path) -> dict[str, Any] | None: """Read a YAML configuration file.""" try: with Path(str(config_file)).open(encoding="utf-8") as stream: yaml_result: dict[str, Any] = yaml.full_load(stream) return yaml_result except yaml.YAMLError: logger.exception(f"Invalid config_file {config_file}") return None def cached_import(module_path: str, class_name: str | None = None) -> Any: """Return cached import of a class from a module path (or retrieve, cache and then return).""" # Check whether module is loaded and fully initialized. if not ((module := sys.modules.get(module_path)) and (spec := getattr(module, "__spec__", None)) and getattr(spec, "_initializing", False) is False): module = import_module(module_path) if class_name: return getattr(module, class_name) return module def import_string(dotted_path: str) -> Any: """Import a dotted module path. Returns: attribute/class designated by the last name in the path Raises: ImportError (if the import failed) """ try: module_path, class_name = dotted_path.rsplit(".", 1) except ValueError as err: msg = f"{dotted_path} doesn't look like a module path" raise ImportError(msg) from err try: return cached_import(module_path, class_name) except AttributeError as err: msg = f'Module "{module_path}" does not define a "{class_name}" attribute/class' raise ImportError(msg) from err Yakifo-amqtt-2637127/docs/000077500000000000000000000000001504664204300152115ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/_static/000077500000000000000000000000001504664204300166375ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/_static/theme_overrides.css000066400000000000000000000000511504664204300225310ustar00rootroot00000000000000.wy-nav-content { max-width: 1100px; } Yakifo-amqtt-2637127/docs/assets/000077500000000000000000000000001504664204300165135ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/assets/amqtt.svg000066400000000000000000000150651504664204300203710ustar00rootroot00000000000000 Yakifo-amqtt-2637127/docs/assets/amqtt_bw.svg000066400000000000000000000150261504664204300210560ustar00rootroot00000000000000 Yakifo-amqtt-2637127/docs/assets/extra.css000066400000000000000000000007761504664204300203620ustar00rootroot00000000000000/* https://coolors.co/e6c229-f17105-d11149-6610f2-1a8fe3 */ .md-header { background: #d11149; } nav.md-tabs { display: none; } h2.doc-heading-parameter { font-size: 16px; } .doc-md-description { font-size: 16px; display: block; } .md-nav__item--section>.md-nav__link[for], .md-nav--lifted>.md-nav__list>.md-nav__item>[for], .md-nav__title { font-size: 14px; color: #840b2d !important; } .md-nav__link--active { color: #f15581 !important; } .admonition { font-size: 16px !important; }Yakifo-amqtt-2637127/docs/assets/images/000077500000000000000000000000001504664204300177605ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/assets/images/favicon.png000066400000000000000000000403511504664204300221160ustar00rootroot00000000000000‰PNG  IHDRzŸΞύ•ΓPLTEψΪγψΧαωήζχΣέφΞΩυΚΦϋότΔσΐΟώφψσ½ΝςΉΚρΆΘρ³Επ­Αν ΆοͺΎόξςξ₯Ίν΅μ™²κ‘«λ–―ύςυώώ鎩ι‰₯θ†’η‚Ÿζ~œζy™ώωϊεu–δq“γnύύγjϊζμβfŠαb‡ΰ]ƒϊβιίY€ήS|έNxέKuΫFqΫAnϋκοΪΜΥuΙ”σoŸν‡|qCz΅ˆgΉ©₯΅­±ιι¬μηœιν5E·Χηΐ&aH―¦°%[[[Z›Ο_PχτΦ«W«=V˜Ή« μ‘–ΆζΦ¦i3γ8枞ΤοΎNˆ)Σ7ŽήΥ½eωܐW3KΩ1φq ιι·§΅§}΄ώ<όΘυΓιο‘ΦQ ιι{Ό«£ύβe~/Πp#ω~šq Cz:Γλνμhg=U§ιώ†.Σ†τtƒηwgGgΫyli{?N±<ߐž°e;;:TZLTΣ8Ή*YΥ?ݐžΖq{{Ϋ™m&*Ή «·³Σ0―σ=g―7PΞ +ρκ±ΪηΣ(Ά|_Ο4ύ§Χ<σiRεSι'o g€§ηώΙπ"ΞL,ͺ4WΣα₯žξλDG»’˜o½­κ‰†τ4„o««§]νώΖ σΝ7Τ<ϐž6Ψ»Θv•℉yL՞kHO8cύ#Τ“¨Δ­7”?ɐžθΌθ=wν υ$ͺP79?§ψI†τDζuέΉ³O©'!‡†ϋvΕ]CzΒ^νΏ_O= Ή΄:Ζ>՞8Bg;”~–€τm+}†!=ρ°ιn¦ž„bF•χ ι FαV1%ϊ­’$– ϐ²'pšˆ <ληϊ5q«(Iηe7€' ΰ…sΨw°X½ƒJnHO ^^Έxz¬X”Ή– ι ΐόΠ9ΥQo"qξeωβ§1€G74τυ€0ίTςhCz€„ΧΠςxψΣύξό£Γ™μΎE= X*x¬!="܁!α£”Σσ\ώαΑο…AŽ΅θ0+Xφ ι‘γΨΈ―ηεgGCΖ·{΅…z9γ5€'$žMkŸ˜Aξ` Θ~€!=<ΒΎAέn΄GtΚφ¦CΒ½rΆƒz˜w ι …½αJ+υm!7€‡ΐ+λ-ύ™πΚΡ%7KÐoœΩK³ΤsΐĜ’ω@Cz|YΈ|U3©=@tΛ|œ!=ŽΨš/*‰" †τΘ™ΏιjΫ7Τ°τŒ%€θg₯NŒ#jVzΖ’ΑΞΥΚ«Yι}ΌedΨ2“έ2€ώΩ΅)½pBεΡΨΰ±/JR1NQ“{wΗHΎ`Fςl2)―₯ηI[vBߞΘΟφ.IνIΟΧj€ύ0wΥ³-y¦”ήΗΫFUVξ@]ι“ž[ΊglΆŒDά7!Ζ©-ιΝOΆSOAγδƒΏ[Η#—¦¦€χεqMύΉΰδv6Ζ”¨H }Ά‘»ΤSΠ0R4ό§Nw¦Z’ήΒ=#ΙVιDj7Π5Φqvؚ‘ήΧGF%δSΩt*•L'“Ν3¦s#π―P#³ά’ž‚Θ₯Ωdͺ@Ί«nœχ‹Υ†τ^ή3»+M$β±d*Χ8cA<”Τ‚τ\ΉGF‚’€‰X¬ε›m&² _΄ ΫlKΗσδΈ[E…% ¬Cbίμυ+=ϋύ%Ž„d(θp‡=n₯χό~-:0b;;‘gΝη©§! ½JοΓ£zκ) #EwÝmΐα%Ρ§τM“5eΝΛοξΪΗ:.SΟCΊ”žν²ψ'8’ώ?LχUκi(Fσέ¨™ Œ|Θ·=Χ~…zͺΠ‘τήά«;r*°έ2Φ{ƒzjџτ>?¨;²΄λί:;ͺi/‘ή€χϊΒυψ“ΩρžμΊF= Ft&=ϋ¨ξνΘ…[EΏUΧ(}IOοvd)΄­Ω[Ε)t%½³ΊϊsNφv φhφVq }VϊΆ#Η67ζ„ BQ…~€g³φSO‰-ο“©°€θFzϊ΅#§·7zF΅η¬¨Š^€·0₯ΟNYΏη¬•±|§ θDzofτ¨’gf΄₯}Hογ΄ώ ͺH;kuw!+;‰†.€χU₯BγήΦA±γΫ™Ρƒτ–τVΚG ΈΟλν>{νKΟ# SO–„·£_Ον_4/=™σ€wj–z8h]zφa 9ήπ<Σω ο—žχŠŽ Ι;k]Wuh:.‡Ά₯7?­›H•Μ¦ϋ?έ|‘iι½z€—Ξ?α•τŒΆςΙΨΡ²τή?Πςμ‘Ϋ\}rzψhψΓ[œΤEFzύμEνδm’]ιύΌ­FΒ]7F="΄*=WƒΜύ±e˜NwšD£sw POΠοv NwšD›sφiήπ*–έ§ž)š”^x “z Œδ}KOkΑO[ -JοΕ½6κ)°‘υ6ŸΧm¨l4(½ωڎ…O―ōξ¨&-JOγΚKΊwg¨η š“žΆ•­‘¨κhMzšV^κwk-[SN 1ιiYy™Υ ±ΥC[Σ°ς²λγrQ„¦€§]εε7,5*-Ioώ‘F+ΥJΎŸΟ¨η ’žf•ηw>ՁΗνHO«ΚΫωρΈΦ]f₯ьτ4ͺΌˆkϊ1υE+Σ¦ς’ΏZ§©η ,‘ήs-*/±ΌkΈ.Κ£ ι=ŸΡžςr«~Γ‚\ MHO‹Κ |ΣY-h΄ = */ώγώυDGӞςr+†·Ά:βKO{Κσ™υ-Ηα₯§9εΕΎ? ž‚6]zZS^v₯ΑPž<—žΦ,ΙΫωλΤSΠ bKΟ;­)εEΎ=’ž‚†ZzΞA-•ΟΛόn7”§‘₯gΡPΙPi³M?Mc»βjl¨7ΥώΔ|25·B ,°τ - /ΫZNyφό₯n‘βΛΜw^Ι;‘ˆ#=w§;F²?ξΦΩΜ™Όp[˜υξ/g¦|² dβH―Ύ‹zUˆ|š£žB6ΛΠ5!O(MN9]a€χCτ>μkBσ^[.άφ|.+ŸKι½Ό-qζ«|ƒw\Α S Τ“¨DΏœk ›ŸΫ¬ʈ£Ό—. _]cLΖqO ι9ο »yμ!Ή·D)ŠμΜ^Σ’]L½Ε5Zν1BHΟΣΧJ=…J€eκε₯«BGΡ½]υ!BH/Cf!Ÿ`JŒ²eΆ†+ZXπWKAz‹DςpΧ ‘‹ΎpYάm)κ·4 ½W"ΧΞ‘y²­ωβκ9(eθy•`Zzιy§„΄‹ϊ(ΐ·pI[ ήζjξ=rιΩFN? §Ι•gkά‚w@ί›Κε©₯η`.XΔ‘νUκ³€½eBhΫqEͺœSK―ρρΚ#-₯ˆ•ηKά€ώ|Xθ}Q±ρŸφA'ΑI2‹ΔΎ½…‘~±]<Υ0_¬ψkZι=$μ›ύD›νψvD£GΌc\¨ΑB*=ϋ€°7·ΐ₯ς<Ύ«Τ§L곕~K)½Χ£m„―^w=a†­Ν*΄{G>#k)₯wAΤ:Ήο„vd{Λ]ΝMΚθͺT€PzΒζάRΪ‘}1M_jOPWι(OχwΎΓ)špœLyΎΜΐΩꏕv\2ιyο —ΟrΐΦΥ ?τ=QMw…;.•τ<-bh€ίI"εΩΫ.uΠΌ2GΞ„ΚŽJzI1οp’¨τ†­{L»³ TΈIIﭘ½ά†;¦Ÿ[m1<τ4 OιΕH Ή`x‚Γ< “R‘B,‰τ\½Bžjο)<Žτ¨˜‡š½ƒε~E"=s/Ε«V#§Pή»λG°!”τ^©h²ΐŸΠομՍiόE₯Όΐ€gŸΡzε_Ζ7ͺΌΈ!Jz/?Κφπ₯ηlBΝκx%tεΩzgΞJ’|ω0|ιYDμ°ΌGOΘό4.zQ7ʍΠ₯χ²r ’λφK.άΤ@ΩhΚoqΨs θΊΝΫ±Γτν³β½ |(oΐΕ–^—xύΟ2Ÿoάψ ϊ.JPώ+†,=cτR“ ^ήΦ·%―©μop₯· ήA/ώ±bΖ8α]ΑK #Θͺg»'œ5!"‘*Ο‡Hͺτ. WF/ΌŠκΒxsKΗξZ₯`JOΌbfΈΚ³χ ιAδKΊ¬uQzσδ₯sN‚«Ό·jη^ϋtΩίΰIΟ6)ZͺͺςΒ qk|π$Sφ7xr­―<ͺς>έ¬ΡλE’μoΠ€χI4Η¦ςΒIΡώz4’eƒ%=ίm€’ ¦ςΎά¨Ρ%―@¬μo€η¬°’ςΒi;θbQޚ„$=³`εU•·8Z»KžΙ”*$=_ΥΦ1Έΰ)Ο[/œ1•(qΠ”cP,» žςΗ;h`.Œ’ ΑΆ[4εyj{Ι+.ϋ+ ι Άέ’)ογm]Φ²PB._ώwl»ΕRžΫ\›ξ‹"v*$[!¨’N¨νKyσ“’yo(ΨP.Ώτ|Bυ ΖRήβc‘–z**5dζώ‰΅έFp”ημ¬ωϋΕ>!Rι ΅έΖ£(Κ{yWΨψΈ,OUψ%oι½)#ύ~αU–G΅’ιX…hΕo gιΉn ΄έfQ”±Ό «cF8+#ΫΝw|%δΏ`d=ΎΉ[Λ.Ϋ"Rε£Vφΰ+=‘Ά[ɁPOΜ“œ©©TNJ¬W ΔUzBm· ώ―αm12ΎΘΖ+ž«6DΪnW*ϊΌΏSσž³cx«XxJO€νΦ[ω܁£qΚΨl‘―VÍ£τDΪn«άλwΪ.‹ΫМ_ΕΆ&h» ύζ<οh-ρ©NͺjI1~h»ρo{φjͺσ»Λ#}­Ϊό†›τΪnqξξ³Ε‡†£OυΆKάτ‘f»MsοΔβΙΡΕΔ6/U} /ιωqX1ΉΌ•g)DBςŸŸV/ιI’l·ω/Ό« =Ώ/^‘^b\2”ΗKzοDi‚#}γέλργ¬(ί2aΚͺΰΙηmsίδ2¬ ~ία;Ύ£ιžaG>A:%+Œτ’”έ._m›΅Rnm’_”·εq‘žχ QU^εkΠσέ0rN"ΩeΆΈH―NΣO*ΞWy Σ†ω$WΉ9 HŽΚϋ)ίOT Dά#ͺŸ )=1ξΛ«gTFλΨΦζS_%€τ’"ά1ό―†ςώ’6ζ±ΉL₯χJ„œοd΅"3 ΚΫ#›‡w υ0ΏpσŒ pΟβ(UΛΚΛg™L*H&΍Υw\Nzaξ.CypHιD*‘M₯©ξ¦ΡςmφΤ&=›M_όlnΏjHyΉd"šŒΕšfΰυv 0ιυ¦¨ηΦν— Qˆ|‘βΡH,v%0ήV‘ΛP³#ν¬†dηvΡΡΏς2ΡH0V?Σ6PψoœXD(ιuΰΨόm(O R,ΎΎ΄=ή£Φ-‘ ιω`.=LΉ”θWyΩέ` k¬ύ\εnf|’^†ή§ž² qY§ΚK…ƒΊΩ^²λŒτžst›ΚDZδͺGεΕCΎpο8mρgιέ€·&―Κ“I*°=0ΨJo…‘ή+ξE²«Ϊζ» /K²φoŸ₯—έ £7¬€3œΤΏ(FšιΰΆωn·0_%ι½η]=±*’ƒΣ>L%@vύΫ—‡ͺ΅²@@z.n.Ω¬rRή+}4Ϋ“vΦvu‰vf^‚<1ΜI 3τ6#f€πVη`/υ,Jΐ.½Χδ…ϋ3‹ΟΈŒλ›ΐEΓHd««_˜6Ε°K―³b—q ΎςQž}XλωΆ±Ν9"ΗKΓ,=ϊ`)/Ÿd Ϋ¨Άk Δ·ΌOG„ΉΞ–€YzέΤΑR ?3ΥλλZn·—χ―"K«<¬£–ϊΖ₯E‡γŠˆ's™$=­ƒβwHe•^;υIάΛ§9Μ~ܚ&‘ξσ"ο³G0JΟ[½ _.Γ~'_ΜU’ΨhDˆ0†€Qz9jΓŸνV£ξ3)°: ‰o6ιΝσ1kΘ‡Οvϋ^“wυ™FΌ}Ψ€wƒψ#β³έΎz ΑΎΚιυ³—‘#άa’ήKκ`).Ϋν‹Aš) ξŽ Q_N Lο2υ’Ηe»uNpνzΐƒΘrŸ+`±H:B”Λvλκ%†PHπwσυΤΐ"½λ`³P—νΦ’­Ά’oι‰Fί £Ξβ²έ~Τ"ύqώ§YΫ7ƒτўτΈl·/jΙ¬p>ί_VυσΡ~έ$Ϋmx’ΪD€σ‘’ΖP’‘^z1ZσεεΉ;΅‘qM‹PJ“Υ³««XE"ΖaΠzΝτŸŠώjε6ˆjι΅’Ϊ]₯―2Ύh₯Οh™ 6ζ V@6Ϊ›ΰε½#Οι”Gf©ευ P+½R“ŠΓv;?«‰Λ­΄ΩF{ΤC₯τά΄žj'όšλΌOτ*‹π7~Υ’‘Q)½]Rινΐ›@<}ZHJͺ׍ςTJΟuxŠΘƒχ£dΔ(Si=Džώˆ:ιEI—ΌςΎhΰ3έωφT[yUP'=4»T|Θyρ·±ΔΟ3τυ3AQ%½·€a‰?YzΒ•Δ6)zph~υζBA«‘κ='M= ΑΫ@EΡ‹,>!=]sAτ^RnOWπ}η“ΰΪόŠEΣqeP#=Eo\y/BKΔ³Cή!*€Gš™ Aίςάw„Ά%η—λυ©<5¦t8ύ·‚HB—υ ;t*<5σβtΠ*MΌΥŽύκ™Ι-·θVy*€—"̏†ΏcψDh ]ŽΠβΤSΰˆb鑆ˆ‚ϋ1^ί7ι6»EΟΚS.½BλkzΊ°Υ€ΈqΙΙOβ»X˜P*$7e ΘοΠ8£Cύ^+O±τΐm ·θ½#κA/ηΚk   ₯#TžτΨ¦οi¦]ŽΨgύ^lP(½BŽϊ΅’Ζθy9„…‰‡Bι.zYhΣοAcA²ί&DύN€’LzNΒψ‰Uΰ_6Az‘OsΤSΐA™τ²tξΞtxΐθK kυP&=ΒzρΛΐ5έΕάnsίΑγ`…E‘τk9Ɓ«‘ˆΉέ¦>κݘw E#<ιύ^ „άnΓqa•gK©o¨o°4Xκ-͘σ&)•LD²—ϋU©DzvΊ‚½‘Ψρ„άn·Φ„›•γ’£Ή₯©₯±ΉD‘‘ƒ¬Δd8μRU¬F‰τκιŠΟύ„υx‰ΈέJΞ›"υ‡w―··w4Yͺ­Γύύ¦όΟUε¦/sΡΉoCΐΎV·ΫŒ]”ͺež₯φŽφΆ‹ LΈu}}R`λŒΒ΅OτΒte»°U άnc樧°‡w³»{@AΫάΣ“Y]™Sς»’p6p`•g»-άvλΜQOΑ³ΣΣqžaΟ―ΏtΡΏ¬ ψœ|ιyΙj'K?`]šm Γ°’‘­*ιή:ΫuŽέ{gξν |—ύYΙ—^œl©Ψ†UžpΫ­δ"m:Ά˜oΎηρ†YfςŽlι½&+#šzσn»Ν;θ*΄:£ύ½°QψζΑάΟ–!9”-½&²βκΨΌ_ΡΆΫμgΕye1ίn„CŽΧ™‘ΠόœŒΗΙ–YΈT6}B΄ν6υ&ςΕΐ·€£ΗίςγU%Wzt%Φ@+ͺ‰ΆέΖ?RΌ±/ϊΉ£ZnμUόεJο2Υ'–Ξ€wV¬ν6"α+ΟηδέΫTuΣ•)=Ί^ΏAσ~ηŊ<ώΑ6ͺ8Φ>”6O­ήV)£$SzΝT™ΐMψn ΥZ~;‰¬ΌW—†Ρ¨¦Ώήψ™3!‹]½|ͺΐΚZrσεμ Pγ-λnύ¨h―”'=²bް½YlB…/cV€vΔ.ίB<ύV)ΜNžτΘ=ΨΆΎ&ΘΡΑTžS²R\―Μ7*νΉ²€η¦ $Λδ!G{!RO;Dεy“W‰ΉΜ7Λo4²€η§²'{@α7κ³ό8΅³< WθξVζ[oΚFΙ’ž,—r½£}˜‚ ,ε9W“ΪΠλξωΚeoΘ‘žνz/δΣVωͺ ’ςά»VςΆ MΆ2ηu9KmT’Rz½βΕγ(οuσ˜­ΪΛMBŽτ¨Š'ϋ Lέ1–0Ί?x‚γ‚ψ Ο})ν “!½p7π\d"ύ‚Œ‹Ύ!ΜCyŽΜuqJέ—LW“!=Qυδ €Ώυ½0υC1”χα:ωο »%Y†τ¨φΫ_}pcΉisޱΜ_yοFΊΛοΡWrΛ­.½DΥCihAΞ=&oœχ+,ά%‘χcΞ_ύκ;Odϊ ΈEϊ¬pc1α—ͺGο2aοš*8η€ΖR2“!=ψ™Θ!Yη8'HΏΫl Ξ)<‘11ϋ€\.qΣ¨ϊ™PUO^ N]€€S„s=Ÿ·7ΕΉΦc ©Ρ‡<‰‘‘ŒrmHπbT°Œ§γ ½8e€­&=ͺ?n@ 0aEΚγ€msG·υΝ cΉ,AέiU“^¦ΖO* 8inY}ގ‹γ',Ι…Sάj# ZYά;Δ°&ηΏpά$+ !ŸS‘±U€G$Ί xOαSrςK 's^”¦ύΥ‰eΏŠτˆφΫUΈh)7F˜HUΌόόΰ_ntp”“ΩΚ#  ΔDˆY Α–P8†·^3$OF+W–Qθ*\˜–M„O&Qgε4ςΗΫbz/JΡ²PΌ’T–MΠJ°ΆT·}GsŸ8y„lƒB₯΄Wγ\ρ₯G$ΊΧΖ.€MOZδ€Ό…{­|ζΔ‰Z/₯G$*yΰ€ΧNΧOπˆ>K“ΓςHdχE :‹C§*JfΏ Β₯d€¦΄©Δές€π`$-杒+I¨fΌξ=ΝΡ― ‘—ι·w4bR9NρΎ’τΆI\ 8Α PM/ύ…GνFOJxΗY)ŠΏ-•€GcOή€³’μζ?σPžύ‚¨qy•)φ+Užδζž‡»΅•-Ή€‡³JeMU,Lˆ’i’6·υΨO€WGUΫ$Θ½-m\?>$ά_1ζ’ΐ ΕΉςΏβΘ ΨΛ†Ιw₯hώ΄μ:sO‹ΗΌ~ί‡ΚKρYτςˆά~ϋ‡ϊ˜³ΓΫ’½š³©£Θ΅T^z!’°u°dA']'ΛC\πΚ›Ώ―-Ζ ŠͺΊ–—‰Q/›J’v¬ϋ@K’ξσ~Vο 2W=’…έhβ¦–ŠoΝ,N’_œΨ°”ύα8₯«ρfμj°K,=ΉΏk²ΕHYι­rΠP’0ΨιΘ‚ rΠΚσ䩊»r’¬τHL+k`‘zQβγx < νͺ–―Ά₯('=7EΤJξf@bϊG’xΣ°_‡νΝ*ε€η£8*ωΑ‚<ήπp`ΙG²CVh+ΰΦ΄Qε/ρΚIdΏυ€tˆΣΠ–•·0₯D[$PNz‹4Ψ‹—–Š;Y_ΝhۜwD‘ΥΆΜ›τœ’75\ΖR+D8γΥ΄N”'kΥCιΦ{TU+ͺΌ‡¬ΐΎόέ(Ο”:ώCιQxΡ=P#YI=ΰνφΝ}­ΖH¦θQϊΟ²SΨ“· κς8I-+ΐΫ­ž”—’‘‘&Q,`ι·΄•“έ Κ{«#ε™’2"W(φΫTƒ4Z*šχvJGΚ3…ŠL%²ΧΕ7‘lq€>4Ψνφν}ϊlN@BE?•”ž™ΰJ%ItΡέnίLλJy'J ””…+# eI “†έnfτ₯ΌΈ ιQ$z‘J m¦d€n·Ύ)έΨσπŸ„JIΟKPu •oξ"*„ΊΟ ςΒΝϊπΫώc³8$₯”τ|ہςίξρO₯ͺ?F.΍ζy—%sΒnRJz`^x‘€gG NΈζZξ³$ < Z‹Dz`A’DΥ±φف»Έ:)Ύ|9WBza‚j`A’q:χmώ\a«&š<‰,-\BzΏο‘L₯¨ ΡΧ€ύ$•βSή/eν±|²sH ιυ’Μ€° ΡFΊ[a*6ΤG‚/?o§l^%€GP»Ϋojόλ΄πPWωΆ¬ž*¨~ZzS[@™ί„1’!0Ή„'υ2pHzχΤ?ώ+3ψ_Ή”ΓˆlΉΎBτ^QWΘβA‰VŸ§₯GpΤ έoέtυΰξC4ΝJψ’+αX?-=‚Ώ|θ–$«³’ށj“π•G!Rr6J|2§€η$hϋ kB·θ-AU•zσh ‘Θ–2<œ’^ΈŠ”!κciΪ«\ 4OλeΜJγΊ]βOIΐUy–¨eί³0γΨnPΧ£δ‚_*υ―"Ho (ƒŒlΏ@²Ο Ρ±š„·δqδ€τlψii ΛQΛi˜q>ιΠ‹±Χ³tν₯““πΟA ΰN/—fd2ΨRž―Τ‰HϋΈΛTύ:)=‚2?›@‘©dαR.˜ΊR―oθ, ώ€HΉΕμ€τπzy °b2'š¨’Ω€ξŠ7ξ‘ύ4Wζ7'€ηΖΟl€re 9Ρ$'ŒYςEς3ΎΞ•ϋΝ ιmβϋ|@ž;šΎΡ&Σ6Œ -|G‡α*&ΣJω›Σ ιυΆOEΣ¨bhΏΝ9ͺΊϊτ4`Z‹•έ ιαΫ'bs0γP-z˜Τu³ώr1 l%+DΰKο5™@`rώD}2r0wƒη΄x9αΛUͺ[W,½<~β6L‰ͺτΫR!ΚράΥW‰‹Φλ*^Z‹΅†Ώκg€zΨΥΘΓΈΠvΙ[χΒ#9oV~@±τπ+εaο’δA˜B 󴡟Ή³OUyD±τπk-lΑψ 6i– i Bz.n·ΙUΏNE γ_πŠαέo 6½Όώά[2-IοΗιά ΞΔ`*‹Έˆ"γ— N(/€[ Ρ“ω:)c7(’ώ’·³Ε!ˆκ/‚λΆήrιI9+ϊ³ρ/Έ˜.-DA+Ώ!άYe Εœ2ί•"ιαί2€Ξh4­bO°QC˜Έ“ 0ι₯°\›ΕqιΩΠ}q˜„ ’Κf§ Ψ¨!ͺ'ίmΪ“‘ŸΛy\zYτΨ‰Lفm‚2¨&S2 0Θ+έ1bξ˜’{κqιαίρ0ζ8ӊΐξΊ₯—P©\`₯UY΅žγΓ_ϊCΥ"šz™Σl”ΧGΩΪLΐΫ:¦Τ‘t\zθoC&ΰΘGbΥΫ0‚ΪabIΙDΒώ~«Š#Ο1ιΉ‘ΪΡΚ& nBΣ£Ε ψ.ˆ;2™t*E»Η{Υ¬“ή:ϊβ€ΉPt˜1νόΗ>Ζsν… dιL2Ν₯FVf،qΗ€‡μπƒŒ2 ε¬Ψίok¨ΐŠμμ^[*s0α)₯—‚I"YτR怏²όMτδcαݐe¦ Ψ„uLzθά0Lι\’P=/{@Ύ‡Ζρ¬Œ|Θ²r‘Ζ?ιΉΠ›yϊADγ€πDIvιύ>49άlγVeφŸτ6ΡΏ„AQβ¦ »YΘMU"F&qίv~–§›θŸτΠχΫ4Μ!dΏu³,$šζΑ‹˜wσΏ+œ·ΑCΏeμVΚ”“ I”h‚=^ΑU)°<Ξ$”žΔμ£8¬o°Ÿ/»E΅&§6ΪϋQŒτ€‡¬…"T/Οpcτ€^λͺρZ•#ι‘·ώΞXA†‘(d•i±hrn{ε ^ψτ" YΫ ΣΚ:σ††j³ˆ΄}榽ηHzθG½0ˆΑ(@P6›a"(^lrπ²GωHzθοDΈ…γΉp?b—²W© OΔ5Λp$=τ[L—Ь σΩ€E°όΗΔR~±ΏοΔΘ¦€,ΜEŠ`γJ3Ÿτ³ιeά;θΉ¦ϋƒ-½(Θ-ΓG&Ίme‘U(›ήv3MΓ_ι‘—§‹€d>{ €·Αz;υ5°,AζϋΡ+•z‡€ˆyΐͺ—`~MŸ@!+Α•ςŽ€ΥεK6κ;+€’£σΡ„¨φn r½Ko ―zy€1°OOL«Ί/ΓΟ|―yρb§©›|qΜΝ‚nΧ“Ζΐ–S³μ4S€΅~‘ϋ/ BΓψ1σϊͺ±”cKiΓE―ΓlΒΞσλ†Σεχί7r;‡M€Ώ ;‘ΙΚςlμUo—Υ•ρŽgώάΥ—G9,@\4•9Œ-½ cŒVψΟ‹gέ„νo4…Ω֞‡()Šέ9šεΙn5*ρ‡± Xœ―"Z.ό–%\‹'H+Q-E<¬!hrŒkΦ ή…Ρ.ωkξXΝzYˆΝRKeμψ8ctΰ0ο7χΜ_Λωc„°(c_Κ™V=μ’²Œϋ%χEΟd:wxΣ° Œf=l Σͺ‡νFcLsΎΒΎζΓΎδsΘͺ§₯ Ϋ™Α&=FΣޞ·ϋήμ ¦©˜~δ―6£τp3ξΰΨ«ˆτ΄ρ ©>ζΤGYτξ_r±ΟzZΌf0ΝΫόΝδΛ@ŠF3Gφ₯‡Ό‚€TΧΣ’τw•8K*+χΒ^°Όω[ ²α"ŸO™ΎΘ‹±dΰΖ°ΎΣA+ώ5" Χq `0Ϋ ΅™6ρzξΉΠ΄xΝhG>$°Ϋx0¬Η`©Aι`τώ* Ϋ5Š~Ν€Xυ›4*†εψΐΉ3ΑˆΒ ²iτj‘†œμ ±lbV¨YΘ„ε«}l²^ Ϋβ ±αbΫ°ΤͺΓΩΚ ³}eα΅, WNω@ΌˆX>LSώŽάwWύΩ98j΄ΏžΕ^υ Π’YωFΔΗ<{ΥeΠW=ˆ?OKÞ+C8ζQo―ώ–yχ LοΆ»™aγ—‡VŠτkΔ WKΗSl‹=C΄nLκA~{Ž–Θ₯ΡΝ-zΘ›ΣΛ!KαγD―»‹½κAΌHi~ω0MY;Giμ˜α:ΓΈR¦Uωpΐp`ΓN"Ηήp!^€α|˜ΞzΘ«ΓΫ‹,½,ϊ† qγCΞͺΦΤ5γTcΩ€΄qRςrZΌfΈK6δ“τ7\†n\A%/§Ε³ς]ŒIzΨASκ₯‡|ŒI ―zrnΣ5ω6Ξπώ"oΈI“EKgφCpΛLfC„<Ά΅LύR‚,½¬&ΟzZ y@>—2Ό7lΞ“B/|ρrCyάI3%τKΨ!jΉ˜GυΕ4Ήκ™?NCVώlΧ-―ώΦhέΕμŸΣdΠ”†£…Wz gΛ8¦τ­θƐE€ ½|˜μόΘΧ8†Ήξb^ί|—Π7\wMΧιΓ΄n!KαKω17—+l&ΘFl©kΗ'n³`^_ ΟšΑ―* ςrΘ+ “τ+_0|+G7ΰζQ…?{…/?FUOKCž+Λ†€(½ν) o^ «ς&Ζt²DΆΤ²Ό5gЌVρΉΒX?FUOKηSδ―6‹Αβξ¬ZSώ½Έ7μ ·žΕ!ϊ䏓Ν5 y0½5n,ι­ου,΄`VΆ*`†°°"obLSFώš0΅7ξˆBt+N`?εΧ²‹['ΡΡmUKfπ«ΈQ+Λ“GΏίšFe\ϋ««%ƒμύPΪCK7ά‘(f<n‡© |ΕO<θ>iGφoCά3΄c¦-Γ”^|”ιιwW¬0σ¨ΘΟGϋg1ep₯αΓoUŸkS ŠΧΒdJ0žgzΤ°s Ό‚τ4hΨ»0†˜"ςQοDΖ€ΞŸό―?όΏESY­‡|{0ˆ˜Ί%‘fςb; ήΝ3B‡ΚΓ_υ \Τ³ά€›%–«Ρ\―Jžωξ?³`w­Θ―‡ΎκΔ;!K­abOz»3ΜC4ξtL€<ΏΚ+HωΊrŒEώΊ¬3„ώΰyεμ²υq½i ΪΦζͺ‡;η<ΫZ‚Ψ„ΗΠΠΆŸη–+9¦ώΫ‚νdΤ`¬h†νV>ˆ–o“I΄Ή³ΝΟ΄ϊOy% η˜ΛYZo*‡uυΰΈ§L¦νYaΆΪyNΓΗνθg=UWz¬οP–―r? „»―fψψΣ²_ζŽύ€I“2rθJ”%Όΐ Ώf"UˆBΥ€όi‚Λ—εΗάρŸ΄yΓΕ•σ«yp€·v¦œδβΤΨ*.‚~Φ³@Ċ^F ·al•BΙήΜή―3τω+C,\Ÿg1±Ε:(Ζ ρΥ΄ΖZF‘Λ%Φf~=ͺβežη1Φ›‘WκXξDΑ7‹)ˆάϝ)Žφ/»ˆΛ°GζBςnHιΝΊ%X{dD:Ωyb2#NJ‚DEoαyLaφοζΨ0ΐD*γ{:œυu²ΖiενmΈΘ±’ &lΜΒ’>€m!Ζ}Ω“~g:ΜzΌp½"Γ«wOύ›₯° wΗ‚ ±HΓ€1ξr_φ˜Ζ‚Ϊΐf—―ΐ T™ΠάXωπ)³*‰Ύ©<ƒ¦7TEπmo?¨π[ cͺŸbv‘jB)°˜$c}ρΗq/oηž:εlΎ¨ΨΜ’pNV<€7†˜­ΰ‹Λ ·ύλ1ΠXρgο'αkHYΙc¦ω‘EsΈ£Ub¦χLDιeΩκΡΗ=ΔίωΌ ΫgjϊΛπ9/α΄Βš3½–->)Έά\uZ{σ#V‰ΐΕh=γoVfO>ΕΔ·›ΐ#]°,ΟL/.ΘΉΫε·–ŸΚΈ˜μI³4ƒπΓtžε]Ίn|EΉΊ·e1Γ3“;p©§Κ›žήhΊ Λ Ώ'½+ˆΕ¦|€_Ήί`†„‘½# Ϊ[―Íτ΅ZMaOg9½$C~Ή3Ϊ“ήRΪJ4θuΪžλ)Ur<ε0κΘη;`‹υrœw…”Σtvš<mΕ§V) …ž5+0ν{BΧΡ€·ϊ%ωΔ΅άΩ*ε™Lχ>ά…ρ?KN胣L†φ7άξΦ–¦Ί3f‹9Ig’£m ϋoBZ½_ΰθβIžλu‚W]†ϋ―¦ ΩYΗ$ΐ(ͺ)μ½lμK­aBϊΣLσ •Ύίη5τγpŒύω4Η>J–ώ $ιm³α*fπύ/γž››ς η%W`˜q£ρzζ@¦Bǁτ°’ίΰΣΌ\Ή!°•RŒš^M°„-§]w`Β 9ά †SΚdŠ=†³ŽO>€)S–ΗΆ>«Ϊ…OΪlΓqapεp΅ΓpJnJ.Y½rKςφJ1 ̘ζo«»$ΕΘ΅€ωp(½gkͺ‘‹Z9Ξΰσ‡ΰ‘{7ξΚ3νΉE?Œ)oͺ‘XmΦ…ςŽjŽύBή2τ%γ€ΰkrΉφ³ϋθ°²σBb₯«ZoώJοI€ƒΣ¨˜T„Σΐ?ήƒ=.,£}Ί£&Σ»aωο|δwχ-޳Αεθfϋc–χiΣ’W`κγ]Θuo 5nϋiώΒy9ϋnjsγ v³žIοΙ\™ƒ’$y-z¦ήNΑY‡–ϋ΄Ξ|¦—ϊ+ΫZατ,JJ>±6Ξρ+ίΈ-zΎœΊkHvŠ•ε‰ΙδΫκν*-ΏtΘοοGn˟λόQ2G Gη@'σS m„³vΎσ,Oα’ώήΤΩή\g±μ~€\&ŸŠF#—γh₯09ΆOνD8ΩΟπq0ημΈ€Gνό2ZeΠyh6qΔΗb»ζΡzS۞εσIˆŠc›}ώ˜_ŽΧ/ξ7³1Gπ:kΞV˜TyGŒT¨SΑύγψιόΏΟΌ^ƏP±yœΡ/jΚώΈ£»•ΐ] ορςiΔ½όŠ>γ±νμeυ _Π17ƒͺΫ$ΆΪΈTΛ}FΪΗfLΟΗUζl'œ“s s1¨B±τξ.Μπ°°8πNP9<ΧT$lδέ~žΆƒœ°Δ>}; +οΖ΄”›άΡ+ Χξ¬·y˜mƒbN:ΒηΗ{q«¨™¬¦ΧΝΧˆ/½~¦·˜"NωŸ&ΎϋΞ}zΒή¬ΙσνRŸ,K‘φžαjJ7(ΗiΧηΝŸΧ Χ½`Β 8š\††LΞμ…ΞjHlsσί~λe)αuΏώω.œφ|nͺγ{a-³gΟφ–έySaΏΏμPƒ#J|άϋtκ±v†ςβΈgLτy:Ϊ[›‹ώž|"†{υιΥ%c&ίM€ΨX€%΄2HeΩσΚ›άξϊzKCIΚeσ™ΜΥ‘6“q± §t˜Ϋƒ‘Ωoάs aΞ”7€§L„ε3Ϋ sCTΝ§Ηp₯\pοŒiρSΰ―΄jžcyΎή)/―;>3C9ŒΔ7Ύ™ 4O…•­ίτι¦Κb:ΉUΓ%jP…Š›κ€³yH™Εγ™α5¨Bεσά˜Ι—Ό T|‘₯iδžφZ€ΪU’_‘ψ€ΐ#γ”g ƒκ·Ψ~Sxγ’Μ YίςScΕ3…Jg§ιΥ₯sUύΉΐFfFσUί °i»{lroφŸm+V ύi3œ’ς‘m6ήσE…W:ΪO­7™2ΙπΞ@Ώ’²υ €·GηžOΦhnh΄4XLωl>—Ηnw6t Τ$5ΠαΏυ|Ϊ7IENDB`‚Yakifo-amqtt-2637127/docs/changelog.md000066400000000000000000000243131504664204300174650ustar00rootroot00000000000000# Changelog ## 0.11.3 API changes: - broker and client configuration via dataclasses and enums instead of unstructured dictionaries (backwards compatible) - `MESSAGE_RECIEVE` event moved to after topic filtering - `MESSAGE_BROADCAST` event added for prior topic filtering - `RETAINED_MESSAGE` event added for messages with retained flag or offline clients without setting clean session flag - method `retain_message` changed to coroutine (broker) - change `add_subscription` to a public method (broker) - add listener type for external servers and api method for passing new connection via `external_connected` method (broker) - for TLS sessions, properly load the key and cert file (client) - added abstract method `get_ssl_info` to `WriterAdapter` Details: * Structural elements for the 0.11.3 release https://github.com/Yakifo/amqtt/pull/265 * Release Candidate Branch for 0.11.3 https://github.com/Yakifo/amqtt/pull/272 * update the configuration for the broker running at test.amqtt.io https://github.com/Yakifo/amqtt/pull/271 * Improved broker script logging https://github.com/Yakifo/amqtt/pull/277 * test.amqtt.io dashboard cleanup https://github.com/Yakifo/amqtt/pull/278 * Structured broker and client configurations https://github.com/Yakifo/amqtt/pull/269 * Determine auth & topic access via external http server https://github.com/Yakifo/amqtt/pull/262 * Plugin: authentication against a relational database https://github.com/Yakifo/amqtt/pull/280 * Fixes #247 : expire disconnected sessions https://github.com/Yakifo/amqtt/pull/279 * Expanded structure for plugin documentation https://github.com/Yakifo/amqtt/pull/281 * Yakifo/amqtt#120 confirms : validate example is functioning https://github.com/Yakifo/amqtt/pull/284 * Yakifo/amqtt#39 : adding W0718 'broad exception caught' https://github.com/Yakifo/amqtt/pull/285 * Documentation improvement for 0.11.3 https://github.com/Yakifo/amqtt/pull/286 * Plugin naming convention https://github.com/Yakifo/amqtt/pull/288 * embed amqtt into an existing server https://github.com/Yakifo/amqtt/pull/283 * Plugin: rebuild of session persistence https://github.com/Yakifo/amqtt/pull/256 * Plugin: determine authentication based on X509 certificates https://github.com/Yakifo/amqtt/pull/264 * Plugin: device 'shadows' to bridge device online/offline states https://github.com/Yakifo/amqtt/pull/282 * Plugin: authenticate against LDAP server https://github.com/Yakifo/amqtt/pull/287 * Sample: broker and client communicating with mqtt over unix socket https://github.com/Yakifo/amqtt/pull/291 * Plugin: jwt authentication and authorization https://github.com/Yakifo/amqtt/pull/289 ## 0.11.2 - config-file based plugin loading [PR #240](https://github.com/Yakifo/amqtt/pull/240) - dockerfile build update to support psutils [PR #239](https://github.com/Yakifo/amqtt/pull/239) - pass client session info to event callbacks [PR #241](https://github.com/Yakifo/amqtt/pull/241) - Require at least one auth [PR #244](https://github.com/Yakifo/amqtt/pull/244) - improvements in retaining messages [PR #248](https://github.com/Yakifo/amqtt/pull/248) - updating docker compose with resource limits [PR #253](https://github.com/Yakifo/amqtt/pull/253) - improve static type checking for plugin's `Config` class [PR #249](https://github.com/Yakifo/amqtt/pull/249) - broker shouldn't allow clients to publish to '$' topics [PR #254](https://github.com/Yakifo/amqtt/pull/254) - publishing to a topic with `*` is allowed, while `#` and `+` are not [PR #251](https://github.com/Yakifo/amqtt/pull/251) - updated samples; plugin config consistency (yaml and python dict) [PR #252](https://github.com/Yakifo/amqtt/pull/252) - add cpu, mem and broker version to dashboard [PR #257](https://github.com/Yakifo/amqtt/pull/257) - [Issue 246](https://github.com/Yakifo/amqtt/issues/246) don't retain QoS 1 or 2 messages if client connects with clean session true - [Issue 175](https://github.com/Yakifo/amqtt/issues/175) plugin examples - [Issue 81](https://github.com/Yakifo/amqtt/issues/81) Abstract factory for plugins - [Issue 74](https://github.com/Yakifo/amqtt/issues/74) ζ¨‘ζ‹Ÿ500δΈͺε’ζˆ·η«―εΉΆε‘οΌŒθΏžζŽ₯broker。 - [Issue 60](https://github.com/Yakifo/amqtt/issues/60) amqtt server not relaying traffic - [Issue 31](https://github.com/Yakifo/amqtt/issues/31) Plugin config in yaml file not under - plugins entry - [Issue 27](https://github.com/Yakifo/amqtt/issues/27) don't retain messages from anonymous clients - [Issue 250](https://github.com/Yakifo/amqtt/issues/250) client doesn't prevent publishing to wildcard topics - [Issue 245](https://github.com/Yakifo/amqtt/issues/245) prevent clients from publishing to `$` topics - [Issue 196](https://github.com/Yakifo/amqtt/issues/196) proposal: enhancement to broker plugin configuration - [Issue 187](https://github.com/Yakifo/amqtt/issues/187) anonymous login allowed even if plugin isn't enabled - [Issue 123](https://github.com/Yakifo/amqtt/issues/123) Messages sent to mqtt can be consumed in time, but they occupy more and more memory ## 0.11.1 - [PR #226](https://github.com/Yakifo/amqtt/pull/226) Consolidate super classes for plugins - [PR #227](https://github.com/Yakifo/amqtt/pull/227) Update sample files - [PR #229](https://github.com/Yakifo/amqtt/pull/229) & [PR #228](https://github.com/Yakifo/amqtt/pull/228) Broken pypi and test.amqtt.io links - [PR #232](https://github.com/Yakifo/amqtt/pull/234) $SYS additions for cpu & mem. ## 0.11.0 - upgrades to support python 3.10, 3.11, 3.12 and 3.13 - complete type hinting of the entire codebase - linting with ruff, pylint and mypy to keep the codebase consistent in format and structure - github workflow CI of linting before pull requests can be merged - run linting with git pre-commit hooks - add docker container - switch to discord - updates to community contribution guidance, code of conduct, etc. - overhaul of the documentation, including move to mkdocs with the materials UI - updated plugin documentation and full docs for the broker/client configuration - updated doc strings and cli help messages, including auto generation of those aspects into the docs - [Issue #215](https://github.com/Yakifo/amqtt/issues/215) test_sys.py fails on github, but not locally - [Issue #210](https://github.com/Yakifo/amqtt/issues/210) NoDataError thrown instead of ConnectError when client fails authentication - [Issue #199](https://github.com/Yakifo/amqtt/issues/199) will message being sent even if client properly disconnects - [Issue #180](https://github.com/Yakifo/amqtt/issues/180) plugin broker sys: incorrect uptime topic - [Issue #178](https://github.com/Yakifo/amqtt/issues/178) consolidate broker configuration documentation - [Issue #170](https://github.com/Yakifo/amqtt/issues/170) compatibility test cases: paho-mqtt - [Issue #159](https://github.com/Yakifo/amqtt/issues/159) Client last will (LWT) example and documentation - [Issue #157](https://github.com/Yakifo/amqtt/issues/157) loop = asyncio.get_event_loop() is deprecated - [Issue #154](https://github.com/Yakifo/amqtt/issues/154) broker rejects connect with empty will message - [Issue #144](https://github.com/Yakifo/amqtt/issues/144) Allow amqtt.client.MQTTClient to always reconnect via config - [Issue #105](https://github.com/Yakifo/amqtt/issues/105) Add stack traces to logging - [Issue #95](https://github.com/Yakifo/amqtt/issues/95) asyncio.get_event_loop() deprecated in Python 3.10 - [Issue #94](https://github.com/Yakifo/amqtt/issues/94) test matrix for dependencies - [Issue #70](https://github.com/Yakifo/amqtt/issues/70) event and plugin documentation - [Issue #67](https://github.com/Yakifo/amqtt/issues/67) MQTTClient fails to raise appropriate exception if URI is broken - [Issue #51](https://github.com/Yakifo/amqtt/issues/51) failing plugin kills the broker - [Issue #48](https://github.com/Yakifo/amqtt/issues/48) Setup unit tests running against different versions of dependencies - [Issue #35](https://github.com/Yakifo/amqtt/issues/35) plugin interface and optimization ## 0.10.2 - create the necessary .readthedocs.yaml to generate sphinx docs from the 0.10.x series ## 0.10.1 - First release under new package name: amqtt - Reworked unit tests - Dropped support for python3.5 and earlier - Added support for python3.8 and 3.9 - Pass in loop to PluginManager, from [PR #126](https://github.com/beerfactory/hbmqtt/pull/126) - Fixes taboo topic checking without session username, from [PR #151](https://github.com/beerfactory/hbmqtt/pull/151) - Move scripts module into hbmqtt module, from [PR #167](https://github.com/beerfactory/hbmqtt/pull/167) - Download mosquitto certificate on the fly - Importing `hbmqtt` is deprecated, use `amqtt` - Security fix: If an attacker could produce a KeyError inside an authentication plugin, the authentication was accepted instead of rejected ## 0.9.5 - fixes: [milestone 0.9.5](https://github.com/njouanin/hbmqtt/milestone/11?closed=1) - fixes: [milestone 0.9.3](https://github.com/njouanin/hbmqtt/milestone/10?closed=1) ## 0.9.2 - fixes: [milestone 0.9.2](https://github.com/beerfactory/hbmqtt/milestone/9?closed=1) ## 0.9.1 - See commit log ## 0.9.0 - fixes: [milestone 0.9.0](https://github.com/beerfactory/hbmqtt/milestone/8?closed=1) - improve plugin performance - support Python 3.6 - upgrade to `websockets` 3.3.0 ## 0.8.0 - fixes: [milestone 0.8.0](https://github.com/njouanin/hbmqtt/milestone/7?closed=1) ## 0.7.3 - fix deliver message client method to raise TimeoutError ([#40](https://github.com/beerfactory/hbmqtt/issues/40)) - fix topic filter matching in broker ([#41](https://github.com/beerfactory/hbmqtt/issues/41)) Version 0.7.2 has been jumped due to troubles with pypi... ## 0.7.1 - Fix [duplicated $SYS topic name](https://github.com/beerfactory/hbmqtt/issues/37) ## 0.7.0 - Fix a [series of issues](https://github.com/beerfactory/hbmqtt/issues?q=milestone%3A0.7+is%3Aclosed) reported by [Christoph Krey](https://github.com/ckrey) ## 0.6.3 - Fix issue [#22](https://github.com/beerfactory/hbmqtt/issues/22) ## 0.6.2 - Fix issue [#20](https://github.com/beerfactory/hbmqtt/issues/20) (`mqtt` subprotocol was missing) - Upgrade to `websockets` 3.0 ## 0.6.1 - Fix issue [#19](https://github.com/beerfactory/hbmqtt/issues/19) ## 0.6 - Added compatibility with Python 3.5 - Rewritten documentation - Add command-line tools `amqtt`, `amqtt_pub` and `amqtt_sub` Yakifo-amqtt-2637127/docs/code_of_conduct.md000066400000000000000000000000331504664204300206440ustar00rootroot00000000000000--8<-- "CODE_OF_CONDUCT.md"Yakifo-amqtt-2637127/docs/contributing.md000066400000000000000000000000311504664204300202340ustar00rootroot00000000000000--8<-- "CONTRIBUTING.md" Yakifo-amqtt-2637127/docs/docker.md000066400000000000000000000016751504664204300170130ustar00rootroot00000000000000# Containerization `amqtt` library is available on [PyPI](https://pypi.python.org/pypi/amqtt), [GitHub](https://github.com/Yakifo/amqtt) and [Read the Docs](http://amqtt.readthedocs.org/). Built from [Dockerfile](https://github.com/Yakifo/amqtt/blob/main/dockerfile), the default `aMQTT` broker is publicly available on [DockerHub](https://hub.docker.com/repository/docker/amqtt/amqtt). ## Launch ```shell $ docker run -d -p 1883:1883 amqtt/amqtt:latest ``` ## Configure and launch The easiest way to provide a custom [aMQTT broker configuration](references/broker_config.md), is to create a yaml file... ```shell $ cp amqtt/scripts/default_broker.yaml broker.yaml ``` and create a docker compose file... ```yaml services: amqtt: image: amqtt container_name: amqtt ports: - "1883:1883" volumes: - ./broker.yaml:/app/conf/broker.yaml ``` and launch with... ```shell $ docker compose -d -f docker-compose.yaml up ``` Yakifo-amqtt-2637127/docs/docs/000077500000000000000000000000001504664204300161415ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/docs/assets/000077500000000000000000000000001504664204300174435ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/docs/assets/amqtt.svg000066400000000000000000000150651504664204300213210ustar00rootroot00000000000000 Yakifo-amqtt-2637127/docs/index.md000066400000000000000000000000231504664204300166350ustar00rootroot00000000000000--8<-- "README.md" Yakifo-amqtt-2637127/docs/license.md000066400000000000000000000000231504664204300171500ustar00rootroot00000000000000--8<-- "LICENSE.md"Yakifo-amqtt-2637127/docs/plugins/000077500000000000000000000000001504664204300166725ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/plugins/auth_db.md000066400000000000000000000022041504664204300206200ustar00rootroot00000000000000# Relational Database for Authentication and Authorization - `amqtt.contrib.auth_db.UserAuthDBPlugin` (authentication) verify a client's ability to connect to broker - `amqtt.contrib.auth_db.TopicAuthDBPlugin` (authorization) determine a client's access to topics Relational database access is supported using SQLAlchemy so MySQL, MariaDB, Postgres and SQLite support is available. For ease of use, the [`user_mgr` command-line utility](auth_db.md/#user_mgr) to add, remove, update and list clients. And the [`topic_mgr` command-line utility](auth_db.md/#topic_mgr) to add client access to subscribe, publish and receive messages on topics. # Authentication Configuration ::: amqtt.contrib.auth_db.UserAuthDBPlugin.Config options: heading_level: 4 extra: class_style: "simple" # Authorization Configuration ::: amqtt.contrib.auth_db.TopicAuthDBPlugin.Config options: heading_level: 4 extra: class_style: "simple" ## CLI ::: mkdocs-typer2 :module: amqtt.contrib.auth_db.user_mgr_cli :name: user_mgr ::: mkdocs-typer2 :module: amqtt.contrib.auth_db.topic_mgr_cli :name: topic_mgr Yakifo-amqtt-2637127/docs/plugins/cert.md000066400000000000000000000132061504664204300201530ustar00rootroot00000000000000# Authentication Using Signed Certificates Using client-specific certificates, signed by a common authority (even if self-signed) provides a highly secure way of authenticating mqtt clients. Often used with IoT devices where a unique certificate can be initialized on initial provisioning. With so many options, X509 certificates can be daunting to create with `openssl`. Included are command line utilities to generate a root self-signed certificate and then the proper broker and device certificates with the correct X509 attributes to enable authenticity. ### Quick start Generate a self-signed root credentials and server credentials: ```shell $ ca_creds --country US --state NY --locality NY --org-name "My Org's Name" --cn "my.domain.name" $ server_creds --country US --org-name "My Org's Name" --cn "my.domain.name" ``` !!! warning "Security of private keys" Your root credential private key and your server key should *never* be shared with anyone. The certificates -- specifically the root CA certificate -- is completely safe to share and will need to be shared along with device credentials when using a self-signed CA. Include in your server config: ```yaml listeners: ssl-mqtt: bind: "127.0.0.1:8883" ssl: true certfile: server.crt keyfile: server.key cafile: ca.crt plugins: amqtt.contrib.cert.CertificateAuthPlugin: uri_domain: my.domain.name ``` Generate a device's credentials: ```shell $ device_creds --country US --org-name "My Org's Name" --device-id myUniqueDeviceId --uri my.domain.name ``` And use these to initialize the `MQTTClient`: ```python import asyncio from amqtt.client import MQTTClient client_config = { 'keyfile': 'myUniqueDeviceId.key', 'certfile': 'myUniqueDeviceId.crt', 'broker': { 'cafile': 'ca.crt' } } async def main(): client = MQTTClient(config=client_config) await client.connect("mqtts://my.domain.name:8883") # publish messages or subscribe to receive asyncio.run(main()) ``` ## Background Often used for IoT devices, this method provides the most secure form of identification. A root certificate, often referenced as a CA certificate -- either issued by a known authority (such as LetsEncrypt) or a self-signed certificate) is used to sign a private key and certificate for the server. Each device/client also gets a unique private key and certificate signed by the same CA certificate; also included in the device certificate is a 'SAN' or SubjectAlternativeName which is the device's unique identifier. Since both server and device certificates are signed by the same CA certificate, the client can verify the server's authenticity; and the server can verify the client's authenticity. And since the device's certificate contains a x509 SAN, the server (with this plugin) can identify the device securely. !!! note "URI and Client ID configuration" `uri_domain` configuration must be set to the same uri used to generate the device credentials when a device is connecting with private key and certificate, the `client_id` must match the device id used to generate the device credentials. Available ore three scripts to help with the key generation and certificate signing: `ca_creds`, `server_creds` and `device_creds`. !!! note "Configuring broker & client for using Self-signed root CA" If using self-signed root credentials, the `cafile` configuration for both broker and client need to be configured with `cafile` set to the `ca.crt`. ## Root & Certificate Credentials The process for generating a server's private key and certificate is only done once. If you have a private key & certificate -- such as one from verifying your webserver's domain with LetsEncrypt -- that you want to use, pass them to the `server_creds` cli. If you'd like to use a self-signed certificate, generate your own CA by running the `ca_creds` cli (make sure your client is configured with `check_hostname` as `False`). ```mermaid --- config: theme: redux --- flowchart LR subgraph ca_cred["ca_cred #40;cli#41; or other CA"] ca["ca key & cert"] end subgraph server_cred["server_cred fl°°40¢ßclifl°°41¢ß"] scsr("certificate signing
request fl°°40¢ßCSRfl°°41¢ß with
SAN of DNS & IP Address") spk["private key"] ssi["sign csr"] end spk -.-> skc["server key & cert"] ca_cred --> ssi spk --> scsr con["country, org
& common name"] --> scsr scsr --> ssi ssi --> skc ``` ## Device credentials For each device, create a device id to generate a device-specific private key and certificate using the `device_creds` cli. Use the same CA as was used for the server (above) so the client & server recognize each other. ```mermaid --- config: theme: redux --- flowchart LR subgraph ca_cred["ca_cred #40;cli#41; or other CA"] ca["ca key & cert"] end subgraph device_cred["device_cred fl°°40¢ßclifl°°41¢ß"] ccsr("certificate signing
request fl°°40¢ßCSRfl°°41¢ß with
SAN of URI & DNS") cpk["private key"] csi["sign csr"] end cpk --> ccsr csi --> ckc[device key & cert] cpk -.-> ckc ccsr --> csi ca_cred --> csi con["country, org
common name
& device id"] --> ccsr ``` ## Configuration ::: amqtt.contrib.cert.UserAuthCertPlugin.Config options: show_source: false heading_level: 4 extra: class_style: "simple" ## Key and Certificate Generation ::: mkdocs-typer2 :module: amqtt.scripts.ca_creds :name: ca_creds ::: mkdocs-typer2 :module: amqtt.scripts.server_creds :name: server_creds ::: mkdocs-typer2 :module: amqtt.scripts.device_creds :name: device_creds Yakifo-amqtt-2637127/docs/plugins/contrib.md000066400000000000000000000041751504664204300206630ustar00rootroot00000000000000# Contributed Plugins These are fully supported plugins but require additional dependencies to be installed: `$ pip install '.[contrib]'` - [Relational Database Auth](auth_db.md)
Grant or deny access to clients based on entries in a relational db (mysql, postgres, maria, sqlite). _Includes manager script to add, remove and create db entries_
- `amqtt.contrib.auth_db.UserAuthDBPlugin` - `amqtt.contrib.auth_db.TopicAuthDBPlugin` - [HTTP Auth](http.md)
Determine client authentication and/or authorization based on response from a separate HTTP server.
- `amqtt.contrib.http.UserAuthHttpPlugin` - `amqtt.contrib.http.TopicAuthHttpPlugin` - [LDAP Auth](ldap.md)
Authenticate a user with an LDAP directory server.
- `amqtt.contrib.ldap.UserAuthLdapPlugin` - `amqtt.contrib.ldap.TopicAuthLdapPlugin` - [Shadows](shadows.md)
Device shadows provide a persistent, cloud-based representation of the state of a device, even when the device is offline. This plugin tracks the desired and reported state of a client and provides MQTT topic-based communication channels to retrieve and update a shadow.
`amqtt.contrib.shadows.ShadowPlugin` - [Certificate Auth](cert.md)
Using client-specific certificates, signed by a common authority (even if self-signed) provides a highly secure way of authenticating mqtt clients. Often used with IoT devices where a unique certificate can be initialized on initial provisioning. _Includes command line utilities to generate root, broker and device certificates with the correct X509 attributes to enable authenticity._
`amqtt.contrib.cert.UserAuthCertPlugin` - [JWT Auth](jwt.md)
Plugin to determine user authentication and topic authorization based on claims in a JWT. - `amqtt.contrib.jwt.UserAuthJwtPlugin` (client authentication) - `amqtt.contrib.jwt.TopicAuthJwtPlugin` (topic authorization) - [Session Persistence](session.md)
Plugin to store session information and retained topic messages in the event that the broker terminates abnormally.
`amqtt.contrib.persistence.SessionDBPlugin` Yakifo-amqtt-2637127/docs/plugins/custom_plugins.md000066400000000000000000000154151504664204300222750ustar00rootroot00000000000000# Custom Plugins With the aMQTT plugins framework, one can add additional functionality to the client or broker without having to rewrite any of the core logic. Plugins can receive broker or client events [events](custom_plugins.md#events), used for [client authentication](custom_plugins.md#authentication-plugins) and controlling [topic access](custom_plugins.md#topic-filter-plugins). ## Overview To create a custom plugin, subclass from `BasePlugin` (client or broker) or `BaseAuthPlugin` (broker only) or `BaseTopicPlugin` (broker only). Each custom plugin may define settings specific to itself by creating a nested (ie. inner) `dataclass` named `Config` which declares each option and a default value (if applicable). A plugin's configuration dataclass will be type-checked and made available from within the `self.config` instance variable. ```python from dataclasses import dataclass, field from amqtt.plugins.base import BasePlugin from amqtt.contexts import BaseContext class OneClassName(BasePlugin[BaseContext]): """This is a plugin with no functionality""" class TwoClassName(BasePlugin[BaseContext]): """This is a plugin with configuration options.""" def __init__(self, context: BaseContext): super().__init__(context) self.my_option_one: str = self.config.option1 async def on_broker_pre_start(self) -> None: print(f"On broker pre-start, my option1 is: {self.my_option_one}") @dataclass class Config: option1: int option3: str = field(default="my_default_value") ``` This plugin class then should be added to the configuration file of the broker or client (or to the `config` dictionary passed to the `Broker` or `MQTTClient`), such as `myBroker.yaml`: ```yaml --- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: module.submodule.file.OneClassName: module.submodule.file.TwoClassName: option1: 123 ``` and then run via `amqtt -c myBroker.yaml`. ??? note "Example: custom plugin within broker script" The example `samples/broker_custom_plugin.py` demonstrates how to load a custom plugin by passing a config dictionary when instantiating a `Broker`. While this example is functional, `samples` is an invalid python module (it does not have a `__init__.py`); it is recommended that custom plugins are placed in a python module. ```python --8<-- "samples/broker_custom_plugin.py" ``` ??? warning "Deprecated: activating plugins using `EntryPoints`" With the aMQTT plugins framework, one can add additional functionality to the client or broker without having to rewrite any of the core logic. To define a custom list of plugins to be loaded, add this section to your `pyproject.toml`" ```toml [project.entry-points."mypackage.mymodule.plugins"] plugin_alias = "module.submodule.file:ClassName" ``` Each plugin has access to the full configuration file through the provided `BaseContext` and can define its own variables to configure its behavior. ::: amqtt.plugins.base.BasePlugin options: show_source: false heading_level: 3 ## Events All plugins are notified of events if the `BasePlugin` subclass implements one or more of these methods: ### Client - `async def on_mqtt_packet_sent(self, *, packet: MQTTPacket[MQTTVariableHeader, MQTTPayload[MQTTVariableHeader], MQTTFixedHeader], session: Session | None = None) -> None` - `async def on_mqtt_packet_received(self, *, packet: MQTTPacket[MQTTVariableHeader, MQTTPayload[MQTTVariableHeader], MQTTFixedHeader], session: Session | None = None) -> None` ### Broker - `async def on_broker_pre_start(self) -> None` - `async def on_broker_post_start(self) -> None` - `async def on_broker_pre_shutdown(self) -> None` - `async def on_broker_post_shutdown(self) -> None` - `async def on_broker_client_connected(self, *, client_id:str, client_session:Session) -> None` - `async def on_broker_client_disconnected(self, *, client_id:str, client_session:Session) -> None` - `async def on_broker_client_connected(self, *, client_id:str) -> None` - `async def on_broker_client_disconnected(self, *, client_id:str) -> None` - `async def on_broker_retained_message(self, *, client_id: str | None, retained_message: RetainedApplicationMessage) -> None` - `async def on_broker_client_subscribed(self, *, client_id: str, topic: str, qos: int) -> None` - `async def on_broker_client_unsubscribed(self, *, client_id: str, topic: str) -> None` - `async def on_broker_message_received(self, *, client_id: str, message: ApplicationMessage) -> None` - `async def on_broker_message_broadcast(self, *, client_id: str, message: ApplicationMessage) -> None` - `async def on_mqtt_packet_sent(self, *, packet: MQTTPacket[MQTTVariableHeader, MQTTPayload[MQTTVariableHeader], MQTTFixedHeader], session: Session | None = None) -> None` - `async def on_mqtt_packet_received(self, *, packet: MQTTPacket[MQTTVariableHeader, MQTTPayload[MQTTVariableHeader], MQTTFixedHeader], session: Session | None = None) -> None` !!! note retained message event if the `client_id` is `None`, the message is retained for a topic if the `retained_message.data` is `None` or empty (`''`), the topic message is being cleared ## Authentication Plugins In addition to receiving any of the event callbacks, a plugin which subclasses from `BaseAuthPlugin` is used by the aMQTT `Broker` to determine if a connection from a client is allowed by implementing the `authenticate` method and returning: - `True` if the session is allowed - `False` if not allowed - `None` if plugin can't determine authentication If there are multiple authentication plugins: - at least one plugin must return `True` to allow access - `False` from any plugin will deny access (i.e. all plugins must return `True` to allow access) - `None` gets ignored from the determination ::: amqtt.plugins.base.BaseAuthPlugin options: show_source: false heading_level: 3 ## Topic Filter Plugins In addition to receiving any of the event callbacks, a plugin which is subclassed from `BaseTopicPlugin` is used by the aMQTT `Broker` to determine if a connected client can send (PUBLISH), receive (RECEIVE) and/or subscribe (SUBSCRIBE) messages to a particular topic by implementing the `topic_filtering` method and returning: - `True` if topic is allowed - `False` if not allowed - `None` will be ignored If there are multiple topic plugins: - at least one plugin must return `True` to allow access - `False` from any plugin will deny access (i.e. all plugins must return `True` to allow access) - `None` will be ignored ::: amqtt.plugins.base.BaseTopicPlugin options: show_source: false heading_level: 3 !!! note A custom plugin class can subclass from both `BaseAuthPlugin` and `BaseTopicPlugin` as long it defines both the `authenticate` and `topic_filtering` method. Yakifo-amqtt-2637127/docs/plugins/http.md000066400000000000000000000101221504664204300201670ustar00rootroot00000000000000# Authentication & Authorization via external HTTP server If clients accessing the broker are managed by another application, it can implement API endpoints that respond with information about client authentication and/or topic-level authorization. - `amqtt.contrib.http.UserAuthHttpPlugin` (client authentication) - `amqtt.contrib.http.TopicAuthHttpPlugin` (topic authorization) Configuration of these plugins is identical (except for the uri name) so that they can be used independently, if desired. # User Auth See the [Request and Response Modes](#request-response-modes) section below for details on `params_mode` and `response_mode`. !!! info "browser-based mqtt over websockets" One of the primary use cases for this plugin is to enable browser-based applications to communicate with mqtt over websockets. !!! warning Care must be taken to make sure the mqtt password is secure (encrypted). For more implementation information: ??? info "recipe for authentication" Provide the client id and username when webpage is initially rendered or passed to the mqtt initialization from stored cookies. If application is secure, the user's password will already be stored as a hashed value and, therefore, cannot be used in this context to authenticate a client. Instead, the application should create its own encrypted key (eg jwt) which the server can then verify when the broker contacts the application. ??? example "mqtt in javascript" Example initialization of mqtt in javascript: import mqtt from 'mqtt'; const url = 'https://path.to.amqtt.broker'; const options = { 'myclientid', connectTimeout: 30000, username: 'myclientid', password: '' // encrypted password }; try { const clientMqtt = await mqtt.connect(url, options); ::: amqtt.contrib.http.UserAuthHttpPlugin.Config options: show_source: false heading_level: 4 extra: class_style: "simple" # Topic ACL See the [Request and Response Modes](#request-response-modes) section below for details on `params_mode` and `response_mode`. ::: amqtt.contrib.http.TopicAuthHttpPlugin.Config options: show_source: false heading_level: 4 extra: class_style: "simple" [//]: # (manually creating the heading so it doesn't show in the sidebar ToC) [](){#request-response-modes}

Request and Response Modes

Each URI endpoint will receive different information in order to determine authentication and authorization; format will depend on `params_mode` configuration attribute (`json` or `form`).: *For user authentication, the request will contain:* - username *(str)* - password *(str)* - client_id *(str)* *For acl check, the request will contain:* - username *(str)* - client_id *(str)* - topic *(str)* - acc *(int)* : client can receive (1), can publish(2), can receive & publish (3) and can subscribe (4) All endpoints should respond with the following, dependent on `response_mode` configuration attribute: *In `status` mode:* - status code: 2xx (granted) or 4xx(denied) or 5xx (noop) !!! note "5xx response" **noop** (no operation): plugin will not participate in the operation and will defer to another plugin to determine access. if there is no other auth/filtering plugin, access will be denied. *In `json` mode:* - status code: 2xx - content-type: application/json - response: {'ok': True } (granted) or {'ok': False, 'error': 'optional error message' } (denied) or { 'error': 'optional error message' } (noop) !!! note "excluded 'ok' key" **noop** (no operation): plugin will not participate in the operation and will defer to another plugin to determine access. if there is no other auth/filtering plugin, access will be denied. *In `text` mode:* - status code: 2xx - content-type: text/plain - response: 'ok' or 'error' !!! note "noop not supported" in text mode, noop (no operation) is not supportedYakifo-amqtt-2637127/docs/plugins/jwt.md000066400000000000000000000025631504664204300200260ustar00rootroot00000000000000# Authentication & Authorization from JWT - `amqtt.contrib.jwt.UserAuthJwtPlugin` (client authentication) - `amqtt.contrib.jwt.TopicAuthJwtPlugin` (topic authorization) Plugin to determine user authentication and topic authorization based on claims in a JWT. # User Authentication For auth, the JWT should include a key as specified in the configuration as `user_clam`: ```python from datetime import datetime, UTC, timedelta claims = { "username": "example_user", "exp": datetime.now(UTC) + timedelta(hours=1), } ``` ::: amqtt.contrib.jwt.UserAuthJwtPlugin.Config options: show_source: false heading_level: 4 extra: class_style: "simple" # Topic Authorization For authorizing a client for certain topics, the token should also include claims for publish, subscribe and receive; keys based on how `publish_claim`, `subscribe_claim` and `receive_claim` are specified in the plugin's configuration. ```python from datetime import datetime, UTC, timedelta claims = { "username": "example_user", "exp": datetime.now(UTC) + timedelta(hours=1), "publish_acl": ['my/topic/#', 'my/+/other'], "subscribe_acl": ['my/+/other'], "receive_acl": ['#'] } ``` ::: amqtt.contrib.jwt.TopicAuthJwtPlugin.Config options: show_source: false heading_level: 4 extra: class_style: "simple" Yakifo-amqtt-2637127/docs/plugins/ldap.md000066400000000000000000000012351504664204300201350ustar00rootroot00000000000000# Authentication with LDAP Server If clients accessing the broker are managed by an LDAP server, this plugin can verify credentials for client authentication and/or topic-level authorization. - `amqtt.contrib.ldap.UserAuthLdapPlugin` (client authentication) - `amqtt.contrib.ldap.TopicAuthLdapPlugin` (topic authorization) Authenticate a user with an LDAP directory server. # User Auth ::: amqtt.contrib.ldap.UserAuthLdapPlugin.Config options: heading_level: 4 extra: class_style: "simple" # Topic Auth (ACL) ::: amqtt.contrib.ldap.TopicAuthLdapPlugin.Config options: heading_level: 4 extra: class_style: "simple" Yakifo-amqtt-2637127/docs/plugins/packaged_plugins.md000066400000000000000000000153651504664204300225260ustar00rootroot00000000000000# Packaged Plugins With the aMQTT plugins framework, one can add additional functionality without having to rewrite core logic in the broker or client. Plugins can be loaded and configured using the `plugins` section of the config file (or parameter passed to the class). ## Broker By default, `EventLoggerPlugin`, `PacketLoggerPlugin`, `AnonymousAuthPlugin` and `BrokerSysPlugin` are activated and configured for the broker: ```yaml --8<-- "amqtt/scripts/default_broker.yaml" ``` ??? warning "Loading plugins from EntryPoints in `pyproject.toml` has been deprecated" Previously, all plugins were loaded from EntryPoints: ```toml --8<-- "pyproject.toml:included" ``` But the previous default config only caused 4 plugins to be active: ```yaml --8<-- "samples/legacy.yaml" ``` ## Client By default, the `PacketLoggerPlugin` is activated and configured for the client: ```yaml --8<-- "amqtt/scripts/default_client.yaml" ``` ## Plugins ### Anonymous (Auth Plugin) `amqtt.plugins.authentication.AnonymousAuthPlugin` Authentication plugin allowing anonymous access. ::: amqtt.plugins.authentication.AnonymousAuthPlugin.Config options: heading_level: 4 extra: class_style: "simple" !!! danger even if `allow_anonymous` is set to `false`, the plugin will still allow access if a username is provided by the client ??? warning "EntryPoint-style configuration is deprecated" ```yaml auth: plugins: - auth_anonymous allow-anonymous: true # if false, providing a username will allow access ``` ### Password File (Auth Plugin) `amqtt.plugins.authentication.FileAuthPlugin` Authentication plugin based on a file-stored user database. ::: amqtt.plugins.authentication.FileAuthPlugin.Config options: heading_level: 4 extra: class_style: "simple" ??? warning "EntryPoint-style configuration is deprecated" ```yaml auth: plugins: - auth_file password-file: /path/to/password_file ``` **File Format** The file includes `username:password` pairs, one per line. The password should be encoded using sha-512 with `mkpasswd -m sha-512` or: ```python import sys from getpass import getpass from passlib.hash import sha512_crypt passwd = input() if not sys.stdin.isatty() else getpass() print(sha512_crypt.hash(passwd)) ``` ### Taboo (Topic Plugin) `amqtt.plugins.topic_checking.TopicTabooPlugin` Prevents using topics named: `prohibited`, `top-secret`, and `data/classified` **Configuration** ```yaml plugins: amqtt.plugins.topic_checking.TopicTabooPlugin: ``` ??? warning "EntryPoint-style configuration is deprecated" ```yaml topic-check: enabled: true plugins: - topic_taboo ``` ### ACL (Topic Plugin) `amqtt.plugins.topic_checking.TopicAccessControlListPlugin` **Configuration** Each acl category are a list a key-value pair, where: > `:["", "", ...]` *(string, list[string])*: username of the client followed by a list of allowed topics (wildcards are supported: `#`, `+`). !!! info "`#` and `$SYS` topics" Per the MQTT 3.1.1 spec 4.7.2, a single `#` will not allow access to `$` broker topics; need to additionally specify `$SYS/#` to allow a client full access subscribe & receive. Also MQTT spec prevents clients from publishing to topics starting with `$`; these will be ignored. If set to `None`, no restrictions are placed on client subscriptions (legacy behavior). An empty list will block clients from using any topics. - `subscribe-acl` *(mapping)*: determines subscription access. - `acl` *(mapping)*: Deprecated and replaced by `subscribe-acl`. - `publish-acl` *(mapping)*: determines publish access. - `receive-acl` *(mapping)*: determines if a message can be sent to a client. !!! info "Reserved usernames" - The username `admin` is allowed access to all topics. - The username `anonymous` will control allowed topics, if using the `auth_anonymous` plugin. ```yaml plugins: amqtt.plugins.topic_checking.TopicAccessControlListPlugin: acl: - username: ["list", "of", "allowed", "topics", "for", "subscribing"] - . publish_acl: - username: ["list", "of", "allowed", "topics", "for", "publishing"] - . ``` ??? warning "EntryPoint-style configuration is deprecated" ```yaml topic-check: enabled: true plugins: - topic_acl publish-acl: - username: ["list", "of", "allowed", "topics", "for", "publishing"] - . acl: - username: ["list", "of", "allowed", "topics", "for", "subscribing"] - . ``` ### $SYS topics `amqtt.plugins.sys.broker.BrokerSysPlugin` Publishes, on a periodic basis, statistics about the broker **Configuration** - `sys_interval` - int, seconds between updates (default: 20) ```yaml plugins: amqtt.plugins.sys.broker.BrokerSysPlugin: sys_interval: 20 # int, seconds between updates ``` **Supported Topics** - `$SYS/broker/version` *(string)* - `$SYS/broker/load/bytes/received` *(int)* - `$SYS/broker/load/bytes/sent` *(int)* - `$SYS/broker/messages/received` *(int)* - `$SYS/broker/messages/sent` *(int)* - `$SYS/broker/time` *(int, current time in epoch seconds)* - `$SYS/broker/uptime` *(int, seconds since broker start)* - `$SYS/broker/uptime/formatted` *(string, start time of broker in UTC)* - `$SYS/broker/clients/connected` *(int, number of currently connected clients)* - `$SYS/broker/clients/disconnected` *(int, number of clients that have disconnected)* - `$SYS/broker/clients/maximum` *(int, maximum number of clients connected)* - `$SYS/broker/clients/total` *(int)* - `$SYS/broker/messages/inflight` *(int)* - `$SYS/broker/messages/inflight/in` *(int)* - `$SYS/broker/messages/inflight/out` *(int)* - `$SYS/broker/messages/inflight/stored` *(int)* - `$SYS/broker/messages/publish/received` *(int)* - `$SYS/broker/messages/publish/sent` *(int)* - `$SYS/broker/messages/retained/count` *(int)* - `$SYS/broker/messages/subscriptions/count` *(int)* - `$SYS/broker/heap/size` *(float, MB)* - `$SYS/broker/heap/maximum` *(float, MB)* - `$SYS/broker/cpu/percent` *(float, %)* - `$SYS/broker/cpu/maximum` *(float, %)* ### Event Logger `amqtt.plugins.logging_amqtt.EventLoggerPlugin` This plugin issues log messages when [broker and mqtt events](custom_plugins.md#events) are triggered: - info level messages for `client connected` and `client disconnected` - debug level for all others ```yaml plugins: amqtt.plugins.logging_amqtt.EventLoggerPlugin: ``` ### Packet Logger `amqtt.plugins.logging_amqtt.PacketLoggerPlugin` This plugin issues debug-level messages for [mqtt events](custom_plugins.md#events): `on_mqtt_packet_sent` and `on_mqtt_packet_received`. ```yaml plugins: amqtt.plugins.logging_amqtt.PacketLoggerPlugin: ``` Yakifo-amqtt-2637127/docs/plugins/session.md000066400000000000000000000005261504664204300207020ustar00rootroot00000000000000# Session Persistence `amqtt.contrib.persistence.SessionDBPlugin` Plugin to store session information and retained topic messages in the event that the broker terminates abnormally. ::: amqtt.contrib.persistence.SessionDBPlugin.Config options: show_source: false heading_level: 4 extra: class_style: "simple" Yakifo-amqtt-2637127/docs/plugins/shadows.md000066400000000000000000000270171504664204300206730ustar00rootroot00000000000000# Device Shadows Plugin Device shadows provide a persistent, cloud-based representation of the state of a device, even when the device is offline. This plugin tracks the desired and reported state of a client and provides MQTT topic-based communication channels to retrieve and update a shadow. Typically, this structure is used for MQTT IoT devices to communicate with a central application. This plugin is patterned after [AWS's IoT Shadow](https://docs.aws.amazon.com/iot/latest/developerguide/iot-device-shadows.html) service. ## How it works All shadow states are associated with a `device id` and `name` and have the following structure: ```json { "state": { "desired": { "property1": "value1" }, "reported": { "property1": "value1" } }, "metadata": { "desired": { "property1": { "timestamp": 1623855600 } }, "reported": { "property1": { "timestamp": 1623855602 } } }, "version": 10, "timestamp": 1623855602 } ``` The `state` is updated by messages to shadow topics and includes key/value pairs, where the value can be any valid json object (int, string, dictionary, list, etc). `metadata` is automatically updated by the plugin based on when the key/values were most recently updated. Both `state` and `metadata` are split between: - desired: the intended state of a device - reported: the actual state of a device A client can update a part or all of the desired or reported state. On any update, the plugin: - updates the 'state' portion of the shadow with any key/values provided in the update - stores a version of the update - tracks the timestamp of each key/value pair change - sends messages that the shadow was updated ## Typical usage As mentioned above, this plugin is often used for MQTT IoT devices to communicate with a central application. The app pushes updates to a device's 'desired' shadow state and the device can confirm the change was made by updating the 'reported' state. With this sequence the 'desired' state matches the 'reported' state and the delta message is empty. In most situations, the app only updates the 'desired' state and the device only updates the 'reported' state. If online, the IoT device will receive and can act on that information immediately. If offline, the app doesn't need to republish or retry a change 'command', waiting for an acknowledgement from the device. If a device is offline, it simply retrieves the configuration changes when it comes back online. Once a device receives its desired state, it should either (1) update its reported state to match the change in desired or (2) if the desired state is invalid, clear that key/value from the desired state. The latter is the only case when a device should update its own 'desired' state. For example, if the app sends a command to set the brightness of a device to 100 lumens, but the device only supports a maximum of 80, it can send an update `'state': {'desired': {'lumens': null}}` to clear the invalid state. The reported state can (and most likely will) include key/values that will never show up in the desired state. For example, the app might set the thermostat to 70 and the device reports both the configuration change of 70 to the thermostat *and* the current temperature of the room. ```json { "state": { "desired": { "thermostat": 68 }, "reported": { "thermostat": 68, "temperature": 78 } } } ``` !!! note "desired and reported state structure" It is important that both the app and the device have the same understanding of the key/value state structure and units. Creating [JSON schemas](https://json-schema.org/) for desired and reported shadow states are very useful as it can provide a clear way of describing the schema. These schemas can also be used to generate [dataclasses](https://pypi.org/project/datamodel-code-generator/), [pojos](https://github.com/joelittlejohn/jsonschema2pojo) or [many other language constructs](https://json-schema.org/tools?query=&sortBy=name&sortOrder=ascending&groupBy=toolingTypes&licenses=&languages=&drafts=&toolingTypes=&environments=&showObsolete=false&supportsBowtie=false#schema-to-code) that can be easily included by both app and device to make state encoding and decoding consistent. ## Shadow state access All shadows are addressed by using specific topics, all of which have the following base: `$shadow//` Clients send either `get` or `update` messages: | Operation | Topic | Direction | Payload | |-------------------------|-------------------------------------------------------|-----------|-----------------------------------------------------| | **Update** | `$shadow/{device_id}/{shadow_name}/update` | β†’ | `{ "state": { "desired" or "reported": ... } }` | | **Get** | `$shadow/{device_id}/{shadow_name}/get` | β†’ | Empty message triggers get accepted or rejected | Then clients can subscribe to any or all of these topics which receive messages issued by the plugin: | Operation | Topic | Direction | Payload | |-------------------------|-------------------------------------------------------|-----------|-----------------------------------------------------| | **Update Accepted** | `$shadow/{device_id}/{shadow_name}/update/accepted` | ← | Full updated document | | **Update Rejected** | `$shadow/{device_id}/{shadow_name}/update/rejected` | ← | Error message | | **Update Documents** | `$shadow/{device_id}/{shadow_name}/update/documents` | ← | Full current & previous shadow documents | | **Get Accepted** | `$shadow/{device_id}/{shadow_name}/get/accepted` | ← | Full shadow document | | **Get Rejected** | `$shadow/{device_id}/{shadow_name}/get/rejected` | ← | Error message | | **Delta** | `$shadow/{device_id}/{shadow_name}/update/delta` | ← | Difference between desired and reported | | **Iota** | `$shadow/{device_id}/{shadow_name}/update/iota` | ← | Difference between desired and reported, with nulls | ## Delta messages While the 'accepted' and 'documents' messages carry the full desired and reported states, this plugin also generates a 'delta' message - containing items in the desired state that are different from those items in the reported state. This topic optimizes for IoT devices which typically have lower bandwidth and not as powerful processing by (1) to reducing the amount of data transmitted and (2) simplifying device implementation as it only needs to respond to differences. While shadows are stateful since delta messages are only based on the desired and reported state and *not on the previous and current state*. Therefore, it doesn't matter if an IoT device is offline and misses a delta message. When it comes back online, the delta is identical. This is also an improvement over a connection without the clean flag and QoS > 0. When an IoT device comes back online, bandwidth isn't consumed and the IoT device does not have to process a backlog of messages to understand how it should behave. For a setting -- such as volume -- that goes from 80 then to 91 and then to 60 while the device is offline, it will only receive a single change that its volume should now be 60. | Reported Shadow State | Desired Shadow State | Resulting Delta Message (`delta`) | |----------------------------------------|------------------------------------------|---------------------------------------| | `{ "temperature": 70 }` | `{ "temperature": 72 }` | `{ "temperature": 72 }` | | `{ "led": "off", "fan": "low" }` | `{ "led": "on", "fan": "low" }` | `{ "led": "on" }` | | `{ "door": "closed" }` | `{ "door": "closed", "alarm": "armed" }` | `{ "alarm": "armed" }` | | `{ "volume": 10 }` | `{ "volume": 10 }` | *(no delta; states match)* | | `{ "brightness": 100 }` | `{ "brightness": 80, "mode": "eco" }` | `{ "brightness": 80, "mode": "eco" }` | | `{ "levels": [1, 10, 4]}` | `{"levels": [1, 4, 10]}` | `{"levels": [1, 4, 10]}` | | `{ "brightness": 100, "mode": "eco" }` | `{ "brightness": 80 }` | `{ "brightness": 80}` | ## Iota messages Typically, null values never show in any received update message as a null signals the removal of a key from the desired or reported state. However, if the app removes a key from the desired state -- such as a piece of state that is no longer needed or applicable -- the device won't receive any notification of this deletion in a delta messages. These messages are very similar to 'delta' messages as they also contain items in the desired state that are different from those in the reported state; it *also* contains any items in the reported state that are *missing* from the desired state (last row in table). | Reported Shadow State | Desired Shadow State | Resulting Delta Message (`delta`) | |----------------------------------------|------------------------------------------|-----------------------------------------| | `{ "temperature": 70 }` | `{ "temperature": 72 }` | `{ "temperature": 72 }` | | `{ "led": "off", "fan": "low" }` | `{ "led": "on", "fan": "low" }` | `{ "led": "on" }` | | `{ "door": "closed" }` | `{ "door": "closed", "alarm": "armed" }` | `{ "alarm": "armed" }` | | `{ "volume": 10 }` | `{ "volume": 10 }` | *(no delta; states match)* | | `{ "brightness": 100 }` | `{ "brightness": 80, "mode": "eco" }` | `{ "brightness": 80, "mode": "eco" }` | | `{ "levels": [1, 10, 4]}` | `{"levels": [1, 4, 10]}` | `{"levels": [1, 4, 10]}` | | `{ "brightness": 100, "mode": "eco" }` | `{ "brightness": 80 }` | `{ "brightness": 80, "mode": null }` | ## Configuration ::: amqtt.contrib.shadows.ShadowPlugin.Config options: show_source: false heading_level: 4 extra: class_style: "simple" ## Security Often a device only needs access to get/update and receive changes in its own shadow state. In addition to the `ShadowPlugin`, included is the `ShadowTopicAuthPlugin`. This allows (authorizes) a device to only subscribe, publish and receive its own topics. ::: amqtt.contrib.shadows.ShadowTopicAuthPlugin.Config options: show_source: false heading_level: 4 extra: class_style: "simple" !!! warning `ShadowTopicAuthPlugin` only handles topic authorization. Another plugin should be used to authenticate client device connections to the broker. See [file auth](packaged_plugins.md#password-file-auth-plugin), [http auth](http.md), [db auth](auth_db.md) or [certificate auth](cert.md) plugins. Or create your own: [auth plugins](custom_plugins.md#authentication-plugins): Yakifo-amqtt-2637127/docs/quickstart.md000066400000000000000000000061321504664204300177270ustar00rootroot00000000000000# Quickstart `aMQTT` is an open source `MQTT` client and broker implementation; these can be integrated into other projects using the appropriate APIs. To get started, three command-line scripts are also installed along with the package: - `amqtt` - the MQTT broker - `amqtt_pub` - an MQTT client to publish messages - `amqtt_sub` - an MQTT client to listen for messages To install the `aMQTT` package: ```bash (venv) $ pip install amqtt ``` ## Running a broker `amqtt` is a command-line tool for the MQTT 3.1.1 compliant broker: ```bash $ amqtt [2015-11-06 22:45:16,470] :: INFO - Listener 'default' bind to 0.0.0.0:1883 (max_connections=-1) ``` See [amqtt reference documentation](references/amqtt.md) for details about available options and settings. ## Publishing messages `amqtt_pub` is a command-line tool which can be used for publishing some messages on a topic. It requires a few arguments like broker URL, topic name, QoS and data to send. Additional options allow more complex use case. Publishing `some_data` to as `/test` topic using a TCP connection to `test.mosquitto.org`: ```bash $ amqtt_pub --url mqtt://test.mosquitto.org -t test -m some_data [2015-11-06 22:21:55,108] :: INFO - amqtt_pub/5135-myhostname.local Connecting to broker [2015-11-06 22:21:55,333] :: INFO - amqtt_pub/5135-myhostname.local Publishing to 'test' [2015-11-06 22:21:55,336] :: INFO - amqtt_pub/5135-myhostname.local Disconnected from broker ``` Websocket connections are also supported: ```bash $ amqtt_pub --url ws://test.mosquitto.org:8080 -t test -m some_data [2015-11-06 22:22:42,542] :: INFO - amqtt_pub/5157-myhostname.local Connecting to broker [2015-11-06 22:22:42,924] :: INFO - amqtt_pub/5157-myhostname.local Publishing to 'test' [2015-11-06 22:22:52,926] :: INFO - amqtt_pub/5157-myhostname.local Disconnected from broker ``` Additionally, TCP connections can be secured via TLS and websockets via SSL. `amqtt_pub` can read from file or stdin and use data read as message payload: ```bash $ some_command | amqtt_pub --url mqtt://localhost -t test -l ``` See [amqtt_pub reference documentation](references/amqtt_pub.md) for details about available options and settings. ## Subscribing a topic `amqtt_sub` is a command-line tool which can be used to subscribe to a specific topics or a topic patterns. Subscribe to the `my/test` topic and the `test/#` topic pattern: ```bash $ amqtt_sub --url mqtt://localhost -t my/test -t test/# ``` This will run and print messages to standard output; it can be stopped by ^C. See [amqtt_sub reference documentation](references/amqtt_sub.md) for details about available options and settings. ## URL Scheme aMQTT command line tools use the `--url` to establish a network connection with the broker. It follows Python's [urlparse](https://docs.python.org/3/library/urllib.parse.html) structure but also adds the [mqtt scheme](https://github.com/mqtt/mqtt.org/wiki/URI-Scheme). ``` [mqtt|ws][s]://[username][:password]@host.domain[:port] ``` Here are some examples: ``` mqtt://localhost mqtt://localhost:1884 mqtt://user:password@localhost ws://test.mosquitto.org wss://user:password@localhost ``` Yakifo-amqtt-2637127/docs/references/000077500000000000000000000000001504664204300173325ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/references/amqtt.md000066400000000000000000000006131504664204300210020ustar00rootroot00000000000000# Broker ::: mkdocs-typer2 :module: amqtt.scripts.broker_script :name: amqtt :pretty: true ## Configuration Without the `-c` argument, the broker will run with the following, default configuration: ```yaml --8<-- "amqtt/scripts/default_broker.yaml" ``` Using the `-c` argument allows for configuration with a YAML structured file; see [broker configuration](broker_config.md). Yakifo-amqtt-2637127/docs/references/amqtt_pub.md000066400000000000000000000025611504664204300216540ustar00rootroot00000000000000# ::: mkdocs-typer2 :module: amqtt.scripts.pub_script :name: amqtt_pub :pretty: true ## Default Configuration Without the `-c` argument, the client will run with the following, default configuration: ```yaml --8<-- "amqtt/scripts/default_client.yaml" ``` Using the `-c` argument allows for configuration with a YAML structured file; see [client configuration](client_config.md). ## Examples Publish temperature information to localhost with QoS 1: ```bash amqtt_pub --url mqtt://localhost -t sensors/temperature -m 32 -q 1 ``` Publish timestamp and temperature information to a remote host on a non-standard port and QoS 0: ```bash amqtt_pub --url mqtt://192.168.1.1:1885 -t sensors/temperature -m "1266193804 32" ``` Publish light switch status. Message is set to retained because there may be a long period of time between light switch events: ```bash amqtt_pub --url mqtt://localhost -r -t switches/kitchen_lights/status -m "on" ``` Send the contents of a file in two ways: ```bash amqtt_pub --url mqtt://localhost -t my/topic -f ./data amqtt_pub --url mqtt://localhost -t my/topic -s < ./data ``` Publish temperature information to localhost with QoS 1 over mqtt encapsulated in a websocket connection and additional headers: ```bash amqtt_pub --url wss://localhost -t sensors/temperature -m 32 -q 1 --extra-headers '{"Authorization": "Bearer "}' ``` Yakifo-amqtt-2637127/docs/references/amqtt_sub.md000066400000000000000000000015741504664204300216620ustar00rootroot00000000000000# ::: mkdocs-typer2 :module: amqtt.scripts.sub_script :name: amqtt_pub :pretty: true ## Default Configuration Without the `-c` argument, the client will run with the following, default configuration: ```yaml --8<-- "amqtt/scripts/default_client.yaml" ``` Using the `-c` argument allows for configuration with a YAML structured file; see [client configuration](client_config.md). ## Examples Subscribe with QoS 0 to all messages published under $SYS/: ```bash amqtt_sub --url mqtt://localhost -t '$SYS/#' -q 0 ``` Subscribe to 10 messages with QoS 2 from /#: ```bash amqtt_sub --url mqtt://localhost -t # -q 2 -n 10 ``` Subscribe with QoS 0 to all messages published under $SYS/ over mqtt encapsulated in a websocket connection and additional headers: ```bash amqtt_sub --url wss://localhost -t '$SYS/#' -q 0 --extra-headers '{"Authorization": "Bearer "}' ``` Yakifo-amqtt-2637127/docs/references/broker.md000066400000000000000000000012251504664204300211400ustar00rootroot00000000000000# Broker API reference The `amqtt.broker.Broker` class provides a complete MQTT 3.1.1 broker implementation. This class allows Python developers to embed a MQTT broker in their own applications. ## Usage example The following example shows how to start a broker using the default configuration: ```python --8<-- "samples/broker_simple.py" ``` This will start the broker and let it run until it is shutdown by `^c`. ## Reference ### Broker API The `amqtt.broker` module provides the following key methods in the `Broker` class: - `start()`: Starts the broker and begins serving - `shutdown()`: Gracefully shuts down the broker ::: amqtt.broker.Broker Yakifo-amqtt-2637127/docs/references/broker_config.md000066400000000000000000000137561504664204300225010ustar00rootroot00000000000000# Broker Configuration This configuration structure is a `amqtt.contexts.BrokerConfig` or a python dictionary with the same structure when instantiating `amqtt.broker.Broker` or as a yaml formatted file passed to the `amqtt` script. If not specified, the `Broker()` will be started with the default `BrokerConfig()`, as represented in yaml format: ```yaml --- listeners: default: type: tcp bind: 0.0.0.0:1883 timeout_disconnect_delay: 0 plugins: amqtt.plugins.logging_amqtt.EventLoggerPlugin: amqtt.plugins.logging_amqtt.PacketLoggerPlugin: amqtt.plugins.authentication.AnonymousAuthPlugin: allow_anonymous: true amqtt.plugins.sys.broker.BrokerSysPlugin: sys_interval: 20 ``` ::: amqtt.contexts.BrokerConfig options: heading_level: 3 extra: class_style: "simple" ??? warning "Deprecated: `auth` configuration settings" **`auth`** Configuration for authentication behaviour: - `plugins` *(list[string])*: defines the list of plugins which are activated as authentication plugins. !!! note Plugins used here must first be defined in the `amqtt.broker.plugins` [entry point](https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/#using-package-metadata). !!! warning If `plugins` is omitted from the `auth` section, all plugins listed in the `amqtt.broker.plugins` entrypoint will be enabled for authentication, including _allowing anonymous login._ `plugins: []` will deny connections from all clients. - `allow-anonymous` *(bool)*: `True` will allow anonymous connections, used by `amqtt.plugins.authentication.AnonymousAuthPlugin`. !!! danger `False` does not disable the `auth_anonymous` plugin; connections will still be allowed as long as a username is provided. If security is required, do not include `auth_anonymous` in the `plugins` list. - `password-file` *(string)*. Path to sha-512 encoded password file, used by `amqtt.plugins.authentication.FileAuthPlugin`. ??? warning "Deprecated: `sys_interval` " **`sys_interval`** *(int)* System status report interval in seconds, used by the `amqtt.plugins.sys.broker.BrokerSysPlugin`. ??? warning "Deprecated: `topic-check` configuration settings" **`topic-check`** Configuration for access control policies for publishing and subscribing to topics: - `enabled` *(bool)*: Enable access control policies (`true`). `false` will allow clients to publish and subscribe to any topic. - `plugins` *(list[string])*: defines the list of plugins which are activated as access control plugins. Note the plugins must be defined in the `amqtt.broker.plugins` [entry point](https://pythonhosted.org/setuptools/setuptools.html#dynamic-discovery-of-services-and-plugins). - `acl` *(list)*: plugin to determine subscription access; if `publish-acl` is not specified, determine both publish and subscription access. The list should be a key-value pair, where: `:[, , ...]` *(string, list[string])*: username of the client followed by a list of allowed topics (wildcards are supported: `#`, `+`). *used by the `amqtt.plugins.topic_acl.TopicAclPlugin`* - `publish-acl` *(list)*: plugin to determine publish access. This parameter defines the list of access control rules; each item is a key-value pair, where: `:[, , ...]` *(string, list[string])*: username of the client followed by a list of allowed topics (wildcards are supported: `#`, `+`). _Reserved usernames (used by the `amqtt.plugins.topic_acl.TopicAclPlugin`)_ - The username `admin` is allowed access to all topic. - The username `anonymous` will control allowed topics if using the `auth_anonymous` plugin. ::: amqtt.contexts.ListenerConfig options: heading_level: 3 extra: class_style: "simple" ## Example When a configuration is passed to the `amqtt` script, here is the equivalent format based on the structures above: ```yaml listeners: default: max-connections: 500 type: tcp my-tcp-1: bind: 127.0.0.1:1883 my-tcp-2: bind: 1.2.3.4:1884 max-connections: 1000 my-tcp-ssl-1: bind: 127.0.0.1:8885 ssl: on cafile: /some/cafile capath: /some/folder capath: 'certificate data' certfile: /some/certfile keyfile: /some/keyfile my-ws-1: bind: 0.0.0.0:8080 type: ws my-wss-1: bind: 0.0.0.0:9003 type: ws ssl: on certfile: /some/certfile keyfile: /some/keyfile timeout-disconnect-delay: 2 plugins: - amqtt.plugins.authentication.AnonymousAuthPlugin: allow-anonymous: true - amqtt.plugin.authentication.FileAuthPlugin: password-file: /some/password-file - amqtt.plugins.topic_checking.TopicAccessControlListPlugin: acl: username1: ['repositories/+/master', 'calendar/#', 'data/memes'] username2: ['calendar/2025/#', 'data/memes'] anonymous: ['calendar/2025/#'] ``` This configuration file would create the following listeners: - `my-tcp-1`: an unsecured TCP listener on port 1883 allowing `500` clients connections simultaneously - `my-tcp-2`: an unsecured TCP listener on port 1884 allowing `1000` client connections - `my-tcp-ssl-1`: a secured TCP listener on port 8883 allowing `500` clients connections simultaneously - `my-ws-1`: an unsecured websocket listener on port 9001 allowing `500` clients connections simultaneously - `my-wss-1`: a secured websocket listener on port 9003 allowing `500` And enable the following topic access: - `username1` to login and subscribe/publish to topics `repositories/+/master`, `calendar/#` and `data/memes` - `username2` to login and subscribe/publish to topics `calendar/2025/#` and `data/memes` - any user not providing credentials (`anonymous`) can only subscribe/publish to `calendar/2025/#` Yakifo-amqtt-2637127/docs/references/client.md000066400000000000000000000162611504664204300211400ustar00rootroot00000000000000# MQTTClient API The `amqtt.client.MQTTClient` class implements the client part of MQTT protocol. It can be used to publish and/or subscribe MQTT message on a broker accessible on the network through TCP or websocket protocol, both secured or unsecured. ## Usage examples ### Subscriber The example below shows how to write a simple MQTT client which subscribes a topic and prints every messages received from the broker: ```python import logging import asyncio from amqtt.client import MQTTClient, ClientException from amqtt.mqtt.constants import QOS_1, QOS_2 logger = logging.getLogger(__name__) async def uptime_coro(): C = MQTTClient() await C.connect('mqtt://test.mosquitto.org/') # Subscribe to '$SYS/broker/uptime' with QOS=1 # Subscribe to '$SYS/broker/load/#' with QOS=2 await C.subscribe([ ('$SYS/broker/uptime', QOS_1), ('$SYS/broker/load/#', QOS_2), ]) try: for i in range(1, 100): message = await C.deliver_message() packet = message.publish_packet print("%d: %s => %s" % (i, packet.variable_header.topic_name, str(packet.payload.data))) await C.unsubscribe(['$SYS/broker/uptime', '$SYS/broker/load/#']) await C.disconnect() except ClientException as ce: logger.error("Client exception: %s" % ce) if __name__ == '__main__': formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) asyncio.get_event_loop().run_until_complete(uptime_coro()) ``` When executed, this script gets the default event loop and asks it to run the `uptime_coro` until it completes. `uptime_coro` starts by initializing a `MQTTClient` instance. The coroutine then calls `connect()` to connect to the broker, here `test.mosquitto.org`. Once connected, the coroutine subscribes to some topics, and then wait for 100 messages. Each message received is simply written to output. Finally, the coroutine unsubscribes from topics and disconnects from the broker. ### Publisher The example below uses the `MQTTClient` class to implement a publisher. This test publish 3 messages asynchronously to the broker on a test topic. For the purposes of the test, each message is published with a different Quality Of Service. This example also shows two methods for publishing messages asynchronously. ```python import logging import asyncio from amqtt.client import MQTTClient from amqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 logger = logging.getLogger(__name__) async def test_coro(): C = MQTTClient() await C.connect('mqtt://test.mosquitto.org/') tasks = [ asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_0')), asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=QOS_1)), asyncio.ensure_future(C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=QOS_2)), ] await asyncio.wait(tasks) logger.info("messages published") await C.disconnect() async def test_coro2(): try: C = MQTTClient() ret = await C.connect('mqtt://test.mosquitto.org:1883/') message = await C.publish('a/b', b'TEST MESSAGE WITH QOS_0', qos=QOS_0) message = await C.publish('a/b', b'TEST MESSAGE WITH QOS_1', qos=QOS_1) message = await C.publish('a/b', b'TEST MESSAGE WITH QOS_2', qos=QOS_2) #print(message) logger.info("messages published") await C.disconnect() except ConnectException as ce: logger.error("Connection failed: %s" % ce) asyncio.get_event_loop().stop() if __name__ == '__main__': formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) asyncio.get_event_loop().run_until_complete(test_coro()) asyncio.get_event_loop().run_until_complete(test_coro2()) ``` As usual, the script runs the publish code through the async loop. `test_coro()` and `test_coro2()` are ran in sequence. Both do the same job. `test_coro()` publishes 3 messages in sequence. `test_coro2()` publishes the same message asynchronously. The difference appears when looking at the sequence of MQTT messages sent. `test_coro()` achieves: ``` amqtt/YDYY;NNRpYQSy3?o -out-> PublishPacket(ts=2015-11-11 21:54:48.843901, fixed=MQTTFixedHeader(length=28, flags=0x0), variable=PublishVariableHeader(topic=a/b, packet_id=None), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_0'")) amqtt/YDYY;NNRpYQSy3?o -out-> PublishPacket(ts=2015-11-11 21:54:48.844152, fixed=MQTTFixedHeader(length=30, flags=0x2), variable=PublishVariableHeader(topic=a/b, packet_id=1), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_1'")) amqtt/YDYY;NNRpYQSy3?o <-in-- PubackPacket(ts=2015-11-11 21:54:48.979665, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=1), payload=None) amqtt/YDYY;NNRpYQSy3?o -out-> PublishPacket(ts=2015-11-11 21:54:48.980886, fixed=MQTTFixedHeader(length=30, flags=0x4), variable=PublishVariableHeader(topic=a/b, packet_id=2), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_2'")) amqtt/YDYY;NNRpYQSy3?o <-in-- PubrecPacket(ts=2015-11-11 21:54:49.029691, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=2), payload=None) amqtt/YDYY;NNRpYQSy3?o -out-> PubrelPacket(ts=2015-11-11 21:54:49.030823, fixed=MQTTFixedHeader(length=2, flags=0x2), variable=PacketIdVariableHeader(packet_id=2), payload=None) amqtt/YDYY;NNRpYQSy3?o <-in-- PubcompPacket(ts=2015-11-11 21:54:49.092514, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=2), payload=None) ``` while `test_coro2()` runs: ``` amqtt/LYRf52W[56SOjW04 -out-> PublishPacket(ts=2015-11-11 21:54:48.466123, fixed=MQTTFixedHeader(length=28, flags=0x0), variable=PublishVariableHeader(topic=a/b, packet_id=None), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_0'")) amqtt/LYRf52W[56SOjW04 -out-> PublishPacket(ts=2015-11-11 21:54:48.466432, fixed=MQTTFixedHeader(length=30, flags=0x2), variable=PublishVariableHeader(topic=a/b, packet_id=1), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_1'")) amqtt/LYRf52W[56SOjW04 -out-> PublishPacket(ts=2015-11-11 21:54:48.466695, fixed=MQTTFixedHeader(length=30, flags=0x4), variable=PublishVariableHeader(topic=a/b, packet_id=2), payload=PublishPayload(data="b'TEST MESSAGE WITH QOS_2'")) amqtt/LYRf52W[56SOjW04 <-in-- PubackPacket(ts=2015-11-11 21:54:48.613062, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=1), payload=None) amqtt/LYRf52W[56SOjW04 <-in-- PubrecPacket(ts=2015-11-11 21:54:48.661073, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=2), payload=None) amqtt/LYRf52W[56SOjW04 -out-> PubrelPacket(ts=2015-11-11 21:54:48.661925, fixed=MQTTFixedHeader(length=2, flags=0x2), variable=PacketIdVariableHeader(packet_id=2), payload=None) amqtt/LYRf52W[56SOjW04 <-in-- PubcompPacket(ts=2015-11-11 21:54:48.713107, fixed=MQTTFixedHeader(length=2, flags=0x0), variable=PacketIdVariableHeader(packet_id=2), payload=None) ``` Both coroutines have the same results except that `test_coro2()` manages messages flow in parallel which may be more efficient. ::: amqtt.client.MQTTClient Yakifo-amqtt-2637127/docs/references/client_config.md000066400000000000000000000031531504664204300224610ustar00rootroot00000000000000# Client Configuration This configuration structure is either a `amqtt.contexts.ClientConfig` or a python dictionary with identical structure when instantiating `amqtt.broker.MQTTClient` or as a yaml formatted file passed to the `amqtt_pub` script. If not specified, the `MQTTClient()` will be started with the default `ClientConfig()`, as represented in yaml format: ```yaml --- keep_alive: 10 ping_delay: 1 default_qos: 0 default_retain: false auto_reconnect: true connection_timeout: 60 reconnect_retries: 2 reconnect_max_interval: 10 cleansession: true broker: uri: "mqtt://127.0.0.1" plugins: amqtt.plugins.logging_amqtt.PacketLoggerPlugin: ``` ::: amqtt.contexts.ClientConfig options: heading_level: 3 extra: class_style: "simple" ::: amqtt.contexts.TopicConfig options: heading_level: 3 extra: class_style: "simple" ::: amqtt.contexts.WillConfig options: heading_level: 3 extra: class_style: "simple" ::: amqtt.contexts.ConnectionConfig options: heading_level: 3 extra: class_style: "simple" ## Example A more expansive `ClientConfig` in equivalent yaml format: ```yaml keep_alive: 10 ping_delay: 1 default_qos: 0 default_retain: false auto_reconnect: true reconnect_max_interval: 5 reconnect_retries: 10 topics: topic/subtopic: qos: 0 topic/other: qos: 2 retain: true will: topic: will/messages message: "client ABC has disconnected" qos: 1 retain: false broker: uri: 'mqtt://localhost:1883' cafile: '/path/to/ca/file' plugins: - amqtt.plugins.logging_amqtt.PacketLoggerPlugin: ``` Yakifo-amqtt-2637127/docs/references/common.md000066400000000000000000000011011504664204300211350ustar00rootroot00000000000000# Common API This document describes `aMQTT` common API both used by [MQTT Client](client.md) and [Broker](broker.md). ## ApplicationMessage ::: amqtt.session.ApplicationMessage options: heading_level: 3 ## IncomingApplicationMessage Represents messages received from MQTT clients. ::: amqtt.session.IncomingApplicationMessage options: heading_level: 3 ## OutgoingApplicationMessage Inherits from ApplicationMessage. Represents messages to be sent to MQTT clients. ::: amqtt.session.OutgoingApplicationMessage options: heading_level: 3 Yakifo-amqtt-2637127/docs/scripts/000077500000000000000000000000001504664204300167005ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/scripts/exts.py000066400000000000000000000126441504664204300202440ustar00rootroot00000000000000import ast import pprint from typing import Any import griffe from griffe import Inspector, ObjectNode, Visitor, Attribute from amqtt.contexts import default_listeners, default_broker_plugins, default_client_plugins from amqtt.contrib.auth_db.plugin import default_hash_scheme default_factory_map = { 'default_listeners': default_listeners(), 'default_broker_plugins': default_broker_plugins(), 'default_client_plugins': default_client_plugins(), 'default_hash_scheme': default_hash_scheme() } def get_qualified_name(node: ast.AST) -> str | None: """Recursively build the qualified name from an AST node.""" if isinstance(node, ast.Name): return node.id elif isinstance(node, ast.Attribute): parent = get_qualified_name(node.value) if parent: return f"{parent}.{node.attr}" return node.attr elif isinstance(node, ast.Call): # e.g., uuid.uuid4() return get_qualified_name(node.func) return None def get_fully_qualified_name(call_node): """ Extracts the fully qualified name from an ast.Call node. """ if isinstance(call_node.func, ast.Name): # Direct function call (e.g., "my_function(arg)") return call_node.func.id elif isinstance(call_node.func, ast.Attribute): # Method call or qualified name (e.g., "obj.method(arg)" or "module.submodule.function(arg)") parts = [] current = call_node.func while isinstance(current, ast.Attribute): parts.append(current.attr) current = current.value if isinstance(current, ast.Name): parts.append(current.id) return ".".join(reversed(parts)) else: # Handle other potential cases (e.g., ast.Subscript) if necessary return None def get_callable_name(node): if isinstance(node, ast.Name): return node.id elif isinstance(node, ast.Attribute): return f"{get_callable_name(node.value)}.{node.attr}" return None def evaluate_callable_node(node): try: # Wrap the node in an Expression so it can be compiled expr = ast.Expression(body=node) compiled = compile(expr, filename="", mode="eval") return eval(compiled, {"__builtins__": __builtins__, "list": list, "dict": dict}) except Exception as e: return f"" class DataclassDefaultFactoryExtension(griffe.Extension): """Renders the output of a dataclasses field which uses a default factory. def other_field_defaults(): return {'item1': 'value1', 'item2': 'value2'} @dataclass class MyDataClass: my_field: dict[str, Any] = field(default_factory=dict) my_other_field: dict[str, Any] = field(default_factory=other_field_defaults) instead of documentation rendering this as: ``` class MyDataClass: my_field: dict[str, Any] = dict() my_other_field: dict[str, Any] = other_field_defaults() ``` it will be displayed with the output of factory functions for more clarity: ``` class MyDataClass: my_field: dict[str, Any] = {} my_other_field: dict[str, Any] = {'item1': 'value1', 'item2': 'value2'} ``` _note_ : for any custom default factory function, it must be added to the `default_factory_map` in this file as `griffe` doesn't provide a straightforward mechanism with its AST to dynamically import/call the function. """ def on_attribute_instance( self, *, node: ast.AST | ObjectNode, attr: Attribute, agent: Visitor | Inspector, **kwargs: Any, ) -> None: """Called for every `node` and/or `attr` on a file's AST.""" if not hasattr(node, "value"): return if isinstance(node.value, ast.Call): # Search for all of the `default_factory` fields. default_factory_value: str | None = None for kw in node.value.keywords: if kw.arg == "default_factory": # based on the node type, return the proper function name match get_callable_name(kw.value): # `dict` and `list` are common default factory functions case 'dict': default_factory_value = "{}" case 'list': default_factory_value = "[]" case _: # otherwise, see the nodes is in our map for the custom default factory function callable_name = get_callable_name(kw.value) if callable_name in default_factory_map: default_factory_value = pprint.pformat(default_factory_map[callable_name], indent=4, width=80, sort_dicts=False) else: # if not, display as the default default_factory_value = f"{callable_name}()" # store the information in the griffe attribute, which is what is passed to the template for rendering if "dataclass_ext" not in attr.extra: attr.extra["dataclass_ext"] = {} attr.extra["dataclass_ext"]["has_default_factory"] = False if default_factory_value is not None: attr.extra["dataclass_ext"]["has_default_factory"] = True attr.extra["dataclass_ext"]["default_factory"] = default_factory_value Yakifo-amqtt-2637127/docs/security.md000066400000000000000000000000241504664204300173760ustar00rootroot00000000000000--8<-- "SECURITY.md"Yakifo-amqtt-2637127/docs/support.md000066400000000000000000000000241504664204300172430ustar00rootroot00000000000000--8<-- "SUPPORT.md" Yakifo-amqtt-2637127/docs/templates/000077500000000000000000000000001504664204300172075ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/templates/README000066400000000000000000000000501504664204300200620ustar00rootroot00000000000000template overrides for mkdocs-materials Yakifo-amqtt-2637127/docs/templates/python/000077500000000000000000000000001504664204300205305ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/templates/python/material/000077500000000000000000000000001504664204300223265ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/templates/python/material/amqtt.class.jinja000066400000000000000000000004561504664204300256020ustar00rootroot00000000000000{% extends "_base/class.html.jinja" %} {% block signature scoped %} {% if config.extra.class_style != 'simple' %} {{ super() }} {% endif %} {% endblock signature %} {% block bases scoped %} {% if config.extra.class_style != 'simple' %} {{ super() }} {% endif %} {% endblock bases %} Yakifo-amqtt-2637127/docs/templates/python/material/backlinks.html.jinja000066400000000000000000000002311504664204300262430ustar00rootroot00000000000000{% extends "_base/backlinks.html.jinja" %} {% block logs scoped %}

backlinks.html.jinja

{% endblock logs %} Yakifo-amqtt-2637127/docs/templates/python/material/class.html.jinja000066400000000000000000000005051504664204300254130ustar00rootroot00000000000000{% extends "_base/class.html.jinja" %} {% block logs scoped %} {% if config.extra.template_log_display %}

class.html.jinja

{% endif %} {% endblock logs %} {% block signature scoped %} {% if config.extra.class_style == 'simple' %}{% else %} {{ super() }} {% endif %} {% endblock signature %}Yakifo-amqtt-2637127/docs/templates/python/material/docstring/000077500000000000000000000000001504664204300243225ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs/templates/python/material/docstring/attributes.html.jinja000066400000000000000000000012141504664204300304660ustar00rootroot00000000000000{% extends "_base/docstring/attributes.html.jinja" %} {% block logs scoped %} {% if config.extra.template_log_display %}

docstring/attributes.html.jinja

{% endif %} {% endblock logs %} {% block table_style scoped %} {% if config.extra.class_style == 'simple' %}{% else %} {{ super() }} {% endif %} {% endblock table_style %} {% block list_style scoped %} {% if config.extra.class_style == 'simple' %}{% else %} {{ super() }} {% endif %} {% endblock list_style %} {% block spacy_style scoped %} {% if config.extra.class_style == 'simple' %}{% else %} {{ super() }} {% endif %} {% endblock spacy_style %} Yakifo-amqtt-2637127/docs/templates/python/material/docstring/functions.html.jinja000066400000000000000000000011511504664204300303100ustar00rootroot00000000000000{% extends "_base/docstring/functions.html.jinja" %} {% block logs scoped %} {% if config.extra.template_log_display %}

docstring/functions.html.jinja

{% endif %} {% endblock logs %} {% block table_style scoped %} {% if config.extra.class_style == 'simple' %}{% else %} {{ super() }} {% endif %} {% endblock %} {% block list_style scoped %} {% if config.extra.class_style == 'simple' %}{% else %} {{ super() }} {% endif %} {% endblock %} {% block spacy_style scoped %} {% if config.extra.class_style == 'simple' %}{% else %} {{ super() }} {% endif %} {% endblock %} Yakifo-amqtt-2637127/docs/templates/python/material/expression.html.jinja000066400000000000000000000002551504664204300265070ustar00rootroot00000000000000{% if 'default_factory' in expression.__str__() %} {{ obj.extra.dataclass_ext.default_factory | safe }} {% else %} {% extends "_base/expression.html.jinja" %} {% endif %}Yakifo-amqtt-2637127/docs/templates/python/material/function.html.jinja000066400000000000000000000003741504664204300261370ustar00rootroot00000000000000{% if config.extra.class_style == 'simple' %}{% else %}{% extends "_base/function.html.jinja" %}{% endif %} {% block logs scoped %} {% if config.extra.template_log_display %}

function.html.jinja

{% endif %} {% endblock logs %} Yakifo-amqtt-2637127/docs_test/000077500000000000000000000000001504664204300162505ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_test/.env000066400000000000000000000001131504664204300170340ustar00rootroot00000000000000VITE_MQTT_WS_TYPE = ws VITE_MQTT_WS_HOST = not_set VITE_MQTT_WS_PORT = 9999Yakifo-amqtt-2637127/docs_test/.env.development000066400000000000000000000001161504664204300213600ustar00rootroot00000000000000VITE_MQTT_WS_TYPE = ws VITE_MQTT_WS_HOST = localhost VITE_MQTT_WS_PORT = 8080 Yakifo-amqtt-2637127/docs_test/.env.production000066400000000000000000000001231504664204300212220ustar00rootroot00000000000000VITE_MQTT_WS_TYPE = wss VITE_MQTT_WS_HOST = test.amqtt.io VITE_MQTT_WS_PORT = 8443 Yakifo-amqtt-2637127/docs_test/.gitignore000066400000000000000000000003751504664204300202450ustar00rootroot00000000000000# Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? Yakifo-amqtt-2637127/docs_test/eslint.config.js000066400000000000000000000013361504664204300213530ustar00rootroot00000000000000import js from '@eslint/js' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' export default tseslint.config( { ignores: ['dist'] }, { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, }, rules: { ...reactHooks.configs.recommended.rules, 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, ], }, }, ) Yakifo-amqtt-2637127/docs_test/index.html000066400000000000000000000005341504664204300202470ustar00rootroot00000000000000 Test Site Dashboard
Yakifo-amqtt-2637127/docs_test/package-lock.json000066400000000000000000007672521504664204300215070ustar00rootroot00000000000000{ "name": "amqttio", "version": "0.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "amqttio", "version": "0.11.0", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "@mui/icons-material": "^7.1.1", "@mui/material": "^7.1.1", "@mui/x-charts": "^8.4.0", "@mui/x-data-grid": "^8.4.0", "@mui/x-date-pickers": "^8.4.0", "@mui/x-tree-view": "^8.4.0", "@react-spring/web": "^10.0.1", "dayjs": "^1.11.13", "mqtt": "^5.13.0", "react": "^19.1.0", "react-countup": "^6.5.3", "react-dom": "^19.1.0" }, "devDependencies": { "@eslint/js": "^9.25.0", "@svgr/webpack": "^8.1.0", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", "vite": "^6.3.5", "vite-plugin-svgr": "^4.3.0" } }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-module-transforms": "^7.27.1", "@babel/helpers": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/babel" } }, "node_modules/@babel/generator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "dependencies": { "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-define-polyfill-provider": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", "dev": true, "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, "dependencies": { "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-wrap-function": "^7.27.1", "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-replace-supers": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "dev": true, "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", "dev": true, "dependencies": { "@babel/template": "^7.27.1", "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, "dependencies": { "@babel/template": "^7.27.1", "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dependencies": { "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-jsx": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-typescript": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-async-generator-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-async-to-generator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-block-scoping": { "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.3.tgz", "integrity": "sha512-+F8CnfhuLhwUACIJMLWnjz6zvzYM2r0yeIHKlbgfw7ml8rOMJsXNXV/hyRcb3nb493gRs4WvYpQAndWj/qQmkQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-class-properties": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-class-static-block": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "node_modules/@babel/plugin-transform-classes": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.27.1", "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-classes/node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/template": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-destructuring": { "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-dotall-regex": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-duplicate-keys": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-json-strings": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-literals": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-modules-commonjs": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-modules-systemjs": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "dev": true, "dependencies": { "@babel/helper-module-transforms": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-transform-new-target": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-numeric-separator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-object-rest-spread": { "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz", "integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.27.3", "@babel/plugin-transform-parameters": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-optional-chaining": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-parameters": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-private-methods": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-private-property-in-object": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-constant-elements": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-display-name": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.27.1.tgz", "integrity": "sha512-p9+Vl3yuHPmkirRrg021XiP+EETmPMQTLr6Ayjj85RLNEbb3Eya/4VI0vAdzQG9SEAl2Lnt7fy5lZyMzjYoZQQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-jsx": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-jsx-development": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", "dev": true, "dependencies": { "@babel/plugin-transform-react-jsx": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-jsx-source": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-react-pure-annotations": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.27.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.4.tgz", "integrity": "sha512-Glp/0n8xuj+E1588otw5rjJkTXfzW7FjH3IIUrfqiZOPQCd2vbg8e+DQE8jK9g4V5/zrxFW+D9WM9gboRPELpQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-spread": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-sticky-regex": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-template-literals": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz", "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "node_modules/@babel/preset-env": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", "dev": true, "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.27.1", "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", "@babel/plugin-transform-async-generator-functions": "^7.27.1", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", "@babel/plugin-transform-block-scoping": "^7.27.1", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.27.1", "@babel/plugin-transform-classes": "^7.27.1", "@babel/plugin-transform-computed-properties": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.27.1", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", "@babel/plugin-transform-exponentiation-operator": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", "@babel/plugin-transform-json-strings": "^7.27.1", "@babel/plugin-transform-literals": "^7.27.1", "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-modules-systemjs": "^7.27.1", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", "@babel/plugin-transform-object-rest-spread": "^7.27.2", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", "@babel/plugin-transform-parameters": "^7.27.1", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", "@babel/plugin-transform-regenerator": "^7.27.1", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", "@babel/plugin-transform-spread": "^7.27.1", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", "@babel/plugin-transform-unicode-property-regex": "^7.27.1", "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/preset-react": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-transform-react-display-name": "^7.27.1", "@babel/plugin-transform-react-jsx": "^7.27.1", "@babel/plugin-transform-react-jsx-development": "^7.27.1", "@babel/plugin-transform-react-pure-annotations": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/preset-typescript": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "engines": { "node": ">=6.9.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/runtime": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.1", "@babel/parser": "^7.27.1", "@babel/template": "^7.27.1", "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse/node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "engines": { "node": ">=4" } }, "node_modules/@babel/types": { "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, "node_modules/@emotion/cache": { "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "node_modules/@emotion/hash": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" }, "node_modules/@emotion/is-prop-valid": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", "dependencies": { "@emotion/memoize": "^0.9.0" } }, "node_modules/@emotion/memoize": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" }, "node_modules/@emotion/react": { "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true } } }, "node_modules/@emotion/serialize": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "node_modules/@emotion/sheet": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" }, "node_modules/@emotion/styled": { "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/is-prop-valid": "^1.3.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2" }, "peerDependencies": { "@emotion/react": "^11.0.0-rc.0", "react": ">=16.8.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true } } }, "node_modules/@emotion/unitless": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", "peerDependencies": { "react": ">=16.8.0" } }, "node_modules/@emotion/utils": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==" }, "node_modules/@emotion/weak-memoize": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", "cpu": [ "ppc64" ], "dev": true, "optional": true, "os": [ "aix" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-arm": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", "cpu": [ "arm" ], "dev": true, "optional": true, "os": [ "android" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "android" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/android-x64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "android" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "darwin" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "darwin" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "freebsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "freebsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", "cpu": [ "arm" ], "dev": true, "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", "cpu": [ "ia32" ], "dev": true, "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", "cpu": [ "loong64" ], "dev": true, "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", "cpu": [ "mips64el" ], "dev": true, "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", "cpu": [ "ppc64" ], "dev": true, "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", "cpu": [ "riscv64" ], "dev": true, "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", "cpu": [ "s390x" ], "dev": true, "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "linux" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "netbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "netbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "openbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "openbsd" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "sunos" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "win32" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", "cpu": [ "ia32" ], "dev": true, "optional": true, "os": [ "win32" ], "engines": { "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "win32" ], "engines": { "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/config-helpers": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { "version": "9.27.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@fortawesome/fontawesome-common-types": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-free": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-brands-svg-icons": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz", "integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==", "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { "version": "6.7.2", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.2" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/react-fontawesome": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~1 || ~6", "react": ">=16.3" } }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { "version": "0.16.6", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "engines": { "node": ">=18.18" }, "funding": { "type": "github", "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "engines": { "node": ">=12.22" }, "funding": { "type": "github", "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "engines": { "node": ">=18.18" }, "funding": { "type": "github", "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@mui/core-downloads-tracker": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.1.tgz", "integrity": "sha512-yBckQs4aQ8mqukLnPC6ivIRv6guhaXi8snVl00VtyojBbm+l6VbVhyTSZ68Abcx7Ah8B+GZhrB7BOli+e+9LkQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" } }, "node_modules/@mui/icons-material": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.1.tgz", "integrity": "sha512-X37+Yc8QpEnl0sYmz+WcLFy2dWgNRzbswDzLPXG7QU1XDVlP5TPp1HXjdmCupOWLL/I9m1fyhcyZl8/HPpp/Cg==", "dependencies": { "@babel/runtime": "^7.27.1" }, "engines": { "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@mui/material": "^7.1.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true } } }, "node_modules/@mui/material": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.1.1.tgz", "integrity": "sha512-mTpdmdZCaHCGOH3SrYM41+XKvNL0iQfM9KlYgpSjgadXx/fEKhhvOktxm8++Xw6FFeOHoOiV+lzOI8X1rsv71A==", "dependencies": { "@babel/runtime": "^7.27.1", "@mui/core-downloads-tracker": "^7.1.1", "@mui/system": "^7.1.1", "@mui/types": "^7.4.3", "@mui/utils": "^7.1.1", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1", "react-is": "^19.1.0", "react-transition-group": "^4.4.5" }, "engines": { "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@mui/material-pigment-css": "^7.1.1", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { "optional": true }, "@emotion/styled": { "optional": true }, "@mui/material-pigment-css": { "optional": true }, "@types/react": { "optional": true } } }, "node_modules/@mui/private-theming": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.1.tgz", "integrity": "sha512-M8NbLUx+armk2ZuaxBkkMk11ultnWmrPlN0Xe3jUEaBChg/mcxa5HWIWS1EE4DF36WRACaAHVAvyekWlDQf0PQ==", "dependencies": { "@babel/runtime": "^7.27.1", "@mui/utils": "^7.1.1", "prop-types": "^15.8.1" }, "engines": { "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true } } }, "node_modules/@mui/styled-engine": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.1.tgz", "integrity": "sha512-R2wpzmSN127j26HrCPYVQ53vvMcT5DaKLoWkrfwUYq3cYytL6TQrCH8JBH3z79B6g4nMZZVoaXrxO757AlShaw==", "dependencies": { "@babel/runtime": "^7.27.1", "@emotion/cache": "^11.13.5", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { "optional": true }, "@emotion/styled": { "optional": true } } }, "node_modules/@mui/system": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.1.tgz", "integrity": "sha512-Kj1uhiqnj4Zo7PDjAOghtXJtNABunWvhcRU0O7RQJ7WOxeynoH6wXPcilphV8QTFtkKaip8EiNJRiCD+B3eROA==", "dependencies": { "@babel/runtime": "^7.27.1", "@mui/private-theming": "^7.1.1", "@mui/styled-engine": "^7.1.1", "@mui/types": "^7.4.3", "@mui/utils": "^7.1.1", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" }, "engines": { "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { "optional": true }, "@emotion/styled": { "optional": true }, "@types/react": { "optional": true } } }, "node_modules/@mui/types": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.3.tgz", "integrity": "sha512-2UCEiK29vtiZTeLdS2d4GndBKacVyxGvReznGXGr+CzW/YhjIX+OHUdCIczZjzcRAgKBGmE9zCIgoV9FleuyRQ==", "dependencies": { "@babel/runtime": "^7.27.1" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true } } }, "node_modules/@mui/utils": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.1.tgz", "integrity": "sha512-BkOt2q7MBYl7pweY2JWwfrlahhp+uGLR8S+EhiyRaofeRYUWL2YKbSGQvN4hgSN1i8poN0PaUiii1kEMrchvzg==", "dependencies": { "@babel/runtime": "^7.27.1", "@mui/types": "^7.4.3", "@types/prop-types": "^15.7.14", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-is": "^19.1.0" }, "engines": { "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { "optional": true } } }, "node_modules/@mui/x-charts": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-8.4.0.tgz", "integrity": "sha512-XXXt6cHgpTTkLWIImBy0OPD0FwuOdux4AprP/0Zvs0PXuM9D9eeN1piZvo5gjZbPHSsCzIyZzwx9PGncFScgEQ==", "dependencies": { "@babel/runtime": "^7.27.1", "@mui/utils": "^7.0.2", "@mui/x-charts-vendor": "8.4.0", "@mui/x-internals": "8.4.0", "bezier-easing": "^2.1.0", "clsx": "^2.1.1", "prop-types": "^15.8.1", "reselect": "^5.1.1", "use-sync-external-store": "^1.5.0" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { "optional": true }, "@emotion/styled": { "optional": true } } }, "node_modules/@mui/x-charts-vendor": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-8.4.0.tgz", "integrity": "sha512-0VvwJSeFezJTnjoKg5YUbbI82a60+VZfH2RyqaosmKH516lKYKSCuAFmkj4vUBP6+ZJPZDW5mWI3VhwD4DN6hg==", "dependencies": { "@babel/runtime": "^7.27.1", "@types/d3-color": "^3.1.3", "@types/d3-delaunay": "^6.0.4", "@types/d3-interpolate": "^3.0.4", "@types/d3-scale": "^4.0.9", "@types/d3-shape": "^3.1.7", "@types/d3-time": "^3.0.4", "@types/d3-timer": "^3.0.2", "d3-color": "^3.1.0", "d3-delaunay": "^6.0.4", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", "d3-time": "^3.1.0", "d3-timer": "^3.0.1", "delaunator": "^5.0.1", "robust-predicates": "^3.0.2" } }, "node_modules/@mui/x-data-grid": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.4.0.tgz", "integrity": "sha512-c0fgMhvQTjCSo3LgRK1Mdk2msktCl9uwMYUYlP6bbqJ7I03IvS+1aZ+s3nSLmaq1aVh7sE2Bnuz63OnVerTLJA==", "dependencies": { "@babel/runtime": "^7.27.1", "@mui/utils": "^7.0.2", "@mui/x-internals": "8.4.0", "clsx": "^2.1.1", "prop-types": "^15.8.1", "reselect": "^5.1.1", "use-sync-external-store": "^1.5.0" }, "engines": { "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { "optional": true }, "@emotion/styled": { "optional": true } } }, "node_modules/@mui/x-date-pickers": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.4.0.tgz", "integrity": "sha512-x7jI7JnKK25xL3yjD2Z1r86gAWtabKj9ogI2WDKd/v9WwE1VxmDD/NTiXprEZFo9psPOoqr+juPGDz5Cb2v7jw==", "dependencies": { "@babel/runtime": "^7.27.1", "@mui/utils": "^7.0.2", "@mui/x-internals": "8.4.0", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "engines": { "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", "dayjs": "^1.10.7", "luxon": "^3.0.2", "moment": "^2.29.4", "moment-hijri": "^2.1.2 || ^3.0.0", "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { "optional": true }, "@emotion/styled": { "optional": true }, "date-fns": { "optional": true }, "date-fns-jalali": { "optional": true }, "dayjs": { "optional": true }, "luxon": { "optional": true }, "moment": { "optional": true }, "moment-hijri": { "optional": true }, "moment-jalaali": { "optional": true } } }, "node_modules/@mui/x-internals": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.4.0.tgz", "integrity": "sha512-Z7FCahC4MLfTVzEwnKOB7P1fiR9DzFuMzHOPRNaMXc/rsS7unbtBKAG94yvsRzReCyjzZUVA7h37lnQ1DoPKJw==", "dependencies": { "@babel/runtime": "^7.27.1", "@mui/utils": "^7.0.2" }, "engines": { "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@mui/x-tree-view": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-8.4.0.tgz", "integrity": "sha512-TsweCJ2fazVIvVdNq7KuY7BEInPMPeWCCxG4XxHyoN3GdSa/8a+S4dH6iww3yaYE5nBXZQV3wZm940W4eV1uYw==", "dependencies": { "@babel/runtime": "^7.27.1", "@mui/utils": "^7.0.2", "@mui/x-internals": "8.4.0", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5", "reselect": "^5.1.1", "use-sync-external-store": "^1.5.0" }, "engines": { "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@emotion/react": "^11.9.0", "@emotion/styled": "^11.8.1", "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@emotion/react": { "optional": true }, "@emotion/styled": { "optional": true } } }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" }, "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" }, "engines": { "node": ">= 8" } }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" } }, "node_modules/@react-spring/animated": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.1.tgz", "integrity": "sha512-BGL3hA66Y8Qm3KmRZUlfG/mFbDPYajgil2/jOP0VXf2+o2WPVmcDps/eEgdDqgf5Pv9eBbyj7LschLMuSjlW3Q==", "dependencies": { "@react-spring/shared": "~10.0.1", "@react-spring/types": "~10.0.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@react-spring/core": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.0.1.tgz", "integrity": "sha512-KaMMsN1qHuVTsFpg/5ajAVye7OEqhYbCq0g4aKM9bnSZlDBBYpO7Uf+9eixyXN8YEbF+YXaYj9eoWDs+npZ+sA==", "dependencies": { "@react-spring/animated": "~10.0.1", "@react-spring/shared": "~10.0.1", "@react-spring/types": "~10.0.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/react-spring/donate" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@react-spring/rafz": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.1.tgz", "integrity": "sha512-UrzG/d6Is+9i0aCAjsjWRqIlFFiC4lFqFHrH63zK935z2YDU95TOFio4VKGISJ5SG0xq4ULy7c1V3KU+XvL+Yg==" }, "node_modules/@react-spring/shared": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.1.tgz", "integrity": "sha512-KR2tmjDShPruI/GGPfAZOOLvDgkhFseabjvxzZFFggJMPkyICLjO0J6mCIoGtdJSuHywZyc4Mmlgi+C88lS00g==", "dependencies": { "@react-spring/rafz": "~10.0.1", "@react-spring/types": "~10.0.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@react-spring/types": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.0.1.tgz", "integrity": "sha512-Fk1wYVAKL+ZTYK+4YFDpHf3Slsy59pfFFvnnTfRjQQFGlyIo4VejPtDs3CbDiuBjM135YztRyZjIH2VbycB+ZQ==" }, "node_modules/@react-spring/web": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-10.0.1.tgz", "integrity": "sha512-FgQk02OqFrYyJBTTnBTWAU0WPzkHkKXauc6aeexcvATvLapUxwnfGuLlsLYF8BYjEVfkivPT04ziAue6zyRBtQ==", "dependencies": { "@react-spring/animated": "~10.0.1", "@react-spring/core": "~10.0.1", "@react-spring/shared": "~10.0.1", "@react-spring/types": "~10.0.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.9", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==", "dev": true }, "node_modules/@rollup/pluginutils": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", "dev": true, "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { "optional": true } } }, "node_modules/@rollup/pluginutils/node_modules/picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz", "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==", "cpu": [ "arm" ], "dev": true, "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz", "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz", "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz", "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz", "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz", "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz", "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==", "cpu": [ "arm" ], "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz", "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==", "cpu": [ "arm" ], "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz", "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz", "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz", "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==", "cpu": [ "loong64" ], "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz", "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==", "cpu": [ "ppc64" ], "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz", "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==", "cpu": [ "riscv64" ], "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz", "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==", "cpu": [ "riscv64" ], "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz", "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==", "cpu": [ "s390x" ], "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz", "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz", "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz", "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==", "cpu": [ "arm64" ], "dev": true, "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz", "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==", "cpu": [ "ia32" ], "dev": true, "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz", "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==", "cpu": [ "x64" ], "dev": true, "optional": true, "os": [ "win32" ] }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", "dev": true, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", "dev": true, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", "dev": true, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", "dev": true, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@svgr/babel-plugin-svg-dynamic-title": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", "dev": true, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@svgr/babel-plugin-svg-em-dimensions": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", "dev": true, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@svgr/babel-plugin-transform-react-native-svg": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", "dev": true, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@svgr/babel-plugin-transform-svg-component": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", "dev": true, "engines": { "node": ">=12" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@svgr/babel-preset": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", "dev": true, "dependencies": { "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", "@svgr/babel-plugin-transform-svg-component": "8.0.0" }, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "node_modules/@svgr/core": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "camelcase": "^6.2.0", "cosmiconfig": "^8.1.3", "snake-case": "^3.0.4" }, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/core/node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "engines": { "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { "typescript": ">=4.9.5" }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, "node_modules/@svgr/hast-util-to-babel-ast": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", "dev": true, "dependencies": { "@babel/types": "^7.21.3", "entities": "^4.4.0" }, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@svgr/plugin-jsx": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", "dev": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", "@svgr/hast-util-to-babel-ast": "8.0.0", "svg-parser": "^2.0.4" }, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { "@svgr/core": "*" } }, "node_modules/@svgr/plugin-svgo": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", "dev": true, "dependencies": { "cosmiconfig": "^8.1.3", "deepmerge": "^4.3.1", "svgo": "^3.0.2" }, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" }, "peerDependencies": { "@svgr/core": "*" } }, "node_modules/@svgr/plugin-svgo/node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "engines": { "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/d-fischer" }, "peerDependencies": { "typescript": ">=4.9.5" }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, "node_modules/@svgr/webpack": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", "dev": true, "dependencies": { "@babel/core": "^7.21.3", "@babel/plugin-transform-react-constant-elements": "^7.21.3", "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.21.0", "@svgr/core": "8.1.0", "@svgr/plugin-jsx": "8.1.0", "@svgr/plugin-svgo": "8.1.0" }, "engines": { "node": ">=14" }, "funding": { "type": "github", "url": "https://github.com/sponsors/gregberge" } }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "dev": true, "engines": { "node": ">=10.13.0" } }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "node_modules/@types/babel__generator": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__traverse": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", "dev": true, "dependencies": { "@babel/types": "^7.20.7" } }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" }, "node_modules/@types/d3-delaunay": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "dependencies": { "@types/d3-color": "*" } }, "node_modules/@types/d3-path": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" }, "node_modules/@types/d3-scale": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "dependencies": { "@types/d3-time": "*" } }, "node_modules/@types/d3-shape": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "dependencies": { "@types/d3-path": "*" } }, "node_modules/@types/d3-time": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/node": { "version": "22.15.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.24.tgz", "integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==", "dependencies": { "undici-types": "~6.21.0" } }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==" }, "node_modules/@types/react": { "version": "19.1.5", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { "version": "19.1.5", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", "dev": true, "peerDependencies": { "@types/react": "^19.0.0" } }, "node_modules/@types/react-transition-group": { "version": "4.4.12", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", "peerDependencies": { "@types/react": "*" } }, "node_modules/@types/readable-stream": { "version": "4.0.20", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.20.tgz", "integrity": "sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==", "dependencies": { "@types/node": "*" } }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", "integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/type-utils": "8.32.1", "@typescript-eslint/utils": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "dev": true, "dependencies": { "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz", "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "dev": true, "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/type-utils": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz", "integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==", "dev": true, "dependencies": { "@typescript-eslint/typescript-estree": "8.32.1", "@typescript-eslint/utils": "8.32.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz", "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/typescript-estree": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz", "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dev": true, "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "bin": { "semver": "bin/semver.js" }, "engines": { "node": ">=10" } }, "node_modules/@typescript-eslint/utils": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz", "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/typescript-estree": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz", "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "dev": true, "dependencies": { "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@vitejs/plugin-react": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz", "integrity": "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==", "dev": true, "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", "@rolldown/pluginutils": "1.0.0-beta.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "dependencies": { "event-target-shim": "^5.0.0" }, "engines": { "node": ">=6.5" } }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "bin": { "acorn": "bin/acorn" }, "engines": { "node": ">=0.4.0" } }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { "color-convert": "^2.0.1" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" }, "engines": { "node": ">=10", "npm": ">=6" } }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.13", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.6.4", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", "dev": true, "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3", "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-regenerator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", "dev": true, "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ] }, "node_modules/bezier-easing": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==" }, "node_modules/bl": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.0.tgz", "integrity": "sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw==", "dependencies": { "@types/readable-stream": "^4.0.0", "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^4.2.0" } }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { "version": "4.24.5", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" }, "engines": { "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "engines": { "node": ">=6" } }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/caniuse-lite": { "version": "1.0.30001718", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ] }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "engines": { "node": ">=6" } }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { "color-name": "~1.1.4" }, "engines": { "node": ">=7.0.0" } }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, "engines": { "node": ">= 10" } }, "node_modules/commist": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", "engines": [ "node >= 6.0" ], "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, "node_modules/concat-stream/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" }, "engines": { "node": ">= 6" } }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, "node_modules/core-js-compat": { "version": "3.42.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", "dev": true, "dependencies": { "browserslist": "^4.24.4" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" } }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" }, "engines": { "node": ">=10" } }, "node_modules/cosmiconfig/node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "engines": { "node": ">= 6" } }, "node_modules/countup.js": { "version": "2.8.2", "resolved": "https://registry.npmjs.org/countup.js/-/countup.js-2.8.2.tgz", "integrity": "sha512-UtRoPH6udaru/MOhhZhI/GZHJKAyAxuKItD2Tr7AbrqrOPBX/uejWBBJt8q86169AMqKkE9h9/24kFWbUk/Bag==" }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" }, "engines": { "node": ">= 8" } }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" }, "funding": { "url": "https://github.com/sponsors/fb55" } }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", "dev": true, "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, "node_modules/css-what": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, "engines": { "node": ">= 6" }, "funding": { "url": "https://github.com/sponsors/fb55" } }, "node_modules/csso": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", "dev": true, "dependencies": { "css-tree": "~2.2.0" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", "npm": ">=7.0.0" } }, "node_modules/csso/node_modules/css-tree": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", "dev": true, "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", "npm": ">=7.0.0" } }, "node_modules/csso/node_modules/mdn-data": { "version": "2.0.28", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "dev": true }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "dependencies": { "internmap": "1 - 2" }, "engines": { "node": ">=12" } }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "engines": { "node": ">=12" } }, "node_modules/d3-delaunay": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "dependencies": { "delaunator": "5" }, "engines": { "node": ">=12" } }, "node_modules/d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", "engines": { "node": ">=12" } }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "dependencies": { "d3-color": "1 - 3" }, "engines": { "node": ">=12" } }, "node_modules/d3-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "engines": { "node": ">=12" } }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" }, "engines": { "node": ">=12" } }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "dependencies": { "d3-path": "^3.1.0" }, "engines": { "node": ">=12" } }, "node_modules/d3-time": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "dependencies": { "d3-array": "2 - 3" }, "engines": { "node": ">=12" } }, "node_modules/d3-time-format": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "dependencies": { "d3-time": "1 - 3" }, "engines": { "node": ">=12" } }, "node_modules/d3-timer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "engines": { "node": ">=12" } }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dependencies": { "ms": "^2.1.3" }, "engines": { "node": ">=6.0" }, "peerDependenciesMeta": { "supports-color": { "optional": true } } }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/delaunator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", "dependencies": { "robust-predicates": "^3.0.2" } }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" }, "funding": { "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/fb55" } ] }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "dependencies": { "domelementtype": "^2.3.0" }, "engines": { "node": ">= 4" }, "funding": { "url": "https://github.com/fb55/domhandler?sponsor=1" } }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" } }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "node_modules/electron-to-chromium": { "version": "1.5.157", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz", "integrity": "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==", "dev": true }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "engines": { "node": ">=0.12" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/esbuild": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" }, "engines": { "node": ">=18" }, "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" } }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { "version": "9.27.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.27.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" }, "peerDependencies": { "jiti": "*" }, "peerDependenciesMeta": { "jiti": { "optional": true } } }, "node_modules/eslint-plugin-react-hooks": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "dev": true, "engines": { "node": ">=10" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react-refresh": { "version": "0.4.20", "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", "dev": true, "peerDependencies": { "eslint": ">=8.40" } }, "node_modules/eslint-scope": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" }, "engines": { "node": ">=0.10" } }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, "dependencies": { "estraverse": "^5.2.0" }, "engines": { "node": ">=4.0" } }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "engines": { "node": ">=4.0" } }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "dev": true }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "engines": { "node": ">=6" } }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "engines": { "node": ">=0.8.x" } }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" } }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "dependencies": { "is-glob": "^4.0.1" }, "engines": { "node": ">= 6" } }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "node_modules/fast-unique-numbers": { "version": "8.0.13", "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", "dependencies": { "@babel/runtime": "^7.23.8", "tslib": "^2.6.2" }, "engines": { "node": ">=16.1.0" } }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "dependencies": { "reusify": "^1.0.4" } }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "dependencies": { "flat-cache": "^4.0.0" }, "engines": { "node": ">=16.0.0" } }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, "engines": { "node": ">=8" } }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" }, "engines": { "node": ">=16" } }, "node_modules/flatted": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, "os": [ "darwin" ], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { "is-glob": "^4.0.3" }, "engines": { "node": ">=10.13.0" } }, "node_modules/globals": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, "engines": { "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/help-me": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==" }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "dependencies": { "react-is": "^16.7.0" } }, "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ] }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" }, "engines": { "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "engines": { "node": ">=0.8.19" } }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "engines": { "node": ">=12" } }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" }, "engines": { "node": ">= 12" } }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dependencies": { "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, "engines": { "node": ">=0.10.0" } }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "engines": { "node": ">=0.12.0" } }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, "node_modules/js-sdsl": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", "funding": { "type": "opencollective", "url": "https://opencollective.com/js-sdsl" } }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "bin": { "jsesc": "bin/jsesc" }, "engines": { "node": ">=6" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" }, "engines": { "node": ">=6" } }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" } }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { "p-locate": "^5.0.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "dev": true, "dependencies": { "tslib": "^2.0.3" } }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "dependencies": { "yallist": "^3.0.2" } }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "engines": { "node": ">= 8" } }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, "engines": { "node": "*" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/mqtt": { "version": "5.13.0", "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.13.0.tgz", "integrity": "sha512-pR+z+ChxFl3n8AKLQbTONVOOg/jl4KiKQRBAi78tjd6PksOWvl1nl9L8ZHOZ3MiavZfrUOjok2ddwc1VymGWRg==", "dependencies": { "commist": "^3.2.0", "concat-stream": "^2.0.0", "debug": "^4.4.0", "help-me": "^5.0.0", "lru-cache": "^10.4.3", "minimist": "^1.2.8", "mqtt-packet": "^9.0.2", "number-allocator": "^1.0.14", "readable-stream": "^4.7.0", "rfdc": "^1.4.1", "socks": "^2.8.3", "split2": "^4.2.0", "worker-timers": "^7.1.8", "ws": "^8.18.0" }, "bin": { "mqtt": "build/bin/mqtt.js", "mqtt_pub": "build/bin/pub.js", "mqtt_sub": "build/bin/sub.js" }, "engines": { "node": ">=16.0.0" } }, "node_modules/mqtt-packet": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", "dependencies": { "bl": "^6.0.8", "debug": "^4.3.4", "process-nextick-args": "^2.0.1" } }, "node_modules/mqtt/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], "bin": { "nanoid": "bin/nanoid.cjs" }, "engines": { "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "dev": true, "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "dependencies": { "boolbase": "^1.0.0" }, "funding": { "url": "https://github.com/fb55/nth-check?sponsor=1" } }, "node_modules/number-allocator": { "version": "1.0.14", "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", "dependencies": { "debug": "^4.3.1", "js-sdsl": "4.3.0" } }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "engines": { "node": ">=0.10.0" } }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "dependencies": { "p-limit": "^3.0.2" }, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dependencies": { "callsites": "^3.0.0" }, "engines": { "node": ">=6" } }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" }, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "engines": { "node": ">=8" } }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/postcss/" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "engines": { "node": ">= 0.8.0" } }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "engines": { "node": ">= 0.6.0" } }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" } }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ] }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "engines": { "node": ">=0.10.0" } }, "node_modules/react-countup": { "version": "6.5.3", "resolved": "https://registry.npmjs.org/react-countup/-/react-countup-6.5.3.tgz", "integrity": "sha512-udnqVQitxC7QWADSPDOxVWULkLvKUWrDapn5i53HE4DPRVgs+Y5rr4bo25qEl8jSh+0l2cToJgGMx+clxPM3+w==", "dependencies": { "countup.js": "^2.8.0" }, "peerDependencies": { "react": ">= 16.3.0" } }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "node_modules/react-is": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==" }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "dev": true }, "node_modules/regenerate-unicode-properties": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, "dependencies": { "regenerate": "^1.4.2" }, "engines": { "node": ">=4" } }, "node_modules/regexpu-core": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", "dev": true, "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, "engines": { "node": ">=4" } }, "node_modules/regjsgen": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", "dev": true }, "node_modules/regjsparser": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", "dev": true, "dependencies": { "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/regjsparser/node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { "node": ">=6" } }, "node_modules/reselect": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "engines": { "node": ">=4" } }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, "node_modules/robust-predicates": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" }, "node_modules/rollup": { "version": "4.41.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz", "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==", "dev": true, "dependencies": { "@types/estree": "1.0.7" }, "bin": { "rollup": "dist/bin/rollup" }, "engines": { "node": ">=18.0.0", "npm": ">=8.0.0" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" } }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ], "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/feross" }, { "type": "patreon", "url": "https://www.patreon.com/feross" }, { "type": "consulting", "url": "https://feross.org/support" } ] }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, "engines": { "node": ">=8" } }, "node_modules/shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, "node_modules/snake-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "node_modules/socks": { "version": "2.8.4", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "engines": { "node": ">= 10.x" } }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "engines": { "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { "has-flag": "^4.0.0" }, "engines": { "node": ">=8" } }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "engines": { "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, "node_modules/svgo": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", "dev": true, "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" }, "bin": { "svgo": "bin/svgo" }, "engines": { "node": ">=14.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/svgo" } }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" }, "engines": { "node": ">=12.0.0" }, "funding": { "url": "https://github.com/sponsors/SuperchupuDev" } }, "node_modules/tinyglobby/node_modules/fdir": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, "peerDependencies": { "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { "picomatch": { "optional": true } } }, "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "dependencies": { "is-number": "^7.0.0" }, "engines": { "node": ">=8.0" } }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "engines": { "node": ">=18.12" }, "peerDependencies": { "typescript": ">=4.8.4" } }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { "node": ">=14.17" } }, "node_modules/typescript-eslint": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz", "integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==", "dev": true, "dependencies": { "@typescript-eslint/eslint-plugin": "8.32.1", "@typescript-eslint/parser": "8.32.1", "@typescript-eslint/utils": "8.32.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" }, "engines": { "node": ">=4" } }, "node_modules/unicode-match-property-value-ecmascript": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true, "engines": { "node": ">=4" } }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { "type": "opencollective", "url": "https://opencollective.com/browserslist" }, { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" }, { "type": "github", "url": "https://github.com/sponsors/ai" } ], "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" }, "peerDependencies": { "browserslist": ">= 4.21.0" } }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "dependencies": { "punycode": "^2.1.0" } }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, "jiti": { "optional": true }, "less": { "optional": true }, "lightningcss": { "optional": true }, "sass": { "optional": true }, "sass-embedded": { "optional": true }, "stylus": { "optional": true }, "sugarss": { "optional": true }, "terser": { "optional": true }, "tsx": { "optional": true }, "yaml": { "optional": true } } }, "node_modules/vite-plugin-svgr": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz", "integrity": "sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.1.3", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, "peerDependencies": { "vite": ">=2.6.0" } }, "node_modules/vite/node_modules/fdir": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dev": true, "peerDependencies": { "picomatch": "^3 || ^4" }, "peerDependenciesMeta": { "picomatch": { "optional": true } } }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "engines": { "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" }, "engines": { "node": ">= 8" } }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "engines": { "node": ">=0.10.0" } }, "node_modules/worker-timers": { "version": "7.1.8", "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", "dependencies": { "@babel/runtime": "^7.24.5", "tslib": "^2.6.2", "worker-timers-broker": "^6.1.8", "worker-timers-worker": "^7.0.71" } }, "node_modules/worker-timers-broker": { "version": "6.1.8", "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", "dependencies": { "@babel/runtime": "^7.24.5", "fast-unique-numbers": "^8.0.13", "tslib": "^2.6.2", "worker-timers-worker": "^7.0.71" } }, "node_modules/worker-timers-worker": { "version": "7.0.71", "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", "dependencies": { "@babel/runtime": "^7.24.5", "tslib": "^2.6.2" } }, "node_modules/ws": { "version": "8.18.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { "optional": true }, "utf-8-validate": { "optional": true } } }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, "node_modules/yaml": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, "optional": true, "peer": true, "bin": { "yaml": "bin.mjs" }, "engines": { "node": ">= 14.6" } }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } } } } Yakifo-amqtt-2637127/docs_test/package.json000066400000000000000000000025661504664204300205470ustar00rootroot00000000000000{ "name": "amqttio", "private": true, "version": "0.11.3", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build --mode production", "lint": "eslint .", "preview": "vite preview" }, "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "@mui/icons-material": "^7.1.1", "@mui/material": "^7.1.1", "@mui/x-charts": "^8.4.0", "@mui/x-data-grid": "^8.4.0", "@mui/x-date-pickers": "^8.4.0", "@mui/x-tree-view": "^8.4.0", "@react-spring/web": "^10.0.1", "dayjs": "^1.11.13", "mqtt": "^5.13.0", "react": "^19.1.0", "react-countup": "^6.5.3", "react-dom": "^19.1.0" }, "devDependencies": { "@eslint/js": "^9.25.0", "@svgr/webpack": "^8.1.0", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", "@vitejs/plugin-react": "^4.4.1", "eslint": "^9.25.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", "typescript": "~5.8.3", "typescript-eslint": "^8.30.1", "vite": "^6.3.5", "vite-plugin-svgr": "^4.3.0" } } Yakifo-amqtt-2637127/docs_test/public/000077500000000000000000000000001504664204300175265ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_test/public/favicon.png000066400000000000000000000403511504664204300216640ustar00rootroot00000000000000‰PNG  IHDRzŸΞύ•ΓPLTEψΪγψΧαωήζχΣέφΞΩυΚΦϋότΔσΐΟώφψσ½ΝςΉΚρΆΘρ³Επ­Αν ΆοͺΎόξςξ₯Ίν΅μ™²κ‘«λ–―ύςυώώ鎩ι‰₯θ†’η‚Ÿζ~œζy™ώωϊεu–δq“γnύύγjϊζμβfŠαb‡ΰ]ƒϊβιίY€ήS|έNxέKuΫFqΫAnϋκοΪΜΥuΙ”σoŸν‡|qCz΅ˆgΉ©₯΅­±ιι¬μηœιν5E·Χηΐ&aH―¦°%[[[Z›Ο_PχτΦ«W«=V˜Ή« μ‘–ΆζΦ¦i3γ8枞ΤοΎNˆ)Σ7ŽήΥ½eωܐW3KΩ1φq ιι·§΅§}΄ώ<όΘυΓιο‘ΦQ ιι{Ό«£ύβe~/Πp#ω~šq Cz:Γλνμhg=U§ιώ†.Σ†τtƒηwgGgΫyli{?N±<ߐž°e;;:TZLTΣ8Ή*YΥ?ݐžΖq{{Ϋ™m&*Ή «·³Σ0―σ=g―7PΞ +ρκ±ΪηΣ(Ά|_Ο4ύ§Χ<σiRεSι'o g€§ηώΙπ"ΞL,ͺ4WΣα₯žξλDG»’˜o½­κ‰†τ4„o««§]νώΖ σΝ7Τ<ϐž6Ψ»Θv•℉yL՞kHO8cύ#Τ“¨Δ­7”?ɐžθΌθ=wν υ$ͺP79?§ψI†τDζuέΉ³O©'!‡†ϋvΕ]CzΒ^νΏ_O= Ή΄:Ζ>՞8Bg;”~–€τm+}†!=ρ°ιn¦ž„bF•χ ι FαV1%ϊ­’$– ϐ²'pšˆ <ληϊ5q«(Iηe7€' ΰ…sΨw°X½ƒJnHO ^^Έxz¬X”Ή– ι ΐόΠ9ΥQo"qξeωβ§1€G74τυ€0ίTςhCz€„ΧΠςxψΣύξό£Γ™μΎE= X*x¬!="܁!α£”Σσ\ώαΑο…AŽ΅θ0+Xφ ι‘γΨΈ―ηεgGCΖ·{΅…z9γ5€'$žMkŸ˜Aξ` Θ~€!=<ΒΎAέn΄GtΚφ¦CΒ½rΆƒz˜w ι …½αJ+υm!7€‡ΐ+λ-ύ™πΚΡ%7KÐoœΩK³ΤsΐĜ’ω@Cz|YΈ|U3©=@tΛ|œ!=ŽΨš/*‰" †τΘ™ΏιjΫ7Τ°τŒ%€θg₯NŒ#jVzΖ’ΑΞΥΚ«Yι}ΌedΨ2“έ2€ώΩ΅)½pBεΡΨΰ±/JR1NQ“{wΗHΎ`Fςl2)―₯ηI[vBߞΘΟφ.IνIΟΧj€ύ0wΥ³-y¦”ήΗΫFUVξ@]ι“ž[ΊglΆŒDά7!Ζ©-ιΝOΆSOAγδƒΏ[Η#—¦¦€χεqMύΉΰδv6Ζ”¨H }Ά‘»ΤSΠ0R4ό§Nw¦Z’ήΒ=#ΙVιDj7Π5Φqvؚ‘ήΧGF%δSΩt*•L'“Ν3¦s#π―P#³ά’ž‚Θ₯Ωdͺ@Ί«nœχ‹Υ†τ^ή3»+M$β±d*Χ8cA<”Τ‚τ\ΉGF‚’€‰X¬ε›m&² _΄ ΫlKΗσδΈ[E…% ¬Cbίμυ+=ϋύ%Ž„d(θp‡=n₯χό~-:0b;;‘gΝη©§! ½JοΓ£zκ) #EwÝmΐα%Ρ§τM“5eΝΛοξΪΗ:.SΟCΊ”žν²ψ'8’ώ?LχUκi(Fσέ¨™ Œ|Θ·=Χ~…zͺΠ‘τήά«;r*°έ2Φ{ƒzjџτ>?¨;²΄λί:;ͺi/‘ή€χϊΒυψ“ΩρžμΊF= Ft&=ϋ¨ξνΘ…[EΏUΧ(}IOοvd)΄­Ω[Ε)t%½³ΊϊsNφv φhφVq }VϊΆ#Η67ζ„ BQ…~€g³φSO‰-ο“©°€θFzϊ΅#§·7zF΅η¬¨Š^€·0₯ΟNYΏη¬•±|§ θDzofτ¨’gf΄₯}Hογ΄ώ ͺH;kuw!+;‰†.€χU₯BγήΦA±γΫ™Ρƒτ–τVΚG ΈΟλν>{νKΟ# SO–„·£_Ον_4/=™σ€wj–z8h]zφa 9ήπ<Σω ο—žχŠŽ Ι;k]Wuh:.‡Ά₯7?­›H•Μ¦ϋ?έ|‘iι½z€—Ξ?α•τŒΆςΙΨΡ²τή?Πςμ‘Ϋ\}rzψhψΓ[œΤEFzύμEνδm’]ιύΌ­FΒ]7F="΄*=WƒΜύ±e˜NwšD£sw POΠοv NwšD›sφiήπ*–έ§ž)š”^x “z Œδ}KOkΑO[ -JοΕ½6κ)°‘υ6ŸΧm¨l4(½ωڎ…O―ōξ¨&-JOγΚKΊwg¨η š“žΆ•­‘¨κhMzšV^κwk-[SN 1ιiYy™Υ ±ΥC[Σ°ς²λγrQ„¦€§]εε7,5*-Ioώ‘F+ΥJΎŸΟ¨η ’žf•ηw>ՁΗνHO«ΚΫωρΈΦ]f₯ьτ4ͺΌˆkϊ1υE+Σ¦ς’ΏZ§©η ,‘ήs-*/±ΌkΈ.Κ£ ι=ŸΡžςr«~Γ‚\ MHO‹Κ |ΣY-h΄ = */ώγώυDGӞςr+†·Ά:βKO{Κσ™υ-Ηα₯§9εΕΎ? ž‚6]zZS^v₯ΑPž<—žΦ,ΙΫωλΤSΠ bKΟ;­)εEΎ=’ž‚†ZzΞA-•ΟΛόn7”§‘₯gΡPΙPi³M?Mc»βjl¨7ΥώΔ|25·B ,°τ - /ΫZNyφό₯n‘βΛΜw^Ι;‘ˆ#=w§;F²?ξΦΩΜ™Όp[˜υξ/g¦|² dβH―Ύ‹zUˆ|š£žB6ΛΠ5!O(MN9]a€χCτ>μkBσ^[.άφ|.+ŸKι½Ό-qζ«|ƒw\Α S Τ“¨DΏœk ›ŸΫ¬ʈ£Ό—. _]cLΖqO ι9ο »yμ!Ή·D)ŠμΜ^Σ’]L½Ε5Zν1BHΟΣΧJ=…J€eκε₯«BGΡ½]υ!BH/Cf!Ÿ`JŒ²eΆ†+ZXπWKAz‹DςpΧ ‘‹ΎpYάm)κ·4 ½W"ΧΞ‘y²­ωβκ9(eθy•`Zzιy§„΄‹ϊ(ΐ·pI[ ήζjξ=rιΩFN? §Ι•gkά‚w@ί›Κε©₯η`.XΔ‘νUκ³€½eBhΫqEͺœSK―ρρΚ#-₯ˆ•ηKά€ώ|Xθ}Q±ρŸφA'ΑI2‹ΔΎ½…‘~±]<Υ0_¬ψkZι=$μ›ύD›νψvD£GΌc\¨ΑB*=ϋ€°7·ΐ₯ς<Ύ«Τ§L곕~K)½Χ£m„―^w=a†­Ν*΄{G>#k)₯wAΤ:Ήο„vd{Λ]ΝMΚθͺT€PzΒζάRΪ‘}1M_jOPWι(OχwΎΓ)špœLyΎΜΐΩꏕv\2ιyο —ΟrΐΦΥ ?τ=QMw…;.•τ<-bh€ίI"εΩΫ.uΠΌ2GΞ„ΚŽJzI1οp’¨τ†­{L»³ TΈIIﭘ½ά†;¦Ÿ[m1<τ4 OιΕH Ή`x‚Γ< “R‘B,‰τ\½Bžjο)<Žτ¨˜‡š½ƒε~E"=s/Ε«V#§Pή»λG°!”τ^©h²ΐŸΠομՍiόE₯Όΐ€gŸΡzε_Ζ7ͺΌΈ!Jz/?Κφπ₯ηlBΝκx%tεΩzgΞJ’|ω0|ιYDμ°ΌGOΘό4.zQ7ʍΠ₯χ²r ’λφK.άΤ@ΩhΚoqΨs θΊΝΫ±Γτν³β½ |(oΐΕ–^—xύΟ2Ÿoάψ ϊ.JPώ+†,=cτR“ ^ήΦ·%―©μop₯· ήA/ώ±bΖ8α]ΑK #Θͺg»'œ5!"‘*Ο‡Hͺτ. WF/ΌŠκΒxsKΗξZ₯`JOΌbfΈΚ³χ ιAδKΊ¬uQzσδ₯sN‚«Ό·jη^ϋtΩίΰIΟ6)ZͺͺςΒ qk|π$Sφ7xr­―<ͺς>έ¬ΡλE’μoΠ€χI4Η¦ςΒIΡώz4’eƒ%=ίm€’ ¦ςΎά¨Ρ%―@¬μo€η¬°’ςΒi;θbQޚ„$=³`εU•·8Z»KžΙ”*$=_ΥΦ1Έΰ)Ο[/œ1•(qΠ”cP,» žςΗ;h`.Œ’ ΑΆ[4εyj{Ι+.ϋ+ ι Άέ’)ογm]Φ²PB._ώwl»ΕRžΫ\›ξ‹"v*$[!¨’N¨νKyσ“’yo(ΨP.Ώτ|Bυ ΖRήβc‘–z**5dζώ‰΅έFp”ημ¬ωϋΕ>!Rι ΅έΖ£(Κ{yWΨψΈ,OUψ%oι½)#ύ~αU–G΅’ιX…hΕo gιΉn ΄έfQ”±Ό «cF8+#ΫΝw|%δΏ`d=ΎΉ[Λ.Ϋ"Rε£Vφΰ+=‘Ά[ɁPOΜ“œ©©TNJ¬W ΔUzBm· ώ―αm12ΎΘΖ+ž«6DΪnW*ϊΌΏSσž³cx«XxJO€νΦ[ω܁£qΚΨl‘―VÍ£τDΪn«άλwΪ.‹ΫМ_ΕΆ&h» ύζ<οh-ρ©NͺjI1~h»ρo{φjͺσ»Λ#}­Ϊό†›τΪnqξξ³Ε‡†£OυΆKάτ‘f»MsοΔβΙΡΕΔ6/U} /ιωqX1ΉΌ•g)DBςŸŸV/ιI’l·ω/Ό« =Ώ/^‘^b\2”ΗKzοDi‚#}γέλργ¬(ί2aΚͺΰΙηmsίδ2¬ ~ία;Ύ£ιžaG>A:%+Œτ’”έ._m›΅Rnm’_”·εq‘žχ QU^εkΠσέ0rN"ΩeΆΈH―NΣO*ΞWy Σ†ω$WΉ9 HŽΚϋ)ίOT Dά#ͺŸ )=1ξΛ«gTFλΨΦζS_%€τ’"ά1ό―†ςώ’6ζ±ΉL₯χJ„œοd΅"3 ΚΫ#›‡w υ0ΏpσŒ pΟβ(UΛΚΛg™L*H&΍Υw\Nzaξ.CypHιD*‘M₯©ξ¦ΡςmφΤ&=›M_όlnΏjHyΉd"šŒΕšfΰυv 0ιυ¦¨ηΦν— Qˆ|‘βΡH,v%0ήV‘ΛP³#ν¬†dηvΡΡΏς2ΡH0V?Σ6PψoœXD(ιuΰΨόm(O R,ΎΎ΄=ή£Φ-‘ ιω`.=LΉ”θWyΩέ` k¬ύ\εnf|’^†ή§ž² qY§ΚK…ƒΊΩ^²λŒτžst›ΚDZδͺGεΕCΎpο8mρgιέ€·&―Κ“I*°=0ΨJo…‘ή+ξE²«Ϊζ» /K²φoŸ₯—έ £7¬€3œΤΏ(FšιΰΆωn·0_%ι½η]=±*’ƒΣ>L%@vύΫ—‡ͺ΅²@@z.n.Ω¬rRή+}4Ϋ“vΦvu‰vf^‚<1ΜI 3τ6#f€πVη`/υ,Jΐ.½Χδ…ϋ3‹ΟΈŒλ›ΐEΓHd««_˜6Ε°K―³b—q ΎςQž}XλωΆ±Ν9"ΗKΓ,=ϊ`)/Ÿd Ϋ¨Άk Δ·ΌOG„ΉΞ–€YzέΤΑR ?3ΥλλZn·—χ―"K«<¬£–ϊΖ₯E‡γŠˆ's™$=­ƒβwHe•^;υIάΛ§9Μ~ܚ&‘ξσ"ο³G0JΟ[½ _.Γ~'_ΜU’ΨhDˆ0†€Qz9jΓŸνV£ξ3)°: ‰o6ιΝσ1kΘ‡Οvϋ^“wυ™FΌ}Ψ€wƒψ#β³έΎz ΑΎΚιυ³—‘#άa’ήKκ`).Ϋν‹Aš) ξŽ Q_N Lο2υ’Ηe»uNpνzΐƒΘrŸ+`±H:B”Λvλκ%†PHπwσυΤΐ"½λ`³P—νΦ’­Ά’oι‰Fί £Ξβ²έ~Τ"ύqώ§YΫ7ƒτўτΈl·/jΙ¬p>ί_VυσΡ~έ$Ϋmx’ΪD€σ‘’ΖP’‘^z1ZσεεΉ;΅‘qM‹PJ“Υ³««XE"ΖaΠzΝτŸŠώjε6ˆjι΅’Ϊ]₯―2Ύh₯Οh™ 6ζ V@6Ϊ›ΰε½#Οι”Gf©ευ P+½R“ŠΓv;?«‰Λ­΄ΩF{ΤC₯τά΄žj'όšλΌOτ*‹π7~Υ’‘Q)½]Rινΐ›@<}ZHJͺ׍ςTJΟuxŠΘƒχ£dΔ(Si=Džώˆ:ιEI—ΌςΎhΰ3έωφT[yUP'=4»T|Θyρ·±ΔΟ3τυ3AQ%½·€a‰?YzΒ•Δ6)zph~υζBA«‘κ='M= ΑΫ@EΡ‹,>!=]sAτ^RnOWπ}η“ΰΪόŠEΣqeP#=Eo\y/BKΔ³Cή!*€Gš™ Aίςάw„Ά%η—λυ©<5¦t8ύ·‚HB—υ ;t*<5σβtΠ*MΌΥŽύκ™Ι-·θVy*€—"̏†ΏcψDh ]ŽΠβΤSΰˆb鑆ˆ‚ϋ1^ί7ι6»EΟΚS.½BλkzΊ°Υ€ΈqΙΙOβ»X˜P*$7e ΘοΠ8£Cύ^+O±τΐm ·θ½#κA/ηΚk   ₯#TžτΨ¦οi¦]ŽΨgύ^lP(½BŽϊ΅’Ζθy9„…‰‡Bι.zYhΣοAcA²ί&DύN€’LzNΒψ‰Uΰ_6Az‘OsΤSΐA™τ²tξΞtxΐθK kυP&=ΒzρΛΐ5έΕάnsίΑγ`…E‘τk9Ɓ«‘ˆΉέ¦>κݘw E#<ιύ^ „άnΓqa•gK©o¨o°4Xκ-͘σ&)•LD²—ϋU©DzvΊ‚½‘Ψρ„άn·Φ„›•γ’£Ή₯©₯±ΉD‘‘ƒ¬Δd8μRU¬F‰τκιŠΟύ„υx‰ΈέJΞ›"υ‡w―··w4Yͺ­Γύύ¦όΟUε¦/sΡΉoCΐΎV·ΫŒ]”ͺež₯φŽφΆ‹ LΈu}}R`λŒΒ΅OτΒte»°U άnc樧°‡w³»{@AΫάΣ“Y]™Sς»’p6p`•g»-άvλΜQOΑ³ΣΣqžaΟ―ΏtΡΏ¬ ψœ|ιyΙj'K?`]šm Γ°’‘­*ιή:ΫuŽέ{gξν |—ύYΙ—^œl©Ψ†UžpΫ­δ"m:Ά˜oΎηρ†YfςŽlι½&+#šzσn»Ν;θ*΄:£ύ½°QψζΑάΟ–!9”-½&²βκΨΌ_ΡΆΫμgΕye1ίn„CŽΧ™‘ΠόœŒΗΙ–YΈT6}B΄ν6υ&ςΕΐ·€£ΗίςγU%Wzt%Φ@+ͺ‰ΆέΖ?RΌ±/ϊΉ£ZnμUόεJο2Υ'–Ξ€wV¬ν6"α+ΟηδέΫTuΣ•)=Ί^ΏAσ~ηŊ<ώΑ6ͺ8Φ>”6O­ήV)£$SzΝT™ΐMψn ΥZ~;‰¬ΌW—†Ρ¨¦Ώήψ™3!‹]½|ͺΐΚZrσεμ Pγ-λnύ¨h―”'=²bް½YlB…/cV€vΔ.ίB<ύV)ΜNžτΘ=ΨΆΎ&ΘΡΑTžS²R\―Μ7*νΉ²€η¦ $Λδ!G{!RO;Dεy“W‰ΉΜ7Λo4²€η§²'{@α7κ³ό8΅³< WθξVζ[oΚFΙ’ž,—r½£}˜‚ ,ε9W“ΪΠλξωΚeoΘ‘žνz/δΣVωͺ ’ςά»VςΆ MΆ2ηu9KmT’Rz½βΕγ(οuσ˜­ΪΛMBŽτ¨Š'ϋ Lέ1–0Ί?x‚γ‚ψ Ο})ν “!½p7π\d"ύ‚Œ‹Ύ!ΜCyŽΜuqJέ—LW“!=Qυδ €Ώυ½0υC1”χα:ωο »%Y†τ¨φΫ_}pcΉisޱΜ_yοFΊΛοΡWrΛ­.½DΥCihAΞ=&oœχ+,ά%‘χcΞ_ύκ;Odϊ ΈEϊ¬pc1α—ͺGο2aοš*8η€ΖR2“!=ψ™Θ!Yη8'HΏΫl Ξ)<‘11ϋ€\.qΣ¨ϊ™PUO^ N]€€S„s=Ÿ·7ΕΉΦc ©Ρ‡<‰‘‘ŒrmHπbT°Œ§γ ½8e€­&=ͺ?n@ 0aEΚγ€msG·υΝ cΉ,AέiU“^¦ΖO* 8inY}ގ‹γ',Ι…Sάj# ZYά;Δ°&ηΏpά$+ !ŸS‘±U€G$Ί xOαSrςK 's^”¦ύΥ‰eΏŠτˆφΫUΈh)7F˜HUΌόόΰ_ntp”“ΩΚ#  ΔDˆY Α–P8†·^3$OF+W–Qθ*\˜–M„O&Qgε4ςΗΫbz/JΡ²PΌ’T–MΠJ°ΆT·}GsŸ8y„lƒB₯΄Wγ\ρ₯G$ΊΧΖ.€MOZδ€Ό…{­|ζΔ‰Z/₯G$*yΰ€ΧNΧOπˆ>K“ΓςHdχE :‹C§*JfΏ Β₯d€¦΄©Δές€π`$-杒+I¨fΌξ=ΝΡ― ‘—ι·w4bR9NρΎ’τΆI\ 8Α PM/ύ…GνFOJxΗY)ŠΏ-•€GcOή€³’μζ?σPžύ‚¨qy•)φ+Užδζž‡»΅•-Ή€‡³JeMU,Lˆ’i’6·υΨO€WGUΫ$Θ½-m\?>$ά_1ζ’ΐ ΕΉςΏβΘ ΨΛ†Ιw₯hώ΄μ:sO‹ΗΌ~ί‡ΚKρYτςˆά~ϋ‡ϊ˜³ΓΫ’½š³©£Θ΅T^z!’°u°dA']'ΛC\πΚ›Ώ―-Ζ ŠͺΊ–—‰Q/›J’v¬ϋ@K’ξσ~Vο 2W=’…έhβ¦–ŠoΝ,N’_œΨ°”ύα8₯«ρfμj°K,=ΉΏk²ΕHYι­rΠP’0ΨιΘ‚ rΠΚσ䩊»r’¬τHL+k`‘zQβγx < νͺ–―Ά₯('=7EΤJξf@bϊG’xΣ°_‡νΝ*ε€η£8*ωΑ‚<ήπp`ΙG²CVh+ΰΦ΄Qε/ρΚIdΏυ€tˆΣΠ–•·0₯D[$PNz‹4Ψ‹—–Š;Y_ΝhۜwD‘ΥΆΜ›τœ’75\ΖR+D8γΥ΄N”'kΥCιΦ{TU+ͺΌ‡¬ΐΎόέ(Ο”:ώCιQxΡ=P#YI=ΰνφΝ}­ΖH¦θQϊΟ²SΨ“· κς8I-+ΐΫ­ž”—’‘‘&Q,`ι·΄•“έ Κ{«#ε™’2"W(φΫTƒ4Z*šχvJGΚ3…ŠL%²ΧΕ7‘lq€>4Ψνφν}ϊlN@BE?•”ž™ΰJ%ItΡέnίLλJy'J ””…+# eI “†έnfτ₯ΌΈ ιQ$z‘J m¦d€n·Ύ)έΨσπŸ„JIΟKPu •oξ"*„ΊΟ ςΒΝϊπΫώc³8$₯”τ|ہςίξρO₯ͺ?F.΍ζy—%sΒnRJz`^x‘€gG NΈζZξ³$ < Z‹Dz`A’DΥ±φف»Έ:)Ύ|9WBza‚j`A’q:χmώ\a«&š<‰,-\BzΏο‘L₯¨ ΡΧ€ύ$•βSή/eν±|²sH ιυ’Μ€° ΡFΊ[a*6ΤG‚/?o§l^%€GP»Ϋojόλ΄πPWωΆ¬ž*¨~ZzS[@™ί„1’!0Ή„'υ2pHzχΤ?ώ+3ψ_Ή”ΓˆlΉΎBτ^QWΘβA‰VŸ§₯GpΤ έoέtυΰξC4ΝJψ’+αX?-=‚Ώ|θ–$«³’ށj“π•G!Rr6J|2§€η$hϋ kB·θ-AU•zσh ‘Θ–2<œ’^ΈŠ”!κciΪ«\ 4OλeΜJγΊ]βOIΐUy–¨eί³0γΨnPΧ£δ‚_*υ―"Ho (ƒŒlΏ@²Ο Ρ±š„·δqδ€τlψii ΛQΛi˜q>ιΠ‹±Χ³tν₯““πΟA ΰN/—fd2ΨRž―Τ‰HϋΈΛTύ:)=‚2?›@‘©dαR.˜ΊR―oθ, ώ€HΉΕμ€τπzy °b2'š¨’Ω€ξŠ7ξ‘ύ4Wζ7'€ηΖΟl€re 9Ρ$'ŒYςEς3ΎΞ•ϋΝ ιmβϋ|@ž;šΎΡ&Σ6Œ -|G‡α*&ΣJω›Σ ιυΆOEΣ¨bhΏΝ9ͺΊϊτ4`Z‹•έ ιαΫ'bs0γP-z˜Τu³ώr1 l%+DΰKο5™@`rώD}2r0wƒη΄x9αΛUͺ[W,½<~β6L‰ͺτΫR!ΚράΥW‰‹Φλ*^Z‹΅†Ώκg€zΨΥΘΓΈΠvΙ[χΒ#9oV~@±τπ+εaο’δA˜B 󴡟Ή³OUyD±τπk-lΑψ 6i– i Bz.n·ΙUΏNE γ_πŠαέo 6½Όώά[2-IοΗιά ΞΔ`*‹Έˆ"γ— N(/€[ Ρ“ω:)c7(’ώ’·³Ε!ˆκ/‚λΆήrιI9+ϊ³ρ/Έ˜.-DA+Ώ!άYe Εœ2ί•"ιαί2€Ξh4­bO°QC˜Έ“ 0ι₯°\›ΕqιΩΠ}q˜„ ’Κf§ Ψ¨!ͺ'ίmΪ“‘ŸΛy\zYτΨ‰Lفm‚2¨&S2 0Θ+έ1bξ˜’{κqιαίρ0ζ8ӊΐξΊ₯—P©\`₯UY΅žγΓ_ϊCΥ"šz™Σl”ΧGΩΪLΐΫ:¦Τ‘t\zθoC&ΰΘGbΥΫ0‚ΪabIΙDΒώ~«Š#Ο1ιΉ‘ΪΡΚ& nBΣ£Ε ψ.ˆ;2™t*E»Η{Υ¬“ή:ϊβ€ΉPt˜1νόΗ>Ζsν… dιL2Ν₯FVf،qΗ€‡μπƒŒ2 ε¬Ψίok¨ΐŠμμ^[*s0α)₯—‚I"YτR怏²όMτδcαݐe¦ Ψ„uLzθά0Lι\’P=/{@Ύ‡Ζρ¬Œ|Θ²r‘Ζ?ιΉΠ›yϊADγ€πDIvιύ>49άlγVeφŸτ6ΡΏ„AQβ¦ »YΘMU"F&qίv~–§›θŸτΠχΫ4Μ!dΏu³,$šζΑ‹˜wσΏ+œ·ΑCΏeμVΚ”“ I”h‚=^ΑU)°<Ξ$”žΔμ£8¬o°Ÿ/»E΅&§6ΪϋQŒτ€‡¬…"T/Οpcτ€^λͺρZ•#ι‘·ώΞXA†‘(d•i±hrn{ε ^ψτ" YΫ ΣΚ:σ††j³ˆ΄}榽ηHzθG½0ˆΑ(@P6›a"(^lrπ²GωHzθοDΈ…γΉp?b—²W© OΔ5Λp$=τ[L—Ь σΩ€E°όΗΔR~±ΏοΔΘ¦€,ΜEŠ`γJ3Ÿτ³ιeά;θΉ¦ϋƒ-½(Θ-ΓG&Ίme‘U(›ήv3MΓ_ι‘—§‹€d>{ €·Αz;υ5°,AζϋΡ+•z‡€ˆyΐͺ—`~MŸ@!+Α•ςŽ€ΥεK6κ;+€’£σΡ„¨φn r½Ko ―zy€1°OOL«Ί/ΓΟ|―yρb§©›|qΜΝ‚nΧ“Ζΐ–S³μ4S€΅~‘ϋ/ BΓψ1σϊͺ±”cKiΓE―ΓlΒΞσλ†Σεχί7r;‡M€Ώ ;‘ΙΚςlμUo—Υ•ρŽgώάΥ—G9,@\4•9Œ-½ cŒVψΟ‹gέ„νo4…Ω֞‡()Šέ9šεΙn5*ρ‡± Xœ―"Z.ό–%\‹'H+Q-E<¬!hrŒkΦ ή…Ρ.ωkξXΝzYˆΝRKeμψ8ctΰ0ο7χΜ_Λωc„°(c_Κ™V=μ’²Œϋ%χEΟd:wxΣ° Œf=l Σͺ‡νFcLsΎΒΎζΓΎδsΘͺ§₯ Ϋ™Α&=FΣޞ·ϋήμ ¦©˜~δ―6£τp3ξΰΨ«ˆτ΄ρ ©>ζΤGYτξ_r±ΟzZΌf0ΝΫόΝδΛ@ŠF3Gφ₯‡Ό‚€TΧΣ’τw•8K*+χΒ^°Όω[ ²α"ŸO™ΎΘ‹±dΰΖ°ΎΣA+ώ5" Χq `0Ϋ ΅™6ρzξΉΠ΄xΝhG>$°Ϋx0¬Η`©Aι`τώ* Ϋ5Š~Ν€Xυ›4*†εψΐΉ3ΑˆΒ ²iτj‘†œμ ±lbV¨YΘ„ε«}l²^ Ϋβ ±αbΫ°ΤͺΓΩΚ ³}eα΅, WNω@ΌˆX>LSώŽάwWύΩ98j΄ΏžΕ^υ Π’YωFΔΗ<{ΥeΠW=ˆ?OKÞ+C8ζQo―ώ–yχ LοΆ»™aγ—‡VŠτkΔ WKΗSl‹=C΄nLκA~{Ž–Θ₯ΡΝ-zΘ›ΣΛ!KαγD―»‹½κAΌHi~ω0MY;Giμ˜α:ΓΈR¦Uωpΐp`ΓN"Ηήp!^€α|˜ΞzΘ«ΓΫ‹,½,ϊ† qγCΞͺΦΤ5γTcΩ€΄qRςrZΌfΈK6δ“τ7\†n\A%/§Ε³ς]ŒIzΨASκ₯‡|ŒI ―zrnΣ5ω6Ξπώ"oΈI“EKgφCpΛLfC„<Ά΅LύR‚,½¬&ΟzZ y@>—2Ό7lΞ“B/|ρrCyάI3%τKΨ!jΉ˜GυΕ4Ήκ™?NCVώlΧ-―ώΦhέΕμŸΣdΠ”†£…Wz gΛ8¦τ­θƐE€ ½|˜μόΘΧ8†Ήξb^ί|—Π7\wMΧιΓ΄n!KαKω17—+l&ΘFl©kΗ'n³`^_ ΟšΑ―* ςrΘ+ “τ+_0|+G7ΰζQ…?{…/?FUOKCž+Λ†€(½ν) o^ «ς&Ζt²DΆΤ²Ό5gЌVρΉΒX?FUOKηSδ―6‹Αβξ¬ZSώ½Έ7μ ·žΕ!ϊ䏓Ν5 y0½5n,ι­ου,΄`VΆ*`†°°"obLSFώš0΅7ξˆBt+N`?εΧ²‹['ΡΡmUKfπ«ΈQ+Λ“GΏίšFe\ϋ««%ƒμύPΪCK7ά‘(f<n‡© |ΕO<θ>iGφoCά3΄c¦-Γ”^|”ιιwW¬0σ¨ΘΟGϋg1ep₯αΓoUŸkS ŠΧΒdJ0žgzΤ°s Ό‚τ4hΨ»0†˜"ςQοDΖ€ΞŸό―?όΏESY­‡|{0ˆ˜Ί%‘fςb; ήΝ3B‡ΚΓ_υ \Τ³ά€›%–«Ρ\―Jžωξ?³`w­Θ―‡ΎκΔ;!K­abOz»3ΜC4ξtL€<ΏΚ+HωΊrŒEώΊ¬3„ώΰyεμ²υq½i ΪΦζͺ‡;η<ΫZ‚Ψ„ΗΠΠΆŸη–+9¦ώΫ‚νdΤ`¬h†νV>ˆ–o“I΄Ή³ΝΟ΄ϊOy% η˜ΛYZo*‡uυΰΈ§L¦νYaΆΪyNΓΗνθg=UWz¬οP–―r? „»―fψψΣ²_ζŽύ€I“2rθJ”%Όΐ Ώf"UˆBΥ€όi‚Λ—εΗάρŸ΄yΓΕ•σ«yp€·v¦œδβΤΨ*.‚~Φ³@Ċ^F ·al•BΙήΜή―3τω+C,\Ÿg1±Ε:(Ζ ρΥ΄ΖZF‘Λ%Φf~=ͺβežη1Φ›‘WκXξDΑ7‹)ˆάϝ)Žφ/»ˆΛ°GζBςnHιΝΊ%X{dD:Ωyb2#NJ‚DEoαyLaφοζΨ0ΐD*γ{:œυu²ΖiενmΈΘ±’ &lΜΒ’>€m!Ζ}Ω“~g:ΜzΌp½"Γ«wOύ›₯° wΗ‚ ±HΓ€1ξr_φ˜Ζ‚Ϊΐf—―ΐ T™ΠάXωπ)³*‰Ύ©<ƒ¦7TEπmo?¨π[ cͺŸbv‘jB)°˜$c}ρΗq/oηž:εlΎ¨ΨΜ’pNV<€7†˜­ΰ‹Λ ·ύλ1ΠXρgο'αkHYΙc¦ω‘EsΈ£Ub¦χLDιeΩκΡΗ=ΔίωΌ ΫgjϊΛπ9/α΄Βš3½–->)Έά\uZ{σ#V‰ΐΕh=γoVfO>ΕΔ·›ΐ#]°,ΟL/.ΘΉΫε·–ŸΚΈ˜μI³4ƒπΓtžε]Ίn|EΉΊ·e1Γ3“;p©§Κ›žήhΊ Λ Ώ'½+ˆΕ¦|€_Ήί`†„‘½# Ϊ[―Íτ΅ZMaOg9½$C~Ή3Ϊ“ήRΪJ4θuΪžλ)Ur<ε0κΘη;`‹υrœw…”Σtvš<mΕ§V) …ž5+0ν{BΧΡ€·ϊ%ωΔ΅άΩ*ε™Lχ>ά…ρ?KN胣L†φ7άξΦ–¦Ί3f‹9Ig’£m ϋoBZ½_ΰθβIžλu‚W]†ϋ―¦ ΩYΗ$ΐ(ͺ)μ½lμK­aBϊΣLσ •Ύίη5τγpŒύω4Η>J–ώ $ιm³α*fπύ/γž››ς η%W`˜q£ρzζ@¦Bǁτ°’ίΰΣΌ\Ή!°•RŒš^M°„-§]w`Β 9ά †SΚdŠ=†³ŽO>€)S–ΗΆ>«Ϊ…OΪlΓqapεp΅ΓpJnJ.Y½rKςφJ1 ̘ζo«»$ΕΘ΅€ωp(½gkͺ‘‹Z9Ξΰσ‡ΰ‘{7ξΚ3νΉE?Œ)oͺ‘XmΦ…ςŽjŽύBή2τ%γ€ΰkrΉφ³ϋθ°²σBb₯«ZoώJοI€ƒΣ¨˜T„Σΐ?ήƒ=.,£}Ί£&Σ»aωο|δwχ-޳Αεθfϋc–χiΣ’W`κγ]Θuo 5nϋiώΒy9ϋnjsγ v³žIοΙ\™ƒ’$y-z¦ήNΑY‡–ϋ΄Ξ|¦—ϊ+ΫZατ,JJ>±6Ξρ+ίΈ-zΎœΊkHvŠ•ε‰ΙδΫκν*-ΏtΘοοGn˟λόQ2G Gη@'σS m„³vΎσ,Oα’ώήΤΩή\g±μ~€\&ŸŠF#—γh₯09ΆOνD8ΩΟπq0ημΈ€Gνό2ZeΠyh6qΔΗb»ζΡzS۞εσIˆŠc›}ώ˜_ŽΧ/ξ7³1Gπ:kΞV˜TyGŒT¨SΑύγψιόΏΟΌ^ƏP±yœΡ/jΚώΈ£»•ΐ] ορςiΔ½όŠ>γ±νμeυ _Π17ƒͺΫ$ΆΪΈTΛ}FΪΗfLΟΗUζl'œ“s s1¨B±τξ.Μπ°°8πNP9<ΧT$lδέ~žΆƒœ°Δ>}; +οΖ΄”›άΡ+ Χξ¬·y˜mƒbN:ΒηΗ{q«¨™¬¦ΧΝΧˆ/½~¦·˜"NωŸ&ΎϋΞ}zΒή¬ΙσνRŸ,K‘φžαjJ7(ΗiΧηΝŸΧ Χ½`Β 8š\††LΞμ…ΞjHlsσί~λe)αuΏώω.œφ|nͺγ{a-³gΟφ–έySaΏΏμPƒ#J|άϋtκ±v†ςβΈgLτy:Ϊ[›‹ώž|"†{υιΥ%c&ίM€ΨX€%΄2HeΩσΚ›άξϊzKCIΚeσ™ΜΥ‘6“q± §t˜Ϋƒ‘Ωoάs aΞ”7€§L„ε3Ϋ sCTΝ§Ηp₯\pοŒiρSΰ―΄jžcyΎή)/―;>3C9ŒΔ7Ύ™ 4O…•­ίτι¦Κb:ΉUΓ%jP…Š›κ€³yH™Εγ™α5¨Bεσά˜Ι—Ό T|‘₯iδžφZ€ΪU’_‘ψ€ΐ#γ”g ƒκ·Ψ~Sxγ’Μ YίςScΕ3…Jg§ιΥ₯sUύΉΐFfFσUί °i»{lroφŸm+V ύi3œ’ς‘m6ήσE…W:ΪO­7™2ΙπΞ@Ώ’²υ €·GηžOΦhnh΄4XLωl>—Ηnw6t Τ$5ΠαΏυ|Ϊ7IENDB`‚Yakifo-amqtt-2637127/docs_test/src/000077500000000000000000000000001504664204300170375ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_test/src/App.css000066400000000000000000000011601504664204300202670ustar00rootroot00000000000000#root { max-width: 1280px; margin: 0 auto; text-align: center; } .fil0, .fil1 { fill: #ffffff; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); } @keyframes logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: no-preference) { a:nth-of-type(2) .logo { animation: logo-spin infinite 20s linear; } } .card { padding: 2em; } .read-the-docs { color: #888; } Yakifo-amqtt-2637127/docs_test/src/App.tsx000066400000000000000000000002401504664204300203130ustar00rootroot00000000000000import './App.css' import Dashboard from './dashboard/Dashboard.tsx' function App() { return ( <> ) } export default App Yakifo-amqtt-2637127/docs_test/src/assets/000077500000000000000000000000001504664204300203415ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_test/src/assets/amqtt_icon.svg000066400000000000000000000041331504664204300232210ustar00rootroot00000000000000 Yakifo-amqtt-2637127/docs_test/src/assets/docker.svg000066400000000000000000000040311504664204300223270ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_test/src/assets/helpers.tsx000066400000000000000000000026771504664204300225570ustar00rootroot00000000000000import React from "react"; export type DataPoint = { time: string // ISO format timestamp: number; // epoch milliseconds value: number; }; // Define the type for each entry in the topic_map export type TopicEntry = { current: T; update: React.Dispatch>; }; // Define the topic_map type export type TopicMap = { [topic: string]: TopicEntry; }; // no need for a full uuid, generate a 6-character alphanumeric sequence, in two parts export function getClientID() { const genPart = () => { const rand = (Math.random() * 46656) | 0 // convert random number into an ascii sequence of letters, trimmed to 3 characters return ("000" + rand.toString(36)).slice(-3) } return `web-client-${genPart() + genPart()}` } export function secondsToDhms(seconds: number) { const days = Math.floor(seconds / (24 * 3600)); seconds %= (24 * 3600); const hours = Math.floor(seconds / 3600); seconds %= 3600; const minutes = Math.floor(seconds / 60); seconds = seconds % 60; return { days: days, hours: hours, minutes: minutes, seconds: seconds, }; } export function getMQTTSettings() { return { url: import.meta.env.VITE_MQTT_WS_TYPE + '://' + import.meta.env.VITE_MQTT_WS_HOST + ':' + import.meta.env.VITE_MQTT_WS_PORT, client_id: getClientID(), clean: true, protocol: 'wss', protocolVersion: 4, // MQTT 3.1.1 wsOptions: { protocol: 'mqtt' } } } Yakifo-amqtt-2637127/docs_test/src/assets/readthedocs.svg000066400000000000000000000041521504664204300233510ustar00rootroot00000000000000 Yakifo-amqtt-2637127/docs_test/src/assets/usemqtt.jsx000066400000000000000000000116761504664204300226040ustar00rootroot00000000000000import { useState, useEffect, useCallback, useRef } from 'react'; import mqtt from 'mqtt'; /** * Hook for connecting to an MQTT broker and subscribing to topics. * * @param {Object} settings - Configuration settings for the MQTT connection. * @param {string} settings.url - URL of the MQTT broker. * @param {Object} [settings.config] - Additional configuration options for the MQTT connection. * @param {number} [settings.reconnectDelay] - Reconnect delay in milliseconds (default: 5000). * * @returns {Object} An object containing the MQTT client, connection status, and payload. */ const Status = Object.freeze({ NOT_CONNECTED: 'NOT_CONNECTED', IN_PROGRESS: 'IN_PROGRESS', CONNECTED: 'CONNECTED', }); export default function useMqtt(settings) { const [client, setClient] = useState(null); const [connectionStatus, setConnectionStatus] = useState(Status.NOT_CONNECTED); const [isConnected, setIsConnected] = useState(false); const subscribedTopics = useRef([]); const messageQueue = useRef([]); const [messageTick, setMessageTick] = useState(0); // used to trigger re-renders when new messages arrive const getClientId = () => { return `mqttjs_${settings.client_id}`; }; const mqttConnect = useCallback(async () => { // Prevent multiple connection attempts if (connectionStatus !== Status.NOT_CONNECTED) { // console.log('trying to connect or already connected'); return; } setConnectionStatus(Status.IN_PROGRESS); const clientId = getClientId(); const url = settings.url; const options = { clientId, keepalive: 60, clean: true, reconnectPeriod: 0, // Disable automatic reconnection - we'll handle it ourselves connectTimeout: 30000, rejectUnauthorized: false, ...settings.config }; try { const clientMqtt = await mqtt.connect(url, options); // Set up initial event listeners clientMqtt.on('connect', () => { setConnectionStatus(Status.CONNECTED); console.log('MQTT Connected'); }); clientMqtt.on('error', (err) => { console.error('MQTT Connection error:', err.message); setConnectionStatus(Status.NOT_CONNECTED); // Schedule reconnect separately from the event handler // setTimeout(() => { // const delay = err.message?.includes('Not authorized') // ? Math.max(3000, settings.reconnectDelay || 5000) // : (settings.reconnectDelay || 5000); // // console.log(`Waiting ${delay/1000} seconds before reconnecting...`); // setTimeout(mqttConnect, delay); // }, 0); }); clientMqtt.on('close', () => { console.log('MQTT Connection closed'); setConnectionStatus(Status.NOT_CONNECTED); }); clientMqtt.on('offline', () => { console.log('MQTT Offline'); setConnectionStatus(Status.NOT_CONNECTED); }); clientMqtt.on('message', (_topic, message) => { const payloadMessage = { topic: _topic, message: message.toString() }; messageQueue.current.push(payloadMessage); setMessageTick((t) => t + 1); // trigger processing }); setClient(clientMqtt); } catch (error) { console.error('MQTT Connection error:', error.message); setConnectionStatus(Status.NOT_CONNECTED); await mqttConnect(); } }, [settings]); const mqttDisconnect = useCallback(() => { if (client) { client.end(() => { console.log('MQTT Disconnected'); setConnectionStatus(Status.NOT_CONNECTED); }); } }, [client]); const mqttSubscribe = async (topic) => { if (client && !subscribedTopics.current.includes(topic)) { console.log('MQTT subscribe ', topic); const _clientMqtt = await client.subscribe(topic, { qos: 0, rap: false, rh: 0, }, (error, granted) => { if (error) { console.log('MQTT Subscribe to topics error', error); } else { granted.forEach((item) => { subscribedTopics.current.push(item.topic) }) } }); // setClient(clientMqtt); } }; const mqttUnSubscribe = async (topic) => { if (client) { const _clientMqtt = await client.unsubscribe(topic, (error) => { if (error) { console.log('MQTT Unsubscribe error', error); } }); // setClient(clientMqtt); } }; useEffect(() => { console.log('connectionStatus', connectionStatus); if(connectionStatus === Status.CONNECTED) { setIsConnected(true); } else { subscribedTopics.current = []; setIsConnected(false) } }, [connectionStatus]) useEffect(() => { const _clientMqtt = mqttConnect(); return () => { //mqttDisconnect(); }; }, [mqttConnect, mqttDisconnect]); return { mqttConnect, mqttDisconnect, mqttSubscribe, mqttUnSubscribe, messageQueue, messageTick, isConnected }; }Yakifo-amqtt-2637127/docs_test/src/dashboard/000077500000000000000000000000001504664204300207665ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_test/src/dashboard/Copyright.tsx000066400000000000000000000010531504664204300234750ustar00rootroot00000000000000import Link from '@mui/material/Link'; import Typography from '@mui/material/Typography'; export default function Copyright(props: any) { return ( {'Copyright Β© '} aMQTT.io {' '} {new Date().getFullYear()} {'.'} ); } Yakifo-amqtt-2637127/docs_test/src/dashboard/Dashboard.tsx000066400000000000000000000027601504664204300234220ustar00rootroot00000000000000import { alpha } from '@mui/material/styles'; import CssBaseline from '@mui/material/CssBaseline'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import MainGrid from './components/MainGrid'; import AppTheme from '../shared-theme/AppTheme'; import AmqttLogo from './amqtt_bw.svg'; import AppBar from "@mui/material/AppBar"; import {Toolbar} from "@mui/material"; const xThemeComponents = {}; export default function Dashboard(props: { disableCustomTheme?: boolean }) { return ( website logo {/* Main content */} ({ flexGrow: 1, backgroundColor: theme.vars ? `rgba(${theme.vars.palette.background.defaultChannel} / 1)` : alpha(theme.palette.background.default, 1), overflow: 'auto', })} > ); } Yakifo-amqtt-2637127/docs_test/src/dashboard/amqtt_bw.svg000066400000000000000000000150261504664204300233310ustar00rootroot00000000000000 Yakifo-amqtt-2637127/docs_test/src/dashboard/components/000077500000000000000000000000001504664204300231535ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_test/src/dashboard/components/Counter.tsx000066400000000000000000000014241504664204300253330ustar00rootroot00000000000000import CountUp from "react-countup"; const byte_units = [ 'Bytes', 'KB', 'MB', 'GB', 'TB' ] const update_time = 5; export function ByteCounter(props: any) { let start = props.start; let end = props.end; if(end - start < 200){ return } let unit = byte_units[0]; for (let i = 0; i < byte_units.length; i++) { if( start > 1_000) { start = start / 1000; end = end / 1000; unit = byte_units[i+1]; } } return } export function StandardCounter(props: any) { return }Yakifo-amqtt-2637127/docs_test/src/dashboard/components/DashboardChart.tsx000066400000000000000000000124771504664204300265770ustar00rootroot00000000000000 import { useTheme } from '@mui/material/styles'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import Typography from '@mui/material/Typography'; import Stack from '@mui/material/Stack'; import { LineChart } from '@mui/x-charts/LineChart'; import type { DataPoint } from '../../assets/helpers.jsx'; import {CircularProgress} from "@mui/material"; import {StandardCounter, ByteCounter} from "./Counter.tsx"; import {useRef} from "react"; const currentTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; function formatDate(date: Date) { return date.toLocaleTimeString('en-US', { timeZone: currentTimeZone, hour: '2-digit', minute: '2-digit', second: '2-digit', }); } function AreaGradient({ color, id }: { color: string; id: string }) { return ( ); } function NoDataDisplay(props: any) { return <> {!props.isConnected ?
Connecting...
:
Connected, waiting for data...
} } function LinearChart(props: any) { const theme = useTheme(); const colorPalette = [ theme.palette.primary.light, theme.palette.primary.main, theme.palette.primary.dark, ]; const label: string = props.label || '--'; const baseline: number = props.baseline || 0; return formatDate(new Date(dp.timestamp)) ), tickInterval: (_index, i) => (i + 1) % (Math.floor(props.data.length/10) + 1) === 0, }, ]} series={[ { id: 'direct', label: label, showMark: false, curve: 'linear', stack: 'total', area: true, stackOrder: 'ascending', data: props.data.map( (dp:DataPoint) => dp.value), baseline: baseline } ]} height={175} margin={{ left: 0, right: 20, top: 20, bottom: 20 }} grid={{ horizontal: true }} sx={{ '& .MuiAreaElement-series-organic': { fill: "url('#organic')", }, '& .MuiAreaElement-series-referral': { fill: "url('#referral')", }, '& .MuiAreaElement-series-direct': { fill: "url('#direct')", }, }} hideLegend slotProps={{ legend: { }, }} > } export default function DashboardChart(props: any) { const lastCalc = useRef(0); const calc_per_second = (curValue: DataPoint, lastValue: DataPoint) => { if(!props.isPerSecond) { return ''; } if(!curValue || !lastValue) { return ''; } if(curValue.timestamp - lastValue.timestamp > 0) { const per_second = (curValue.value - lastValue.value) / ((curValue.timestamp - lastValue.timestamp) / 1000); lastCalc.current = Math.trunc(per_second * 10) / 10; } return `${lastCalc.current} / sec`; } return ( {props.title} { props.data.length < 2 ? "" : props.isBytes ? : } {props.label}

{ calc_per_second(props.data[props.data.length-1], props.data[props.data.length-2]) }

{ props.data.length < 2 ? : }
); } Yakifo-amqtt-2637127/docs_test/src/dashboard/components/DescriptionPanel.tsx000066400000000000000000000107601504664204300271620ustar00rootroot00000000000000import Grid from "@mui/material/Grid"; import Typography from "@mui/material/Typography"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faDiscord, faDocker, faGithub, faPython} from "@fortawesome/free-brands-svg-icons"; import rtdIcon from "../../assets/readthedocs.svg"; import {Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material"; export default function DescriptionPanel() { return <> Overview

This is test.amqtt.io.

It hosts a publicly available aMQTT server/broker.

MQTT is a very lightweight protocol that uses a publish/subscribe model. This makes it suitable for "machine to machine" messaging such as with low power sensors or mobile devices.

For more information:

github: Yakifo/amqtt

PyPi: aMQTT

Discord: aMQTT

website logo ReadTheDocs: aMQTT

DockerHub: aMQTT

 

Access Host test.amqtt.io TCP 1883 TLS TCP 8883 Websocket 8080 SSL Websocket 8443

The purpose of this free MQTT broker at test.amqtt.io is to learn about and test the MQTT protocol. It should not be used in production, development, staging or uat environments. Do not to use it to send any sensitive information or personal data into the system as all topics are public. Any illegal use of this MQTT broker is strictly forbidden. By using this MQTT broker located at test.amqtt.io you warrant that you are neither a sanctioned person nor located in a country that is subject to sanctions.

}Yakifo-amqtt-2637127/docs_test/src/dashboard/components/MainGrid.tsx000066400000000000000000000125621504664204300254130ustar00rootroot00000000000000import Grid from '@mui/material/Grid'; import Box from '@mui/material/Box'; import Stack from '@mui/material/Stack'; import Copyright from '../Copyright'; import DashboardChart from './DashboardChart.tsx'; import {useEffect, useState} from "react"; // @ts-ignore import useMqtt from '../../assets/usemqtt'; import {type DataPoint, getMQTTSettings, secondsToDhms, type TopicMap} from '../../assets/helpers'; import DescriptionPanel from "./DescriptionPanel"; export default function MainGrid() { const [sent, setSent] = useState([]); const [received, setReceived] = useState([]); const [bytesIn, setBytesIn] = useState([]); const [bytesOut, setBytesOut] = useState([]); const [clientsConnected, setClientsConnected] = useState([]); const [serverStart, setServerStart] = useState(''); const [serverUptime, setServerUptime] = useState(''); const [cpuPercent, setCpuPercent] = useState([]); const [memSize, setMemSize] = useState([]); const [version, setVersion] = useState(''); const {mqttSubscribe, isConnected, messageQueue, messageTick} = useMqtt(getMQTTSettings()); const topicMap: TopicMap = { '$SYS/broker/messages/publish/sent': {current: sent, update: setSent}, '$SYS/broker/messages/publish/received': {current: received, update: setReceived}, '$SYS/broker/load/bytes/received': {current: bytesIn, update: setBytesIn}, '$SYS/broker/load/bytes/sent': {current: bytesOut, update: setBytesOut}, '$SYS/broker/clients/connected': {current: clientsConnected, update: setClientsConnected}, '$SYS/broker/cpu/percent': {current: cpuPercent, update: setCpuPercent}, '$SYS/broker/heap/size': {current: memSize, update: setMemSize}, }; useEffect(() => { if (isConnected) { for(const topic in topicMap) { mqttSubscribe(topic); } mqttSubscribe('$SYS/broker/version'); mqttSubscribe('$SYS/broker/uptime/formatted'); mqttSubscribe('$SYS/broker/uptime'); } }, [isConnected, mqttSubscribe]); useEffect(() => { while (messageQueue.current.length > 0) { const payload = messageQueue.current.shift()!; try { const d = payload.message; if(payload.topic in topicMap) { const { update } = topicMap[payload.topic]; const newPoint: DataPoint = { time: new Date().toISOString(), timestamp: Date.now(), value: d }; update(current => [...current, newPoint]) } else if (payload.topic === '$SYS/broker/uptime/formatted') { const dt = new Date(d + "Z"); setServerStart(dt.toLocaleString()); } else if (payload.topic === '$SYS/broker/uptime') { const {days, hours, minutes, seconds} = secondsToDhms(d); setServerUptime(`${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`); } else if(payload.topic === '$SYS/broker/version') { setVersion(d); } } catch (e) { console.log(e); } } }, [messageTick, messageQueue]); const upTime = () => { return isConnected && serverUptime ? <> aMQTT broker {version.replace('aMQTT version ', 'v')} started at {serverStart}   up for {serverUptime} : <>; } return ( {/* cards */} theme.spacing(2)}} > theme.spacing(2)}} > {upTime()} ); } Yakifo-amqtt-2637127/docs_test/src/index.css000066400000000000000000000022021504664204300206540ustar00rootroot00000000000000:root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } Yakifo-amqtt-2637127/docs_test/src/main.tsx000066400000000000000000000003461504664204300205260ustar00rootroot00000000000000import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( , ) Yakifo-amqtt-2637127/docs_test/src/shared-theme/000077500000000000000000000000001504664204300214055ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_test/src/shared-theme/AppTheme.tsx000066400000000000000000000043721504664204300236560ustar00rootroot00000000000000import * as React from 'react'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import type { ThemeOptions } from '@mui/material/styles'; import { inputsCustomizations } from './customizations/inputs'; import { dataDisplayCustomizations } from './customizations/dataDisplay'; import { feedbackCustomizations } from './customizations/feedback'; import { navigationCustomizations } from './customizations/navigation'; import { surfacesCustomizations } from './customizations/surfaces'; import { colorSchemes, typography, shadows, shape } from './themePrimitives'; interface AppThemeProps { children: React.ReactNode; /** * This is for the docs site. You can ignore it or remove it. */ disableCustomTheme?: boolean; themeComponents?: ThemeOptions['components']; } //https://coolors.co/568e83-f5fbef-f15581-840b2d-d11149 export default function AppTheme(props: AppThemeProps) { const { children, disableCustomTheme, themeComponents } = props; const theme = React.useMemo(() => { return disableCustomTheme ? {} : createTheme({ // For more details about CSS variables configuration, see https://mui.com/material-ui/customization/css-theme-variables/configuration/ palette: { primary: { light: '#f15581', main: '#d11149', dark: '#840b2d', contrastText: '#fff', }, }, cssVariables: { colorSchemeSelector: 'data-mui-color-scheme', cssVarPrefix: 'template', }, colorSchemes, // Recently added in v6 for building light & dark mode app, see https://mui.com/material-ui/customization/palette/#color-schemes typography, shadows, shape, components: { ...inputsCustomizations, ...dataDisplayCustomizations, ...feedbackCustomizations, ...navigationCustomizations, ...surfacesCustomizations, ...themeComponents, }, }); }, [disableCustomTheme, themeComponents]); if (disableCustomTheme) { return {children}; } return ( {children} ); } Yakifo-amqtt-2637127/docs_test/src/shared-theme/ColorModeIconDropdown.tsx000066400000000000000000000053451504664204300263650ustar00rootroot00000000000000import * as React from 'react'; import DarkModeIcon from '@mui/icons-material/DarkModeRounded'; import LightModeIcon from '@mui/icons-material/LightModeRounded'; import Box from '@mui/material/Box'; import IconButton from '@mui/material/IconButton'; import type { IconButtonOwnProps } from '@mui/material/IconButton'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import { useColorScheme } from '@mui/material/styles'; export default function ColorModeIconDropdown(props: IconButtonOwnProps) { const { mode, systemMode, setMode } = useColorScheme(); const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; const handleMode = (targetMode: 'system' | 'light' | 'dark') => () => { setMode(targetMode); handleClose(); }; if (!mode) { return ( ({ verticalAlign: 'bottom', display: 'inline-flex', width: '2.25rem', height: '2.25rem', borderRadius: (theme.vars || theme).shape.borderRadius, border: '1px solid', borderColor: (theme.vars || theme).palette.divider, })} /> ); } const resolvedMode = (systemMode || mode) as 'light' | 'dark'; const icon = { light: , dark: , }[resolvedMode]; return ( {icon} System Light Dark ); } Yakifo-amqtt-2637127/docs_test/src/shared-theme/ColorModeSelect.tsx000066400000000000000000000014111504664204300251650ustar00rootroot00000000000000import { useColorScheme } from '@mui/material/styles'; import MenuItem from '@mui/material/MenuItem'; import Select from '@mui/material/Select'; import type { SelectProps } from '@mui/material/Select'; export default function ColorModeSelect(props: SelectProps) { const { mode, setMode } = useColorScheme(); if (!mode) { return null; } return ( ); } Yakifo-amqtt-2637127/docs_test/src/shared-theme/customizations/000077500000000000000000000000001504664204300245005ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_test/src/shared-theme/customizations/dataDisplay.tsx000066400000000000000000000144031504664204300275010ustar00rootroot00000000000000import { alpha } from '@mui/material/styles'; import type { Theme, Components } from '@mui/material/styles'; import { svgIconClasses } from '@mui/material/SvgIcon'; import { typographyClasses } from '@mui/material/Typography'; import { buttonBaseClasses } from '@mui/material/ButtonBase'; import { chipClasses } from '@mui/material/Chip'; import { iconButtonClasses } from '@mui/material/IconButton'; import { gray, red, green } from '../themePrimitives'; /* eslint-disable import/prefer-default-export */ export const dataDisplayCustomizations: Components = { MuiList: { styleOverrides: { root: { padding: '8px', display: 'flex', flexDirection: 'column', gap: 0, }, }, }, MuiListItem: { styleOverrides: { root: ({ theme }) => ({ [`& .${svgIconClasses.root}`]: { width: '1rem', height: '1rem', color: (theme.vars || theme).palette.text.secondary, }, [`& .${typographyClasses.root}`]: { fontWeight: 500, }, [`& .${buttonBaseClasses.root}`]: { display: 'flex', gap: 8, padding: '2px 8px', borderRadius: (theme.vars || theme).shape.borderRadius, opacity: 0.7, '&.Mui-selected': { opacity: 1, backgroundColor: alpha(theme.palette.action.selected, 0.3), [`& .${svgIconClasses.root}`]: { color: (theme.vars || theme).palette.text.primary, }, '&:focus-visible': { backgroundColor: alpha(theme.palette.action.selected, 0.3), }, '&:hover': { backgroundColor: alpha(theme.palette.action.selected, 0.5), }, }, '&:focus-visible': { backgroundColor: 'transparent', }, }, }), }, }, MuiListItemText: { styleOverrides: { primary: ({ theme }) => ({ fontSize: theme.typography.body2.fontSize, fontWeight: 500, lineHeight: theme.typography.body2.lineHeight, }), secondary: ({ theme }) => ({ fontSize: theme.typography.caption.fontSize, lineHeight: theme.typography.caption.lineHeight, }), }, }, MuiListSubheader: { styleOverrides: { root: ({ theme }) => ({ backgroundColor: 'transparent', padding: '4px 8px', fontSize: theme.typography.caption.fontSize, fontWeight: 500, lineHeight: theme.typography.caption.lineHeight, }), }, }, MuiListItemIcon: { styleOverrides: { root: { minWidth: 0, }, }, }, MuiChip: { defaultProps: { size: 'small', }, styleOverrides: { root: ({ theme }) => ({ border: '1px solid', borderRadius: '999px', [`& .${chipClasses.label}`]: { fontWeight: 600, }, variants: [ { props: { color: 'default', }, style: { borderColor: gray[200], backgroundColor: gray[100], [`& .${chipClasses.label}`]: { color: gray[500], }, [`& .${chipClasses.icon}`]: { color: gray[500], }, ...theme.applyStyles('dark', { borderColor: gray[700], backgroundColor: gray[800], [`& .${chipClasses.label}`]: { color: gray[300], }, [`& .${chipClasses.icon}`]: { color: gray[300], }, }), }, }, { props: { color: 'success', }, style: { borderColor: green[200], backgroundColor: green[50], [`& .${chipClasses.label}`]: { color: green[500], }, [`& .${chipClasses.icon}`]: { color: green[500], }, ...theme.applyStyles('dark', { borderColor: green[800], backgroundColor: green[900], [`& .${chipClasses.label}`]: { color: green[300], }, [`& .${chipClasses.icon}`]: { color: green[300], }, }), }, }, { props: { color: 'error', }, style: { borderColor: red[100], backgroundColor: red[50], [`& .${chipClasses.label}`]: { color: red[500], }, [`& .${chipClasses.icon}`]: { color: red[500], }, ...theme.applyStyles('dark', { borderColor: red[800], backgroundColor: red[900], [`& .${chipClasses.label}`]: { color: red[200], }, [`& .${chipClasses.icon}`]: { color: red[300], }, }), }, }, { props: { size: 'small' }, style: { maxHeight: 20, [`& .${chipClasses.label}`]: { fontSize: theme.typography.caption.fontSize, }, [`& .${svgIconClasses.root}`]: { fontSize: theme.typography.caption.fontSize, }, }, }, { props: { size: 'medium' }, style: { [`& .${chipClasses.label}`]: { fontSize: theme.typography.caption.fontSize, }, }, }, ], }), }, }, MuiTablePagination: { styleOverrides: { actions: { display: 'flex', gap: 8, marginRight: 6, [`& .${iconButtonClasses.root}`]: { minWidth: 0, width: 36, height: 36, }, }, }, }, MuiIcon: { defaultProps: { fontSize: 'small', }, styleOverrides: { root: { variants: [ { props: { fontSize: 'small', }, style: { fontSize: '1rem', }, }, ], }, }, }, }; Yakifo-amqtt-2637127/docs_test/src/shared-theme/customizations/feedback.tsx000066400000000000000000000024241504664204300267660ustar00rootroot00000000000000import { alpha} from '@mui/material/styles'; import type { Theme, Components } from '@mui/material/styles'; import { gray, orange } from '../themePrimitives'; /* eslint-disable import/prefer-default-export */ export const feedbackCustomizations: Components = { MuiAlert: { styleOverrides: { root: ({ theme }) => ({ borderRadius: 10, backgroundColor: orange[100], color: (theme.vars || theme).palette.text.primary, border: `1px solid ${alpha(orange[300], 0.5)}`, '& .MuiAlert-icon': { color: orange[500], }, ...theme.applyStyles('dark', { backgroundColor: `${alpha(orange[900], 0.5)}`, border: `1px solid ${alpha(orange[800], 0.5)}`, }), }), }, }, MuiDialog: { styleOverrides: { root: ({ theme }) => ({ '& .MuiDialog-paper': { borderRadius: '10px', border: '1px solid', borderColor: (theme.vars || theme).palette.divider, }, }), }, }, MuiLinearProgress: { styleOverrides: { root: ({ theme }) => ({ height: 8, borderRadius: 8, backgroundColor: gray[200], ...theme.applyStyles('dark', { backgroundColor: gray[800], }), }), }, }, }; Yakifo-amqtt-2637127/docs_test/src/shared-theme/customizations/inputs.tsx000066400000000000000000000313661504664204300265730ustar00rootroot00000000000000 import { alpha } from '@mui/material/styles'; import type { Theme, Components } from '@mui/material/styles'; import { outlinedInputClasses } from '@mui/material/OutlinedInput'; import { svgIconClasses } from '@mui/material/SvgIcon'; import { toggleButtonGroupClasses } from '@mui/material/ToggleButtonGroup'; import { toggleButtonClasses } from '@mui/material/ToggleButton'; import CheckBoxOutlineBlankRoundedIcon from '@mui/icons-material/CheckBoxOutlineBlankRounded'; import CheckRoundedIcon from '@mui/icons-material/CheckRounded'; import RemoveRoundedIcon from '@mui/icons-material/RemoveRounded'; import { gray, brand } from '../themePrimitives'; export const inputsCustomizations: Components = { MuiButtonBase: { defaultProps: { disableTouchRipple: true, disableRipple: true, }, styleOverrides: { root: ({ theme }) => ({ boxSizing: 'border-box', transition: 'all 100ms ease-in', '&:focus-visible': { outline: `3px solid ${alpha(theme.palette.primary.main, 0.5)}`, outlineOffset: '2px', }, }), }, }, MuiButton: { styleOverrides: { root: ({ theme }) => ({ boxShadow: 'none', borderRadius: (theme.vars || theme).shape.borderRadius, textTransform: 'none', variants: [ { props: { size: 'small', }, style: { height: '2.25rem', padding: '8px 12px', }, }, { props: { size: 'medium', }, style: { height: '2.5rem', // 40px }, }, { props: { color: 'primary', variant: 'contained', }, style: { color: 'white', backgroundColor: gray[900], backgroundImage: `linear-gradient(to bottom, ${gray[700]}, ${gray[800]})`, boxShadow: `inset 0 1px 0 ${gray[600]}, inset 0 -1px 0 1px hsl(220, 0%, 0%)`, border: `1px solid ${gray[700]}`, '&:hover': { backgroundImage: 'none', backgroundColor: gray[700], boxShadow: 'none', }, '&:active': { backgroundColor: gray[800], }, ...theme.applyStyles('dark', { color: 'black', backgroundColor: gray[50], backgroundImage: `linear-gradient(to bottom, ${gray[100]}, ${gray[50]})`, boxShadow: 'inset 0 -1px 0 hsl(220, 30%, 80%)', border: `1px solid ${gray[50]}`, '&:hover': { backgroundImage: 'none', backgroundColor: gray[300], boxShadow: 'none', }, '&:active': { backgroundColor: gray[400], }, }), }, }, { props: { color: 'secondary', variant: 'contained', }, style: { color: 'white', backgroundColor: brand[300], backgroundImage: `linear-gradient(to bottom, ${alpha(brand[400], 0.8)}, ${brand[500]})`, boxShadow: `inset 0 2px 0 ${alpha(brand[200], 0.2)}, inset 0 -2px 0 ${alpha(brand[700], 0.4)}`, border: `1px solid ${brand[500]}`, '&:hover': { backgroundColor: brand[700], boxShadow: 'none', }, '&:active': { backgroundColor: brand[700], backgroundImage: 'none', }, }, }, { props: { variant: 'outlined', }, style: { color: (theme.vars || theme).palette.text.primary, border: '1px solid', borderColor: gray[200], backgroundColor: alpha(gray[50], 0.3), '&:hover': { backgroundColor: gray[100], borderColor: gray[300], }, '&:active': { backgroundColor: gray[200], }, ...theme.applyStyles('dark', { backgroundColor: gray[800], borderColor: gray[700], '&:hover': { backgroundColor: gray[900], borderColor: gray[600], }, '&:active': { backgroundColor: gray[900], }, }), }, }, { props: { color: 'secondary', variant: 'outlined', }, style: { color: brand[700], border: '1px solid', borderColor: brand[200], backgroundColor: brand[50], '&:hover': { backgroundColor: brand[100], borderColor: brand[400], }, '&:active': { backgroundColor: alpha(brand[200], 0.7), }, ...theme.applyStyles('dark', { color: brand[50], border: '1px solid', borderColor: brand[900], backgroundColor: alpha(brand[900], 0.3), '&:hover': { borderColor: brand[700], backgroundColor: alpha(brand[900], 0.6), }, '&:active': { backgroundColor: alpha(brand[900], 0.5), }, }), }, }, { props: { variant: 'text', }, style: { color: gray[600], '&:hover': { backgroundColor: gray[100], }, '&:active': { backgroundColor: gray[200], }, ...theme.applyStyles('dark', { color: gray[50], '&:hover': { backgroundColor: gray[700], }, '&:active': { backgroundColor: alpha(gray[700], 0.7), }, }), }, }, { props: { color: 'secondary', variant: 'text', }, style: { color: brand[700], '&:hover': { backgroundColor: alpha(brand[100], 0.5), }, '&:active': { backgroundColor: alpha(brand[200], 0.7), }, ...theme.applyStyles('dark', { color: brand[100], '&:hover': { backgroundColor: alpha(brand[900], 0.5), }, '&:active': { backgroundColor: alpha(brand[900], 0.3), }, }), }, }, ], }), }, }, MuiIconButton: { styleOverrides: { root: ({ theme }) => ({ boxShadow: 'none', borderRadius: (theme.vars || theme).shape.borderRadius, textTransform: 'none', fontWeight: theme.typography.fontWeightMedium, letterSpacing: 0, color: (theme.vars || theme).palette.text.primary, border: '1px solid ', borderColor: gray[200], backgroundColor: alpha(gray[50], 0.3), '&:hover': { backgroundColor: gray[100], borderColor: gray[300], }, '&:active': { backgroundColor: gray[200], }, ...theme.applyStyles('dark', { backgroundColor: gray[800], borderColor: gray[700], '&:hover': { backgroundColor: gray[900], borderColor: gray[600], }, '&:active': { backgroundColor: gray[900], }, }), variants: [ { props: { size: 'small', }, style: { width: '2.25rem', height: '2.25rem', padding: '0.25rem', [`& .${svgIconClasses.root}`]: { fontSize: '1rem' }, }, }, { props: { size: 'medium', }, style: { width: '2.5rem', height: '2.5rem', }, }, ], }), }, }, MuiToggleButtonGroup: { styleOverrides: { root: ({ theme }) => ({ borderRadius: '10px', boxShadow: `0 4px 16px ${alpha(gray[400], 0.2)}`, [`& .${toggleButtonGroupClasses.selected}`]: { color: brand[500], }, ...theme.applyStyles('dark', { [`& .${toggleButtonGroupClasses.selected}`]: { color: '#fff', }, boxShadow: `0 4px 16px ${alpha(brand[700], 0.5)}`, }), }), }, }, MuiToggleButton: { styleOverrides: { root: ({ theme }) => ({ padding: '12px 16px', textTransform: 'none', borderRadius: '10px', fontWeight: 500, ...theme.applyStyles('dark', { color: gray[400], boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5)', [`&.${toggleButtonClasses.selected}`]: { color: brand[300], }, }), }), }, }, MuiCheckbox: { defaultProps: { disableRipple: true, icon: ( ), checkedIcon: , indeterminateIcon: , }, styleOverrides: { root: ({ theme }) => ({ margin: 10, height: 16, width: 16, borderRadius: 5, border: '1px solid ', borderColor: alpha(gray[300], 0.8), boxShadow: '0 0 0 1.5px hsla(210, 0%, 0%, 0.04) inset', backgroundColor: alpha(gray[100], 0.4), transition: 'border-color, background-color, 120ms ease-in', '&:hover': { borderColor: brand[300], }, '&.Mui-focusVisible': { outline: `3px solid ${alpha(brand[500], 0.5)}`, outlineOffset: '2px', borderColor: brand[400], }, '&.Mui-checked': { color: 'white', backgroundColor: brand[500], borderColor: brand[500], boxShadow: `none`, '&:hover': { backgroundColor: brand[600], }, }, ...theme.applyStyles('dark', { borderColor: alpha(gray[700], 0.8), boxShadow: '0 0 0 1.5px hsl(210, 0%, 0%) inset', backgroundColor: alpha(gray[900], 0.8), '&:hover': { borderColor: brand[300], }, '&.Mui-focusVisible': { borderColor: brand[400], outline: `3px solid ${alpha(brand[500], 0.5)}`, outlineOffset: '2px', }, }), }), }, }, MuiInputBase: { styleOverrides: { root: { border: 'none', }, input: { '&::placeholder': { opacity: 0.7, color: gray[500], }, }, }, }, MuiOutlinedInput: { styleOverrides: { input: { padding: 0, }, root: ({ theme }) => ({ padding: '8px 12px', color: (theme.vars || theme).palette.text.primary, borderRadius: (theme.vars || theme).shape.borderRadius, border: `1px solid ${(theme.vars || theme).palette.divider}`, backgroundColor: (theme.vars || theme).palette.background.default, transition: 'border 120ms ease-in', '&:hover': { borderColor: gray[400], }, [`&.${outlinedInputClasses.focused}`]: { outline: `3px solid ${alpha(brand[500], 0.5)}`, borderColor: brand[400], }, ...theme.applyStyles('dark', { '&:hover': { borderColor: gray[500], }, }), variants: [ { props: { size: 'small', }, style: { height: '2.25rem', }, }, { props: { size: 'medium', }, style: { height: '2.5rem', }, }, ], }), notchedOutline: { border: 'none', }, }, }, MuiInputAdornment: { styleOverrides: { root: ({ theme }) => ({ color: (theme.vars || theme).palette.grey[500], ...theme.applyStyles('dark', { color: (theme.vars || theme).palette.grey[400], }), }), }, }, MuiFormLabel: { styleOverrides: { root: ({ theme }) => ({ typography: theme.typography.caption, marginBottom: 8, }), }, }, }; Yakifo-amqtt-2637127/docs_test/src/shared-theme/customizations/navigation.tsx000066400000000000000000000203111504664204300273740ustar00rootroot00000000000000import * as React from 'react'; import { alpha } from '@mui/material/styles'; import type { Theme, Components } from '@mui/material/styles'; import type { SvgIconProps } from '@mui/material/SvgIcon'; import { buttonBaseClasses } from '@mui/material/ButtonBase'; import { dividerClasses } from '@mui/material/Divider'; import { menuItemClasses } from '@mui/material/MenuItem'; import { selectClasses } from '@mui/material/Select'; import { tabClasses } from '@mui/material/Tab'; import UnfoldMoreRoundedIcon from '@mui/icons-material/UnfoldMoreRounded'; import { gray, brand } from '../themePrimitives'; /* eslint-disable import/prefer-default-export */ export const navigationCustomizations: Components = { MuiMenuItem: { styleOverrides: { root: ({ theme }) => ({ borderRadius: (theme.vars || theme).shape.borderRadius, padding: '6px 8px', [`&.${menuItemClasses.focusVisible}`]: { backgroundColor: 'transparent', }, [`&.${menuItemClasses.selected}`]: { [`&.${menuItemClasses.focusVisible}`]: { backgroundColor: alpha(theme.palette.action.selected, 0.3), }, }, }), }, }, MuiMenu: { styleOverrides: { list: { gap: '0px', [`&.${dividerClasses.root}`]: { margin: '0 -8px', }, }, paper: ({ theme }) => ({ marginTop: '4px', borderRadius: (theme.vars || theme).shape.borderRadius, border: `1px solid ${(theme.vars || theme).palette.divider}`, backgroundImage: 'none', background: 'hsl(0, 0%, 100%)', boxShadow: 'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px', [`& .${buttonBaseClasses.root}`]: { '&.Mui-selected': { backgroundColor: alpha(theme.palette.action.selected, 0.3), }, }, ...theme.applyStyles('dark', { background: gray[900], boxShadow: 'hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px', }), }), }, }, MuiSelect: { defaultProps: { IconComponent: React.forwardRef((props, ref) => ( )), }, styleOverrides: { root: ({ theme }) => ({ borderRadius: (theme.vars || theme).shape.borderRadius, border: '1px solid', borderColor: gray[200], backgroundColor: (theme.vars || theme).palette.background.paper, boxShadow: `inset 0 1px 0 1px hsla(220, 0%, 100%, 0.6), inset 0 -1px 0 1px hsla(220, 35%, 90%, 0.5)`, '&:hover': { borderColor: gray[300], backgroundColor: (theme.vars || theme).palette.background.paper, boxShadow: 'none', }, [`&.${selectClasses.focused}`]: { outlineOffset: 0, borderColor: gray[400], }, '&:before, &:after': { display: 'none', }, ...theme.applyStyles('dark', { borderRadius: (theme.vars || theme).shape.borderRadius, borderColor: gray[700], backgroundColor: (theme.vars || theme).palette.background.paper, boxShadow: `inset 0 1px 0 1px ${alpha(gray[700], 0.15)}, inset 0 -1px 0 1px hsla(220, 0%, 0%, 0.7)`, '&:hover': { borderColor: alpha(gray[700], 0.7), backgroundColor: (theme.vars || theme).palette.background.paper, boxShadow: 'none', }, [`&.${selectClasses.focused}`]: { outlineOffset: 0, borderColor: gray[900], }, '&:before, &:after': { display: 'none', }, }), }), select: ({ theme }) => ({ display: 'flex', alignItems: 'center', ...theme.applyStyles('dark', { display: 'flex', alignItems: 'center', '&:focus-visible': { backgroundColor: gray[900], }, }), }), }, }, MuiLink: { defaultProps: { underline: 'none', }, styleOverrides: { root: ({ theme }) => ({ color: (theme.vars || theme).palette.text.primary, fontWeight: 500, position: 'relative', textDecoration: 'none', width: 'fit-content', '&::before': { content: '""', position: 'absolute', width: '100%', height: '1px', bottom: 0, left: 0, backgroundColor: (theme.vars || theme).palette.text.secondary, opacity: 0.3, transition: 'width 0.3s ease, opacity 0.3s ease', }, '&:hover::before': { width: 0, }, '&:focus-visible': { outline: `3px solid ${alpha(brand[500], 0.5)}`, outlineOffset: '4px', borderRadius: '2px', }, }), }, }, MuiDrawer: { styleOverrides: { paper: ({ theme }) => ({ backgroundColor: (theme.vars || theme).palette.background.default, }), }, }, MuiPaginationItem: { styleOverrides: { root: ({ theme }) => ({ '&.Mui-selected': { color: 'white', backgroundColor: (theme.vars || theme).palette.grey[900], }, ...theme.applyStyles('dark', { '&.Mui-selected': { color: 'black', backgroundColor: (theme.vars || theme).palette.grey[50], }, }), }), }, }, MuiTabs: { styleOverrides: { root: { minHeight: 'fit-content' }, indicator: ({ theme }) => ({ backgroundColor: (theme.vars || theme).palette.grey[800], ...theme.applyStyles('dark', { backgroundColor: (theme.vars || theme).palette.grey[200], }), }), }, }, MuiTab: { styleOverrides: { root: ({ theme }) => ({ padding: '6px 8px', marginBottom: '8px', textTransform: 'none', minWidth: 'fit-content', minHeight: 'fit-content', color: (theme.vars || theme).palette.text.secondary, borderRadius: (theme.vars || theme).shape.borderRadius, border: '1px solid', borderColor: 'transparent', ':hover': { color: (theme.vars || theme).palette.text.primary, backgroundColor: gray[100], borderColor: gray[200], }, [`&.${tabClasses.selected}`]: { color: gray[900], }, ...theme.applyStyles('dark', { ':hover': { color: (theme.vars || theme).palette.text.primary, backgroundColor: gray[800], borderColor: gray[700], }, [`&.${tabClasses.selected}`]: { color: '#fff', }, }), }), }, }, MuiStepConnector: { styleOverrides: { line: ({ theme }) => ({ borderTop: '1px solid', borderColor: (theme.vars || theme).palette.divider, flex: 1, borderRadius: '99px', }), }, }, MuiStepIcon: { styleOverrides: { root: ({ theme }) => ({ color: 'transparent', border: `1px solid ${gray[400]}`, width: 12, height: 12, borderRadius: '50%', '& text': { display: 'none', }, '&.Mui-active': { border: 'none', color: (theme.vars || theme).palette.primary.main, }, '&.Mui-completed': { border: 'none', color: (theme.vars || theme).palette.success.main, }, ...theme.applyStyles('dark', { border: `1px solid ${gray[700]}`, '&.Mui-active': { border: 'none', color: (theme.vars || theme).palette.primary.light, }, '&.Mui-completed': { border: 'none', color: (theme.vars || theme).palette.success.light, }, }), variants: [ { props: { completed: true }, style: { width: 12, height: 12, }, }, ], }), }, }, MuiStepLabel: { styleOverrides: { label: ({ theme }) => ({ '&.Mui-completed': { opacity: 0.6, ...theme.applyStyles('dark', { opacity: 0.5 }), }, }), }, }, }; Yakifo-amqtt-2637127/docs_test/src/shared-theme/customizations/surfaces.ts000066400000000000000000000056621504664204300266740ustar00rootroot00000000000000import { alpha } from '@mui/material/styles'; import type { Theme, Components } from '@mui/material/styles'; import { gray } from '../themePrimitives'; /* eslint-disable import/prefer-default-export */ export const surfacesCustomizations: Components = { MuiAccordion: { defaultProps: { elevation: 0, disableGutters: true, }, styleOverrides: { root: ({ theme }) => ({ padding: 4, overflow: 'clip', backgroundColor: (theme.vars || theme).palette.background.default, border: '1px solid', borderColor: (theme.vars || theme).palette.divider, ':before': { backgroundColor: 'transparent', }, '&:not(:last-of-type)': { borderBottom: 'none', }, '&:first-of-type': { borderTopLeftRadius: (theme.vars || theme).shape.borderRadius, borderTopRightRadius: (theme.vars || theme).shape.borderRadius, }, '&:last-of-type': { borderBottomLeftRadius: (theme.vars || theme).shape.borderRadius, borderBottomRightRadius: (theme.vars || theme).shape.borderRadius, }, }), }, }, MuiAccordionSummary: { styleOverrides: { root: ({ theme }) => ({ border: 'none', borderRadius: 8, '&:hover': { backgroundColor: gray[50] }, '&:focus-visible': { backgroundColor: 'transparent' }, ...theme.applyStyles('dark', { '&:hover': { backgroundColor: gray[800] }, }), }), }, }, MuiAccordionDetails: { styleOverrides: { root: { mb: 20, border: 'none' }, }, }, MuiPaper: { defaultProps: { elevation: 0, }, }, MuiCard: { styleOverrides: { root: ({ theme }) => { return { padding: 16, gap: 16, transition: 'all 100ms ease', backgroundColor: gray[50], borderRadius: (theme.vars || theme).shape.borderRadius, border: `1px solid ${(theme.vars || theme).palette.divider}`, boxShadow: 'none', ...theme.applyStyles('dark', { backgroundColor: gray[800], }), variants: [ { props: { variant: 'outlined', }, style: { border: `1px solid ${(theme.vars || theme).palette.divider}`, boxShadow: 'none', background: 'hsl(0, 0%, 100%)', ...theme.applyStyles('dark', { background: alpha(gray[900], 0.4), }), }, }, ], }; }, }, }, MuiCardContent: { styleOverrides: { root: { padding: 0, '&:last-child': { paddingBottom: 0 }, }, }, }, MuiCardHeader: { styleOverrides: { root: { padding: 0, }, }, }, MuiCardActions: { styleOverrides: { root: { padding: 0, }, }, }, }; Yakifo-amqtt-2637127/docs_test/src/shared-theme/themePrimitives.ts000066400000000000000000000224341504664204300251400ustar00rootroot00000000000000import { createTheme, alpha } from '@mui/material/styles'; import type { PaletteMode, Shadows } from '@mui/material/styles'; declare module '@mui/material/Paper' { interface PaperPropsVariantOverrides { highlighted: true; } } declare module '@mui/material/styles' { interface ColorRange { 50: string; 100: string; 200: string; 300: string; 400: string; 500: string; 600: string; 700: string; 800: string; 900: string; } interface PaletteColor extends ColorRange {} interface Palette { baseShadow: string; } } const defaultTheme = createTheme(); const customShadows: Shadows = [...defaultTheme.shadows]; export const brand = { 50: 'hsl(210, 100%, 95%)', 100: 'hsl(210, 100%, 92%)', 200: 'hsl(210, 100%, 80%)', 300: 'hsl(210, 100%, 65%)', 400: 'hsl(210, 98%, 48%)', 500: 'hsl(210, 98%, 42%)', 600: 'hsl(210, 98%, 55%)', 700: 'hsl(210, 100%, 35%)', 800: 'hsl(210, 100%, 16%)', 900: 'hsl(210, 100%, 21%)', }; export const gray = { 50: 'hsl(220, 35%, 97%)', 100: 'hsl(220, 30%, 94%)', 200: 'hsl(220, 20%, 88%)', 300: 'hsl(220, 20%, 80%)', 400: 'hsl(220, 20%, 65%)', 500: 'hsl(220, 20%, 42%)', 600: 'hsl(220, 20%, 35%)', 700: 'hsl(220, 20%, 25%)', 800: 'hsl(220, 30%, 6%)', 900: 'hsl(220, 35%, 3%)', }; export const green = { 50: 'hsl(120, 80%, 98%)', 100: 'hsl(120, 75%, 94%)', 200: 'hsl(120, 75%, 87%)', 300: 'hsl(120, 61%, 77%)', 400: 'hsl(120, 44%, 53%)', 500: 'hsl(120, 59%, 30%)', 600: 'hsl(120, 70%, 25%)', 700: 'hsl(120, 75%, 16%)', 800: 'hsl(120, 84%, 10%)', 900: 'hsl(120, 87%, 6%)', }; export const orange = { 50: 'hsl(45, 100%, 97%)', 100: 'hsl(45, 92%, 90%)', 200: 'hsl(45, 94%, 80%)', 300: 'hsl(45, 90%, 65%)', 400: 'hsl(45, 90%, 40%)', 500: 'hsl(45, 90%, 35%)', 600: 'hsl(45, 91%, 25%)', 700: 'hsl(45, 94%, 20%)', 800: 'hsl(45, 95%, 16%)', 900: 'hsl(45, 93%, 12%)', }; export const red = { 50: 'hsl(0, 100%, 97%)', 100: 'hsl(0, 92%, 90%)', 200: 'hsl(0, 94%, 80%)', 300: 'hsl(0, 90%, 65%)', 400: 'hsl(0, 90%, 40%)', 500: 'hsl(0, 90%, 30%)', 600: 'hsl(0, 91%, 25%)', 700: 'hsl(0, 94%, 18%)', 800: 'hsl(0, 95%, 12%)', 900: 'hsl(0, 93%, 6%)', }; export const getDesignTokens = (mode: PaletteMode) => { customShadows[1] = mode === 'dark' ? 'hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px' : 'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px'; return { palette: { mode, primary: { light: brand[200], main: brand[400], dark: brand[700], contrastText: brand[50], ...(mode === 'dark' && { contrastText: brand[50], light: brand[300], main: brand[400], dark: brand[700], }), }, info: { light: brand[100], main: brand[300], dark: brand[600], contrastText: gray[50], ...(mode === 'dark' && { contrastText: brand[300], light: brand[500], main: brand[700], dark: brand[900], }), }, warning: { light: orange[300], main: orange[400], dark: orange[800], ...(mode === 'dark' && { light: orange[400], main: orange[500], dark: orange[700], }), }, error: { light: red[300], main: red[400], dark: red[800], ...(mode === 'dark' && { light: red[400], main: red[500], dark: red[700], }), }, success: { light: green[300], main: green[400], dark: green[800], ...(mode === 'dark' && { light: green[400], main: green[500], dark: green[700], }), }, grey: { ...gray, }, divider: mode === 'dark' ? alpha(gray[700], 0.6) : alpha(gray[300], 0.4), background: { default: 'hsl(0, 0%, 99%)', paper: 'hsl(220, 35%, 97%)', ...(mode === 'dark' && { default: gray[900], paper: 'hsl(220, 30%, 7%)' }), }, text: { primary: gray[800], secondary: gray[600], warning: orange[400], ...(mode === 'dark' && { primary: 'hsl(0, 0%, 100%)', secondary: gray[400] }), }, action: { hover: alpha(gray[200], 0.2), selected: `${alpha(gray[200], 0.3)}`, ...(mode === 'dark' && { hover: alpha(gray[600], 0.2), selected: alpha(gray[600], 0.3), }), }, }, typography: { fontFamily: 'Inter, sans-serif', h1: { fontSize: defaultTheme.typography.pxToRem(48), fontWeight: 600, lineHeight: 1.2, letterSpacing: -0.5, }, h2: { fontSize: defaultTheme.typography.pxToRem(36), fontWeight: 600, lineHeight: 1.2, }, h3: { fontSize: defaultTheme.typography.pxToRem(30), lineHeight: 1.2, }, h4: { fontSize: defaultTheme.typography.pxToRem(24), fontWeight: 600, lineHeight: 1.5, }, h5: { fontSize: defaultTheme.typography.pxToRem(20), fontWeight: 600, }, h6: { fontSize: defaultTheme.typography.pxToRem(18), fontWeight: 600, }, subtitle1: { fontSize: defaultTheme.typography.pxToRem(18), }, subtitle2: { fontSize: defaultTheme.typography.pxToRem(14), fontWeight: 500, }, body1: { fontSize: defaultTheme.typography.pxToRem(14), }, body2: { fontSize: defaultTheme.typography.pxToRem(14), fontWeight: 400, }, caption: { fontSize: defaultTheme.typography.pxToRem(12), fontWeight: 400, }, }, shape: { borderRadius: 8, }, shadows: customShadows, }; }; export const colorSchemes = { light: { palette: { primary: { light: brand[200], main: brand[400], dark: brand[700], contrastText: brand[50], }, info: { light: brand[100], main: brand[300], dark: brand[600], contrastText: gray[50], }, warning: { light: orange[300], main: orange[400], dark: orange[800], }, error: { light: red[300], main: red[400], dark: red[800], }, success: { light: green[300], main: green[400], dark: green[800], }, grey: { ...gray, }, divider: alpha(gray[300], 0.4), background: { default: 'hsl(0, 0%, 99%)', paper: 'hsl(220, 35%, 97%)', }, text: { primary: gray[800], secondary: gray[600], warning: orange[400], }, action: { hover: alpha(gray[200], 0.2), selected: `${alpha(gray[200], 0.3)}`, }, baseShadow: 'hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px', }, }, dark: { palette: { primary: { contrastText: brand[50], light: brand[300], main: brand[400], dark: brand[700], }, info: { contrastText: brand[300], light: brand[500], main: brand[700], dark: brand[900], }, warning: { light: orange[400], main: orange[500], dark: orange[700], }, error: { light: red[400], main: red[500], dark: red[700], }, success: { light: green[400], main: green[500], dark: green[700], }, grey: { ...gray, }, divider: alpha(gray[700], 0.6), background: { default: gray[900], paper: 'hsl(220, 30%, 7%)', }, text: { primary: 'hsl(0, 0%, 100%)', secondary: gray[400], }, action: { hover: alpha(gray[600], 0.2), selected: alpha(gray[600], 0.3), }, baseShadow: 'hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px', }, }, }; export const typography = { fontFamily: 'Inter, sans-serif', h1: { fontSize: defaultTheme.typography.pxToRem(48), fontWeight: 600, lineHeight: 1.2, letterSpacing: -0.5, }, h2: { fontSize: defaultTheme.typography.pxToRem(36), fontWeight: 600, lineHeight: 1.2, }, h3: { fontSize: defaultTheme.typography.pxToRem(30), lineHeight: 1.2, }, h4: { fontSize: defaultTheme.typography.pxToRem(24), fontWeight: 600, lineHeight: 1.5, }, h5: { fontSize: defaultTheme.typography.pxToRem(20), fontWeight: 600, }, h6: { fontSize: defaultTheme.typography.pxToRem(18), fontWeight: 600, }, subtitle1: { fontSize: defaultTheme.typography.pxToRem(18), }, subtitle2: { fontSize: defaultTheme.typography.pxToRem(14), fontWeight: 500, }, body1: { fontSize: defaultTheme.typography.pxToRem(14), }, body2: { fontSize: defaultTheme.typography.pxToRem(14), fontWeight: 400, }, caption: { fontSize: defaultTheme.typography.pxToRem(12), fontWeight: 400, }, }; export const shape = { borderRadius: 8, }; // @ts-ignore const defaultShadows: Shadows = [ 'none', 'var(--template-palette-baseShadow)', ...defaultTheme.shadows.slice(2), ]; export const shadows = defaultShadows; Yakifo-amqtt-2637127/docs_test/src/vite-env.d.ts000066400000000000000000000000461504664204300213660ustar00rootroot00000000000000/// Yakifo-amqtt-2637127/docs_test/test.amqtt.io.yaml000066400000000000000000000013301504664204300216430ustar00rootroot00000000000000--- listeners: default: type: tcp bind: 0.0.0.0:1883 std-ws: type: ws bind: 0.0.0.0:8080 tls-mqtt: type: tcp bind: 0.0.0.0:8883 ssl: on certfile: /etc/letsencrypt/live/test.amqtt.io/fullchain.pem keyfile: /etc/letsencrypt/live/test.amqtt.io/privkey.pem tls-ws: type: ws bind: 0.0.0.0:8443 ssl: on certfile: /etc/letsencrypt/live/test.amqtt.io/fullchain.pem keyfile: /etc/letsencrypt/live/test.amqtt.io/privkey.pem plugins: amqtt.plugins.logging_amqtt.EventLoggerPlugin: amqtt.plugins.logging_amqtt.PacketLoggerPlugin: amqtt.plugins.authentication.AnonymousAuthPlugin: allow_anonymous: true amqtt.plugins.sys.broker.BrokerSysPlugin: sys_interval: 2 Yakifo-amqtt-2637127/docs_test/test.amqtt.local.yaml000066400000000000000000000006031504664204300223300ustar00rootroot00000000000000--- listeners: default: type: tcp bind: 0.0.0.0:1883 std-ws: type: ws bind: 0.0.0.0:8080 session_expiry_interval: 60 plugins: amqtt.plugins.logging_amqtt.EventLoggerPlugin: amqtt.plugins.logging_amqtt.PacketLoggerPlugin: amqtt.plugins.authentication.AnonymousAuthPlugin: allow_anonymous: true amqtt.plugins.sys.broker.BrokerSysPlugin: sys_interval: 2 Yakifo-amqtt-2637127/docs_test/tsconfig.app.json000066400000000000000000000012761504664204300215440ustar00rootroot00000000000000{ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, "include": ["src"] } Yakifo-amqtt-2637127/docs_test/tsconfig.json000066400000000000000000000001671504664204300207630ustar00rootroot00000000000000{ "files": [], "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } ] } Yakifo-amqtt-2637127/docs_test/tsconfig.node.json000066400000000000000000000011661504664204300217070ustar00rootroot00000000000000{ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2022", "lib": ["ES2023"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "moduleDetection": "force", "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, "include": ["vite.config.ts"] } Yakifo-amqtt-2637127/docs_test/vite.config.ts000066400000000000000000000002651504664204300210360ustar00rootroot00000000000000import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import svgr from 'vite-plugin-svgr'; export default defineConfig({ plugins: [react(), svgr()], }); Yakifo-amqtt-2637127/docs_web/000077500000000000000000000000001504664204300160465ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_web/assets/000077500000000000000000000000001504664204300173505ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_web/assets/amqtt.svg000066400000000000000000000150651504664204300212260ustar00rootroot00000000000000 Yakifo-amqtt-2637127/docs_web/assets/amqtt_bw.svg000066400000000000000000000150261504664204300217130ustar00rootroot00000000000000 Yakifo-amqtt-2637127/docs_web/assets/discord.svg000066400000000000000000000021751504664204300215250ustar00rootroot00000000000000 Yakifo-amqtt-2637127/docs_web/assets/docker.svg000066400000000000000000000040311504664204300213360ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_web/assets/extra.css000066400000000000000000000016521504664204300212110ustar00rootroot00000000000000/* https://coolors.co/e6c229-f17105-d11149-6610f2-1a8fe3 */ .md-header { background: #d11149; } body > div.md-container > main > div > div.md-content > article > a:nth-child(2), body > div.md-container > main > div > div.md-content > article > a:nth-child(1), body > div.md-container > main > div > div.md-sidebar.md-sidebar--primary > div, body > div.md-container > main > div > div.md-sidebar.md-sidebar--secondary > div, #home { display: none; height: 0; } .md-footer-meta__inner { display: block; height: 50px; } .md-copyright { display: none; } .md-footer-meta__inner { color: #000 !important; } nav.md-tabs { display: none; } h2.doc-heading-parameter { font-size: 16px; } .doc-md-description { font-size: 16px; display: block; } img[alt=readthedocs], img[alt=pypi], img[alt=github], img[alt=discord] { width: 20px; vertical-align: -4px; } img[alt=dockerhub] { width: 26px; vertical-align: -4px; } Yakifo-amqtt-2637127/docs_web/assets/github.svg000066400000000000000000000027771504664204300213700ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_web/assets/gitter.svg000066400000000000000000000006071504664204300213720ustar00rootroot00000000000000 Yakifo-amqtt-2637127/docs_web/assets/images/000077500000000000000000000000001504664204300206155ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_web/assets/images/favicon.png000066400000000000000000000403511504664204300227530ustar00rootroot00000000000000‰PNG  IHDRzŸΞύ•ΓPLTEψΪγψΧαωήζχΣέφΞΩυΚΦϋότΔσΐΟώφψσ½ΝςΉΚρΆΘρ³Επ­Αν ΆοͺΎόξςξ₯Ίν΅μ™²κ‘«λ–―ύςυώώ鎩ι‰₯θ†’η‚Ÿζ~œζy™ώωϊεu–δq“γnύύγjϊζμβfŠαb‡ΰ]ƒϊβιίY€ήS|έNxέKuΫFqΫAnϋκοΪΜΥuΙ”σoŸν‡|qCz΅ˆgΉ©₯΅­±ιι¬μηœιν5E·Χηΐ&aH―¦°%[[[Z›Ο_PχτΦ«W«=V˜Ή« μ‘–ΆζΦ¦i3γ8枞ΤοΎNˆ)Σ7ŽήΥ½eωܐW3KΩ1φq ιι·§΅§}΄ώ<όΘυΓιο‘ΦQ ιι{Ό«£ύβe~/Πp#ω~šq Cz:Γλνμhg=U§ιώ†.Σ†τtƒηwgGgΫyli{?N±<ߐž°e;;:TZLTΣ8Ή*YΥ?ݐžΖq{{Ϋ™m&*Ή «·³Σ0―σ=g―7PΞ +ρκ±ΪηΣ(Ά|_Ο4ύ§Χ<σiRεSι'o g€§ηώΙπ"ΞL,ͺ4WΣα₯žξλDG»’˜o½­κ‰†τ4„o««§]νώΖ σΝ7Τ<ϐž6Ψ»Θv•℉yL՞kHO8cύ#Τ“¨Δ­7”?ɐžθΌθ=wν υ$ͺP79?§ψI†τDζuέΉ³O©'!‡†ϋvΕ]CzΒ^νΏ_O= Ή΄:Ζ>՞8Bg;”~–€τm+}†!=ρ°ιn¦ž„bF•χ ι FαV1%ϊ­’$– ϐ²'pšˆ <ληϊ5q«(Iηe7€' ΰ…sΨw°X½ƒJnHO ^^Έxz¬X”Ή– ι ΐόΠ9ΥQo"qξeωβ§1€G74τυ€0ίTςhCz€„ΧΠςxψΣύξό£Γ™μΎE= X*x¬!="܁!α£”Σσ\ώαΑο…AŽ΅θ0+Xφ ι‘γΨΈ―ηεgGCΖ·{΅…z9γ5€'$žMkŸ˜Aξ` Θ~€!=<ΒΎAέn΄GtΚφ¦CΒ½rΆƒz˜w ι …½αJ+υm!7€‡ΐ+λ-ύ™πΚΡ%7KÐoœΩK³ΤsΐĜ’ω@Cz|YΈ|U3©=@tΛ|œ!=ŽΨš/*‰" †τΘ™ΏιjΫ7Τ°τŒ%€θg₯NŒ#jVzΖ’ΑΞΥΚ«Yι}ΌedΨ2“έ2€ώΩ΅)½pBεΡΨΰ±/JR1NQ“{wΗHΎ`Fςl2)―₯ηI[vBߞΘΟφ.IνIΟΧj€ύ0wΥ³-y¦”ήΗΫFUVξ@]ι“ž[ΊglΆŒDά7!Ζ©-ιΝOΆSOAγδƒΏ[Η#—¦¦€χεqMύΉΰδv6Ζ”¨H }Ά‘»ΤSΠ0R4ό§Nw¦Z’ήΒ=#ΙVιDj7Π5Φqvؚ‘ήΧGF%δSΩt*•L'“Ν3¦s#π―P#³ά’ž‚Θ₯Ωdͺ@Ί«nœχ‹Υ†τ^ή3»+M$β±d*Χ8cA<”Τ‚τ\ΉGF‚’€‰X¬ε›m&² _΄ ΫlKΗσδΈ[E…% ¬Cbίμυ+=ϋύ%Ž„d(θp‡=n₯χό~-:0b;;‘gΝη©§! ½JοΓ£zκ) #EwÝmΐα%Ρ§τM“5eΝΛοξΪΗ:.SΟCΊ”žν²ψ'8’ώ?LχUκi(Fσέ¨™ Œ|Θ·=Χ~…zͺΠ‘τήά«;r*°έ2Φ{ƒzjџτ>?¨;²΄λί:;ͺi/‘ή€χϊΒυψ“ΩρžμΊF= Ft&=ϋ¨ξνΘ…[EΏUΧ(}IOοvd)΄­Ω[Ε)t%½³ΊϊsNφv φhφVq }VϊΆ#Η67ζ„ BQ…~€g³φSO‰-ο“©°€θFzϊ΅#§·7zF΅η¬¨Š^€·0₯ΟNYΏη¬•±|§ θDzofτ¨’gf΄₯}Hογ΄ώ ͺH;kuw!+;‰†.€χU₯BγήΦA±γΫ™Ρƒτ–τVΚG ΈΟλν>{νKΟ# SO–„·£_Ον_4/=™σ€wj–z8h]zφa 9ήπ<Σω ο—žχŠŽ Ι;k]Wuh:.‡Ά₯7?­›H•Μ¦ϋ?έ|‘iι½z€—Ξ?α•τŒΆςΙΨΡ²τή?Πςμ‘Ϋ\}rzψhψΓ[œΤEFzύμEνδm’]ιύΌ­FΒ]7F="΄*=WƒΜύ±e˜NwšD£sw POΠοv NwšD›sφiήπ*–έ§ž)š”^x “z Œδ}KOkΑO[ -JοΕ½6κ)°‘υ6ŸΧm¨l4(½ωڎ…O―ōξ¨&-JOγΚKΊwg¨η š“žΆ•­‘¨κhMzšV^κwk-[SN 1ιiYy™Υ ±ΥC[Σ°ς²λγrQ„¦€§]εε7,5*-Ioώ‘F+ΥJΎŸΟ¨η ’žf•ηw>ՁΗνHO«ΚΫωρΈΦ]f₯ьτ4ͺΌˆkϊ1υE+Σ¦ς’ΏZ§©η ,‘ήs-*/±ΌkΈ.Κ£ ι=ŸΡžςr«~Γ‚\ MHO‹Κ |ΣY-h΄ = */ώγώυDGӞςr+†·Ά:βKO{Κσ™υ-Ηα₯§9εΕΎ? ž‚6]zZS^v₯ΑPž<—žΦ,ΙΫωλΤSΠ bKΟ;­)εEΎ=’ž‚†ZzΞA-•ΟΛόn7”§‘₯gΡPΙPi³M?Mc»βjl¨7ΥώΔ|25·B ,°τ - /ΫZNyφό₯n‘βΛΜw^Ι;‘ˆ#=w§;F²?ξΦΩΜ™Όp[˜υξ/g¦|² dβH―Ύ‹zUˆ|š£žB6ΛΠ5!O(MN9]a€χCτ>μkBσ^[.άφ|.+ŸKι½Ό-qζ«|ƒw\Α S Τ“¨DΏœk ›ŸΫ¬ʈ£Ό—. _]cLΖqO ι9ο »yμ!Ή·D)ŠμΜ^Σ’]L½Ε5Zν1BHΟΣΧJ=…J€eκε₯«BGΡ½]υ!BH/Cf!Ÿ`JŒ²eΆ†+ZXπWKAz‹DςpΧ ‘‹ΎpYάm)κ·4 ½W"ΧΞ‘y²­ωβκ9(eθy•`Zzιy§„΄‹ϊ(ΐ·pI[ ήζjξ=rιΩFN? §Ι•gkά‚w@ί›Κε©₯η`.XΔ‘νUκ³€½eBhΫqEͺœSK―ρρΚ#-₯ˆ•ηKά€ώ|Xθ}Q±ρŸφA'ΑI2‹ΔΎ½…‘~±]<Υ0_¬ψkZι=$μ›ύD›νψvD£GΌc\¨ΑB*=ϋ€°7·ΐ₯ς<Ύ«Τ§L곕~K)½Χ£m„―^w=a†­Ν*΄{G>#k)₯wAΤ:Ήο„vd{Λ]ΝMΚθͺT€PzΒζάRΪ‘}1M_jOPWι(OχwΎΓ)špœLyΎΜΐΩꏕv\2ιyο —ΟrΐΦΥ ?τ=QMw…;.•τ<-bh€ίI"εΩΫ.uΠΌ2GΞ„ΚŽJzI1οp’¨τ†­{L»³ TΈIIﭘ½ά†;¦Ÿ[m1<τ4 OιΕH Ή`x‚Γ< “R‘B,‰τ\½Bžjο)<Žτ¨˜‡š½ƒε~E"=s/Ε«V#§Pή»λG°!”τ^©h²ΐŸΠομՍiόE₯Όΐ€gŸΡzε_Ζ7ͺΌΈ!Jz/?Κφπ₯ηlBΝκx%tεΩzgΞJ’|ω0|ιYDμ°ΌGOΘό4.zQ7ʍΠ₯χ²r ’λφK.άΤ@ΩhΚoqΨs θΊΝΫ±Γτν³β½ |(oΐΕ–^—xύΟ2Ÿoάψ ϊ.JPώ+†,=cτR“ ^ήΦ·%―©μop₯· ήA/ώ±bΖ8α]ΑK #Θͺg»'œ5!"‘*Ο‡Hͺτ. WF/ΌŠκΒxsKΗξZ₯`JOΌbfΈΚ³χ ιAδKΊ¬uQzσδ₯sN‚«Ό·jη^ϋtΩίΰIΟ6)ZͺͺςΒ qk|π$Sφ7xr­―<ͺς>έ¬ΡλE’μoΠ€χI4Η¦ςΒIΡώz4’eƒ%=ίm€’ ¦ςΎά¨Ρ%―@¬μo€η¬°’ςΒi;θbQޚ„$=³`εU•·8Z»KžΙ”*$=_ΥΦ1Έΰ)Ο[/œ1•(qΠ”cP,» žςΗ;h`.Œ’ ΑΆ[4εyj{Ι+.ϋ+ ι Άέ’)ογm]Φ²PB._ώwl»ΕRžΫ\›ξ‹"v*$[!¨’N¨νKyσ“’yo(ΨP.Ώτ|Bυ ΖRήβc‘–z**5dζώ‰΅έFp”ημ¬ωϋΕ>!Rι ΅έΖ£(Κ{yWΨψΈ,OUψ%oι½)#ύ~αU–G΅’ιX…hΕo gιΉn ΄έfQ”±Ό «cF8+#ΫΝw|%δΏ`d=ΎΉ[Λ.Ϋ"Rε£Vφΰ+=‘Ά[ɁPOΜ“œ©©TNJ¬W ΔUzBm· ώ―αm12ΎΘΖ+ž«6DΪnW*ϊΌΏSσž³cx«XxJO€νΦ[ω܁£qΚΨl‘―VÍ£τDΪn«άλwΪ.‹ΫМ_ΕΆ&h» ύζ<οh-ρ©NͺjI1~h»ρo{φjͺσ»Λ#}­Ϊό†›τΪnqξξ³Ε‡†£OυΆKάτ‘f»MsοΔβΙΡΕΔ6/U} /ιωqX1ΉΌ•g)DBςŸŸV/ιI’l·ω/Ό« =Ώ/^‘^b\2”ΗKzοDi‚#}γέλργ¬(ί2aΚͺΰΙηmsίδ2¬ ~ία;Ύ£ιžaG>A:%+Œτ’”έ._m›΅Rnm’_”·εq‘žχ QU^εkΠσέ0rN"ΩeΆΈH―NΣO*ΞWy Σ†ω$WΉ9 HŽΚϋ)ίOT Dά#ͺŸ )=1ξΛ«gTFλΨΦζS_%€τ’"ά1ό―†ςώ’6ζ±ΉL₯χJ„œοd΅"3 ΚΫ#›‡w υ0ΏpσŒ pΟβ(UΛΚΛg™L*H&΍Υw\Nzaξ.CypHιD*‘M₯©ξ¦ΡςmφΤ&=›M_όlnΏjHyΉd"šŒΕšfΰυv 0ιυ¦¨ηΦν— Qˆ|‘βΡH,v%0ήV‘ΛP³#ν¬†dηvΡΡΏς2ΡH0V?Σ6PψoœXD(ιuΰΨόm(O R,ΎΎ΄=ή£Φ-‘ ιω`.=LΉ”θWyΩέ` k¬ύ\εnf|’^†ή§ž² qY§ΚK…ƒΊΩ^²λŒτžst›ΚDZδͺGεΕCΎpο8mρgιέ€·&―Κ“I*°=0ΨJo…‘ή+ξE²«Ϊζ» /K²φoŸ₯—έ £7¬€3œΤΏ(FšιΰΆωn·0_%ι½η]=±*’ƒΣ>L%@vύΫ—‡ͺ΅²@@z.n.Ω¬rRή+}4Ϋ“vΦvu‰vf^‚<1ΜI 3τ6#f€πVη`/υ,Jΐ.½Χδ…ϋ3‹ΟΈŒλ›ΐEΓHd««_˜6Ε°K―³b—q ΎςQž}XλωΆ±Ν9"ΗKΓ,=ϊ`)/Ÿd Ϋ¨Άk Δ·ΌOG„ΉΞ–€YzέΤΑR ?3ΥλλZn·—χ―"K«<¬£–ϊΖ₯E‡γŠˆ's™$=­ƒβwHe•^;υIάΛ§9Μ~ܚ&‘ξσ"ο³G0JΟ[½ _.Γ~'_ΜU’ΨhDˆ0†€Qz9jΓŸνV£ξ3)°: ‰o6ιΝσ1kΘ‡Οvϋ^“wυ™FΌ}Ψ€wƒψ#β³έΎz ΑΎΚιυ³—‘#άa’ήKκ`).Ϋν‹Aš) ξŽ Q_N Lο2υ’Ηe»uNpνzΐƒΘrŸ+`±H:B”Λvλκ%†PHπwσυΤΐ"½λ`³P—νΦ’­Ά’oι‰Fί £Ξβ²έ~Τ"ύqώ§YΫ7ƒτўτΈl·/jΙ¬p>ί_VυσΡ~έ$Ϋmx’ΪD€σ‘’ΖP’‘^z1ZσεεΉ;΅‘qM‹PJ“Υ³««XE"ΖaΠzΝτŸŠώjε6ˆjι΅’Ϊ]₯―2Ύh₯Οh™ 6ζ V@6Ϊ›ΰε½#Οι”Gf©ευ P+½R“ŠΓv;?«‰Λ­΄ΩF{ΤC₯τά΄žj'όšλΌOτ*‹π7~Υ’‘Q)½]Rινΐ›@<}ZHJͺ׍ςTJΟuxŠΘƒχ£dΔ(Si=Džώˆ:ιEI—ΌςΎhΰ3έωφT[yUP'=4»T|Θyρ·±ΔΟ3τυ3AQ%½·€a‰?YzΒ•Δ6)zph~υζBA«‘κ='M= ΑΫ@EΡ‹,>!=]sAτ^RnOWπ}η“ΰΪόŠEΣqeP#=Eo\y/BKΔ³Cή!*€Gš™ Aίςάw„Ά%η—λυ©<5¦t8ύ·‚HB—υ ;t*<5σβtΠ*MΌΥŽύκ™Ι-·θVy*€—"̏†ΏcψDh ]ŽΠβΤSΰˆb鑆ˆ‚ϋ1^ί7ι6»EΟΚS.½BλkzΊ°Υ€ΈqΙΙOβ»X˜P*$7e ΘοΠ8£Cύ^+O±τΐm ·θ½#κA/ηΚk   ₯#TžτΨ¦οi¦]ŽΨgύ^lP(½BŽϊ΅’Ζθy9„…‰‡Bι.zYhΣοAcA²ί&DύN€’LzNΒψ‰Uΰ_6Az‘OsΤSΐA™τ²tξΞtxΐθK kυP&=ΒzρΛΐ5έΕάnsίΑγ`…E‘τk9Ɓ«‘ˆΉέ¦>κݘw E#<ιύ^ „άnΓqa•gK©o¨o°4Xκ-͘σ&)•LD²—ϋU©DzvΊ‚½‘Ψρ„άn·Φ„›•γ’£Ή₯©₯±ΉD‘‘ƒ¬Δd8μRU¬F‰τκιŠΟύ„υx‰ΈέJΞ›"υ‡w―··w4Yͺ­Γύύ¦όΟUε¦/sΡΉoCΐΎV·ΫŒ]”ͺež₯φŽφΆ‹ LΈu}}R`λŒΒ΅OτΒte»°U άnc樧°‡w³»{@AΫάΣ“Y]™Sς»’p6p`•g»-άvλΜQOΑ³ΣΣqžaΟ―ΏtΡΏ¬ ψœ|ιyΙj'K?`]šm Γ°’‘­*ιή:ΫuŽέ{gξν |—ύYΙ—^œl©Ψ†UžpΫ­δ"m:Ά˜oΎηρ†YfςŽlι½&+#šzσn»Ν;θ*΄:£ύ½°QψζΑάΟ–!9”-½&²βκΨΌ_ΡΆΫμgΕye1ίn„CŽΧ™‘ΠόœŒΗΙ–YΈT6}B΄ν6υ&ςΕΐ·€£ΗίςγU%Wzt%Φ@+ͺ‰ΆέΖ?RΌ±/ϊΉ£ZnμUόεJο2Υ'–Ξ€wV¬ν6"α+ΟηδέΫTuΣ•)=Ί^ΏAσ~ηŊ<ώΑ6ͺ8Φ>”6O­ήV)£$SzΝT™ΐMψn ΥZ~;‰¬ΌW—†Ρ¨¦Ώήψ™3!‹]½|ͺΐΚZrσεμ Pγ-λnύ¨h―”'=²bް½YlB…/cV€vΔ.ίB<ύV)ΜNžτΘ=ΨΆΎ&ΘΡΑTžS²R\―Μ7*νΉ²€η¦ $Λδ!G{!RO;Dεy“W‰ΉΜ7Λo4²€η§²'{@α7κ³ό8΅³< WθξVζ[oΚFΙ’ž,—r½£}˜‚ ,ε9W“ΪΠλξωΚeoΘ‘žνz/δΣVωͺ ’ςά»VςΆ MΆ2ηu9KmT’Rz½βΕγ(οuσ˜­ΪΛMBŽτ¨Š'ϋ Lέ1–0Ί?x‚γ‚ψ Ο})ν “!½p7π\d"ύ‚Œ‹Ύ!ΜCyŽΜuqJέ—LW“!=Qυδ €Ώυ½0υC1”χα:ωο »%Y†τ¨φΫ_}pcΉisޱΜ_yοFΊΛοΡWrΛ­.½DΥCihAΞ=&oœχ+,ά%‘χcΞ_ύκ;Odϊ ΈEϊ¬pc1α—ͺGο2aοš*8η€ΖR2“!=ψ™Θ!Yη8'HΏΫl Ξ)<‘11ϋ€\.qΣ¨ϊ™PUO^ N]€€S„s=Ÿ·7ΕΉΦc ©Ρ‡<‰‘‘ŒrmHπbT°Œ§γ ½8e€­&=ͺ?n@ 0aEΚγ€msG·υΝ cΉ,AέiU“^¦ΖO* 8inY}ގ‹γ',Ι…Sάj# ZYά;Δ°&ηΏpά$+ !ŸS‘±U€G$Ί xOαSrςK 's^”¦ύΥ‰eΏŠτˆφΫUΈh)7F˜HUΌόόΰ_ntp”“ΩΚ#  ΔDˆY Α–P8†·^3$OF+W–Qθ*\˜–M„O&Qgε4ςΗΫbz/JΡ²PΌ’T–MΠJ°ΆT·}GsŸ8y„lƒB₯΄Wγ\ρ₯G$ΊΧΖ.€MOZδ€Ό…{­|ζΔ‰Z/₯G$*yΰ€ΧNΧOπˆ>K“ΓςHdχE :‹C§*JfΏ Β₯d€¦΄©Δές€π`$-杒+I¨fΌξ=ΝΡ― ‘—ι·w4bR9NρΎ’τΆI\ 8Α PM/ύ…GνFOJxΗY)ŠΏ-•€GcOή€³’μζ?σPžύ‚¨qy•)φ+Užδζž‡»΅•-Ή€‡³JeMU,Lˆ’i’6·υΨO€WGUΫ$Θ½-m\?>$ά_1ζ’ΐ ΕΉςΏβΘ ΨΛ†Ιw₯hώ΄μ:sO‹ΗΌ~ί‡ΚKρYτςˆά~ϋ‡ϊ˜³ΓΫ’½š³©£Θ΅T^z!’°u°dA']'ΛC\πΚ›Ώ―-Ζ ŠͺΊ–—‰Q/›J’v¬ϋ@K’ξσ~Vο 2W=’…έhβ¦–ŠoΝ,N’_œΨ°”ύα8₯«ρfμj°K,=ΉΏk²ΕHYι­rΠP’0ΨιΘ‚ rΠΚσ䩊»r’¬τHL+k`‘zQβγx < νͺ–―Ά₯('=7EΤJξf@bϊG’xΣ°_‡νΝ*ε€η£8*ωΑ‚<ήπp`ΙG²CVh+ΰΦ΄Qε/ρΚIdΏυ€tˆΣΠ–•·0₯D[$PNz‹4Ψ‹—–Š;Y_ΝhۜwD‘ΥΆΜ›τœ’75\ΖR+D8γΥ΄N”'kΥCιΦ{TU+ͺΌ‡¬ΐΎόέ(Ο”:ώCιQxΡ=P#YI=ΰνφΝ}­ΖH¦θQϊΟ²SΨ“· κς8I-+ΐΫ­ž”—’‘‘&Q,`ι·΄•“έ Κ{«#ε™’2"W(φΫTƒ4Z*šχvJGΚ3…ŠL%²ΧΕ7‘lq€>4Ψνφν}ϊlN@BE?•”ž™ΰJ%ItΡέnίLλJy'J ””…+# eI “†έnfτ₯ΌΈ ιQ$z‘J m¦d€n·Ύ)έΨσπŸ„JIΟKPu •oξ"*„ΊΟ ςΒΝϊπΫώc³8$₯”τ|ہςίξρO₯ͺ?F.΍ζy—%sΒnRJz`^x‘€gG NΈζZξ³$ < Z‹Dz`A’DΥ±φف»Έ:)Ύ|9WBza‚j`A’q:χmώ\a«&š<‰,-\BzΏο‘L₯¨ ΡΧ€ύ$•βSή/eν±|²sH ιυ’Μ€° ΡFΊ[a*6ΤG‚/?o§l^%€GP»Ϋojόλ΄πPWωΆ¬ž*¨~ZzS[@™ί„1’!0Ή„'υ2pHzχΤ?ώ+3ψ_Ή”ΓˆlΉΎBτ^QWΘβA‰VŸ§₯GpΤ έoέtυΰξC4ΝJψ’+αX?-=‚Ώ|θ–$«³’ށj“π•G!Rr6J|2§€η$hϋ kB·θ-AU•zσh ‘Θ–2<œ’^ΈŠ”!κciΪ«\ 4OλeΜJγΊ]βOIΐUy–¨eί³0γΨnPΧ£δ‚_*υ―"Ho (ƒŒlΏ@²Ο Ρ±š„·δqδ€τlψii ΛQΛi˜q>ιΠ‹±Χ³tν₯““πΟA ΰN/—fd2ΨRž―Τ‰HϋΈΛTύ:)=‚2?›@‘©dαR.˜ΊR―oθ, ώ€HΉΕμ€τπzy °b2'š¨’Ω€ξŠ7ξ‘ύ4Wζ7'€ηΖΟl€re 9Ρ$'ŒYςEς3ΎΞ•ϋΝ ιmβϋ|@ž;šΎΡ&Σ6Œ -|G‡α*&ΣJω›Σ ιυΆOEΣ¨bhΏΝ9ͺΊϊτ4`Z‹•έ ιαΫ'bs0γP-z˜Τu³ώr1 l%+DΰKο5™@`rώD}2r0wƒη΄x9αΛUͺ[W,½<~β6L‰ͺτΫR!ΚράΥW‰‹Φλ*^Z‹΅†Ώκg€zΨΥΘΓΈΠvΙ[χΒ#9oV~@±τπ+εaο’δA˜B 󴡟Ή³OUyD±τπk-lΑψ 6i– i Bz.n·ΙUΏNE γ_πŠαέo 6½Όώά[2-IοΗιά ΞΔ`*‹Έˆ"γ— N(/€[ Ρ“ω:)c7(’ώ’·³Ε!ˆκ/‚λΆήrιI9+ϊ³ρ/Έ˜.-DA+Ώ!άYe Εœ2ί•"ιαί2€Ξh4­bO°QC˜Έ“ 0ι₯°\›ΕqιΩΠ}q˜„ ’Κf§ Ψ¨!ͺ'ίmΪ“‘ŸΛy\zYτΨ‰Lفm‚2¨&S2 0Θ+έ1bξ˜’{κqιαίρ0ζ8ӊΐξΊ₯—P©\`₯UY΅žγΓ_ϊCΥ"šz™Σl”ΧGΩΪLΐΫ:¦Τ‘t\zθoC&ΰΘGbΥΫ0‚ΪabIΙDΒώ~«Š#Ο1ιΉ‘ΪΡΚ& nBΣ£Ε ψ.ˆ;2™t*E»Η{Υ¬“ή:ϊβ€ΉPt˜1νόΗ>Ζsν… dιL2Ν₯FVf،qΗ€‡μπƒŒ2 ε¬Ψίok¨ΐŠμμ^[*s0α)₯—‚I"YτR怏²όMτδcαݐe¦ Ψ„uLzθά0Lι\’P=/{@Ύ‡Ζρ¬Œ|Θ²r‘Ζ?ιΉΠ›yϊADγ€πDIvιύ>49άlγVeφŸτ6ΡΏ„AQβ¦ »YΘMU"F&qίv~–§›θŸτΠχΫ4Μ!dΏu³,$šζΑ‹˜wσΏ+œ·ΑCΏeμVΚ”“ I”h‚=^ΑU)°<Ξ$”žΔμ£8¬o°Ÿ/»E΅&§6ΪϋQŒτ€‡¬…"T/Οpcτ€^λͺρZ•#ι‘·ώΞXA†‘(d•i±hrn{ε ^ψτ" YΫ ΣΚ:σ††j³ˆ΄}榽ηHzθG½0ˆΑ(@P6›a"(^lrπ²GωHzθοDΈ…γΉp?b—²W© OΔ5Λp$=τ[L—Ь σΩ€E°όΗΔR~±ΏοΔΘ¦€,ΜEŠ`γJ3Ÿτ³ιeά;θΉ¦ϋƒ-½(Θ-ΓG&Ίme‘U(›ήv3MΓ_ι‘—§‹€d>{ €·Αz;υ5°,AζϋΡ+•z‡€ˆyΐͺ—`~MŸ@!+Α•ςŽ€ΥεK6κ;+€’£σΡ„¨φn r½Ko ―zy€1°OOL«Ί/ΓΟ|―yρb§©›|qΜΝ‚nΧ“Ζΐ–S³μ4S€΅~‘ϋ/ BΓψ1σϊͺ±”cKiΓE―ΓlΒΞσλ†Σεχί7r;‡M€Ώ ;‘ΙΚςlμUo—Υ•ρŽgώάΥ—G9,@\4•9Œ-½ cŒVψΟ‹gέ„νo4…Ω֞‡()Šέ9šεΙn5*ρ‡± Xœ―"Z.ό–%\‹'H+Q-E<¬!hrŒkΦ ή…Ρ.ωkξXΝzYˆΝRKeμψ8ctΰ0ο7χΜ_Λωc„°(c_Κ™V=μ’²Œϋ%χEΟd:wxΣ° Œf=l Σͺ‡νFcLsΎΒΎζΓΎδsΘͺ§₯ Ϋ™Α&=FΣޞ·ϋήμ ¦©˜~δ―6£τp3ξΰΨ«ˆτ΄ρ ©>ζΤGYτξ_r±ΟzZΌf0ΝΫόΝδΛ@ŠF3Gφ₯‡Ό‚€TΧΣ’τw•8K*+χΒ^°Όω[ ²α"ŸO™ΎΘ‹±dΰΖ°ΎΣA+ώ5" Χq `0Ϋ ΅™6ρzξΉΠ΄xΝhG>$°Ϋx0¬Η`©Aι`τώ* Ϋ5Š~Ν€Xυ›4*†εψΐΉ3ΑˆΒ ²iτj‘†œμ ±lbV¨YΘ„ε«}l²^ Ϋβ ±αbΫ°ΤͺΓΩΚ ³}eα΅, WNω@ΌˆX>LSώŽάwWύΩ98j΄ΏžΕ^υ Π’YωFΔΗ<{ΥeΠW=ˆ?OKÞ+C8ζQo―ώ–yχ LοΆ»™aγ—‡VŠτkΔ WKΗSl‹=C΄nLκA~{Ž–Θ₯ΡΝ-zΘ›ΣΛ!KαγD―»‹½κAΌHi~ω0MY;Giμ˜α:ΓΈR¦Uωpΐp`ΓN"Ηήp!^€α|˜ΞzΘ«ΓΫ‹,½,ϊ† qγCΞͺΦΤ5γTcΩ€΄qRςrZΌfΈK6δ“τ7\†n\A%/§Ε³ς]ŒIzΨASκ₯‡|ŒI ―zrnΣ5ω6Ξπώ"oΈI“EKgφCpΛLfC„<Ά΅LύR‚,½¬&ΟzZ y@>—2Ό7lΞ“B/|ρrCyάI3%τKΨ!jΉ˜GυΕ4Ήκ™?NCVώlΧ-―ώΦhέΕμŸΣdΠ”†£…Wz gΛ8¦τ­θƐE€ ½|˜μόΘΧ8†Ήξb^ί|—Π7\wMΧιΓ΄n!KαKω17—+l&ΘFl©kΗ'n³`^_ ΟšΑ―* ςrΘ+ “τ+_0|+G7ΰζQ…?{…/?FUOKCž+Λ†€(½ν) o^ «ς&Ζt²DΆΤ²Ό5gЌVρΉΒX?FUOKηSδ―6‹Αβξ¬ZSώ½Έ7μ ·žΕ!ϊ䏓Ν5 y0½5n,ι­ου,΄`VΆ*`†°°"obLSFώš0΅7ξˆBt+N`?εΧ²‹['ΡΡmUKfπ«ΈQ+Λ“GΏίšFe\ϋ««%ƒμύPΪCK7ά‘(f<n‡© |ΕO<θ>iGφoCά3΄c¦-Γ”^|”ιιwW¬0σ¨ΘΟGϋg1ep₯αΓoUŸkS ŠΧΒdJ0žgzΤ°s Ό‚τ4hΨ»0†˜"ςQοDΖ€ΞŸό―?όΏESY­‡|{0ˆ˜Ί%‘fςb; ήΝ3B‡ΚΓ_υ \Τ³ά€›%–«Ρ\―Jžωξ?³`w­Θ―‡ΎκΔ;!K­abOz»3ΜC4ξtL€<ΏΚ+HωΊrŒEώΊ¬3„ώΰyεμ²υq½i ΪΦζͺ‡;η<ΫZ‚Ψ„ΗΠΠΆŸη–+9¦ώΫ‚νdΤ`¬h†νV>ˆ–o“I΄Ή³ΝΟ΄ϊOy% η˜ΛYZo*‡uυΰΈ§L¦νYaΆΪyNΓΗνθg=UWz¬οP–―r? „»―fψψΣ²_ζŽύ€I“2rθJ”%Όΐ Ώf"UˆBΥ€όi‚Λ—εΗάρŸ΄yΓΕ•σ«yp€·v¦œδβΤΨ*.‚~Φ³@Ċ^F ·al•BΙήΜή―3τω+C,\Ÿg1±Ε:(Ζ ρΥ΄ΖZF‘Λ%Φf~=ͺβežη1Φ›‘WκXξDΑ7‹)ˆάϝ)Žφ/»ˆΛ°GζBςnHιΝΊ%X{dD:Ωyb2#NJ‚DEoαyLaφοζΨ0ΐD*γ{:œυu²ΖiενmΈΘ±’ &lΜΒ’>€m!Ζ}Ω“~g:ΜzΌp½"Γ«wOύ›₯° wΗ‚ ±HΓ€1ξr_φ˜Ζ‚Ϊΐf—―ΐ T™ΠάXωπ)³*‰Ύ©<ƒ¦7TEπmo?¨π[ cͺŸbv‘jB)°˜$c}ρΗq/oηž:εlΎ¨ΨΜ’pNV<€7†˜­ΰ‹Λ ·ύλ1ΠXρgο'αkHYΙc¦ω‘EsΈ£Ub¦χLDιeΩκΡΗ=ΔίωΌ ΫgjϊΛπ9/α΄Βš3½–->)Έά\uZ{σ#V‰ΐΕh=γoVfO>ΕΔ·›ΐ#]°,ΟL/.ΘΉΫε·–ŸΚΈ˜μI³4ƒπΓtžε]Ίn|EΉΊ·e1Γ3“;p©§Κ›žήhΊ Λ Ώ'½+ˆΕ¦|€_Ήί`†„‘½# Ϊ[―Íτ΅ZMaOg9½$C~Ή3Ϊ“ήRΪJ4θuΪžλ)Ur<ε0κΘη;`‹υrœw…”Σtvš<mΕ§V) …ž5+0ν{BΧΡ€·ϊ%ωΔ΅άΩ*ε™Lχ>ά…ρ?KN胣L†φ7άξΦ–¦Ί3f‹9Ig’£m ϋoBZ½_ΰθβIžλu‚W]†ϋ―¦ ΩYΗ$ΐ(ͺ)μ½lμK­aBϊΣLσ •Ύίη5τγpŒύω4Η>J–ώ $ιm³α*fπύ/γž››ς η%W`˜q£ρzζ@¦Bǁτ°’ίΰΣΌ\Ή!°•RŒš^M°„-§]w`Β 9ά †SΚdŠ=†³ŽO>€)S–ΗΆ>«Ϊ…OΪlΓqapεp΅ΓpJnJ.Y½rKςφJ1 ̘ζo«»$ΕΘ΅€ωp(½gkͺ‘‹Z9Ξΰσ‡ΰ‘{7ξΚ3νΉE?Œ)oͺ‘XmΦ…ςŽjŽύBή2τ%γ€ΰkrΉφ³ϋθ°²σBb₯«ZoώJοI€ƒΣ¨˜T„Σΐ?ήƒ=.,£}Ί£&Σ»aωο|δwχ-޳Αεθfϋc–χiΣ’W`κγ]Θuo 5nϋiώΒy9ϋnjsγ v³žIοΙ\™ƒ’$y-z¦ήNΑY‡–ϋ΄Ξ|¦—ϊ+ΫZατ,JJ>±6Ξρ+ίΈ-zΎœΊkHvŠ•ε‰ΙδΫκν*-ΏtΘοοGn˟λόQ2G Gη@'σS m„³vΎσ,Oα’ώήΤΩή\g±μ~€\&ŸŠF#—γh₯09ΆOνD8ΩΟπq0ημΈ€Gνό2ZeΠyh6qΔΗb»ζΡzS۞εσIˆŠc›}ώ˜_ŽΧ/ξ7³1Gπ:kΞV˜TyGŒT¨SΑύγψιόΏΟΌ^ƏP±yœΡ/jΚώΈ£»•ΐ] ορςiΔ½όŠ>γ±νμeυ _Π17ƒͺΫ$ΆΪΈTΛ}FΪΗfLΟΗUζl'œ“s s1¨B±τξ.Μπ°°8πNP9<ΧT$lδέ~žΆƒœ°Δ>}; +οΖ΄”›άΡ+ Χξ¬·y˜mƒbN:ΒηΗ{q«¨™¬¦ΧΝΧˆ/½~¦·˜"NωŸ&ΎϋΞ}zΒή¬ΙσνRŸ,K‘φžαjJ7(ΗiΧηΝŸΧ Χ½`Β 8š\††LΞμ…ΞjHlsσί~λe)αuΏώω.œφ|nͺγ{a-³gΟφ–έySaΏΏμPƒ#J|άϋtκ±v†ςβΈgLτy:Ϊ[›‹ώž|"†{υιΥ%c&ίM€ΨX€%΄2HeΩσΚ›άξϊzKCIΚeσ™ΜΥ‘6“q± §t˜Ϋƒ‘Ωoάs aΞ”7€§L„ε3Ϋ sCTΝ§Ηp₯\pοŒiρSΰ―΄jžcyΎή)/―;>3C9ŒΔ7Ύ™ 4O…•­ίτι¦Κb:ΉUΓ%jP…Š›κ€³yH™Εγ™α5¨Bεσά˜Ι—Ό T|‘₯iδžφZ€ΪU’_‘ψ€ΐ#γ”g ƒκ·Ψ~Sxγ’Μ YίςScΕ3…Jg§ιΥ₯sUύΉΐFfFσUί °i»{lroφŸm+V ύi3œ’ς‘m6ήσE…W:ΪO­7™2ΙπΞ@Ώ’²υ €·GηžOΦhnh΄4XLωl>—Ηnw6t Τ$5ΠαΏυ|Ϊ7IENDB`‚Yakifo-amqtt-2637127/docs_web/assets/python.svg000066400000000000000000000017431504664204300214170ustar00rootroot00000000000000Yakifo-amqtt-2637127/docs_web/assets/readthedocs.svg000066400000000000000000000041521504664204300223600ustar00rootroot00000000000000 Yakifo-amqtt-2637127/docs_web/index.md000066400000000000000000000037311504664204300175030ustar00rootroot00000000000000# Home ![assets/amqtt.svg](assets/amqtt.svg) `aMQTT` is an open source [MQTT](http://www.mqtt.org) broker and client, natively implemented with Python's [asyncio](https://docs.python.org/3/library/asyncio.html). ## Features - Full set of [MQTT 3.1.1](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html) protocol specifications - Communication over multiple TCP and/or websocket ports, including support for SSL/TLS - Support QoS 0, QoS 1 and QoS 2 messages flow - Client auto-reconnection on network lost - Plugin framework for functionality expansion; included plugins: - `$SYS` topic publishing - AWS IOT-style shadow states - x509 certificate authentication (including cli cert creation) - Secure file-based password authentication - Configuration-based topic authorization - MySQL, Postgres & SQLite user and/or topic auth (including cli manager) - External server (HTTP) user and/or topic auth - LDAP user and/or topic auth - JWT user and/or topic auth - Fail over session persistence ## Installation `amqtt` is available on ![pypi](assets/python.svg) [PyPI](https://pypi.python.org/pypi/amqtt) ## Documentation `amqtt` docs are available on ![readthedocs](assets/readthedocs.svg) [Read the Docs](http://amqtt.readthedocs.org/). ## Containerization Launch from ![dockerhub](assets/docker.svg) [DockerHub](https://hub.docker.com/repositories/amqtt) ```shell $ docker run -d -p 1883:1883 amqtt/amqtt:latest ``` ## Testing The `amqtt` project runs a test aMQTT broker/server at [test.amqtt.io](https://test.amqtt.io) which supports: MQTT, MQTT over TLS, websocket, secure websockets. ## Support `amqtt` development is available on ![github](assets/github.svg) [GitHub](https://github.com/Yakifo/amqtt). Bug reports, patches and suggestions welcome! ![github](assets/github.svg) [Open an issue](https://github.com/Yakifo/amqtt/issues/new) or join the ![discord](assets/discord.svg) [discord community](https://discord.gg/S3sP6dDaF3). Yakifo-amqtt-2637127/mkdocs.rtd.yml000066400000000000000000000124371504664204300170630ustar00rootroot00000000000000# MKDocs configuration file for generating RTD (readthedocs) documentation site_name: "aMQTT" site_description: "Python's asyncio-native MQTT broker and client." site_url: http://github.com repo_url: https://github.com/Yakifo/amqtt repo_name: Yakifo/amqtt site_dir: "dist/rtd" watch: - mkdocs.rtd.yml - README.md - CONTRIBUTING.md - SUPPORT.md - SECURITY.md - CODE_OF_CONDUCT.md - CONTRIBUTING.md - docs - amqtt - samples copyright: 'amqtt.io Β© 2025' edit_uri: edit/main/docs/ validation: omitted_files: warn absolute_links: warn unrecognized_links: warn nav: - Home: - Overview: index.md - Quickstart: quickstart.md - Console scripts: - Broker: references/amqtt.md - Publisher: references/amqtt_pub.md - Subscriber: references/amqtt_sub.md - Programming API: - Broker: references/broker.md - Client: references/client.md - Common: references/common.md - Configuration: - Broker: references/broker_config.md - Client: references/client_config.md - Plugins: - Packaged: plugins/packaged_plugins.md - Custom: plugins/custom_plugins.md - Contributed: - plugins/contrib.md - Database Auth: plugins/auth_db.md - HTTP Auth: plugins/http.md - LDAP Auth: plugins/ldap.md - Shadows: plugins/shadows.md - Certificate Auth: plugins/cert.md - JWT Auth: plugins/jwt.md - Session persistence: plugins/session.md - Reference: - Containerization: docker.md - Support: support.md - Contributing: contributing.md - Change log: changelog.md - Coverage: coverage.md - Code of Conduct: code_of_conduct.md - Security: security.md - License: license.md theme: name: material logo: assets/amqtt_bw.svg features: - toc.integrate - announce.dismiss - content.action.edit - content.action.view - content.code.annotate - content.code.copy - content.tooltips - navigation.footer - navigation.instant.preview - navigation.path - navigation.sections - navigation.tabs - navigation.tabs.sticky - navigation.top - search.highlight - search.suggest - toc.follow - version palette: # Palette toggle for light mode - scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - scheme: slate toggle: icon: material/brightness-4 name: Switch to light mode extra_css: - assets/extra.css #extra_javascript: #- assets/extra.js markdown_extensions: - attr_list - admonition - callouts: strip_period: false - footnotes - pymdownx.details - pymdownx.highlight: pygments_lang_class: true - pymdownx.magiclink - pymdownx.snippets: base_path: !relative $config_dir check_paths: true - pymdownx.superfences: custom_fences: - name: mermaid class: mermaid format: !!python/name:pymdownx.superfences.fence_code_format - pymdownx.tabbed: alternate_style: true slugify: !!python/object/apply:pymdownx.slugs.slugify kwds: case: lower - pymdownx.tasklist: custom_checkbox: true - pymdownx.tilde - toc: permalink: "Β€" toc_depth: 3 plugins: - search - autorefs - open-in-new-tab - markdown-exec - section-index - coverage - mkdocs-typer2: - mkdocstrings: custom_templates: 'docs/templates' handlers: python: paths: [amqtt] options: # extra: # template_log_display: true docstring_options: ignore_init_summary: true docstring_section_style: list filters: ["!^_"] heading_level: 2 inherited_members: true merge_init_into_class: true parameter_headings: true separate_signature: true show_root_heading: true show_root_full_path: false show_signature_annotations: true show_source: false show_symbol_type_heading: true show_symbol_type_toc: true signature_crossrefs: true summary: true extensions: - docs/scripts/exts.py:DataclassDefaultFactoryExtension: paths: [mypkg.mymod.myobj] - git-revision-date-localized: enabled: !ENV [DEPLOY, false] enable_creation_date: true type: timeago #- redirects: # redirect_maps: # original_file.md: new/file_location.md - minify: minify_html: !ENV [DEPLOY, false] - group: enabled: !ENV [MATERIAL_INSIDERS, false] plugins: - typeset extra: version: provider: readthedocs default: v0.11.0 warning: true social: - icon: fontawesome/brands/github link: https://github.com/pawamoy - icon: fontawesome/brands/mastodon link: https://fosstodon.org/@pawamoy - icon: fontawesome/brands/twitter link: https://twitter.com/pawamoy - icon: fontawesome/brands/gitter link: https://gitter.im/mkdocstrings/community - icon: fontawesome/brands/python link: https://pypi.org/project/mkdocstrings/ Yakifo-amqtt-2637127/mkdocs.web.yml000066400000000000000000000044171504664204300170460ustar00rootroot00000000000000site_name: "" site_description: "MQTT broker and client natively implemented with Python's asyncio" site_url: http://github.com repo_url: https://github.com/Yakifo/amqtt repo_name: Yakifo/amqtt site_dir: "dist/web" docs_dir: docs_web watch: [mkdocs.web.yml, docs_web] copyright: "amqtt.io Β© 2025" edit_uri: edit/main/docs/ validation: omitted_files: warn absolute_links: warn unrecognized_links: warn theme: name: material logo: assets/amqtt_bw.svg features: - announce.dismiss - content.action.edit - content.action.view - content.code.annotate - content.code.copy - content.tooltips - navigation.footer - navigation.instant.preview - navigation.path - navigation.sections - navigation.tabs - navigation.tabs.sticky - navigation.top - search.highlight - search.suggest - toc.follow palette: # Palette toggle for light mode - scheme: default toggle: icon: material/brightness-7 name: Switch to dark mode # Palette toggle for dark mode - scheme: slate toggle: icon: material/brightness-4 name: Switch to light mode extra_css: - assets/extra.css #extra_javascript: #- assets/extra.js markdown_extensions: - attr_list - admonition - callouts: strip_period: false - footnotes - pymdownx.details - pymdownx.highlight: pygments_lang_class: true - pymdownx.magiclink - pymdownx.snippets: base_path: !relative $config_dir check_paths: true - pymdownx.superfences - pymdownx.tabbed: alternate_style: true slugify: !!python/object/apply:pymdownx.slugs.slugify kwds: case: lower - pymdownx.tasklist: custom_checkbox: true - pymdownx.tilde - toc: permalink: "Β€" plugins: - autorefs - markdown-exec - section-index - open-in-new-tab - git-revision-date-localized: enabled: !ENV [DEPLOY, false] enable_creation_date: true type: timeago - exclude: glob: - node_modules/* regex: - '.*\.(tmp|bin|tar)$' #- redirects: # redirect_maps: # original_file.md: new/file_location.md - minify: minify_html: !ENV [DEPLOY, false] - group: enabled: !ENV [MATERIAL_INSIDERS, false] plugins: - typeset Yakifo-amqtt-2637127/pyproject.toml000066400000000000000000000226451504664204300172060ustar00rootroot00000000000000[build-system] requires = ["hatchling", "hatch-vcs", "uv-dynamic-versioning"] build-backend = "hatchling.build" [project] name = "amqtt" description = "Python's asyncio-native MQTT broker and client." classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Operating System :: POSIX", "Operating System :: MacOS", "Operating System :: Microsoft :: Windows", "Topic :: Communications", "Topic :: Internet", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13" ] version = "0.11.3" requires-python = ">=3.10.0" readme = "README.md" license = { text = "MIT" } authors = [{ name = "aMQTT Contributors" }] dependencies = [ "transitions==0.9.2", # https://pypi.org/project/transitions "websockets==15.0.1", # https://pypi.org/project/websockets "passlib==1.7.4", # https://pypi.org/project/passlib "PyYAML==6.0.2", # https://pypi.org/project/PyYAML "typer==0.15.4", "dacite>=1.9.2", "psutil>=7.0.0" ] [dependency-groups] dev = [ "aiosqlite>=0.21.0", "greenlet>=3.2.3", "hatch>=1.14.1", "hypothesis>=6.130.8", "mypy>=1.15.0", "paho-mqtt>=2.1.0", "poethepoet>=0.34.0", "pre-commit>=4.2.0", # https://pypi.org/project/pre-commit "psutil>=7.0.0", # https://pypi.org/project/psutil "pyhamcrest>=2.1.0", "pylint>=3.3.6", # https://pypi.org/project/pylint "pyopenssl>=25.1.0", "pytest-asyncio>=0.26.0", # https://pypi.org/project/pytest-asyncio "pytest-cov>=6.1.0", # https://pypi.org/project/pytest-cov "pytest-docker>=3.2.3", "pytest-logdog>=0.1.0", # https://pypi.org/project/pytest-logdog "pytest-timeout>=2.3.1", # https://pypi.org/project/pytest-timeout "pytest>=8.3.5", # https://pypi.org/project/pytest "ruff>=0.11.3", # https://pypi.org/project/ruff "setuptools>=78.1.0", "sqlalchemy[mypy]>=2.0.41", "types-mock>=5.2.0.20250306", # https://pypi.org/project/types-mock "types-PyYAML>=6.0.12.20250402", # https://pypi.org/project/types-PyYAML "types-setuptools>=78.1.0.20250329", # https://pypi.org/project/types-setuptools ] docs = [ "griffe>=1.11.1", "markdown-callouts>=0.4", "markdown-exec>=1.8", "mkdocs>=1.6", "mkdocs-coverage>=1.0", "mkdocs-git-revision-date-localized-plugin>=1.2", "mkdocs-llmstxt>=0.1", "mkdocs-material>=9.5", "mkdocs-minify-plugin>=0.8", "mkdocs-redirects>=1.2.1", "mkdocs-section-index>=0.3", "mkdocstrings-python>=1.16.2", # YORE: EOL 3.10: Remove line. "tomli>=2.0; python_version < '3.11'", "mkdocs-typer2>=0.1.4", "mkdocs-open-in-new-tab>=1.0.8", "mkdocs-exclude>=1.0.2", ] [project.optional-dependencies] ci = ["coveralls==4.0.1"] contrib = [ "cryptography>=45.0.3", "aiosqlite>=0.21.0", "greenlet>=3.2.3", "sqlalchemy[asyncio]>=2.0.41", "argon2-cffi>=25.1.0", "aiohttp>=3.12.13", "pyjwt>=2.10.1", "python-ldap>=3.4.4", "mergedeep>=1.3.4", "jsonschema>=4.25.0", "pyopenssl>=25.1.0" ] [project.scripts] amqtt = "amqtt.scripts.broker_script:main" amqtt_pub = "amqtt.scripts.pub_script:main" amqtt_sub = "amqtt.scripts.sub_script:main" ca_creds = "amqtt.scripts.ca_creds:main" server_creds = "amqtt.scripts.server_creds:main" device_creds = "amqtt.scripts.device_creds:main" user_mgr = "amqtt.scripts.manage_users:main" topic_mgr = "amqtt.scripts.manage_topics:main" [tool.hatch.build] exclude = [ ".venv*", "tests/", "**/tests/", "**/*.pyc", "**/__pycache__/", "**/README.md", ] [tool.hatch.build.targets.sdist] include = ["/amqtt", "README.md", "docs/assets/amqtt.svg"] [tool.hatch.version] source = "vcs" [tool.hatch.publish.indexes.testpypi] url = "https://test.pypi.org/legacy/" # ___________________________________ PLUGINS __________________________________ [project.entry-points."amqtt.test.plugins"] test_plugin = "tests.plugins.test_manager:EmptyTestPlugin" event_plugin = "tests.plugins.test_manager:EventTestPlugin" packet_logger_plugin = "amqtt.plugins.logging_amqtt:PacketLoggerPlugin" # --8<-- [start:included] [project.entry-points."amqtt.broker.plugins"] event_logger_plugin = "amqtt.plugins.logging_amqtt:EventLoggerPlugin" packet_logger_plugin = "amqtt.plugins.logging_amqtt:PacketLoggerPlugin" auth_anonymous = "amqtt.plugins.authentication:AnonymousAuthPlugin" auth_file = "amqtt.plugins.authentication:FileAuthPlugin" topic_taboo = "amqtt.plugins.topic_checking:TopicTabooPlugin" topic_acl = "amqtt.plugins.topic_checking:TopicAccessControlListPlugin" broker_sys = "amqtt.plugins.sys.broker:BrokerSysPlugin" # --8<-- [end:included] [project.entry-points."amqtt.client.plugins"] packet_logger_plugin = "amqtt.plugins.logging_amqtt:PacketLoggerPlugin" # ____________________________________ RUFF ____________________________________ # https://docs.astral.sh/ruff/settings/ [tool.ruff] line-length = 130 fix = true extend-exclude = ["docs/", "samples/"] [tool.ruff.format] indent-style = "space" docstring-code-format = true [tool.ruff.lint] preview = true select = ["ALL"] extend-select = [ "UP", # pyupgrade "D", # pydocstyle, ] ignore = [ "FBT001", # Checks for the use of boolean positional arguments in function definitions. "FBT002", # Checks for the use of boolean positional arguments in function definitions. "G004", # Logging statement uses f-string "D100", # Missing docstring in public module "D101", # Missing docstring in public class "D102", # Missing docstring in public method "D107", # Missing docstring in `__init__` "D203", # Incorrect blank line before class (mutually exclusive D211) "D213", # Multi-line summary second line (mutually exclusive D212) "FIX002", # Checks for "TODO" comments. "TD002", # TODO Missing author. "TD003", # TODO Missing issue link for this TODO. "ANN401", # Dynamically typed expressions (typing.Any) are disallowed "ARG002", # Unused method argument "PERF203",# try-except penalty within loops (3.10 only), "COM812", # rule causes conflicts when used with the formatter, # ignore certain preview rules "DOC", "PLW", "PLR", "CPY", "PLC", "RUF052", "B903" ] [tool.ruff.lint.per-file-ignores] "tests/**" = ["ALL"] "amqtt/scripts/*_script.py" = ["FBT003", "E501"] [tool.ruff.lint.flake8-pytest-style] fixture-parentheses = false [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" [tool.ruff.lint.isort] combine-as-imports = true force-sort-within-sections = true case-sensitive = true extra-standard-library = ["typing_extensions"] [tool.ruff.lint.mccabe] max-complexity = 42 [tool.ruff.lint.pylint] max-args = 12 max-branches = 42 max-statements = 143 max-returns = 10 # ----------------------------------- PYTEST ----------------------------------- [tool.pytest.ini_options] addopts = ["--cov=amqtt", "--cov-report=term-missing", "--cov-report=html"] testpaths = ["tests"] asyncio_mode = "auto" timeout = 15 asyncio_default_fixture_loop_scope = "function" #addopts = ["--tb=short", "--capture=tee-sys"] #log_cli = true log_level = "DEBUG" # ------------------------------------ MYPY ------------------------------------ [tool.mypy] exclude = ["^tests/.*", "^docs/.*", "^samples/.*"] follow_imports = "silent" show_error_codes = true ignore_missing_imports = true strict_equality = true warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true warn_unused_ignores = true check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true warn_return_any = true warn_unreachable = true strict = true # ----------------------------------- PYLINT ----------------------------------- [tool.pylint.MAIN] jobs = 2 ignore = ["tests"] fail-on = ["I"] max-line-length = 130 ignore-paths = ["amqtt/plugins/persistence.py"] [tool.pylint.BASIC] # Good variable names which should always be accepted, separated by a comma. good-names = ["i", "j", "k", "e", "ex", "f", "_", "T", "x", "y", "id", "tg"] [tool.pylint."MESSAGES CONTROL"] # Reasons disabled: # duplicate-code - unavoidable # too-many-* - are not enforced for the sake of readability disable = [ "duplicate-code", "fixme", "invalid-name", "line-too-long", "logging-fstring-interpolation", "missing-class-docstring", "missing-function-docstring", "missing-module-docstring", "protected-access", "redefined-slots-in-subclass", "too-few-public-methods", "too-many-arguments", "too-many-instance-attributes", "unused-argument", ] [tool.pylint.REPORTS] score = false [tool.pylint.FORMAT] expected-line-ending-format = "LF" [tool.pylint.EXCEPTIONS] overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"] [tool.pylint.REFACTORING] max-nested-blocks = 5 never-returning-functions = ["sys.exit", "argparse.parse_error"] [tool.pylint.DESIGN] max-branches = 32 # too-many-branches max-locals = 20 # too-many-locals max-module-lines = 1500 # too-many-lines max-parents = 10 # too-many-parents max-positional-arguments = 10 # too-many-positional-arguments max-public-methods = 25 # too-many-public-methods max-returns = 7 # too-many-returns max-statements = 90 # too-many-statements # ---------------------------------- COVERAGE ---------------------------------- [tool.coverage.run] branch = true source = ["amqtt"] [tool.coverage.report] show_missing = true skip_covered = true Yakifo-amqtt-2637127/samples/000077500000000000000000000000001504664204300157255ustar00rootroot00000000000000Yakifo-amqtt-2637127/samples/broker.yaml000066400000000000000000000003271504664204300200770ustar00rootroot00000000000000--- listeners: default: type: tcp bind: 0.0.0.0:1883 std-ws: type: ws bind: 0.0.0.0:8080 sys_interval: 2 auth: plugins: - auth_anonymous allow-anonymous: true topic-check: enabled: False Yakifo-amqtt-2637127/samples/broker_acl.py000066400000000000000000000037731504664204300204140ustar00rootroot00000000000000import asyncio import logging from pathlib import Path from amqtt.broker import Broker """ This sample shows how to run a broker with the topic check acl plugin """ logger = logging.getLogger(__name__) config = { "listeners": { "default": { "type": "tcp", "bind": "0.0.0.0:1883", }, "ws-mqtt": { "bind": "127.0.0.1:8080", "type": "ws", "max_connections": 10, }, }, "plugins": { "amqtt.plugins.authentication.AnonymousAuthPlugin": { "allow_anonymous": True}, "amqtt.plugins.authentication.FileAuthPlugin": { "password_file": Path(__file__).parent / "passwd", }, "amqtt.plugins.sys.broker.BrokerSysPlugin": { "sys_interval": 10}, "amqtt.plugins.topic_checking.TopicAccessControlListPlugin": { "acl": { # username: [list of allowed topics] "test": ["repositories/+/master", "calendar/#", "data/memes"], "anonymous": [], } } } } async def main_loop(): broker = Broker(config) try: await broker.start() while True: await asyncio.sleep(1) except asyncio.CancelledError: await broker.shutdown() async def main() -> None: t = asyncio.create_task(main_loop()) try: await t except asyncio.CancelledError: pass def __main__(): formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) task = loop.create_task(main()) try: loop.run_until_complete(task) except KeyboardInterrupt: logger.info("KeyboardInterrupt received. Stopping server...") task.cancel() loop.run_until_complete(task) # Ensure task finishes cleanup finally: logger.info("Server stopped.") loop.close() if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/broker_custom_plugin.py000066400000000000000000000041751504664204300225420ustar00rootroot00000000000000import asyncio from dataclasses import dataclass import logging from amqtt.broker import Broker from amqtt.plugins.base import BasePlugin from amqtt.session import Session """ This sample shows how to run a broker without stacktraces on keyboard interrupt """ logger = logging.getLogger(__name__) class RemoteInfoPlugin(BasePlugin): async def on_broker_client_connected(self, *, client_id:str, client_session:Session) -> None: display_port_str = f"on port '{client_session.remote_port}'" if self.config.display_port else "" logger.info(f"client '{client_id}' connected from" f" '{client_session.remote_address}' {display_port_str}") @dataclass class Config: display_port: bool = False config = { "listeners": { "default": { "type": "tcp", "bind": "0.0.0.0:1883", }, "ws-mqtt": { "bind": "127.0.0.1:8080", "type": "ws", "max_connections": 10, }, }, "plugins": { "amqtt.plugins.authentication.AnonymousAuthPlugin": { "allow_anonymous": True}, "samples.broker_custom_plugin.RemoteInfoPlugin": { "display_port": True }, } } async def main_loop(): broker = Broker(config) try: await broker.start() while True: await asyncio.sleep(1) except asyncio.CancelledError: await broker.shutdown() async def main(): t = asyncio.create_task(main_loop()) try: await t except asyncio.CancelledError: pass def __main__(): formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) task = loop.create_task(main()) try: loop.run_until_complete(task) except KeyboardInterrupt: logger.info("KeyboardInterrupt received. Stopping server...") task.cancel() loop.run_until_complete(task) # Ensure task finishes cleanup finally: logger.info("Server stopped.") loop.close() if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/broker_simple.py000066400000000000000000000012141504664204300211320ustar00rootroot00000000000000import asyncio from asyncio import CancelledError import logging from amqtt.broker import Broker """ This sample shows how to run a broker """ formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) async def run_server() -> None: broker = Broker() try: await broker.start() while True: await asyncio.sleep(1) except CancelledError: await broker.shutdown() def __main__(): try: asyncio.run(run_server()) except KeyboardInterrupt: print("Server exiting...") if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/broker_start.py000066400000000000000000000033421504664204300210020ustar00rootroot00000000000000import asyncio import logging from pathlib import Path from amqtt.broker import Broker """ This sample shows how to run a broker without stacktraces on keyboard interrupt """ logger = logging.getLogger(__name__) config = { "listeners": { "default": { "type": "tcp", "bind": "0.0.0.0:1883", }, "ws-mqtt": { "bind": "127.0.0.1:8080", "type": "ws", "max_connections": 10, }, }, "plugins": { "amqtt.plugins.authentication.AnonymousAuthPlugin": { "allow_anonymous": True}, "amqtt.plugins.authentication.FileAuthPlugin": { "password_file": Path(__file__).parent / "passwd", }, "amqtt.plugins.sys.broker.BrokerSysPlugin": { "sys_interval": 10}, } } async def main_loop(): broker = Broker(config) try: await broker.start() while True: await asyncio.sleep(1) except asyncio.CancelledError: await broker.shutdown() async def main(): t = asyncio.create_task(main_loop()) try: await t except asyncio.CancelledError: pass def __main__(): formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) task = loop.create_task(main()) try: loop.run_until_complete(task) except KeyboardInterrupt: logger.info("KeyboardInterrupt received. Stopping server...") task.cancel() loop.run_until_complete(task) # Ensure task finishes cleanup finally: logger.info("Server stopped.") loop.close() if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/broker_taboo.py000066400000000000000000000034301504664204300207470ustar00rootroot00000000000000import asyncio import logging from pathlib import Path from amqtt.broker import Broker """ This sample shows how to run a broker with the topic check taboo plugin """ logger = logging.getLogger(__name__) config = { "listeners": { "default": { "type": "tcp", "bind": "0.0.0.0:1883", }, "ws-mqtt": { "bind": "127.0.0.1:8080", "type": "ws", "max_connections": 10, }, }, "plugins": { "amqtt.plugins.authentication.AnonymousAuthPlugin": {"allow_anonymous": True}, "amqtt.plugins.authentication.FileAuthPlugin": { "password_file": Path(__file__).parent / "passwd", }, "amqtt.plugins.sys.broker.BrokerSysPlugin": {"sys_interval": 10}, "amqtt.plugins.topic_checking.TopicTabooPlugin": {}, } } async def main_loop(): broker = Broker(config) try: await broker.start() while True: await asyncio.sleep(1) except asyncio.CancelledError: await broker.shutdown() async def main(): t = asyncio.create_task(main_loop()) try: await t except asyncio.CancelledError: pass def __main__(): formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) task = loop.create_task(main()) try: loop.run_until_complete(task) except KeyboardInterrupt: logger.info("KeyboardInterrupt received. Stopping server...") task.cancel() loop.run_until_complete(task) # Ensure task finishes cleanup finally: logger.info("Server stopped.") loop.close() if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/client_keepalive.py000066400000000000000000000013541504664204300216050ustar00rootroot00000000000000import asyncio from asyncio import CancelledError import logging from amqtt.client import MQTTClient """ This sample shows how to run an idle client """ logger = logging.getLogger(__name__) config = { "keep_alive": 5, "ping_delay": 1, } async def main() -> None: client = MQTTClient(config=config) try: await client.connect("mqtt://localhost:1883/") logger.info("client connected") await asyncio.sleep(15) except CancelledError: pass await client.disconnect() def __main__(): formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) asyncio.run(main()) if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/client_publish.py000066400000000000000000000033151504664204300213050ustar00rootroot00000000000000import asyncio import logging from amqtt.client import ConnectError, MQTTClient from amqtt.mqtt.constants import QOS_1, QOS_2 """ This sample shows how to publish messages to broker using different QOS """ logger = logging.getLogger(__name__) config = { "will": { "topic": "/will/client", "message": b"Dead or alive", "qos": QOS_1, "retain": True, }, } async def test_coro1() -> None: client = MQTTClient() await client.connect("mqtt://localhost:1883/") tasks = [ asyncio.ensure_future(client.publish("a/b", b"TEST MESSAGE WITH QOS_0")), asyncio.ensure_future(client.publish("a/b", b"TEST MESSAGE WITH QOS_1", qos=QOS_1)), asyncio.ensure_future(client.publish("a/b", b"TEST MESSAGE WITH QOS_2", qos=QOS_2)), ] await asyncio.wait(tasks) logger.info("test_coro1 messages published") await client.disconnect() async def test_coro2() -> None: try: client = MQTTClient(config={"auto_reconnect": False, "connection_timeout": 1}) await client.connect("mqtt://localhost:1884/") await client.publish("a/b", b"TEST MESSAGE WITH QOS_0", qos=0x00) await client.publish("a/b", b"TEST MESSAGE WITH QOS_1", qos=0x01) await client.publish("a/b", b"TEST MESSAGE WITH QOS_2", qos=0x02) logger.info("test_coro2 messages published") await client.disconnect() except ConnectError: logger.info("Connection failed", exc_info=True) def __main__(): formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) asyncio.run(test_coro1()) asyncio.run(test_coro2()) if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/client_publish_acl.py000066400000000000000000000023521504664204300221240ustar00rootroot00000000000000import asyncio import logging from amqtt.client import ConnectError, MQTTClient from amqtt.mqtt.constants import QOS_1 """ This sample shows how to publish messages to broker running either `samples/broker_acl.py` or `samples/broker_taboo.py`. """ logger = logging.getLogger(__name__) async def test_coro() -> None: try: client = MQTTClient() await client.connect("mqtt://0.0.0.0:1883") await client.publish("data/classified", b"TOP SECRET", qos=QOS_1) await client.publish("data/memes", b"REAL FUN", qos=QOS_1) await client.publish("repositories/amqtt/master", b"NEW STABLE RELEASE", qos=QOS_1) await client.publish( "repositories/amqtt/devel", b"THIS NEEDS TO BE CHECKED", qos=QOS_1, ) await client.publish("calendar/amqtt/releases", b"NEW RELEASE", qos=QOS_1) logger.info("messages published") await client.disconnect() except ConnectError: logger.exception("ERROR: Connection failed") def __main__(): formatter = "[%(asctime)s] :: %(levelname)s :: %(name)s :: %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) asyncio.run(test_coro()) if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/client_publish_ssl.py000066400000000000000000000031221504664204300221620ustar00rootroot00000000000000import argparse import asyncio import logging from amqtt.client import MQTTClient from amqtt.mqtt.constants import QOS_1, QOS_2 """ This sample shows how to publish messages to secure broker. Use `openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem -subj "/CN=localhost"` to generate a self-signed certificate for the broker to use. """ logger = logging.getLogger(__name__) config = { "will": { "topic": "/will/client", "message": "Dead or alive", "qos": QOS_1, "retain": True, }, "auto_reconnect": False, "check_hostname": False, "certfile": "", } async def test_coro(certfile: str) -> None: config["certfile"] = certfile client = MQTTClient(config=config) await client.connect("mqtts://localhost:8883") tasks = [ asyncio.ensure_future(client.publish("a/b", b"TEST MESSAGE WITH QOS_0")), asyncio.ensure_future(client.publish("a/b", b"TEST MESSAGE WITH QOS_1", qos=QOS_1)), asyncio.ensure_future(client.publish("a/b", b"TEST MESSAGE WITH QOS_2", qos=QOS_2)), ] await asyncio.wait(tasks) logger.info("messages published") await client.disconnect() def __main__(): formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) parser = argparse.ArgumentParser() parser.add_argument("--cert", default="cert.pem", help="path & file to verify server's authenticity") args = parser.parse_args() asyncio.run(test_coro(args.cert)) if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/client_publish_ws.py000066400000000000000000000021411504664204300220120ustar00rootroot00000000000000import asyncio import logging from amqtt.client import MQTTClient from amqtt.mqtt.constants import QOS_1, QOS_2 """ This sample shows how to publish messages to secure websocket broker """ logger = logging.getLogger(__name__) config = { "will": { "topic": "/will/client", "message": "Dead or alive", "qos": QOS_1, "retain": True, } } client = MQTTClient(config=config) async def test_coro() -> None: await client.connect("ws://localhost:8080/") tasks = [ asyncio.ensure_future(client.publish("a/b", b"TEST MESSAGE WITH QOS_0")), asyncio.ensure_future(client.publish("a/b", b"TEST MESSAGE WITH QOS_1", qos=QOS_1)), asyncio.ensure_future(client.publish("a/b", b"TEST MESSAGE WITH QOS_2", qos=QOS_2)), ] await asyncio.wait(tasks) logger.info("messages published") await client.disconnect() def __main__(): formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) asyncio.run(test_coro()) if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/client_subscribe.py000066400000000000000000000022661504664204300216240ustar00rootroot00000000000000import asyncio import logging from amqtt.client import ClientError, MQTTClient from amqtt.mqtt.constants import QOS_1, QOS_2 """ This sample shows how to subscribe to different $SYS topics and how to receive incoming messages """ logger = logging.getLogger(__name__) async def uptime_coro() -> None: client = MQTTClient(config={"auto_reconnect": False}) await client.connect("mqtt://localhost:1883") await client.subscribe( [ ("$SYS/broker/uptime", QOS_1), ("$SYS/broker/load/#", QOS_2), ], ) logger.info("Subscribed") try: for _i in range(1, 10): if msg := await client.deliver_message(): logger.info(f"{msg.topic} >> {msg.data.decode()}") await client.unsubscribe(["$SYS/broker/uptime", "$SYS/broker/load/#"]) logger.info("UnSubscribed") await client.disconnect() except ClientError: logger.exception("Client exception") def __main__(): formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) asyncio.run(uptime_coro()) if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/client_subscribe_acl.py000066400000000000000000000030721504664204300224370ustar00rootroot00000000000000import asyncio import logging from amqtt.client import ClientError, MQTTClient from amqtt.mqtt.constants import QOS_1 """ Run `samples/broker_acl.py` or `samples/broker_taboo.py` This sample shows how to subscribe to different topics, some of which are allowed. """ logger = logging.getLogger(__name__) async def uptime_coro() -> None: client = MQTTClient() await client.connect("mqtt://test:test@0.0.0.0:1883") result = await client.subscribe( [ ("$SYS/#", QOS_1), # Topic forbidden when running `broker_acl.py` ("data/memes", QOS_1), # Topic allowed ("data/classified", QOS_1), # Topic forbidden ("repositories/amqtt/master", QOS_1), # Topic allowed ("repositories/amqtt/devel", QOS_1), # Topic forbidden when running `broker_acl.py` ("calendar/amqtt/releases", QOS_1), # Topic allowed ], ) logger.info(f"Subscribed results: {result}") try: for _i in range(1, 100): if msg := await client.deliver_message(): logger.info(f"{msg.topic} >> {msg.data.decode()}") await client.unsubscribe(["$SYS/#", "data/memes"]) logger.info("UnSubscribed") await client.disconnect() except ClientError: logger.exception("Client exception") def __main__(): formatter = "[%(asctime)s] {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) asyncio.get_event_loop().run_until_complete(uptime_coro()) if __name__ == "__main__": __main__() Yakifo-amqtt-2637127/samples/docker-compose.yaml000066400000000000000000000007671504664204300215350ustar00rootroot00000000000000services: amqtt: image: amqtt/amqtt container_name: amqtt deploy: resources: limits: cpus: '3' memory: 8g reservations: cpus: '3' memory: 8g ports: - "1883:1883" - "8080:8080" - "8443:8443" - "8883:8883" logging: driver: "json-file" options: max-size: "10m" max-file: "3" volumes: - ./broker.yaml:/app/conf/broker.yaml - /etc/letsencrypt:/app/cert:ro Yakifo-amqtt-2637127/samples/http_server_integration.py000066400000000000000000000136361504664204300232600ustar00rootroot00000000000000import asyncio import io import logging import ssl import aiohttp from aiohttp import web from amqtt.adapters import ReaderAdapter, WriterAdapter from amqtt.broker import Broker from amqtt.contexts import BrokerConfig, ListenerConfig, ListenerType logger = logging.getLogger(__name__) MQTT_LISTENER_NAME = "myMqttListener" async def hello(request): """Get request handler""" return web.Response(text="Hello, world") class WebSocketResponseReader(ReaderAdapter): """Interface to allow mqtt broker to read from an aiohttp websocket connection.""" def __init__(self, ws: web.WebSocketResponse): self.ws = ws self.buffer = bytearray() async def read(self, n: int = -1) -> bytes: """Read 'n' bytes from the datastream, if < 0 read all available bytes Raises: BrokerPipeError : if reading on a closed websocket connection """ # continue until buffer contains at least the amount of data being requested while not self.buffer or len(self.buffer) < n: # if the websocket is closed if self.ws.closed: raise BrokenPipeError try: # read from stream msg = await asyncio.wait_for(self.ws.receive(), timeout=0.5) # mqtt streams should always be binary... if msg.type == aiohttp.WSMsgType.BINARY: self.buffer.extend(msg.data) elif msg.type == aiohttp.WSMsgType.CLOSE: raise BrokenPipeError except asyncio.TimeoutError: raise BrokenPipeError # return all bytes currently in the buffer if n == -1: result = bytes(self.buffer) self.buffer.clear() # return the requested number of bytes from the buffer else: result = self.buffer[:n] del self.buffer[:n] return result def feed_eof(self) -> None: pass class WebSocketResponseWriter(WriterAdapter): """Interface to allow mqtt broker to write to an aiohttp websocket connection.""" def __init__(self, ws: web.WebSocketResponse, request: web.Request): super().__init__() self.ws = ws # needed for `get_peer_info` # https://docs.python.org/3/library/socket.html#socket.socket.getpeername peer_name = request.transport.get_extra_info("peername") if peer_name is not None: self.client_ip, self.port = peer_name[0:2] else: self.client_ip, self.port = request.remote, 0 # interpret AF_INET6 self.client_ip = "localhost" if self.client_ip == "::1" else self.client_ip self._stream = io.BytesIO(b"") def write(self, data: bytes) -> None: """Add bytes to stream buffer.""" self._stream.write(data) async def drain(self) -> None: """Send the collected bytes in the buffer to the websocket connection.""" data = self._stream.getvalue() if data and len(data): await self.ws.send_bytes(data) self._stream = io.BytesIO(b"") def get_peer_info(self) -> tuple[str, int] | None: return self.client_ip, self.port async def close(self) -> None: # no clean up needed, stream will be gc along with instance pass def get_ssl_info(self) -> ssl.SSLObject | None: pass async def mqtt_websocket_handler(request: web.Request) -> web.StreamResponse: # establish connection by responding to the websocket request with the 'mqtt' protocol ws = web.WebSocketResponse(protocols=["mqtt"]) await ws.prepare(request) # access the broker created when the server started b: Broker = request.app["broker"] # hand-off the websocket data stream to the broker for handling # `listener_name` is the same name of the externalized listener in the broker config await b.external_connected(WebSocketResponseReader(ws), WebSocketResponseWriter(ws, request), MQTT_LISTENER_NAME) logger.debug("websocket connection closed") return ws async def websocket_handler(request: web.Request) -> web.StreamResponse: ws = web.WebSocketResponse() await ws.prepare(request) async for msg in ws: logging.info(msg) logging.info("websocket connection closed") return ws def main(): # create an `aiohttp` server lp = asyncio.get_event_loop() app = web.Application() app.add_routes( [ web.get("/", hello), # http get request/response route web.get("/ws", websocket_handler), # standard websocket handler web.get("/mqtt", mqtt_websocket_handler), # websocket handler for mqtt connections ]) # create background task for running the `amqtt` broker app.cleanup_ctx.append(run_broker) # make sure that both `aiohttp` server and `amqtt` broker run in the same loop # so the server can hand off the connection to the broker (prevents attached-to-a-different-loop `RuntimeError`) web.run_app(app, loop=lp) async def run_broker(_app): """App init function to start (and then shutdown) the `amqtt` broker. https://docs.aiohttp.org/en/stable/web_advanced.html#background-tasks """ # standard TCP connection as well as an externalized-listener cfg = BrokerConfig( listeners={ "default":ListenerConfig(type=ListenerType.TCP, bind="127.0.0.1:1883"), MQTT_LISTENER_NAME: ListenerConfig(type=ListenerType.EXTERNAL), } ) # make sure the `Broker` runs in the same loop as the aiohttp server loop = asyncio.get_event_loop() broker = Broker(config=cfg, loop=loop) # store broker instance so that incoming requests can hand off processing of a datastream _app["broker"] = broker # start the broker await broker.start() # pass control back to web app yield # closing activities await broker.shutdown() if __name__ == "__main__": logging.basicConfig(level=logging.INFO) main() Yakifo-amqtt-2637127/samples/legacy.yaml000066400000000000000000000002551504664204300200570ustar00rootroot00000000000000--- listeners: default: type: tcp bind: 0.0.0.0:1883 sys_interval: 20 auth: plugins: - auth_anonymous allow-anonymous: true topic-check: enabled: False Yakifo-amqtt-2637127/samples/mosquitto.org.crt000066400000000000000000000026541504664204300213000ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIEAzCCAuugAwIBAgIUBY1hlCGvdj4NhBXkZ/uLUZNILAwwDQYJKoZIhvcNAQEL BQAwgZAxCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwG A1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1vc3F1aXR0bzELMAkGA1UECwwCQ0ExFjAU BgNVBAMMDW1vc3F1aXR0by5vcmcxHzAdBgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hv by5vcmcwHhcNMjAwNjA5MTEwNjM5WhcNMzAwNjA3MTEwNjM5WjCBkDELMAkGA1UE BhMCR0IxFzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTES MBAGA1UECgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVp dHRvLm9yZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBAME0HKmIzfTOwkKLT3THHe+ObdizamPg UZmD64Tf3zJdNeYGYn4CEXbyP6fy3tWc8S2boW6dzrH8SdFf9uo320GJA9B7U1FW Te3xda/Lm3JFfaHjkWw7jBwcauQZjpGINHapHRlpiCZsquAthOgxW9SgDgYlGzEA s06pkEFiMw+qDfLo/sxFKB6vQlFekMeCymjLCbNwPJyqyhFmPWwio/PDMruBTzPH 3cioBnrJWKXc3OjXdLGFJOfj7pP0j/dr2LH72eSvv3PQQFl90CZPFhrCUcRHSSxo E6yjGOdnz7f6PveLIB574kQORwt8ePn0yidrTC1ictikED3nHYhMUOUCAwEAAaNT MFEwHQYDVR0OBBYEFPVV6xBUFPiGKDyo5V3+Hbh4N9YSMB8GA1UdIwQYMBaAFPVV 6xBUFPiGKDyo5V3+Hbh4N9YSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL BQADggEBAGa9kS21N70ThM6/Hj9D7mbVxKLBjVWe2TPsGfbl3rEDfZ+OKRZ2j6AC 6r7jb4TZO3dzF2p6dgbrlU71Y/4K0TdzIjRj3cQ3KSm41JvUQ0hZ/c04iGDg/xWf +pp58nfPAYwuerruPNWmlStWAXf0UTqRtg4hQDWBuUFDJTuWuuBvEXudz74eh/wK sMwfu1HFvjy5Z0iMDU8PUDepjVolOCue9ashlS4EB5IECdSR2TItnAIiIwimx839 LdUdRudafMu5T5Xma182OC0/u/xRlEm+tvKGGmfFcN0piqVl8OrSPBgIlb+1IKJE m/XriWr/Cq4h/JfB7NTsezVslgkBaoU= -----END CERTIFICATE----- Yakifo-amqtt-2637127/samples/passwd000066400000000000000000000002471504664204300171540ustar00rootroot00000000000000# Test user with 'test' password encrypted with sha-512 test:$6$l4zQEHEcowc1Pnv4$HHrh8xnsZoLItQ8BmpFHM4r6q5UqK3DnXp2GaTm5zp5buQ7NheY3Xt9f6godVKbEtA.hOC7IEDwnok3pbAOip.Yakifo-amqtt-2637127/samples/unix_sockets.py000066400000000000000000000133051504664204300210170ustar00rootroot00000000000000import asyncio from asyncio import StreamWriter, StreamReader, Event from functools import partial import logging from pathlib import Path import ssl import typer from amqtt.adapters import ReaderAdapter, WriterAdapter from amqtt.broker import Broker from amqtt.client import ClientContext from amqtt.contexts import BrokerConfig, ClientConfig, ListenerConfig, ListenerType from amqtt.mqtt.protocol.client_handler import ClientProtocolHandler from amqtt.plugins.manager import PluginManager from amqtt.session import Session logger = logging.getLogger(__name__) app = typer.Typer(add_completion=False, rich_markup_mode=None) # Usage: unix_sockets.py [OPTIONS] COMMAND [ARGS]... # # Options: # --help Show this message and exit. # # Commands: # broker Run an mqtt broker that communicates over a unix (file) socket. # client Run an mqtt client that communicates over a unix (file) socket. class UnixStreamReaderAdapter(ReaderAdapter): def __init__(self, reader: StreamReader) -> None: self._reader = reader async def read(self, n:int = -1) -> bytes: if n < 0: return await self._reader.read() return await self._reader.readexactly(n) def feed_eof(self) -> None: return self._reader.feed_eof() class UnixStreamWriterAdapter(WriterAdapter): def __init__(self, writer: StreamWriter) -> None: self._writer = writer self.is_closed = Event() def write(self, data: bytes) -> None: if not self.is_closed.is_set(): self._writer.write(data) async def drain(self) -> None: if self.is_closed.is_set(): await self._writer.drain() def get_peer_info(self) -> tuple[str, int]: extra_info = self._writer.get_extra_info("socket") return extra_info.getsockname(), 0 async def close(self) -> None: if self.is_closed.is_set(): return self.is_closed.set() await self._writer.drain() if self._writer.can_write_eof(): self._writer.write_eof() self._writer.close() with contextlib.suppress(AttributeError): await self._writer.wait_closed() def get_ssl_info(self) -> ssl.SSLObject | None: pass async def run_broker(socket_file: Path): # configure the broker with a single, external listener cfg = BrokerConfig( listeners={ "default": ListenerConfig( type=ListenerType.EXTERNAL ) }, plugins={ "amqtt.plugins.logging_amqtt.EventLoggerPlugin":{}, "amqtt.plugins.logging_amqtt.PacketLoggerPlugin":{}, "amqtt.plugins.authentication.AnonymousAuthPlugin":{"allow_anonymous":True}, } ) b = Broker(cfg) # new connection handler async def unix_stream_connected(reader: StreamReader, writer: StreamWriter, listener_name: str): logger.info("received new unix connection....") # wraps the reader/writer in a compatible interface r = UnixStreamReaderAdapter(reader) w = UnixStreamWriterAdapter(writer) # passes the connection to the broker for protocol communications await b.external_connected(reader=r, writer=w, listener_name=listener_name) await asyncio.start_unix_server(partial(unix_stream_connected, listener_name="default"), path=socket_file) await b.start() try: logger.info("starting mqtt unix server") # run until ctrl-c while True: await asyncio.sleep(1) except KeyboardInterrupt: await b.shutdown() @app.command() def broker( socket_file: str | None = typer.Option("/tmp/mqtt", "-s", "--socket", help="path and file for unix socket"), verbose: bool = typer.Option(False, "-v", "--verbose", help="set logging level to DEBUG"), ): """Run an mqtt broker that communicates over a unix (file) socket.""" logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO) asyncio.run(run_broker(Path(socket_file))) async def run_client(socket_file: Path): # 'MQTTClient' establishes the connection but uses the ClientProtocolHandler for MQTT protocol communications # create a plugin manager config = ClientConfig() context = ClientContext() context.config = config plugins_manager = PluginManager("amqtt.client.plugins", context) # create a client protocol handler cph = ClientProtocolHandler(plugins_manager) # connect to the unix socket conn_reader, conn_writer = await asyncio.open_unix_connection(path=socket_file) # anonymous session connection just needs a client_id s = Session() s.client_id = "myUnixClientID" # wraps the reader/writer in compatible interface r = UnixStreamReaderAdapter(conn_reader) w = UnixStreamWriterAdapter(conn_writer) # pass the connection to the protocol handler for mqtt communications and initiate CONNECT/CONNACK cph.attach(session=s, reader=r, writer=w) logger.debug("handler attached") ret = await cph.mqtt_connect() logger.info(f"client connected: {ret}") try: while True: # periodically send a message await cph.mqtt_publish("my/topic", b"my message", 0, False) await asyncio.sleep(1) except KeyboardInterrupt: cph.detach() @app.command() def client( socket_file: str | None = typer.Option("/tmp/mqtt", "-s", "--socket", help="path and file for unix socket"), verbose: bool = typer.Option(False, "-v", "--verbose", help="set logging level to DEBUG"), ): """Run an mqtt client that communicates over a unix (file) socket.""" logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO) asyncio.run(run_client(Path(socket_file))) if __name__ == "__main__": app() Yakifo-amqtt-2637127/scripts/000077500000000000000000000000001504664204300157505ustar00rootroot00000000000000Yakifo-amqtt-2637127/scripts/run-in-env.sh000077500000000000000000000023251504664204300203070ustar00rootroot00000000000000#!/usr/bin/env bash set -eu # Enable debug mode if DEBUG=true is set in the environment DEBUG=${DEBUG:-false} if [ "$DEBUG" = "true" ]; then set -x fi # Resolve project root directory my_path=$(git rev-parse --show-toplevel) # Activate pyenv virtualenv if .python-version exists if [ -s "${my_path}/.python-version" ]; then PYENV_VERSION=$(head -n 1 "${my_path}/.python-version") if command -v pyenv >/dev/null 2>&1; then export PYENV_VERSION echo "Activating pyenv version: ${PYENV_VERSION}" >&2 else echo "Warning: pyenv not found, skipping pyenv activation." >&2 fi fi # Check for and activate common virtual environments venvs=("venv" ".venv") for venv in "${venvs[@]}"; do activate_script="${my_path}/${venv}/bin/activate" if [ -f "$activate_script" ]; then echo "Activating virtual environment: ${venv}" >&2 # Deactivate any existing venv and activate the new one deactivate 2>/dev/null || true # shellcheck source=/dev/null . "$activate_script" break fi done # Check if we are in a virtual environment if [ -z "${VIRTUAL_ENV:-}" ]; then echo "Warning: No virtual environment found. Running in global Python environment." >&2 fi # Execute the specified command exec "$@" Yakifo-amqtt-2637127/scripts/update-requirements.sh000077500000000000000000000022431504664204300223130ustar00rootroot00000000000000#!/usr/bin/env bash set -eu # List of package names packages=(transitions websockets passlib docopt PyYAML) echo "" >requirements.txt # Loop through the packages for package in "${packages[@]}"; do # Get the latest version number using jq and curl latest_version=$(curl -s "https://pypi.org/pypi/${package}/json" | jq -r '.releases | keys | .[]' | sort -V | tail -n 1) # Print the formatted output echo "\"${package}==${latest_version}\", # https://pypi.org/project/${package}" >>requirements.txt done # ------------------------------------------------------------------------------ packages_dev=(hypothesis mypy pre-commit psutil pylint pytest-asyncio pytest-cov pytest-logdog pytest-timeout pytest ruff setuptools types-mock types-PyYAML types-setuptools) echo "" >requirements-dev.txt # Loop through the packages for package in "${packages_dev[@]}"; do # Get the latest version number using jq and curl latest_version=$(curl -s "https://pypi.org/pypi/${package}/json" | jq -r '.releases | keys | .[]' | sort -V | tail -n 1) # Print the formatted output echo "\"${package}>=${latest_version}\", # https://pypi.org/project/${package}" >>requirements-dev.txt done Yakifo-amqtt-2637127/tests/000077500000000000000000000000001504664204300154235ustar00rootroot00000000000000Yakifo-amqtt-2637127/tests/conftest.py000066400000000000000000000112741504664204300176270ustar00rootroot00000000000000import logging import subprocess from pathlib import Path import tempfile from typing import Any import unittest.mock import urllib.request import pytest from amqtt.broker import Broker from amqtt.contexts import BaseContext from amqtt.plugins.base import BasePlugin log = logging.getLogger(__name__) pytest_plugins = ["pytest_logdog"] @pytest.fixture def rsa_keys(): tmp_dir = tempfile.TemporaryDirectory(prefix='amqtt-test-') cert = Path(tmp_dir.name) / "cert.pem" key = Path(tmp_dir.name) / "key.pem" cmd = f'openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout {key} -out {cert} -subj "/CN=localhost"' subprocess.run(cmd, shell=True, capture_output=True, text=True) yield cert, key tmp_dir.cleanup() @pytest.fixture def test_config(rsa_keys): certfile, keyfile = rsa_keys yield { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 15}, "mqtts": { "type": "tcp", "bind": "127.0.0.1:1884", "max_connections": 15, "ssl": True, "certfile": certfile, "keyfile": keyfile }, "ws": {"type": "ws", "bind": "127.0.0.1:8080", "max_connections": 15}, "wss": { "type": "ws", "bind": "127.0.0.1:8081", "max_connections": 15, "ssl": True, 'certfile': certfile, 'keyfile': keyfile}, }, "sys_interval": 0, "auth": { "allow-anonymous": True, } } test_config_acl: dict[str, int | dict[str, Any]] = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1884", "max_connections": 10}, }, "sys_interval": 0, "auth": { "plugins": ["auth_file"], "password-file": Path(__file__).resolve().parent / "plugins" / "passwd", }, "topic-check": { "enabled": True, "plugins": ["topic_acl"], "acl": { "user1": ["public/#"], "user2": ["#"], }, "publish-acl": {"user1": ["public/subtopic/#"]}, }, } @pytest.fixture def mock_plugin_manager(): with (unittest.mock.patch("amqtt.broker.PluginManager") as plugin_manager): plugin_manager_instance = plugin_manager.return_value # disable topic filtering when using the mock manager plugin_manager_instance.is_topic_filtering_enabled.return_value = False # allow any connection when using the mock manager plugin_manager_instance.map_plugin_auth = unittest.mock.AsyncMock(return_value={ BasePlugin(BaseContext()): True }) yield plugin_manager @pytest.fixture async def broker_fixture(test_config): with pytest.warns(DeprecationWarning): broker = Broker(test_config, plugin_namespace="amqtt.test.plugins") await broker.start() assert broker.transitions.is_started() assert broker._sessions == {} assert "default" in broker._servers yield broker if not broker.transitions.is_stopped(): await broker.shutdown() @pytest.fixture async def broker(mock_plugin_manager, test_config): # just making sure the mock is in place before we start our broker assert mock_plugin_manager is not None broker = Broker(test_config, plugin_namespace="amqtt.test.plugins") await broker.start() assert broker.transitions.is_started() assert broker._sessions == {} assert "default" in broker._servers yield broker if not broker.transitions.is_stopped(): await broker.shutdown() @pytest.fixture async def acl_broker(): broker = Broker( test_config_acl, plugin_namespace="amqtt.broker.plugins", ) await broker.start() assert broker.transitions.is_started() assert broker._sessions == {} assert "default" in broker._servers yield broker if not broker.transitions.is_stopped(): await broker.shutdown() @pytest.fixture(scope="module") def ca_file_fixture(): temp_dir = Path(tempfile.mkdtemp(prefix="amqtt-test-")) url = "http://test.mosquitto.org/ssl/mosquitto.org.crt" ca_file = temp_dir / "mosquitto.org.crt" urllib.request.urlretrieve(url, str(ca_file)) log.info(f"Stored mosquitto cert at {ca_file}") # Yield the CA file path for tests yield ca_file # Cleanup after the tests if temp_dir.exists(): for file in temp_dir.iterdir(): file.unlink() temp_dir.rmdir() def pytest_addoption(parser): parser.addoption( "--mock-docker", action="store", default="false", help="for environments where docker isn't available, mock calls which require docker", )Yakifo-amqtt-2637127/tests/contrib/000077500000000000000000000000001504664204300170635ustar00rootroot00000000000000Yakifo-amqtt-2637127/tests/contrib/test_cert.py000066400000000000000000000152561504664204300214420ustar00rootroot00000000000000import asyncio import logging import shutil import subprocess import tempfile from pathlib import Path from unittest.mock import MagicMock from OpenSSL import crypto import pytest from amqtt.broker import BrokerContext, Broker from amqtt.client import MQTTClient from amqtt.contrib.cert import UserAuthCertPlugin from amqtt.errors import ConnectError from amqtt.scripts.server_creds import server_creds as get_server_creds from amqtt.scripts.device_creds import device_creds as get_device_creds from amqtt.scripts.ca_creds import ca_creds as get_ca_creds from amqtt.session import Session logger = logging.getLogger(__name__) @pytest.fixture def temp_directory(): temp_dir = Path(tempfile.mkdtemp(prefix="amqtt-test-")) yield temp_dir logger.critical(temp_dir) # shutil.rmtree(temp_dir) @pytest.fixture def ca_creds(temp_directory): get_ca_creds(country='US', state="NY", locality="NYC", org_name="aMQTT", cn="aMQTT", output_dir=str(temp_directory)) ca_key = temp_directory / "ca.key" ca_crt = temp_directory / "ca.crt" return ca_key, ca_crt @pytest.fixture def server_creds(ca_creds, temp_directory): ca_key = temp_directory / "ca.key" ca_crt = temp_directory / "ca.crt" get_server_creds(country='US', org_name='aMQTT', cn='aMQTT', output_dir=str(temp_directory), ca_key_fn=str(ca_key), ca_crt_fn=str(ca_crt)) server_key = temp_directory / "server.key" server_crt = temp_directory / "server.crt" yield server_key, server_crt @pytest.fixture def device_creds(ca_creds, temp_directory): ca_key, ca_crt = ca_creds get_device_creds(country='US', org_name='aMQTT', device_id="mydeviceid", uri='test.amqtt.io', output_dir=str(temp_directory), ca_key_fn=str(ca_key), ca_crt_fn=str(ca_crt)) yield temp_directory / "mydeviceid.key", temp_directory / "mydeviceid.crt" def test_device_cert(temp_directory, ca_creds, server_creds, device_creds): ca_key, ca_crt = ca_creds server_key, server_crt = server_creds device_key, device_crt = device_creds assert ca_key.exists() assert ca_crt.exists() assert server_key.exists() assert server_crt.exists() assert device_key.exists() assert device_crt.exists() r = subprocess.run(f"openssl x509 -in {str(device_crt)} -noout -text", shell=True, capture_output=True, text=True, check=True) assert "URI:spiffe://test.amqtt.io/device/mydeviceid, DNS:mydeviceid.local" in r.stdout @pytest.fixture def ssl_object_mock(device_creds): device_key, device_crt = device_creds with device_crt.open("rb") as f: cert = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) mock = MagicMock() mock.getpeercert.return_value = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert) yield mock @pytest.mark.parametrize("uri_domain,client_id,expected_result", [ ('test.amqtt.io', 'mydeviceid', True), ('test.amqtt.io', 'notmydeviceid', False), ('other.amqtt.io', 'mydeviceid', False), ]) @pytest.mark.asyncio async def test_cert_plugin(ssl_object_mock, uri_domain, client_id, expected_result): empty_cfg = { 'listeners': {'default': {'type':'tcp', 'bind':'127.0.0.1:1883'}}, 'plugins': {} } bc = BrokerContext(broker=Broker(config=empty_cfg)) bc.config = UserAuthCertPlugin.Config(uri_domain=uri_domain) cert_auth_plugin = UserAuthCertPlugin(bc) s = Session() s.client_id = client_id s.ssl_object = ssl_object_mock assert await cert_auth_plugin.authenticate(session=s) == expected_result @pytest.mark.asyncio async def test_client_broker_cert_authentication(ca_creds, server_creds, device_creds): ca_key, ca_crt = ca_creds server_key, server_crt = server_creds device_key, device_crt = device_creds broker_config = { 'listeners': { 'default': { 'type':'tcp', 'bind':'127.0.0.1:8883', 'ssl': True, 'keyfile': server_key, 'certfile': server_crt, 'cafile': ca_crt, } }, 'plugins': { 'amqtt.plugins.logging_amqtt.PacketLoggerPlugin':{}, 'amqtt.contrib.cert.UserAuthCertPlugin': {'uri_domain': 'test.amqtt.io'}, } } b = Broker(config=broker_config) await b.start() await asyncio.sleep(1) client_config = { 'auto_reconnect': False, 'broker': { 'cafile': ca_crt, 'certfile': device_crt, 'keyfile': device_key } } c = MQTTClient(config=client_config, client_id='mydeviceid') await c.connect('mqtts://127.0.0.1:8883') await asyncio.sleep(0.1) assert 'mydeviceid' in b._sessions s, _ = b._sessions['mydeviceid'] assert s.transitions.state == "connected" await asyncio.sleep(0.1) await c.disconnect() await asyncio.sleep(0.1) await b.shutdown() def ssl_error_logger(loop, context): logger.critical("Asyncio SSL error:", context.get("message")) exc = repr(context.get("exception")) assert "exception" not in context, f"Exception: {exc}" @pytest.mark.asyncio async def test_client_broker_wrong_certs(ca_creds, server_creds, device_creds): loop = asyncio.get_event_loop() loop.set_exception_handler(ssl_error_logger) loop.set_debug(True) ca_key, ca_crt = ca_creds server_key, server_crt = server_creds device_key, device_crt = device_creds broker_config = { 'listeners': { 'default': { 'type':'tcp', 'bind':'127.0.0.1:8883', 'ssl': True, 'keyfile': server_key, 'certfile': server_crt, 'cafile': ca_crt, } }, 'plugins': { 'amqtt.plugins.logging_amqtt.PacketLoggerPlugin':{}, 'amqtt.contrib.cert.UserAuthCertPlugin': {'uri_domain': 'test.amqtt.io'}, } } b = Broker(config=broker_config) await b.start() await asyncio.sleep(1) # generate a different ca certificate and make sure the connection fails temp_dir = Path(tempfile.mkdtemp(prefix="amqtt-test-")) get_ca_creds(country='US', state="NY", locality="NYC", org_name="aMQTT", cn="aMQTT", output_dir=str(temp_dir)) wrong_ca_crt = temp_dir / 'ca.crt' client_config = { 'auto_reconnect': False, 'connection': { 'cafile': wrong_ca_crt, 'certfile': device_crt, 'keyfile': device_key, } } c = MQTTClient(config=client_config, client_id='mydeviceid') with pytest.raises(ConnectError, match='.+?SSL: CERTIFICATE_VERIFY_FAILED.+?'): await c.connect('mqtts://127.0.0.1:8883') await b.shutdown()Yakifo-amqtt-2637127/tests/contrib/test_contrib.py000066400000000000000000000000001504664204300221220ustar00rootroot00000000000000Yakifo-amqtt-2637127/tests/contrib/test_db_plugin.py000066400000000000000000000411601504664204300224410ustar00rootroot00000000000000import asyncio import sqlite3 import tempfile from pathlib import Path import pytest import aiosqlite from passlib.context import CryptContext from amqtt.broker import BrokerContext, Broker from amqtt.client import MQTTClient from amqtt.contexts import Action from amqtt.contrib.auth_db.models import AllowedTopic, PasswordHasher from amqtt.contrib.auth_db.plugin import UserAuthDBPlugin, TopicAuthDBPlugin from amqtt.contrib.auth_db.managers import UserManager, TopicManager from amqtt.errors import ConnectError, MQTTError from amqtt.mqtt.constants import QOS_1, QOS_0 from amqtt.session import Session from argon2 import PasswordHasher as ArgonPasswordHasher from argon2.exceptions import VerifyMismatchError @pytest.fixture def password_hasher(): pwd_hasher = PasswordHasher() pwd_hasher.crypt_context = CryptContext(schemes=["argon2", ], deprecated="auto") yield pwd_hasher @pytest.fixture def db_file(): with tempfile.TemporaryDirectory() as temp_dir: with tempfile.NamedTemporaryFile(mode='wb', delete=True) as tmp: yield Path(temp_dir) / f"{tmp.name}.db" @pytest.fixture def db_connection(db_file): test_db_connect = f"sqlite+aiosqlite:///{db_file}" yield test_db_connect @pytest.fixture @pytest.mark.asyncio async def user_manager(password_hasher, db_connection): um = UserManager(db_connection) await um.db_sync() yield um @pytest.fixture @pytest.mark.asyncio async def topic_manager(password_hasher, db_connection): tm = TopicManager(db_connection) await tm.db_sync() yield tm # ###################################### # Tests for the UserAuthDBPlugin @pytest.mark.asyncio async def test_create_user(user_manager, db_file, db_connection): await user_manager.create_user_auth("myuser", "mypassword") async with aiosqlite.connect(db_file) as db_conn: db_conn.row_factory = sqlite3.Row # Set the row_factory has_user = False async with await db_conn.execute("SELECT * FROM user_auth") as cursor: for row in await cursor.fetchall(): assert row['username'] == "myuser" assert row['password_hash'] != "mypassword" assert '$argon2' in row['password_hash'] ph = ArgonPasswordHasher() ph.verify(row['password_hash'], "mypassword") with pytest.raises(VerifyMismatchError): ph.verify(row['password_hash'], "mywrongpassword") has_user = True assert has_user, "user was not created" @pytest.mark.asyncio async def test_list_users(user_manager, db_file, db_connection): await user_manager.create_user_auth("myuser", "mypassword") await user_manager.create_user_auth("otheruser", "mypassword") await user_manager.create_user_auth("anotheruser", "mypassword") assert len(list(await user_manager.list_user_auths())) == 3 @pytest.mark.asyncio async def test_list_empty_users(user_manager, db_file, db_connection): assert len(list(await user_manager.list_user_auths())) == 0 @pytest.mark.asyncio async def test_password_change(user_manager, db_file, db_connection): new_user = await user_manager.create_user_auth("myuser", "mypassword") await user_manager.update_user_auth_password("myuser", "mynewpassword") async with aiosqlite.connect(db_file) as db_conn: db_conn.row_factory = sqlite3.Row # Set the row_factory has_user = False async with await db_conn.execute("SELECT * FROM user_auth") as cursor: for row in await cursor.fetchall(): assert row['password_hash'] != new_user._password_hash ph = ArgonPasswordHasher() with pytest.raises(VerifyMismatchError): ph.verify(row['password_hash'], "mypassword") has_user = True assert has_user, "user was not found" @pytest.mark.asyncio async def test_remove_users(user_manager, db_file, db_connection): await user_manager.create_user_auth("myuser", "mypassword") await user_manager.create_user_auth("otheruser", "mypassword") await user_manager.create_user_auth("anotheruser", "mypassword") assert len(list(await user_manager.list_user_auths())) == 3 await user_manager.delete_user_auth("myuser") assert len(list(await user_manager.list_user_auths())) == 2 async with aiosqlite.connect(db_file) as db_conn: db_conn.row_factory = sqlite3.Row # Set the row_factory test_run = False async with await db_conn.execute("SELECT * FROM user_auth") as cursor: for row in await cursor.fetchall(): assert row['username'] in ("otheruser", "anotheruser") test_run = True assert test_run, "users weren't not found" @pytest.mark.parametrize("user_pwd,session_pwd,outcome", [ ("mypassword", "mypassword", True), ("mypassword", "myotherpassword", False), ]) @pytest.mark.asyncio async def test_db_auth(db_connection, user_manager, user_pwd, session_pwd, outcome): await user_manager.create_user_auth("myuser", user_pwd) broker_context = BrokerContext(broker=Broker()) broker_context.config = UserAuthDBPlugin.Config( connection=db_connection ) db_auth_plugin = UserAuthDBPlugin(context=broker_context) s = Session() s.username = "myuser" s.password = session_pwd assert await db_auth_plugin.authenticate(session=s) == outcome @pytest.mark.asyncio async def test_client_authentication(user_manager, db_connection): user = await user_manager.create_user_auth("myuser", "mypassword") assert user is not None broker_cfg = { 'listeners': { 'default': {'type': 'tcp', 'bind': '127.0.0.1:1883'}}, 'plugins': { 'amqtt.contrib.auth_db.UserAuthDBPlugin': { 'connection': db_connection, } } } broker = Broker(config=broker_cfg) await broker.start() await asyncio.sleep(0.1) client = MQTTClient(client_id='myclientid', config={'auto_reconnect': False}) await client.connect("mqtt://myuser:mypassword@127.0.0.1:1883") await client.subscribe([ ("my/topic", QOS_1) ]) await asyncio.sleep(0.1) await client.publish("my/topic", b"test") await asyncio.sleep(0.1) message = await client.deliver_message(timeout_duration=1) assert message.topic == "my/topic" await asyncio.sleep(0.1) await client.disconnect() await asyncio.sleep(0.1) await broker.shutdown() @pytest.mark.parametrize("client_pwd", [ ("mywrongpassword", ), ("", ), ]) @pytest.mark.asyncio async def test_client_blocked(user_manager, db_connection, client_pwd): user = await user_manager.create_user_auth("myuser", "mypassword") assert user is not None broker_cfg = { 'listeners': { 'default': {'type': 'tcp', 'bind': '127.0.0.1:1883'}}, 'plugins': { 'amqtt.contrib.auth_db.UserAuthDBPlugin': { 'connection': db_connection, } } } broker = Broker(config=broker_cfg) await broker.start() await asyncio.sleep(0.1) client = MQTTClient(client_id='myclientid', config={'auto_reconnect': False}) with pytest.raises(ConnectError): await client.connect(f"mqtt://myuser:{client_pwd}@127.0.0.1:1883") await broker.shutdown() await asyncio.sleep(0.1) # ###################################### # Tests for the TopicAuthDBPlugin def test_allowed_topic_match(): at = AllowedTopic(topic="my/topic") assert "my/topic" in at at2 = AllowedTopic(topic="my/other/topic") assert "my/other/topic" in at2 assert "my/another/topic" not in at2 at3 = AllowedTopic(topic="my/#") assert "my/other" in at3 assert "my/other/topic" in at3 assert "other/topic" not in at3 at4 = AllowedTopic(topic="my/other/#") assert "my/other" in at4 assert "my/other/topic" in at4 assert "other/topic" not in at4 assert "/my/other/topic" not in at4 at5 = AllowedTopic(topic="my/+/topic") assert "my/other/topic" in at5 assert "my/another/topic" in at5 assert "my/other/another/topic" not in at5 at6 = AllowedTopic(topic="my/other/topic") assert at6 == at2 assert at2 == at6 assert at6 != at assert at6 not in at5 def test_allowed_topic_list_match(): at1 = AllowedTopic(topic="one/topic") at2 = AllowedTopic(topic="one/other/topic") at3 = AllowedTopic(topic="two/+/topic") at4 = AllowedTopic(topic="three/topic/#") at_list = [at1, at2, at3, at4] assert "one/topic" in at_list assert "two/other/topic" in at_list assert "two/another/topic" in at_list assert "three/topic" in at_list assert "three/topic/other" in at_list def test_remove_topic_list(): at1 = AllowedTopic(topic="my/topic") at2 = AllowedTopic(topic="my/other/topic") at3 = AllowedTopic(topic="my/another/topic") at_list = [at1, at2, at3] at4 = AllowedTopic(topic="my/topic") at5 = AllowedTopic(topic="my/not/topic") at_list.remove(at4) with pytest.raises(ValueError): at_list.remove(at5) @pytest.mark.asyncio async def test_add_topic_to_client(db_file, user_manager, topic_manager, db_connection): client_id = "myuser" topic_auth = await topic_manager.create_topic_auth(client_id) assert topic_auth is not None topic_list = await topic_manager.add_allowed_topic(client_id, "my/topic", Action.PUBLISH) assert len(topic_list) > 0 async with aiosqlite.connect(db_file) as db_conn: db_conn.row_factory = sqlite3.Row # Set the row_factory user_found = False async with await db_conn.execute("SELECT * FROM topic_auth") as cursor: for row in await cursor.fetchall(): assert row['username'] == client_id assert "my/topic" in row['publish_acl'] user_found = True assert user_found @pytest.mark.asyncio async def test_invalid_dollar_topic_for_publish(db_file, user_manager, topic_manager, db_connection): client_id = "myuser" topic_auth = await topic_manager.create_topic_auth(client_id) assert topic_auth is not None with pytest.raises(MQTTError): await topic_manager.add_allowed_topic(client_id, "$MY/topic", Action.PUBLISH) async with aiosqlite.connect(db_file) as db_conn: db_conn.row_factory = sqlite3.Row # Set the row_factory auth_topic_found = False async with await db_conn.execute("SELECT * FROM topic_auth") as cursor: for row in await cursor.fetchall(): assert row['username'] == client_id assert "$MY/topic" not in row['publish_acl'] auth_topic_found = True assert auth_topic_found @pytest.mark.asyncio async def test_remove_topic_from_client(db_file, user_manager, topic_manager, db_connection): client_id = "myuser" topic_auth = await topic_manager.create_topic_auth(client_id) assert topic_auth is not None user = await user_manager.create_user_auth(client_id, "mypassword") assert user is not None await topic_manager.add_allowed_topic(client_id, "my/topic", Action.PUBLISH) await topic_manager.add_allowed_topic(client_id, "my/other/topic", Action.PUBLISH) topic_list = await topic_manager.add_allowed_topic(client_id, "my/another/topic", Action.PUBLISH) assert len(topic_list) == 3 async with aiosqlite.connect(db_file) as db_conn: db_conn.row_factory = sqlite3.Row # Set the row_factory user_found = False async with await db_conn.execute("SELECT * FROM topic_auth") as cursor: for row in await cursor.fetchall(): assert row['username'] == client_id assert "my/topic" in row['publish_acl'] assert "my/other/topic" in row['publish_acl'] assert "my/another/topic" in row['publish_acl'] user_found = True assert user_found topic_list = await topic_manager.remove_allowed_topic(client_id, "my/other/topic", Action.PUBLISH) assert len(topic_list) == 2 async with aiosqlite.connect(db_file) as db_conn: db_conn.row_factory = sqlite3.Row # Set the row_factory user_found = False async with await db_conn.execute("SELECT * FROM topic_auth") as cursor: for row in await cursor.fetchall(): assert row['username'] == client_id assert "my/topic" in row['publish_acl'] assert "my/other/topic" not in row['publish_acl'] assert "my/another/topic" in row['publish_acl'] user_found = True assert user_found @pytest.mark.asyncio async def test_remove_missing_topic(db_file, user_manager, topic_manager, db_connection): client_id = "myuser" topic_auth = await topic_manager.create_topic_auth(client_id) assert topic_auth is not None user = await user_manager.create_user_auth(client_id, "mypassword") assert user is not None await topic_manager.add_allowed_topic(client_id, "my/topic", Action.PUBLISH) await topic_manager.add_allowed_topic(client_id, "my/other/topic", Action.PUBLISH) topic_list = await topic_manager.add_allowed_topic(client_id, "my/another/topic", Action.PUBLISH) assert len(topic_list) == 3 with pytest.raises(MQTTError): await topic_manager.remove_allowed_topic(client_id, "my/not/topic", Action.PUBLISH) @pytest.mark.asyncio async def test_remove_topic_wrong_action(db_file, user_manager, topic_manager, db_connection): client_id = "myuser" topic_auth = await topic_manager.create_topic_auth(client_id) assert topic_auth is not None user = await user_manager.create_user_auth(client_id, "mypassword") assert user is not None await topic_manager.add_allowed_topic(client_id, "my/topic", Action.PUBLISH) await topic_manager.add_allowed_topic(client_id, "my/other/topic", Action.PUBLISH) topic_list = await topic_manager.add_allowed_topic(client_id, "my/another/topic", Action.PUBLISH) assert len(topic_list) == 3 with pytest.raises(MQTTError): await topic_manager.remove_allowed_topic(client_id, "my/other/topic", Action.SUBSCRIBE) @pytest.mark.parametrize("acl_topic,msg_topic,outcome", [ ("my/topic", "my/topic", True), ("my/topic", "my/other/topic", False), ("my/#", "my/other/topic", True), ("my/#", "my/another/topic", True), ]) @pytest.mark.asyncio async def test_topic_publish_filter_plugin(db_file, topic_manager, db_connection, acl_topic, msg_topic, outcome): client_id = "myuser" user = await topic_manager.create_topic_auth(client_id) assert user is not None await topic_manager.add_allowed_topic(client_id, acl_topic, Action.PUBLISH) broker_context = BrokerContext(broker=Broker()) broker_context.config = TopicAuthDBPlugin.Config( connection=db_connection ) db_auth_plugin = TopicAuthDBPlugin(context=broker_context) s = Session() s.username = client_id assert await db_auth_plugin.topic_filtering(session=s, topic=msg_topic, action=Action.PUBLISH) == outcome,\ f"topic filter responded incorrectly: {not outcome} vs {outcome}." @pytest.mark.asyncio async def test_topic_subscribe(db_file, topic_manager, db_connection): broker_cfg = { 'listeners': { 'default': {'type': 'tcp', 'bind': '127.0.0.1:1883'}}, 'plugins': { 'amqtt.plugins.authentication.AnonymousAuthPlugin': {}, 'amqtt.contrib.auth_db.TopicAuthDBPlugin': { 'connection': db_connection }, 'amqtt.plugins.sys.broker.BrokerSysPlugin': { 'sys_interval' : 2 } } } client_id = "myuser" topic_auth = await topic_manager.create_topic_auth(client_id) assert topic_auth is not None sub_topic_list = await topic_manager.add_allowed_topic(client_id, '$SYS/#', Action.SUBSCRIBE) rcv_topic_list = await topic_manager.add_allowed_topic(client_id, '$SYS/#', Action.RECEIVE) assert len(sub_topic_list) > 0 assert len(rcv_topic_list) > 0 broker = Broker(config=broker_cfg) await broker.start() await asyncio.sleep(0.1) client = MQTTClient(client_id='myclientid', config={'auto_reconnect': False}) await client.connect("mqtt://myuser:mypassword@127.0.0.1:1883") ret = await client.subscribe([ ('$SYS/broker/clients/connected', QOS_0) ]) assert ret == [0x0,] await asyncio.sleep(0.1) message_received = False try: message = await client.deliver_message(timeout_duration=4) assert message.topic == '$SYS/broker/clients/connected' message_received = True except asyncio.TimeoutError: pass assert message_received, "Did not receive a $SYS message" await asyncio.sleep(0.1) await client.disconnect() await asyncio.sleep(0.1) await broker.shutdown() Yakifo-amqtt-2637127/tests/contrib/test_db_scripts.py000066400000000000000000000320001504664204300226230ustar00rootroot00000000000000import asyncio import logging import tempfile from pathlib import Path import pytest from passlib.context import CryptContext from typer.testing import CliRunner from amqtt.contexts import Action from amqtt.contrib.auth_db.managers import TopicManager, UserManager from amqtt.contrib.auth_db.models import PasswordHasher, AllowedTopic from amqtt.contrib.auth_db.topic_mgr_cli import topic_app from amqtt.contrib.auth_db.user_mgr_cli import user_app runner = CliRunner() @pytest.fixture def password_hasher(): pwd_hasher = PasswordHasher() pwd_hasher.crypt_context = CryptContext(schemes=["argon2", ], deprecated="auto") yield pwd_hasher @pytest.fixture def db_file(): with tempfile.TemporaryDirectory() as temp_dir: with tempfile.NamedTemporaryFile(mode='wb', delete=True) as tmp: yield Path(temp_dir) / f"{tmp.name}.db" @pytest.fixture def db_connection(db_file): test_db_connect = f"sqlite+aiosqlite:///{db_file}" yield test_db_connect @pytest.fixture @pytest.mark.asyncio async def user_manager(password_hasher, db_connection): um = UserManager(db_connection) await um.db_sync() yield um @pytest.fixture @pytest.mark.asyncio async def topic_manager(password_hasher, db_connection): tm = TopicManager(db_connection) await tm.db_sync() yield tm @pytest.mark.parametrize("app,error_msg", [ (user_app, "user cli"), (topic_app, "topic cli"), ]) def test_cli_mgr_no_params(app, error_msg): result = runner.invoke(app, []) assert result.exit_code == 0, f"{result.output}" @pytest.mark.parametrize("app,error_msg", [ (user_app, "user cli"), (topic_app, "topic cli"), ]) def test_cli_mgr_no_db_type(app, error_msg): result = runner.invoke(topic_app, ["sync"]) assert result.exit_code == 2 @pytest.mark.parametrize("app,error_msg", [ (user_app, "user cli"), (topic_app, "topic cli"), ]) def test_cli_mgr_no_db_username(app, error_msg, caplog): with caplog.at_level(logging.INFO): result = runner.invoke(app, ["-d", "mysql", "sync"]) assert result.exit_code == 1 assert "DB access requires a username be provided." in caplog.text @pytest.mark.parametrize("app,error_msg", [ (user_app, "user cli"), (topic_app, "topic cli"), ]) def test_cli_mgr_db_not_installed(app, error_msg, caplog): with caplog.at_level(logging.INFO): result = runner.invoke(app, ["-d", "mysql", "-u", "mydbname", "sync",], input="mydbpassword\n" ) assert result.exit_code == 1 assert isinstance(result.exception, ModuleNotFoundError) @pytest.mark.parametrize("app,error_msg", [ (user_app, "user cli"), (topic_app, "topic cli"), ]) def test_cli_mgr_sync(db_file, app, error_msg, caplog): with caplog.at_level(logging.INFO): result = runner.invoke(app, [ "-d", "sqlite", "-f", f"{db_file}", "sync" ]) assert result.exit_code == 0 assert "Success: database synced." in caplog.text @pytest.mark.parametrize("app,success_msg", [ (user_app, "authentications"), (topic_app, "authorizations"), ]) def test_topic_empty_list(db_file, topic_manager, caplog, app, success_msg): with caplog.at_level(logging.INFO): result = runner.invoke(app, [ "-d", "sqlite", "-f", f"{db_file}", "list" ]) assert result.exit_code == 0 assert f"No client {success_msg} exist." in caplog.text def test_user_mgr_list_clients(db_file, user_manager, caplog): async def init_user_auths(): await user_manager.create_user_auth("client123", "randompassword") await user_manager.create_user_auth("client456", "randompassword") asyncio.run(init_user_auths()) with caplog.at_level(logging.INFO): result = runner.invoke(user_app, [ "-d", "sqlite", "-f", f"{db_file}", "list" ]) assert result.exit_code == 0 info_records = [record for record in caplog.records if record.levelname == "INFO"] assert 'client123' in info_records[0].message assert 'client456' in info_records[1].message def test_topic_mgr_list_clients(db_file, topic_manager, caplog): async def init_topic_auths(): await topic_manager.create_topic_auth('device123') await topic_manager.create_topic_auth('device456') await topic_manager.add_allowed_topic('device123', 'my/topic', Action.SUBSCRIBE) await topic_manager.add_allowed_topic('device456', 'my/topic', Action.PUBLISH) asyncio.run(init_topic_auths()) with caplog.at_level(logging.INFO): result = runner.invoke(topic_app, [ "-d", "sqlite", "-f", f"{db_file}", "list" ]) assert result.exit_code == 0 info_records = [record for record in caplog.records if record.levelname == "INFO"] assert 'device123' in info_records[0].message assert 'my/topic' in info_records[0].message assert 'device456' in info_records[1].message assert 'my/topic' in info_records[1].message def test_user_mgr_add_auth_missing_param(db_file, user_manager, caplog): with caplog.at_level(logging.INFO): result = runner.invoke(user_app, [ "-d", "sqlite", "-f", f"{db_file}", "add" ]) assert result.exit_code == 2 def test_add_allowed_topic_missing_param(db_file, topic_manager, caplog): with caplog.at_level(logging.INFO): result = runner.invoke(topic_app, [ "-d", "sqlite", "-f", f"{db_file}", "add", "-c", "client123", "my/topic" ]) assert result.exit_code == 2 def test_user_mgr_add_auth_missing_password(db_file, user_manager, caplog): with caplog.at_level(logging.INFO): result = runner.invoke(user_app, [ "-d", "sqlite", "-f", f"{db_file}", "add", "-c", 'client123' ], input=" \n") assert result.exit_code == 1 assert "Error: client password cannot be empty." in caplog.text, caplog.text async def verify_add(): user_auth = await user_manager.get_user_auth('client123') assert user_auth is None asyncio.run(verify_add()) def test_user_mgr_add_auth(db_file, user_manager, caplog): with caplog.at_level(logging.INFO): result = runner.invoke(user_app, [ "-d", "sqlite", "-f", f"{db_file}", "add", "-c", 'client123' ], input="dbpassword\nuserpassword\n") assert result.exit_code == 0 assert "Success: created 'client123'" in caplog.text, caplog.text async def verify_add(): user_auth = await user_manager.get_user_auth('client123') assert user_auth is not None asyncio.run(verify_add()) def test_add_allowed_topic(db_file, topic_manager, caplog): with caplog.at_level(logging.INFO): result = runner.invoke(topic_app, [ "-d", "sqlite", "-f", f"{db_file}", "add", "-c", "client123", "-a", "publish", "my/topic" ]) assert result.exit_code == 0 assert "Success: topic 'my/topic' added to publish for 'client123'" in caplog.text async def verify_add(): topic_auth = await topic_manager.get_topic_auth('client123') assert topic_auth is not None assert AllowedTopic('my/topic') in topic_auth.publish_acl asyncio.run(verify_add()) def test_remove_user_auth_mismatch(db_file, user_manager, caplog): async def init_user_auths(): await user_manager.create_user_auth("client123", "randompassword") asyncio.run(init_user_auths()) with caplog.at_level(logging.INFO): result = runner.invoke(user_app, [ "-d", "sqlite", "-f", f"{db_file}", "rm", "-c", "device123", ]) assert result.exit_code == 1, caplog.text assert "Error: client 'device123' does not exist." in caplog.text, result.output def test_remove_allowed_topic_mismatch(db_file, topic_manager, caplog): async def init_topic_auths(): await topic_manager.create_topic_auth('device123') await topic_manager.add_allowed_topic('device123', 'my/topic', Action.SUBSCRIBE) asyncio.run(init_topic_auths()) with caplog.at_level(logging.INFO): result = runner.invoke(topic_app, [ "-d", "sqlite", "-f", f"{db_file}", "rm", "-c", "device123", "-a", "publish", "my/topic" ]) assert result.exit_code == 1, caplog.text assert "Error: topic 'my/topic' not in the publish allow list for device123." in caplog.text def test_remove_user_auth_abort(db_file, user_manager, caplog): async def init_user_auths(): await user_manager.create_user_auth("client123", "randompassword") asyncio.run(init_user_auths()) with caplog.at_level(logging.INFO): result = runner.invoke(user_app, [ "-d", "sqlite", "-f", f"{db_file}", "rm", "-c", "client123", ], input="N\n") assert result.exit_code == 0, caplog.text async def verify_user_exists(): assert await user_manager.get_user_auth('client123') is not None asyncio.run(verify_user_exists()) def test_remove_user_auth_confirm(db_file, user_manager, caplog): async def init_user_auths(): await user_manager.create_user_auth("client123", "randompassword") asyncio.run(init_user_auths()) with caplog.at_level(logging.INFO): result = runner.invoke(user_app, [ "-d", "sqlite", "-f", f"{db_file}", "rm", "-c", "client123", ], input="y\n") assert result.exit_code == 0, caplog.text assert "Success: 'client123' was removed." in caplog.text, result.output async def verify_user_removed(): assert await user_manager.get_user_auth('client123') is None asyncio.run(verify_user_removed()) def test_remove_allowed_topic(db_file, topic_manager, caplog): async def init_topic_auth(): await topic_manager.create_topic_auth('device123') await topic_manager.add_allowed_topic('device123', 'my/topic', Action.SUBSCRIBE) asyncio.run(init_topic_auth()) with caplog.at_level(logging.INFO): result = runner.invoke(topic_app, [ "-d", "sqlite", "-f", f"{db_file}", "rm", "-c", "device123", "-a", "subscribe", "my/topic" ]) assert result.exit_code == 0 assert "Success: removed topic 'my/topic' from subscribe for 'device123'" in caplog.text, caplog.text def test_user_mgr_change_password_empty(db_file, user_manager, caplog): async def init_user_auths(): await user_manager.create_user_auth("client123", "randompassword") asyncio.run(init_user_auths()) with caplog.at_level(logging.INFO): result = runner.invoke(user_app, [ "-d", "sqlite", "-f", f"{db_file}", "pwd", "-c", "client123", ], input=" \n") assert result.exit_code == 1, caplog.text assert "Error: client password cannot be empty." in caplog.text, caplog.text def test_user_mgr_change_password(db_file, user_manager, caplog): async def init_user_auths(): await user_manager.create_user_auth("client123", "randompassword") user_auth = await user_manager.get_user_auth('client123') return user_auth._password_hash orig_pwd_hash = asyncio.run(init_user_auths()) with caplog.at_level(logging.INFO): result = runner.invoke(user_app, [ "-d", "sqlite", "-f", f"{db_file}", "pwd", "-c", "client123", ], input="myotherpassword\n") assert result.exit_code == 0, caplog.text assert "Success: client 'client123' password updated." in caplog.text, caplog.text async def verify_user_exists(pwd_hash): user_auth = await user_manager.get_user_auth('client123') assert user_auth is not None assert user_auth._password_hash != pwd_hash asyncio.run(verify_user_exists(orig_pwd_hash)) @pytest.mark.asyncio async def test_user_mgr_cli(): cmd = [ "user_mgr", "--help"] proc = await asyncio.create_subprocess_shell( " ".join(cmd), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await asyncio.sleep(0.2) stdout, stderr = await proc.communicate() assert proc.returncode == 0, f"user_mgr error code: {proc.returncode} - {stdout} - {stderr}" @pytest.mark.asyncio async def test_topic_mgr_cli(): cmd = [ "topic_mgr", "--help"] proc = await asyncio.create_subprocess_shell( " ".join(cmd), stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await asyncio.sleep(0.2) stdout, stderr = await proc.communicate() assert proc.returncode == 0, f"topic_mgr error code: {proc.returncode} - {stdout} - {stderr}" Yakifo-amqtt-2637127/tests/contrib/test_http.py000066400000000000000000000143621504664204300214610ustar00rootroot00000000000000import logging from enum import Enum, auto import pytest import pytest_asyncio from aiohttp import web from aiohttp.web import Response from amqtt.broker import BrokerContext, Broker from amqtt.contexts import Action from amqtt.contrib.http import UserAuthHttpPlugin, TopicAuthHttpPlugin, ParamsMode, ResponseMode, RequestMethod from amqtt.session import Session logger = logging.getLogger(__name__) def determine_response(d, matcher_field) -> Response: """The request's parameters determine what response is expected.""" if d['username'] == 'json': # special case, i_am_null respond with None if d[matcher_field] == 'i_am_null': return web.json_response({'Ok': None}) # otherwise, respond depending on if username and client_id match return web.json_response({'Ok': d['username'] == d[matcher_field]}) elif d['username'] == 'status': if d[matcher_field] == 'i_am_null': return web.Response(status=500) return web.Response(status=200) if d['username'] == d[matcher_field] else web.Response(status=400) else: # text return web.Response(text='ok' if d['username'] == d[matcher_field] else 'error') @pytest.fixture async def empty_broker(): config = {'listeners': {'default': { 'type': 'tcp', 'bind': '127.0.0.1:1883'}}, 'plugins': {}} broker = Broker(config) yield broker async def all_request_handler(request: web.Request) -> Response: """The url and method type is used determine how the data was passed to the request.""" if 'form' in str(request.url): if request.method == 'GET': d = request.query else: d = dict(await request.post()) else: d = dict(await request.json()) if '/user' in str(request.url): matcher = 'password' assert 'username' in d assert 'password' in d assert 'client_id' in d else: assert 'username' in d assert 'client_id' in d assert 'topic' in d assert 'acc' in d assert 1 <= int(d['acc']) <= 4 matcher = 'client_id' return determine_response(d, matcher) @pytest_asyncio.fixture async def http_server(): app = web.Application() # create all the routes for the various configuration options to handle the various use cases routes = [] for test_kind in ('user', 'acl'): for test_data in ('json', 'form'): for method in ('get', 'post', 'put'): func_method = getattr(web, method) routes.append(func_method(f'/{test_kind}/{test_data}', all_request_handler)) app.add_routes(routes) runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, "127.0.0.1", 8080) await site.start() yield f"http://127.0.0.1:8080" await runner.cleanup() def test_server_up_and_down(http_server): pass class TypeOfHttpTest(Enum): AUTH = auto() ACL = auto() def generate_use_cases(kind: TypeOfHttpTest): # generate all variations of the plugin's configuration options for both auth and topic # e.g. (TestKind.AUTH, '/user/json', RequestMethod.GET, ParamsMode.JSON, ResponseMode.JSON, 'json', 'json', True), cases: list[tuple[str, str, RequestMethod, ParamsMode, ResponseMode, str, str, bool]] = [] root_url = 'user' if kind == TypeOfHttpTest.AUTH else 'acl' for request in RequestMethod: for params in ParamsMode: for response in ResponseMode: url = f'/{root_url}/json' if params == ParamsMode.JSON else f'/{root_url}/form' for is_authenticated in [True, False, None]: if is_authenticated is None: pwd = 'i_am_null' elif is_authenticated: pwd = f'{response.value}' else: pwd = f'not{response.value}' if response == ResponseMode.TEXT and is_authenticated is None: is_authenticated = False case = (kind, url, request, params, response, response.value, f"{pwd}", is_authenticated) cases.append(case) return cases def test_generated_use_cases(): cases = generate_use_cases(kind=TypeOfHttpTest.AUTH) assert len(cases) == 54 @pytest.mark.parametrize("kind,url,request_method,params_mode,response_mode,username,matcher,is_allowed", generate_use_cases(TypeOfHttpTest.AUTH)) @pytest.mark.asyncio async def test_request_auth_response(empty_broker, http_server, kind, url, request_method, params_mode, response_mode, username, matcher, is_allowed): context = BrokerContext(broker=empty_broker) context.config = UserAuthHttpPlugin.Config( host="127.0.0.1", port=8080, user_uri=url, request_method=request_method, params_mode=params_mode, response_mode=response_mode, ) http_acl = UserAuthHttpPlugin(context) logger.warning(f'kind is {kind}') session = Session() session.client_id = "my_client_id" session.username = username session.password = matcher assert await http_acl.authenticate(session=session) == is_allowed await http_acl.on_broker_pre_shutdown() @pytest.mark.parametrize("kind,url,request_method,params_mode,response_mode,username,matcher,is_allowed", generate_use_cases(TypeOfHttpTest.ACL)) @pytest.mark.asyncio async def test_request_topic_response(empty_broker, http_server, kind, url, request_method, params_mode, response_mode, username, matcher, is_allowed): context = BrokerContext(broker=empty_broker) context.config = TopicAuthHttpPlugin.Config( host="127.0.0.1", port=8080, topic_uri=url, request_method=request_method, params_mode=params_mode, response_mode=response_mode, ) http_acl = TopicAuthHttpPlugin(context) s = Session() s.username = username s.client_id = matcher t = 'my/topic' a = Action.PUBLISH assert await http_acl.topic_filtering(session=s, topic=t, action=a) == is_allowed await http_acl.on_broker_pre_shutdown() # if kind == TestKind.ACL: # else:Yakifo-amqtt-2637127/tests/contrib/test_jwt.py000066400000000000000000000072161504664204300213060ustar00rootroot00000000000000import asyncio import logging import secrets try: from datetime import UTC, datetime, timedelta except ImportError: from datetime import datetime, timezone, timedelta UTC = timezone.utc import jwt import pytest from amqtt.broker import BrokerContext, Broker from amqtt.client import MQTTClient from amqtt.contexts import Action, ListenerConfig, BrokerConfig from amqtt.contrib.jwt import UserAuthJwtPlugin, TopicAuthJwtPlugin from amqtt.mqtt.constants import QOS_0 from amqtt.session import Session @pytest.fixture def secret_key(): return secrets.token_urlsafe(32) @pytest.mark.parametrize("exp_time, outcome", [ (datetime.now(UTC) + timedelta(hours=1), True), (datetime.now(UTC) - timedelta(hours=1), False), ]) @pytest.mark.asyncio async def test_user_jwt_plugin(secret_key, exp_time, outcome): payload = { "username": "example_user", "exp": exp_time } ctx = BrokerContext(Broker()) ctx.config = UserAuthJwtPlugin.Config( secret_key=secret_key, user_claim='username' ) jwt_plugin = UserAuthJwtPlugin(context=ctx) s = Session() s.username = "example_user" s.password = jwt.encode(payload, secret_key, algorithm="HS256") assert await jwt_plugin.authenticate(session=s) == outcome, "access should have been granted" @pytest.mark.asyncio async def test_topic_jwt_plugin(secret_key): payload = { "username": "example_user", "exp": datetime.now(UTC) + timedelta(hours=1), "publish_acl": ['my/topic/#', 'my/+/other'] } ctx = BrokerContext(Broker()) ctx.config = TopicAuthJwtPlugin.Config( secret_key=secret_key, publish_claim='publish_acl', subscribe_claim='subscribe_acl', receive_claim='receive_acl' ) jwt_plugin = TopicAuthJwtPlugin(context=ctx) s = Session() s.username = "example_user" s.password = jwt.encode(payload, secret_key, algorithm="HS256") assert await jwt_plugin.topic_filtering(session=s, topic="my/topic/one", action=Action.PUBLISH), "access should be granted" @pytest.mark.asyncio async def test_broker_with_jwt_plugin(secret_key, caplog): payload = { "username": "example_user", "exp": datetime.now(UTC) + timedelta(hours=1), "publish_acl": ['my/topic/#', 'my/+/other'], "subscribe_acl": ['my/+/other'], } username = "example_user" password = jwt.encode(payload, secret_key, algorithm="HS256") cfg = BrokerConfig( listeners={'default': ListenerConfig()}, plugins={ 'amqtt.contrib.jwt.UserAuthJwtPlugin': { 'secret_key': secret_key, 'user_claim': 'username', }, 'amqtt.contrib.jwt.TopicAuthJwtPlugin': { 'secret_key': secret_key, 'publish_claim': 'publish_acl', 'subscribe_claim': 'subscribe_acl', 'receive_claim': 'receive_acl' } } ) with caplog.at_level(logging.INFO): b = Broker(config=cfg) await b.start() await asyncio.sleep(0.1) c = MQTTClient() await c.connect(f'mqtt://{username}:{password}@localhost:1883') await asyncio.sleep(0.1) result = await c.subscribe([('my/one', QOS_0)]) assert result == [128, ] result = await c.subscribe([('my/one/other', QOS_0)]) assert result == [0] await c.publish('my/one', b'message should not get published') await asyncio.sleep(0.1) assert "not allowed to publish to TOPIC my/one" in caplog.text await asyncio.sleep(0.1) await c.disconnect() await asyncio.sleep(0.1) await b.shutdown() Yakifo-amqtt-2637127/tests/contrib/test_ldap.py000066400000000000000000000120451504664204300214160ustar00rootroot00000000000000import asyncio import ldap import time from pathlib import Path from unittest.mock import patch, MagicMock import pytest from amqtt.broker import BrokerContext, Broker from amqtt.client import MQTTClient from amqtt.contexts import BrokerConfig, ListenerConfig, ClientConfig, Action from amqtt.contrib.ldap import UserAuthLdapPlugin, TopicAuthLdapPlugin from amqtt.errors import ConnectError from amqtt.session import Session # Pin the project name to avoid creating multiple stacks @pytest.fixture(scope="session") def docker_compose_project_name() -> str: return "openldap" # Stop the stack before starting a new one @pytest.fixture(scope="session") def docker_setup(): return ["down -v", "up --build -d"] @pytest.fixture(scope="session") def docker_compose_file(pytestconfig): return Path(pytestconfig.rootdir) / "tests/fixtures/ldap" / "docker-compose.yml" @pytest.fixture(scope="session") def ldap_service_docker(docker_ip, docker_services): """Ensure that HTTP service is up and responsive.""" # `port_for` takes a container port and returns the corresponding host port port = docker_services.port_for("openldap", 389) url = "ldap://{}:{}".format(docker_ip, port) time.sleep(2) return url @pytest.fixture(scope="session") def ldap_service_mock(): """Ensure that HTTP service is up and responsive.""" # `port_for` takes a container port and returns the corresponding host port url = "ldap://localhost:0" mock_ldap_object = MagicMock() def mock_simple_s(*args, **kwargs): return [ ('dn', {'uid':'jdoe', 'publishACL':[b'my/topic/one', b'my/topic/two']}), ] def mock_simple_bind_s(*args): p = args[1] if p in ("adminpassword", "johndoepassword"): return raise ldap.INVALID_CREDENTIALS mock_ldap_object.search_s.side_effect = mock_simple_s mock_ldap_object.simple_bind_s.side_effect = mock_simple_bind_s with patch("ldap.initialize", return_value=mock_ldap_object): yield url @pytest.fixture(scope="session") def ldap_service(request): mode = request.config.getoption("--mock-docker") if mode: return request.getfixturevalue("ldap_service_mock") return request.getfixturevalue("ldap_service_docker") @pytest.mark.asyncio async def test_ldap_user_plugin(ldap_service): ctx = BrokerContext(Broker()) ctx.config = UserAuthLdapPlugin.Config( server=ldap_service, base_dn="dc=amqtt,dc=io", user_attribute="uid", bind_dn="cn=admin,dc=amqtt,dc=io", bind_password="adminpassword", ) ldap_plugin = UserAuthLdapPlugin(context=ctx) s = Session() s.username = "jdoe" s.password = "johndoepassword" assert await ldap_plugin.authenticate(session=s), "could not authenticate user" @pytest.mark.asyncio async def test_ldap_user(ldap_service): cfg = BrokerConfig( listeners={ 'default' : ListenerConfig() }, plugins={ 'amqtt.contrib.ldap.UserAuthLdapPlugin': { 'server': ldap_service, 'base_dn': 'dc=amqtt,dc=io', 'user_attribute': 'uid', 'bind_dn': 'cn=admin,dc=amqtt,dc=io', 'bind_password': 'adminpassword', }, } ) broker = Broker(config=cfg) await broker.start() await asyncio.sleep(0.1) client = MQTTClient(config=ClientConfig(auto_reconnect=False)) await client.connect('mqtt://jdoe:johndoepassword@127.0.0.1:1883') await asyncio.sleep(0.1) await client.publish('my/topic', b'my message') await asyncio.sleep(0.1) await client.disconnect() await broker.shutdown() @pytest.mark.asyncio async def test_ldap_user_invalid_creds(ldap_service): cfg = BrokerConfig( listeners={ 'default' : ListenerConfig() }, plugins={ 'amqtt.contrib.ldap.UserAuthLdapPlugin': { 'server': ldap_service, 'base_dn': 'dc=amqtt,dc=io', 'user_attribute': 'uid', 'bind_dn': 'cn=admin,dc=amqtt,dc=io', 'bind_password': 'adminpassword', }, } ) broker = Broker(config=cfg) await broker.start() await asyncio.sleep(0.1) client = MQTTClient(config=ClientConfig(auto_reconnect=False)) with pytest.raises(ConnectError): await client.connect('mqtt://jdoe:wrongpassword@127.0.0.1:1883') await broker.shutdown() @pytest.mark.asyncio async def test_ldap_topic_plugin(ldap_service): ctx = BrokerContext(Broker()) ctx.config = TopicAuthLdapPlugin.Config( server=ldap_service, base_dn="dc=amqtt,dc=io", user_attribute="uid", bind_dn="cn=admin,dc=amqtt,dc=io", bind_password="adminpassword", publish_attribute="publishACL", subscribe_attribute="subscribeACL", receive_attribute="receiveACL" ) ldap_plugin = TopicAuthLdapPlugin(context=ctx) s = Session() s.username = "jdoe" s.password = "wrongpassword" assert await ldap_plugin.topic_filtering(session=s, topic='my/topic/one', action=Action.PUBLISH), "access not granted" Yakifo-amqtt-2637127/tests/contrib/test_persistence.py000066400000000000000000000500141504664204300230200ustar00rootroot00000000000000import asyncio import logging from pathlib import Path import sqlite3 import pytest import aiosqlite from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy import select from amqtt.broker import Broker, BrokerContext, RetainedApplicationMessage from amqtt.client import MQTTClient from amqtt.mqtt.constants import QOS_1 from amqtt.contrib.persistence import SessionDBPlugin, Subscription, StoredSession, RetainedMessage from amqtt.session import Session formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) logger = logging.getLogger(__name__) @pytest.fixture async def db_file(): db_file = Path(__file__).parent / "amqtt.db" if db_file.exists(): raise NotImplementedError("existing db file found, should it be cleaned up?") yield db_file if db_file.exists(): db_file.unlink() @pytest.fixture async def broker_context(): cfg = { 'listeners': { 'default': {'type': 'tcp', 'bind': 'localhost:1883' }}, 'plugins': {} } context = BrokerContext(broker=Broker(config=cfg)) yield context @pytest.fixture async def db_session_factory(db_file): engine = create_async_engine(f"sqlite+aiosqlite:///{str(db_file)}") factory = async_sessionmaker(engine, expire_on_commit=False) yield factory @pytest.mark.asyncio async def test_initialize_tables(db_file, broker_context): broker_context.config = SessionDBPlugin.Config(file=db_file) session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() assert db_file.exists() conn = sqlite3.connect(str(db_file)) cursor = conn.cursor() table_name = 'stored_sessions' cursor.execute(f"PRAGMA table_info({table_name});") rows = cursor.fetchall() column_names = [row[1] for row in rows] assert len(column_names) > 1 @pytest.mark.asyncio async def test_create_stored_session(db_file, broker_context, db_session_factory): broker_context.config = SessionDBPlugin.Config(file=db_file) session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() async with db_session_factory() as db_session: async with db_session.begin(): stored_session = await session_db_plugin._get_or_create_session(db_session, 'test_client_1') assert stored_session.client_id == 'test_client_1' async with aiosqlite.connect(str(db_file)) as db: async with await db.execute("SELECT * FROM stored_sessions") as cursor: async for row in cursor: assert row[1] == 'test_client_1' @pytest.mark.asyncio async def test_get_stored_session(db_file, broker_context, db_session_factory): broker_context.config = SessionDBPlugin.Config(file=db_file) session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() async with aiosqlite.connect(str(db_file)) as db: sql = """INSERT INTO stored_sessions ( client_id, clean_session, will_flag, will_qos, keep_alive, retained, subscriptions ) VALUES ( 'test_client_1', 1, 0, 1, 60, '[]', '[{"topic":"sensors/#","qos":1}]' )""" await db.execute(sql) await db.commit() async with db_session_factory() as db_session: async with db_session.begin(): stored_session = await session_db_plugin._get_or_create_session(db_session, 'test_client_1') assert stored_session.subscriptions == [Subscription(topic='sensors/#', qos=1)] @pytest.mark.asyncio async def test_update_stored_session(db_file, broker_context, db_session_factory): broker_context.config = SessionDBPlugin.Config(file=db_file) # create session for client id (without subscription) await broker_context.add_subscription('test_client_1', None, None) session = broker_context.get_session('test_client_1') assert session is not None session.clean_session = False session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() # initialize with stored client session async with aiosqlite.connect(str(db_file)) as db: sql = """INSERT INTO stored_sessions ( client_id, clean_session, will_flag, will_qos, keep_alive, retained, subscriptions ) VALUES ( 'test_client_1', 1, 0, 1, 60, '[]', '[{"topic":"sensors/#","qos":1}]' )""" await db.execute(sql) await db.commit() await session_db_plugin.on_broker_client_subscribed(client_id='test_client_1', topic='my/topic', qos=2) # verify that the stored session has been updated with the new subscription has_stored_session = False async with aiosqlite.connect(str(db_file)) as db: async with await db.execute("SELECT * FROM stored_sessions") as cursor: async for row in cursor: assert row[1] == 'test_client_1' assert row[-1] == '[{"topic": "sensors/#", "qos": 1}, {"topic": "my/topic", "qos": 2}]' has_stored_session = True assert has_stored_session, "stored session wasn't updated" @pytest.mark.asyncio async def test_client_connected_with_clean_session(db_file, broker_context, db_session_factory) -> None: broker_context.config = SessionDBPlugin.Config(file=db_file) session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() session = Session() session.client_id = 'test_client_connected' session.is_anonymous = False session.clean_session = True await session_db_plugin.on_broker_client_connected(client_id='test_client_connected', client_session=session) async with aiosqlite.connect(str(db_file)) as db_conn: db_conn.row_factory = sqlite3.Row async with await db_conn.execute("SELECT * FROM stored_sessions") as cursor: assert len(await cursor.fetchall()) == 0 @pytest.mark.asyncio async def test_client_connected_anonymous_session(db_file, broker_context, db_session_factory) -> None: broker_context.config = SessionDBPlugin.Config(file=db_file) session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() session = Session() session.is_anonymous = True session.client_id = 'test_client_connected' await session_db_plugin.on_broker_client_connected(client_id='test_client_connected', client_session=session) async with aiosqlite.connect(str(db_file)) as db_conn: db_conn.row_factory = sqlite3.Row # Set the row_factory async with await db_conn.execute("SELECT * FROM stored_sessions") as cursor: assert len(await cursor.fetchall()) == 0 @pytest.mark.asyncio async def test_client_connected_and_stored_session(db_file, broker_context, db_session_factory) -> None: broker_context.config = SessionDBPlugin.Config(file=db_file) session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() session = Session() session.client_id = 'test_client_connected' session.is_anonymous = False session.clean_session = False session.will_flag = True session.will_qos = 1 session.will_topic = 'my/will/topic' session.will_retain = False session.will_message = b'test connected client has a last will (and testament) message' session.keep_alive = 42 await session_db_plugin.on_broker_client_connected(client_id='test_client_connected', client_session=session) has_stored_session = False async with aiosqlite.connect(str(db_file)) as db_conn: db_conn.row_factory = sqlite3.Row # Set the row_factory async with await db_conn.execute("SELECT * FROM stored_sessions") as cursor: for row in await cursor.fetchall(): assert row['client_id'] == 'test_client_connected' assert row['clean_session'] == False assert row['will_flag'] == True assert row['will_qos'] == 1 assert row['will_topic'] == 'my/will/topic' assert row['will_retain'] == False assert row['will_message'] == b'test connected client has a last will (and testament) message' assert row['keep_alive'] == 42 has_stored_session = True assert has_stored_session, "client session wasn't stored" @pytest.mark.asyncio async def test_repopulate_stored_sessions(db_file, broker_context, db_session_factory) -> None: broker_context.config = SessionDBPlugin.Config(file=db_file) session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() async with aiosqlite.connect(str(db_file)) as db: sql = """INSERT INTO stored_sessions ( client_id, clean_session, will_flag, will_qos, keep_alive, retained, subscriptions ) VALUES ( 'test_client_1', 1, 0, 1, 60, '[{"topic":"sensors/#","data":"this message is retained when client reconnects","qos":1}]', '[{"topic":"sensors/#","qos":1}]' )""" await db.execute(sql) await db.commit() await session_db_plugin.on_broker_post_start() session = broker_context.get_session('test_client_1') assert session is not None assert session.retained_messages.qsize() == 1 assert 'sensors/#' in broker_context._broker_instance._subscriptions # ugly: b/c _subscriptions is a list of dictionaries of tuples assert broker_context._broker_instance._subscriptions['sensors/#'][0][1] == 1 @pytest.mark.asyncio async def test_client_retained_message(db_file, broker_context, db_session_factory) -> None: broker_context.config = SessionDBPlugin.Config(file=db_file) session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() # add a session to the broker await broker_context.add_subscription('test_retained_client', None, None) # update the session so that it's retained session = broker_context.get_session('test_retained_client') assert session is not None session.is_anonymous = False session.clean_session = False session.transitions.disconnect() retained_message = RetainedApplicationMessage(source_session=session, topic='my/retained/topic', data=b'retain message for disconnected client', qos=2) await session_db_plugin.on_broker_retained_message(client_id='test_retained_client', retained_message=retained_message) async with db_session_factory() as db_session: async with db_session.begin(): stmt = select(StoredSession).filter(StoredSession.client_id == 'test_retained_client') stored_session = await db_session.scalar(stmt) assert stored_session is not None assert len(stored_session.retained) > 0 assert RetainedMessage(topic='my/retained/topic', data='retained message', qos=2) @pytest.mark.asyncio async def test_topic_retained_message(db_file, broker_context, db_session_factory) -> None: broker_context.config = SessionDBPlugin.Config(file=db_file, clear_on_shutdown=False) session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() # add a session to the broker await broker_context.add_subscription('test_retained_client', None, None) # update the session so that it's retained session = broker_context.get_session('test_retained_client') assert session is not None session.is_anonymous = False session.clean_session = False session.transitions.disconnect() retained_message = RetainedApplicationMessage(source_session=session, topic='my/retained/topic', data=b'retained message', qos=2) await session_db_plugin.on_broker_retained_message(client_id=None, retained_message=retained_message) has_stored_message = False async with aiosqlite.connect(str(db_file)) as db_conn: db_conn.row_factory = sqlite3.Row # Set the row_factory async with await db_conn.execute("SELECT * FROM stored_messages") as cursor: # assert(len(await cursor.fetchall()) > 0) for row in await cursor.fetchall(): assert row['topic'] == 'my/retained/topic' assert row['data'] == b'retained message' assert row['qos'] == 2 has_stored_message = True assert has_stored_message, "retained topic message wasn't stored" @pytest.mark.asyncio async def test_topic_clear_retained_message(db_file, broker_context, db_session_factory) -> None: broker_context.config = SessionDBPlugin.Config(file=db_file) session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() # add a session to the broker await broker_context.add_subscription('test_retained_client', None, None) # update the session so that it's retained session = broker_context.get_session('test_retained_client') assert session is not None session.is_anonymous = False session.clean_session = False session.transitions.disconnect() retained_message = RetainedApplicationMessage(source_session=session, topic='my/retained/topic', data=b'', qos=0) await session_db_plugin.on_broker_retained_message(client_id=None, retained_message=retained_message) async with aiosqlite.connect(str(db_file)) as db_conn: db_conn.row_factory = sqlite3.Row async with await db_conn.execute("SELECT * FROM stored_messages") as cursor: assert(len(await cursor.fetchall()) == 0) @pytest.mark.asyncio async def test_restoring_retained_message(db_file, broker_context, db_session_factory) -> None: broker_context.config = SessionDBPlugin.Config(file=db_file) session_db_plugin = SessionDBPlugin(broker_context) await session_db_plugin.on_broker_pre_start() stmts = ("INSERT INTO stored_messages VALUES(1,'my/retained/topic1',X'72657461696e6564206d657373616765',2)", "INSERT INTO stored_messages VALUES(2,'my/retained/topic2',X'72657461696e6564206d65737361676532',2)", "INSERT INTO stored_messages VALUES(3,'my/retained/topic3',X'72657461696e6564206d65737361676533',2)") async with aiosqlite.connect(str(db_file)) as db: for stmt in stmts: await db.execute(stmt) await db.commit() await session_db_plugin.on_broker_post_start() assert len(broker_context.retained_messages) == 3 assert 'my/retained/topic1' in broker_context.retained_messages assert 'my/retained/topic2' in broker_context.retained_messages assert 'my/retained/topic3' in broker_context.retained_messages @pytest.fixture def db_config(db_file): return { 'listeners': { 'default': { 'type': 'tcp', 'bind': '127.0.0.1:1883' } }, 'plugins': { 'amqtt.plugins.authentication.AnonymousAuthPlugin': {'allow_anonymous': False}, 'amqtt.contrib.persistence.SessionDBPlugin': { 'file': db_file, 'clear_on_shutdown': False } } } @pytest.mark.asyncio async def test_broker_client_no_cleanup(db_file, db_config) -> None: b1 = Broker(config=db_config) await b1.start() await asyncio.sleep(0.1) c1 = MQTTClient(client_id='test_client1', config={'auto_reconnect':False}) await c1.connect("mqtt://myUsername@127.0.0.1:1883", cleansession=False) # test that this message is retained for the topic upon restore await c1.publish("my/retained/topic", b'retained message for topic my/retained/topic', retain=True) await asyncio.sleep(0.2) await c1.disconnect() await b1.shutdown() # new broker should load the previous broker's db file since clean_on_shutdown is false in config b2 = Broker(config=db_config) await b2.start() await asyncio.sleep(0.1) # upon subscribing to topic with retained message, it should be received c2 = MQTTClient(client_id='test_client2', config={'auto_reconnect':False}) await c2.connect("mqtt://myOtherUsername@localhost:1883", cleansession=False) await c2.subscribe([ ('my/retained/topic', QOS_1) ]) msg = await c2.deliver_message(timeout_duration=1) assert msg is not None assert msg.topic == "my/retained/topic" assert msg.data == b'retained message for topic my/retained/topic' await c2.disconnect() await b2.shutdown() @pytest.mark.asyncio async def test_broker_client_retain_subscription(db_file, db_config) -> None: b1 = Broker(config=db_config) await b1.start() await asyncio.sleep(0.1) c1 = MQTTClient(client_id='test_client1', config={'auto_reconnect':False}) await c1.connect("mqtt://myUsername@127.0.0.1:1883", cleansession=False) # test to make sure the subscription is re-established upon reconnection after broker restart (clear_on_shutdown = False) ret = await c1.subscribe([ ('my/offline/topic', QOS_1) ]) assert ret == [QOS_1,] await asyncio.sleep(0.2) await c1.disconnect() await asyncio.sleep(0.1) await b1.shutdown() # new broker should load the previous broker's db file b2 = Broker(config=db_config) await b2.start() await asyncio.sleep(0.1) # client1's subscription should have been restored, so when it connects, it should receive this message c2 = MQTTClient(client_id='test_client2', config={'auto_reconnect':False}) await c2.connect("mqtt://myOtherUsername@localhost:1883", cleansession=False) await c2.publish('my/offline/topic', b'standard message to be retained for offline clients') await asyncio.sleep(0.1) await c2.disconnect() await c1.reconnect(cleansession=False) await asyncio.sleep(0.1) msg = await c1.deliver_message(timeout_duration=2) assert msg is not None assert msg.topic == "my/offline/topic" assert msg.data == b'standard message to be retained for offline clients' await c1.disconnect() await b2.shutdown() @pytest.mark.asyncio async def test_broker_client_retain_message(db_file, db_config) -> None: """test to make sure that the retained message because client1 is offline, gets sent when back online after broker restart.""" b1 = Broker(config=db_config) await b1.start() await asyncio.sleep(0.1) c1 = MQTTClient(client_id='test_client1', config={'auto_reconnect':False}) await c1.connect("mqtt://myUsername@127.0.0.1:1883", cleansession=False) # subscribe to a topic with QOS_1 so that we receive messages, even if we're disconnected when sent ret = await c1.subscribe([ ('my/offline/topic', QOS_1) ]) assert ret == [QOS_1,] await asyncio.sleep(0.2) # go offline await c1.disconnect() await asyncio.sleep(0.1) # another client sends a message to previously subscribed to topic c2 = MQTTClient(client_id='test_client2', config={'auto_reconnect':False}) await c2.connect("mqtt://myOtherUsername@localhost:1883", cleansession=False) # this message should be delivered after broker stops and restarts (and client connects) await c2.publish('my/offline/topic', b'standard message to be retained for offline clients') await asyncio.sleep(0.1) await c2.disconnect() await asyncio.sleep(0.1) await b1.shutdown() # new broker should load the previous broker's db file since we declared clear_on_shutdown = False in config b2 = Broker(config=db_config) await b2.start() await asyncio.sleep(0.1) # when first client reconnects, it should receive the message that had been previously retained for it await c1.reconnect(cleansession=False) await asyncio.sleep(0.1) msg = await c1.deliver_message(timeout_duration=2) assert msg is not None assert msg.topic == "my/offline/topic" assert msg.data == b'standard message to be retained for offline clients' # client should also receive a message if send on this topic await c1.publish("my/offline/topic", b"online message should also be received") await asyncio.sleep(0.1) msg = await c1.deliver_message(timeout_duration=2) assert msg is not None assert msg.topic == "my/offline/topic" assert msg.data == b'online message should also be received' await c1.disconnect() await b2.shutdown() Yakifo-amqtt-2637127/tests/contrib/test_shadows.py000066400000000000000000000267441504664204300221610ustar00rootroot00000000000000import json import sqlite3 import tempfile from pathlib import Path from unittest.mock import patch, call, ANY import aiosqlite import pytest from jsonschema import validate from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from amqtt.broker import BrokerContext, Broker from amqtt.contrib.shadows import ShadowPlugin from amqtt.contrib.shadows.models import Shadow, ShadowUpdateError from amqtt.contrib.shadows.states import StateDocument, State, MetaTimestamp from amqtt.mqtt.constants import QOS_0 from amqtt.session import IncomingApplicationMessage from tests.contrib.test_shadows_schema import * @pytest.fixture def db_file(): with tempfile.TemporaryDirectory() as temp_dir: with tempfile.NamedTemporaryFile(mode='wb', delete=True) as tmp: yield Path(temp_dir) / f"{tmp.name}.db" @pytest.fixture def db_connection(db_file): test_db_connect = f"sqlite+aiosqlite:///{db_file}" yield test_db_connect @pytest.fixture @pytest.mark.asyncio async def db_session_maker(db_connection): engine = create_async_engine(f"{db_connection}") db_session_maker = async_sessionmaker(engine, expire_on_commit=False) yield db_session_maker @pytest.fixture @pytest.mark.asyncio async def shadow_plugin(db_connection): cfg = ShadowPlugin.Config(connection=db_connection) ctx = BrokerContext(broker=Broker()) ctx.config = cfg shadow_plugin = ShadowPlugin(ctx) await shadow_plugin.on_broker_pre_start() yield shadow_plugin @pytest.mark.asyncio async def test_shadow_find_latest_empty(db_session_maker, shadow_plugin): async with db_session_maker() as db_session, db_session.begin(): shadow = await Shadow.latest_version(session=db_session, device_id='device123', name="myShadowName") assert shadow is None @pytest.mark.asyncio async def test_shadow_create_new(db_file, db_connection, db_session_maker, shadow_plugin): async with db_session_maker() as db_session, db_session.begin(): shadow = Shadow(device_id='device123', name="myShadowName") db_session.add(shadow) await db_session.commit() async with aiosqlite.connect(db_file) as db_conn: db_conn.row_factory = sqlite3.Row # Set the row_factory has_shadow = False async with await db_conn.execute("SELECT * FROM shadows_shadow") as cursor: for row in await cursor.fetchall(): assert row['name'] == 'myShadowName' assert row['device_id'] == 'device123' assert row['state'] == '{}' has_shadow = True assert has_shadow, "Shadow was not created." @pytest.mark.asyncio async def test_shadow_create_find_empty_state(db_connection, db_session_maker, shadow_plugin): async with db_session_maker() as db_session, db_session.begin(): shadow = Shadow(device_id='device123', name="myShadowName") db_session.add(shadow) await db_session.commit() await db_session.flush() async with db_session_maker() as db_session, db_session.begin(): shadow = await Shadow.latest_version(session=db_session, device_id='device123', name="myShadowName") assert shadow is not None assert shadow.version == 1 assert shadow.state == StateDocument() @pytest.mark.asyncio async def test_shadow_create_find_state_doc(db_connection, db_session_maker, shadow_plugin): state_doc = StateDocument( state=State( desired={'item1': 'value1', 'item2': 'value2'}, reported={'item3': 'value3', 'item4': 'value4'}, ) ) async with db_session_maker() as db_session, db_session.begin(): shadow = Shadow(device_id='device123', name="myShadowName") shadow.state = state_doc db_session.add(shadow) await db_session.commit() await db_session.flush() def new_equal(a, b): diff = abs(a.timestamp - b.timestamp) return diff <= 2 async with db_session_maker() as db_session, db_session.begin(): shadow = await Shadow.latest_version(session=db_session, device_id='device123', name="myShadowName") assert shadow is not None assert shadow.version == 1 with patch.object(MetaTimestamp, "__eq__", new=new_equal) as mocked_mqtt_publish: assert shadow.state == state_doc @pytest.mark.asyncio async def test_shadow_update_state(db_connection, db_session_maker, shadow_plugin): state_doc = StateDocument( state=State( desired={'item1': 'value1', 'item2': 'value2'}, reported={'item3': 'value3', 'item4': 'value4'}, ) ) async with db_session_maker() as db_session, db_session.begin(): shadow = Shadow(device_id='device123', name="myShadowName") shadow.state = state_doc db_session.add(shadow) await db_session.commit() await db_session.flush() async with db_session_maker() as db_session, db_session.begin(): shadow = await Shadow.latest_version(session=db_session, device_id='device123', name="myShadowName") assert shadow is not None shadow.state = StateDocument( state=State( desired={'item5': 'value5', 'item6': 'value6'}, reported={'item7': 'value7', 'item8': 'value8'}, ) ) with pytest.raises(ShadowUpdateError): await db_session.commit() @pytest.mark.asyncio async def test_shadow_update_state(db_connection, db_session_maker, shadow_plugin): state_doc = StateDocument( state=State( desired={'item1': 'value1', 'item2': 'value2'}, reported={'item3': 'value3', 'item4': 'value4'}, ) ) async with db_session_maker() as db_session, db_session.begin(): shadow = Shadow(device_id='device123', name="myShadowName") shadow.state = state_doc db_session.add(shadow) await db_session.commit() async with db_session_maker() as db_session, db_session.begin(): shadow = await Shadow.latest_version(session=db_session, device_id='device123', name="myShadowName") assert shadow is not None shadow.state += StateDocument( state=State( desired={'item1': 'value1a', 'item6': 'value6'} ) ) await db_session.commit() async with db_session_maker() as db_session, db_session.begin(): shadow_list = await Shadow.all(db_session, "device123", "myShadowName") assert len(shadow_list) == 2 async with db_session_maker() as db_session, db_session.begin(): shadow = await Shadow.latest_version(session=db_session, device_id='device123', name="myShadowName") assert shadow is not None assert shadow.version == 2 assert shadow.state.state.desired == {'item1': 'value1a', 'item2': 'value2', 'item6': 'value6'} assert shadow.state.state.reported == {'item3': 'value3', 'item4': 'value4'} @pytest.mark.asyncio async def test_shadow_plugin_get_rejected(shadow_plugin): """test """ with patch.object(BrokerContext, 'broadcast_message', return_value=None) as mock_method: msg = IncomingApplicationMessage(packet_id=1, topic='$shadow/myClient123/myShadow/get', qos=QOS_0, data=json.dumps({}).encode('utf-8'), retain=False) await shadow_plugin.on_broker_message_received(client_id="myClient123", message=msg) mock_method.assert_called() topic, message = mock_method.call_args[0] assert topic == '$shadow/myClient123/myShadow/get/rejected' validate(instance=json.loads(message.decode('utf-8')), schema=get_rejected_schema) @pytest.mark.asyncio async def test_shadow_plugin_update_accepted(shadow_plugin): with patch.object(BrokerContext, 'broadcast_message', return_value=None) as mock_method: update_msg = { 'state': { 'desired': { 'item1': 'value1', 'item2': 'value2' } } } validate(instance=update_msg, schema=update_schema) msg = IncomingApplicationMessage(packet_id=1, topic='$shadow/myClient123/myShadow/update', qos=QOS_0, data=json.dumps(update_msg).encode('utf-8'), retain=False) await shadow_plugin.on_broker_message_received(client_id="myClient123", message=msg) accepted_call = call('$shadow/myClient123/myShadow/update/accepted', ANY) document_call = call('$shadow/myClient123/myShadow/update/documents', ANY) delta_call = call('$shadow/myClient123/myShadow/update/delta', ANY) iota_call = call('$shadow/myClient123/myShadow/update/iota', ANY) mock_method.assert_has_calls( [ accepted_call, document_call, delta_call, iota_call, ], any_order=True, ) for actual in mock_method.call_args_list: if actual == accepted_call: validate(instance=json.loads(actual.args[1].decode('utf-8')), schema=update_accepted_schema) elif actual == document_call: validate(instance=json.loads(actual.args[1].decode('utf-8')), schema=update_documents_schema) elif actual == delta_call: validate(instance=json.loads(actual.args[1].decode('utf-8')), schema=delta_schema) elif actual == iota_call: validate(instance=json.loads(actual.args[1].decode('utf-8')), schema=delta_schema) else: assert False, "unknown call made to broadcast" @pytest.mark.asyncio async def test_shadow_plugin_get_accepted(shadow_plugin): with patch.object(BrokerContext, 'broadcast_message', return_value=None) as mock_method: update_msg = { 'state': { 'desired': { 'item1': 'value1', 'item2': 'value2' } } } update_msg = IncomingApplicationMessage(packet_id=1, topic='$shadow/myClient123/myShadow/update', qos=QOS_0, data=json.dumps(update_msg).encode('utf-8'), retain=False) await shadow_plugin.on_broker_message_received(client_id="myClient123", message=update_msg) mock_method.reset_mock() get_msg = IncomingApplicationMessage(packet_id=1, topic='$shadow/myClient123/myShadow/get', qos=QOS_0, data=json.dumps({}).encode('utf-8'), retain=False) await shadow_plugin.on_broker_message_received(client_id="myClient123", message=get_msg) get_accepted = call('$shadow/myClient123/myShadow/get/accepted', ANY) mock_method.assert_has_calls( [get_accepted] ) has_msg = False for actual in mock_method.call_args_list: if actual == get_accepted: validate(instance=json.loads(actual.args[1].decode('utf-8')), schema=get_accepted_schema) has_msg = True assert has_msg, "could not find the broadcast call for get accepted" Yakifo-amqtt-2637127/tests/contrib/test_shadows_schema.py000066400000000000000000000112431504664204300234650ustar00rootroot00000000000000get_schema = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "AWS IoT Shadow Get Request", "type": "object", "properties": { "clientToken": { "type": "string" } }, "additionalProperties": False } get_accepted_schema = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "AWS IoT Shadow Get Accepted", "type": "object", "properties": { "state": { "type": "object", "properties": { "desired": { "type": "object" }, "reported": { "type": "object" } } }, "metadata": { "type": "object" }, "version": { "type": "integer" }, "timestamp": { "type": "integer" }, "clientToken": { "type": "string" } }, "required": ["state", "version", "timestamp"], "additionalProperties": False } get_rejected_schema = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "AWS IoT Shadow Get Rejected", "type": "object", "properties": { "code": { "type": "integer" }, "message": { "type": "string" }, "clientToken": { "type": "string" } }, "required": ["code", "message"], "additionalProperties": False } update_schema = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "AWS IoT Shadow Update", "type": "object", "properties": { "state": { "type": "object", "properties": { "desired": { "type": "object" }, "reported": { "type": "object" } }, "additionalProperties": False }, "clientToken": { "type": "string" }, "version": { "type": "integer" } }, "additionalProperties": False } update_accepted_schema = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "AWS IoT Shadow Update Accepted", "type": "object", "properties": { "state": { "type": "object", "properties": { "desired": { "type": "object" }, "reported": { "type": "object" } } }, "metadata": { "type": "object", "properties": { "desired": { "type": "object" }, "reported": { "type": "object" } } }, "version": { "type": "integer" }, "timestamp": { "type": "integer" }, "clientToken": { "type": "string" } }, "required": ["version", "timestamp"], "additionalProperties": False } update_rejected_schema = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "AWS IoT Shadow Update Rejected", "type": "object", "properties": { "code": { "type": "integer" }, "message": { "type": "string" }, "clientToken": { "type": "string" } }, "required": ["code", "message"], "additionalProperties": False } delta_schema = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "AWS IoT Shadow Delta", "type": "object", "properties": { "state": { "type": "object" }, "metadata": { "type": "object" }, "version": { "type": "integer" }, "timestamp": { "type": "integer" } }, "required": ["state", "version", "timestamp"], "additionalProperties": False } update_documents_schema = { "$schema": "http://json-schema.org/draft-07/schema#", "title": "AWS IoT Shadow Update Documents", "type": "object", "properties": { "previous": { "type": "object", "properties": { "state": { "type": "object", "properties": { "desired": { "type": "object" }, "reported": { "type": "object" } }, "additionalProperties": True }, "metadata": { "type": "object", "properties": { "desired": { "type": "object" }, "reported": { "type": "object" } }, "additionalProperties": True }, "version": { "type": "integer" }, "timestamp": { "type": "integer" } }, "required": ["state", "metadata", "version", "timestamp"], "additionalProperties": False }, "current": { "type": "object", "properties": { "state": { "type": "object", "properties": { "desired": { "type": "object" }, "reported": { "type": "object" } }, "additionalProperties": True }, "metadata": { "type": "object", "properties": { "desired": { "type": "object" }, "reported": { "type": "object" } }, "additionalProperties": True }, "version": { "type": "integer" }, "timestamp": { "type": "integer" } }, "required": ["state", "metadata", "version", "timestamp"], "additionalProperties": False }, "timestamp": { "type": "integer" } }, "required": ["previous", "current", "timestamp"], "additionalProperties": False } Yakifo-amqtt-2637127/tests/contrib/test_state.py000066400000000000000000000246301504664204300216210ustar00rootroot00000000000000import asyncio import math import pytest import time from hamcrest import equal_to, assert_that, close_to, has_key, instance_of, has_entry from amqtt.contrib.shadows import ShadowPlugin, ShadowOperation from amqtt.contrib.shadows.states import State, StateDocument, calculate_delta_update, calculate_iota_update, \ MetaTimestamp @pytest.mark.parametrize("topic,client_id,shadow_name,message_type,is_match", [ ('$shadow/myclientid/myshadow/get', 'myclientid', 'myshadow', ShadowOperation.GET, True), ('$shadow/myshadow/get', '', '', '', False) ]) def test_shadow_topic_match(topic, client_id, shadow_name, message_type, is_match): # broker_context = BrokerContext(broker=Broker()) # shadow_plugin = ShadowPlugin(context=broker_context) shadow_topic = ShadowPlugin.shadow_topic_match(topic) if is_match: assert shadow_topic.device_id == client_id assert shadow_topic.name == shadow_name assert shadow_topic.message_op in ShadowOperation assert shadow_topic.message_op == message_type else: assert shadow_topic is None @pytest.mark.asyncio async def test_state_add(): cur_time = math.floor(time.time()) data = { 'state':{ 'desired': { 'item1': 'value1a', 'item2': 'value2a' }, 'reported': { 'item1': 'value1a', 'item2': 'value2b' } } } meta = { 'metadata': { 'desired': { 'item1': 10, 'item2': 20 }, 'reported': { 'item1': 11, 'item2': 21 } } } data_state = State.from_dict(data['state']) meta_state = State.from_dict(meta['metadata']) state_document_one = StateDocument(state=data_state, metadata=meta_state) await asyncio.sleep(2) data_update = { 'state':{ 'desired': { 'item2': 'value2a' }, 'reported': { 'item1': 'value1c', 'item2': 'value2c' } } } state_document_two = StateDocument.from_dict(data_update) final_doc = state_document_one + state_document_two assert final_doc.state.desired['item1'] == 'value1a' assert final_doc.metadata.desired['item1'] == 10 assert final_doc.state.desired['item2'] == 'value2a' assert final_doc.metadata.desired['item2'] > cur_time assert final_doc.state.reported['item1'] == 'value1c' assert final_doc.metadata.reported['item1'] > cur_time assert final_doc.state.reported['item1'] == 'value1c' assert final_doc.metadata.reported['item1'] > cur_time def test_state_from_dict() -> None: state_dict = { 'desired': {'keyA': 'valueA', 'keyB': 'valueB'}, 'reported': {'keyC': 'valueC', 'keyD': 'valueD'} } state = State.from_dict(state_dict) assert_that(state.desired['keyA'], equal_to('valueA')) assert_that(state.desired['keyB'], equal_to('valueB')) assert_that(state.reported['keyC'], equal_to('valueC')) assert_that(state.reported['keyD'], equal_to('valueD')) def test_state_doc_from_dict() -> None: now = int(time.time()) state_dict = { 'state': { 'desired': {'keyA': 'valueA', 'keyB': 'valueB'}, 'reported': {'keyC': 'valueC', 'keyD': 'valueD'} } } state_doc = StateDocument.from_dict(state_dict) assert_that(state_doc.state.desired['keyA'], equal_to('valueA')) assert_that(state_doc.state.desired['keyB'], equal_to('valueB')) assert_that(state_doc.metadata.desired, has_key('keyA')) # noqa assert_that(state_doc.metadata.desired, has_key('keyB')) # noqa assert_that(state_doc.metadata.desired['keyA'], instance_of(MetaTimestamp)) # noqa assert_that(state_doc.metadata.desired['keyB'], instance_of(MetaTimestamp)) # noqa assert_that(state_doc.metadata.desired['keyA'], close_to(now, 0)) # noqa assert_that(state_doc.metadata.desired['keyA'], equal_to(state_doc.metadata.desired['keyB'])) def test_state_doc_including_meta() -> None: now = int(time.time()) state1 = State( desired={'keyA': 'valueA', 'keyB': 'valueB'}, reported={'keyC': 'valueC', 'keyD': 'valueD'} ) meta1 = State( desired={'keyA': now - 100, 'keyB': now - 110}, reported={'keyC': now - 90, 'keyD': now - 120} ) state_doc1 = StateDocument( state=state1, metadata=meta1 ) state2 = State( desired={'keyA': 'valueA', 'keyB': 'valueB'}, reported={'keyC': 'valueC', 'keyD': 'valueD'} ) meta2 = State( desired={'keyA': now - 5, 'keyB': now - 5}, reported={'keyC': now - 5, 'keyD': now - 5} ) state_doc2 = StateDocument( state=state2, metadata=meta2 ) new_doc = state_doc1 + state_doc2 assert_that(new_doc.metadata.desired['keyA'], equal_to(now - 5)) assert_that(new_doc.metadata.reported['keyC'], equal_to(now - 5)) def test_state_doc_plus_new_key_update() -> None: now = int(time.time()) state = State( desired={'keyA': 'valueA', 'keyB': 'valueB'}, reported={'keyC': 'valueC', 'keyD': 'valueD'} ) meta = State( desired={'keyA': now - 100, 'keyB': now - 110}, reported={'keyC': now - 90, 'keyD': now - 120} ) state_doc = StateDocument( state=state, metadata=meta ) update_dict = {'state': {'reported': {'keyE': 'valueE', 'keyF': 'valueF'}}} update = StateDocument.from_dict(update_dict) next_doc = state_doc + update assert_that(next_doc.state.reported, has_key('keyC')) assert_that(next_doc.metadata.reported['keyC'], equal_to(now - 90)) assert_that(next_doc.metadata.reported['keyD'], equal_to(now - 120)) assert_that(next_doc.state.reported, has_key('keyE')) assert_that(next_doc.state.reported, has_key('keyF')) assert_that(next_doc.metadata.reported['keyE'], close_to(now, 1)) assert_that(next_doc.metadata.reported['keyF'], close_to(now, 1)) def test_state_with_updated_keys() -> None: now = int(time.time()) state = State( desired={'keyA': 'valueA', 'keyB': 'valueB'}, reported={'keyC': 'valueC', 'keyD': 'valueD'} ) meta = State( desired={'keyA': now - 100, 'keyB': now - 110}, reported={'keyC': now - 90, 'keyD': now - 120} ) state_doc = StateDocument( state=state, metadata=meta ) update_dict = {'state': {'reported': {'keyD': 'valueD'}}} update = StateDocument.from_dict(update_dict) next_doc = state_doc + update assert_that(next_doc.state.reported, has_key('keyC')) assert_that(next_doc.state.reported, has_key('keyD')) assert_that(next_doc.metadata.reported['keyC'], equal_to(now - 90)) assert_that(next_doc.metadata.reported['keyD'], close_to(now, 1)) def test_update_with_empty_initial_state() -> None: now = int(time.time()) prev_doc = StateDocument.from_dict({}) state = State( desired={'keyA': 'valueA', 'keyB': 'valueB'}, reported={'keyC': 'valueC', 'keyD': 'valueD'} ) state_doc = StateDocument(state=state) new_doc = prev_doc + state_doc assert_that(state_doc.state.desired, equal_to(new_doc.state.desired)) assert_that(state_doc.state.reported, equal_to(new_doc.state.reported)) assert_that(state_doc.metadata.reported, has_key('keyC')) assert_that(state_doc.metadata.reported['keyC'], close_to(now, 1)) def test_update_with_clearing_key() -> None: state = State( desired={'keyA': 'valueA', 'keyB': 'valueB'}, reported={'keyC': 'valueC', 'keyD': 'valueD'} ) state_doc = StateDocument(state=state) update_doc = StateDocument.from_dict({'state': {'reported': {'keyC': None}}}) new_doc = state_doc + update_doc assert_that(new_doc.state.reported, has_entry(equal_to('keyC'), equal_to(None))) def test_empty_desired_state() -> None: state_doc = StateDocument.from_dict({ 'state': { 'reported': { 'items': ['value1', 'value2', 'value3'] } } }) diff = calculate_delta_update(state_doc.state.desired, state_doc.state.reported) assert diff == {} def test_empty_reported_state() -> None: state_doc = StateDocument.from_dict({ 'state': { 'desired': { 'items': ['value1', 'value2', 'value3'] } } }) diff = calculate_delta_update(state_doc.state.desired, state_doc.state.reported) assert diff == {'items': ['value1', 'value2', 'value3']} def test_matching_desired_reported_state() -> None: state_doc = StateDocument.from_dict({ 'state': { 'desired': { 'items': ['value1', 'value2', 'value3'] }, 'reported': { 'items': ['value1', 'value2', 'value3'] } } }) diff = calculate_delta_update(state_doc.state.desired, state_doc.state.reported) assert diff == {} def test_out_of_order_list() -> None: state_doc = StateDocument.from_dict({ 'state': { 'desired': { 'items': ['value1', 'value2', 'value3'] }, 'reported': { 'items': ['value2', 'value1', 'value3'] } } }) diff = calculate_delta_update(state_doc.state.desired, state_doc.state.reported) assert diff == { 'items': ['value1', 'value2', 'value3'] } def test_states_with_connector_transaction() -> None: state_doc = StateDocument.from_dict({ 'state': { 'desired': {}, 'reported': {"transaction": False, "transactionId": "5678", "tag": "ghijk"} } }) diff = calculate_iota_update(state_doc.state.desired, state_doc.state.reported) assert diff == {"transaction": None, "transactionId": None, "tag": None} def test_extra_reported_into_desired_with_overlap() -> None: state_doc = StateDocument.from_dict({ 'state': { 'desired': {"connectors": [1, 2, 3]}, 'reported': {"status": None, "heartbeat": "2025-02-07T04:16:51.431Z", "connectors": [1, 2, 3], "module_version": "2.0.1", "restartTime": "2025-02-07T03:16:51.431Z"} }}) diff = calculate_iota_update(state_doc.state.desired, state_doc.state.reported) assert diff == {"status": None, "heartbeat": None, "module_version": None, "restartTime": None} Yakifo-amqtt-2637127/tests/fixtures/000077500000000000000000000000001504664204300172745ustar00rootroot00000000000000Yakifo-amqtt-2637127/tests/fixtures/ldap/000077500000000000000000000000001504664204300202145ustar00rootroot00000000000000Yakifo-amqtt-2637127/tests/fixtures/ldap/customuser.schema000066400000000000000000000012161504664204300236070ustar00rootroot00000000000000attributetype ( 1.3.6.1.4.1.4203.666.1.1 NAME 'publishACL' DESC 'topics for publishing' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15) attributetype ( 1.3.6.1.4.1.4203.666.1.2 NAME 'subscribeACL' DESC 'topics for subscribing' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15) attributetype ( 1.3.6.1.4.1.4203.666.1.3 NAME 'receiveACL' DESC 'topics for receiving' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15) objectclass ( 1.3.6.1.4.1.4203.666.2.1 NAME 'customuserinfo' DESC 'User info with custom attributes' SUP inetOrgPerson STRUCTURAL MUST ( cn $ sn ) MAY ( publishACL $ subscribeACL $ receiveACL ) ) Yakifo-amqtt-2637127/tests/fixtures/ldap/customusers.ldif000066400000000000000000000007161504664204300234540ustar00rootroot00000000000000dn: ou=users,dc=amqtt,dc=io objectClass: organizationalUnit ou: users description: Organizational Unit for storing user entries dn: uid=jdoe,ou=users,dc=amqtt,dc=io objectClass: inetOrgPerson objectClass: customuserinfo cn: John Doe sn: Doe uid: jdoe mail: jdoe@amqtt.io # `slappasswd -s johndoepassword` userPassword: {SSHA}ANVSnjfMu85vXHNS5XW7i4EHGJ8VjMtu publishACL: my/topic/# publishACL: other/+/topic subscribeACL: my/topic/two receiveACL: my/topic/three Yakifo-amqtt-2637127/tests/fixtures/ldap/docker-compose.yml000066400000000000000000000011411504664204300236460ustar00rootroot00000000000000version: '3' services: openldap: image: osixia/openldap:latest container_name: openldap command: [--copy-service] restart: always volumes: - ./customuser.schema:/container/service/slapd/assets/config/bootstrap/schema/customuser.schema - ./customusers.ldif:/container/service/slapd/assets/config/bootstrap/ldif/50-customusers.ldif - ldap-data:/var/lib/ldap - ldap-config:/etc/ldap/slapd.d ports: - "1389:389" - "1636:636" environment: - LDAP_ADMIN_PASSWORD=adminpassword - LDAP_DOMAIN=amqtt.io volumes: ldap-data: ldap-config: Yakifo-amqtt-2637127/tests/mqtt/000077500000000000000000000000001504664204300164105ustar00rootroot00000000000000Yakifo-amqtt-2637127/tests/mqtt/protocol/000077500000000000000000000000001504664204300202515ustar00rootroot00000000000000Yakifo-amqtt-2637127/tests/mqtt/protocol/test_handler.py000066400000000000000000000542571504664204300233140ustar00rootroot00000000000000import asyncio import logging import secrets from typing import Any import unittest from amqtt.adapters import StreamReaderAdapter, StreamWriterAdapter from amqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 from amqtt.mqtt.protocol.handler import ProtocolHandler from amqtt.mqtt.puback import PubackPacket from amqtt.mqtt.pubcomp import PubcompPacket from amqtt.mqtt.publish import PublishPacket from amqtt.mqtt.pubrec import PubrecPacket from amqtt.mqtt.pubrel import PubrelPacket from amqtt.plugins.manager import PluginManager from amqtt.session import IncomingApplicationMessage, OutgoingApplicationMessage, Session formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) log = logging.getLogger(__name__) def rand_packet_id(): return secrets.randbelow(65536) def adapt(reader, writer): return StreamReaderAdapter(reader), StreamWriterAdapter(writer) class ProtocolHandlerTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.plugin_manager = PluginManager("amqtt.test.plugins", context=None) def tearDown(self): self.loop.close() def test_init_handler(self): async def test_coro() -> None: try: Session() handler = ProtocolHandler(self.plugin_manager) assert handler.session is None assert handler._loop is self.loop self.check_empty_waiters(handler) future.set_result(True) except Exception as ae: future.set_exception(ae) future: asyncio.Future[Any] = asyncio.Future() self.loop.run_until_complete(test_coro()) exception = future.exception() if exception: raise exception def test_start_stop(self): async def server_mock(reader, writer) -> None: writer.close() # python 3.12 requires an explicit close await writer.wait_closed() async def test_coro() -> None: try: s = Session() reader, writer = await asyncio.open_connection("127.0.0.1", 8888) reader_adapted, writer_adapted = adapt(reader, writer) handler = ProtocolHandler(self.plugin_manager) handler.attach(s, reader_adapted, writer_adapted) await self.start_handler(handler, s) await self.stop_handler(handler, s) future.set_result(True) except Exception as ae: future.set_exception(ae) future: asyncio.Future[Any] = asyncio.Future() coro = asyncio.start_server(server_mock, "127.0.0.1", 8888) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) exception = future.exception() if exception: raise exception def test_publish_qos0(self): async def server_mock(reader, writer) -> None: try: packet = await PublishPacket.from_stream(reader) assert packet.variable_header.topic_name == "/topic" assert packet.qos == QOS_0 assert packet.packet_id is None writer.close() # python 3.12 requires an explicit close await writer.wait_closed() except Exception as ae: future.set_exception(ae) async def test_coro() -> None: try: s = Session() reader, writer = await asyncio.open_connection("127.0.0.1", 8888) reader_adapted, writer_adapted = adapt(reader, writer) handler = ProtocolHandler(self.plugin_manager) handler.attach(s, reader_adapted, writer_adapted) await self.start_handler(handler, s) message = await handler.mqtt_publish( "/topic", b"test_data", QOS_0, False, ) assert isinstance(message, OutgoingApplicationMessage) assert message.publish_packet is not None assert message.puback_packet is None assert message.pubrec_packet is None assert message.pubrel_packet is None assert message.pubcomp_packet is None await self.stop_handler(handler, s) future.set_result(True) except Exception as ae: future.set_exception(ae) future: asyncio.Future[Any] = asyncio.Future() coro = asyncio.start_server(server_mock, "127.0.0.1", 8888) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) exception = future.exception() if exception: raise exception def test_publish_qos1(self): self.handler: ProtocolHandler | None = None async def server_mock(reader, writer) -> None: packet = await PublishPacket.from_stream(reader) try: assert packet.variable_header.topic_name == "/topic" assert packet.qos == QOS_1 assert packet.packet_id is not None assert packet.packet_id in self.session.inflight_out assert self.handler is not None assert packet.packet_id in self.handler._puback_waiters puback = PubackPacket.build(packet.packet_id) await puback.to_stream(writer) writer.close() # python 3.12 requires an explicit close await writer.wait_closed() except Exception as ae: future.set_exception(ae) async def test_coro() -> None: try: reader, writer = await asyncio.open_connection("127.0.0.1", 8888) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager) self.handler.attach(self.session, reader_adapted, writer_adapted) await self.start_handler(self.handler, self.session) message = await self.handler.mqtt_publish( "/topic", b"test_data", QOS_1, False, ) assert isinstance(message, OutgoingApplicationMessage) assert message.publish_packet is not None assert message.puback_packet is not None assert message.pubrec_packet is None assert message.pubrel_packet is None assert message.pubcomp_packet is None await self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future: asyncio.Future[Any] = asyncio.Future() coro = asyncio.start_server(server_mock, "127.0.0.1", 8888) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) exception = future.exception() if exception: raise exception def test_publish_qos2(self): async def server_mock(reader, writer) -> None: try: packet = await PublishPacket.from_stream(reader) assert packet.topic_name == "/topic" assert packet.qos == QOS_2 assert packet.packet_id is not None assert packet.packet_id in self.session.inflight_out assert self.handler is not None assert packet.packet_id in self.handler._pubrec_waiters pubrec = PubrecPacket.build(packet.packet_id) await pubrec.to_stream(writer) await PubrelPacket.from_stream(reader) assert packet.packet_id in self.handler._pubcomp_waiters pubcomp = PubcompPacket.build(packet.packet_id) await pubcomp.to_stream(writer) writer.close() # python 3.12 requires an explicit close await writer.wait_closed() except Exception as ae: future.set_exception(ae) async def test_coro() -> None: try: reader, writer = await asyncio.open_connection("127.0.0.1", 8888) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager) self.handler.attach(self.session, reader_adapted, writer_adapted) await self.start_handler(self.handler, self.session) message = await self.handler.mqtt_publish( "/topic", b"test_data", QOS_2, False, ) assert isinstance(message, OutgoingApplicationMessage) assert message.publish_packet is not None assert message.puback_packet is None assert message.pubrec_packet is not None assert message.pubrel_packet is not None assert message.pubcomp_packet is not None await self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future: asyncio.Future[Any] = asyncio.Future() coro = asyncio.start_server(server_mock, "127.0.0.1", 8888) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) exception = future.exception() if exception: raise exception def test_receive_qos0(self): async def server_mock(reader, writer) -> None: packet = PublishPacket.build( "/topic", b"test_data", rand_packet_id(), False, QOS_0, False, ) await packet.to_stream(writer) writer.close() # python 3.12 requires an explicit close await writer.wait_closed() async def test_coro() -> None: try: reader, writer = await asyncio.open_connection("127.0.0.1", 8888) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager) self.handler.attach(self.session, reader_adapted, writer_adapted) await self.start_handler(self.handler, self.session) message = await self.handler.mqtt_deliver_next_message() assert isinstance(message, IncomingApplicationMessage) assert message.publish_packet is not None assert message.puback_packet is None assert message.pubrec_packet is None assert message.pubrel_packet is None assert message.pubcomp_packet is None await self.stop_handler(self.handler, self.session) future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future: asyncio.Future[Any] = asyncio.Future() coro = asyncio.start_server(server_mock, "127.0.0.1", 8888) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) exception = future.exception() if exception: raise exception def test_receive_qos1(self): async def server_mock(reader, writer) -> None: try: packet = PublishPacket.build( "/topic", b"test_data", rand_packet_id(), False, QOS_1, False, ) await packet.to_stream(writer) puback = await PubackPacket.from_stream(reader) assert puback is not None assert packet.packet_id == puback.packet_id writer.close() # python 3.12 requires an explicit close await writer.wait_closed() except Exception as ae: future.set_exception(ae) async def test_coro() -> None: try: reader, writer = await asyncio.open_connection("127.0.0.1", 8888) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager) self.handler.attach(self.session, reader_adapted, writer_adapted) await self.start_handler(self.handler, self.session) message = await self.handler.mqtt_deliver_next_message() assert isinstance(message, IncomingApplicationMessage) assert message.publish_packet is not None assert message.puback_packet is not None assert message.pubrec_packet is None assert message.pubrel_packet is None assert message.pubcomp_packet is None await self.stop_handler(self.handler, self.session) future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future: asyncio.Future[Any] = asyncio.Future() self.event = asyncio.Event() coro = asyncio.start_server(server_mock, "127.0.0.1", 8888) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) exception = future.exception() if exception: raise exception def test_receive_qos2(self): async def server_mock(reader, writer) -> None: try: packet = PublishPacket.build( "/topic", b"test_data", rand_packet_id(), False, QOS_2, False, ) await packet.to_stream(writer) pubrec = await PubrecPacket.from_stream(reader) assert pubrec is not None assert packet.packet_id == pubrec.packet_id assert self.handler is not None assert packet.packet_id in self.handler._pubrel_waiters pubrel = PubrelPacket.build(packet.packet_id) await pubrel.to_stream(writer) pubcomp = await PubcompPacket.from_stream(reader) assert pubcomp is not None assert packet.packet_id == pubcomp.packet_id writer.close() # python 3.12 requires an explicit close await writer.wait_closed() except Exception as ae: future.set_exception(ae) async def test_coro() -> None: try: reader, writer = await asyncio.open_connection("127.0.0.1", 8888) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager) self.handler.attach(self.session, reader_adapted, writer_adapted) await self.start_handler(self.handler, self.session) message = await self.handler.mqtt_deliver_next_message() assert isinstance(message, IncomingApplicationMessage) assert message.publish_packet is not None assert message.puback_packet is None assert message.pubrec_packet is not None assert message.pubrel_packet is not None assert message.pubcomp_packet is not None await self.stop_handler(self.handler, self.session) future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() future: asyncio.Future[Any] = asyncio.Future() coro = asyncio.start_server(server_mock, "127.0.0.1", 8888) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) exception = future.exception() if exception: raise exception async def start_handler(self, handler, session): self.check_empty_waiters(handler) self.check_no_message(session) await handler.start() assert handler._reader_ready async def stop_handler(self, handler, session): await handler.stop() assert handler._reader_stopped self.check_empty_waiters(handler) self.check_no_message(session) def check_empty_waiters(self, handler): assert not handler._puback_waiters assert not handler._pubrec_waiters assert not handler._pubrel_waiters assert not handler._pubcomp_waiters def check_no_message(self, session): assert not session.inflight_out assert not session.inflight_in def test_publish_qos1_retry(self): async def server_mock(reader, writer) -> None: packet = await PublishPacket.from_stream(reader) try: assert packet.topic_name == "/topic" assert packet.qos == QOS_1 assert packet.packet_id is not None assert packet.packet_id in self.session.inflight_out assert self.handler is not None assert packet.packet_id in self.handler._puback_waiters puback = PubackPacket.build(packet.packet_id) await puback.to_stream(writer) writer.close() # python 3.12 requires an explicit close await writer.wait_closed() except Exception as ae: future.set_exception(ae) async def test_coro() -> None: try: reader, writer = await asyncio.open_connection("127.0.0.1", 8888) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager) self.handler.attach(self.session, reader_adapted, writer_adapted) await self.handler.start() await self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() message = OutgoingApplicationMessage(1, "/topic", QOS_1, b"test_data", False) message.publish_packet = PublishPacket.build( "/topic", b"test_data", rand_packet_id(), False, QOS_1, False, ) self.session.inflight_out[1] = message future: asyncio.Future[Any] = asyncio.Future() coro = asyncio.start_server(server_mock, "127.0.0.1", 8888) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) exception = future.exception() if exception: raise exception def test_publish_qos2_retry(self): async def server_mock(reader, writer) -> None: try: packet = await PublishPacket.from_stream(reader) assert packet.topic_name == "/topic" assert packet.qos == QOS_2 assert packet.packet_id is not None assert packet.packet_id in self.session.inflight_out assert self.handler is not None assert packet.packet_id in self.handler._pubrec_waiters pubrec = PubrecPacket.build(packet.packet_id) await pubrec.to_stream(writer) await PubrelPacket.from_stream(reader) assert packet.packet_id in self.handler._pubcomp_waiters pubcomp = PubcompPacket.build(packet.packet_id) await pubcomp.to_stream(writer) writer.close() # python 3.12 requires an explicit close await writer.wait_closed() except Exception as ae: future.set_exception(ae) async def test_coro() -> None: try: reader, writer = await asyncio.open_connection("127.0.0.1", 8888) reader_adapted, writer_adapted = adapt(reader, writer) self.handler = ProtocolHandler(self.plugin_manager) self.handler.attach(self.session, reader_adapted, writer_adapted) await self.handler.start() await self.stop_handler(self.handler, self.session) if not future.done(): future.set_result(True) except Exception as ae: future.set_exception(ae) self.handler = None self.session = Session() message = OutgoingApplicationMessage(1, "/topic", QOS_2, b"test_data", False) message.publish_packet = PublishPacket.build( "/topic", b"test_data", rand_packet_id(), False, QOS_2, False, ) self.session.inflight_out[1] = message future: asyncio.Future[Any] = asyncio.Future() coro = asyncio.start_server(server_mock, "127.0.0.1", 8888) server = self.loop.run_until_complete(coro) self.loop.run_until_complete(test_coro()) server.close() self.loop.run_until_complete(server.wait_closed()) exception = future.exception() if exception: raise exception Yakifo-amqtt-2637127/tests/mqtt/test_connack.py000066400000000000000000000011621504664204300214350ustar00rootroot00000000000000import pytest from amqtt.errors import AMQTTError from amqtt.mqtt.connack import ConnackPacket from amqtt.mqtt.packet import MQTTFixedHeader, PUBLISH def test_incorrect_fixed_header(): header = MQTTFixedHeader(PUBLISH, 0x00) with pytest.raises(AMQTTError): _ = ConnackPacket(fixed=header) @pytest.mark.parametrize("prop", [ "return_code", "session_parent" ]) def test_empty_variable_header(prop): packet = ConnackPacket() with pytest.raises(ValueError): assert getattr(packet, prop) is not None with pytest.raises(ValueError): assert setattr(packet, prop, "a value") Yakifo-amqtt-2637127/tests/mqtt/test_connect.py000066400000000000000000000167761504664204300214730ustar00rootroot00000000000000import asyncio import unittest import pytest from amqtt.adapters import BufferReader from amqtt.errors import AMQTTError from amqtt.mqtt.connect import ConnectPacket, ConnectPayload, ConnectVariableHeader from amqtt.mqtt.packet import CONNECT, MQTTFixedHeader, PUBLISH class ConnectPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_decode_ok(self): data = ( b"\x10\x3e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789" b"\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password" ) stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) assert message.variable_header is not None assert message.variable_header.proto_name == "MQTT" assert message.variable_header.proto_level == 4 assert message.variable_header.username_flag assert message.variable_header.password_flag assert not message.variable_header.will_retain_flag assert message.variable_header.will_qos == 1 assert message.variable_header.will_flag assert message.variable_header.clean_session_flag assert not message.variable_header.reserved_flag assert message.payload is not None assert message.payload.client_id == "0123456789" assert message.payload.will_topic == "WillTopic" assert message.payload.will_message == b"WillMessage" assert message.payload.username == "user" assert message.payload.password == "password" def test_decode_ok_will_flag(self): data = b"\x10\x26\x00\x04MQTT\x04\xca\x00\x00\x00\x0a0123456789\x00\x04user\x00\x08password" stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) assert message.variable_header is not None assert message.variable_header.proto_name == "MQTT" assert message.variable_header.proto_level == 4 assert message.variable_header.username_flag assert message.variable_header.password_flag assert not message.variable_header.will_retain_flag assert message.variable_header.will_qos == 1 assert not message.variable_header.will_flag assert message.variable_header.clean_session_flag assert not message.variable_header.reserved_flag assert message.payload is not None assert message.payload.client_id == "0123456789" assert message.payload.will_topic is None assert message.payload.will_message is None assert message.payload.username == "user" assert message.payload.password == "password" def test_decode_fail_reserved_flag(self): data = ( b"\x10\x3e\x00\x04MQTT\x04\xcf\x00\x00\x00\x0a0123456789" b"\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password" ) stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) assert message.variable_header is not None assert message.variable_header.reserved_flag def test_decode_fail_miss_clientId(self): data = b"\x10\x0a\x00\x04MQTT\x04\xce\x00\x00" stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) assert message.payload is not None assert message.payload.client_id is not None def test_decode_fail_miss_willtopic(self): data = b"\x10\x16\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789" stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) assert message.payload is not None assert message.payload.will_topic is None def test_decode_fail_miss_username(self): data = b"\x10\x2e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage" stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) assert message.payload is not None assert message.payload.username is None def test_decode_fail_miss_password(self): data = b"\x10\x34\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user" stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) assert message.payload is not None assert message.payload.password is None def test_encode(self): header = MQTTFixedHeader(CONNECT, 0x00, 0) variable_header = ConnectVariableHeader(0xCE, 0, "MQTT", 4) payload = ConnectPayload( "0123456789", "WillTopic", b"WillMessage", "user", "password", ) message = ConnectPacket(header, variable_header, payload) encoded = message.to_bytes() assert ( encoded == b"\x10>\x00\x04MQTT\x04\xce\x00\x00\x00\n0123456789\x00\tWillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password" ) def test_getattr_ok(self): data = ( b"\x10\x3e\x00\x04MQTT\x04\xce\x00\x00\x00\x0a0123456789" b"\x00\x09WillTopic\x00\x0bWillMessage\x00\x04user\x00\x08password" ) stream = BufferReader(data) message = self.loop.run_until_complete(ConnectPacket.from_stream(stream)) assert message.variable_header is not None assert message.variable_header.proto_name == "MQTT" assert message.proto_name == "MQTT" assert message.variable_header.proto_level == 4 assert message.proto_level == 4 assert message.variable_header.username_flag assert message.username_flag assert message.variable_header.password_flag assert message.password_flag assert not message.variable_header.will_retain_flag assert not message.will_retain_flag assert message.variable_header.will_qos == 1 assert message.will_qos == 1 assert message.variable_header.will_flag assert message.will_flag assert message.variable_header.clean_session_flag assert message.clean_session_flag assert not message.variable_header.reserved_flag assert not message.reserved_flag assert message.payload is not None assert message.payload.client_id == "0123456789" assert message.client_id == "0123456789" assert message.payload.will_topic == "WillTopic" assert message.will_topic == "WillTopic" assert message.payload.will_message == b"WillMessage" assert message.will_message == b"WillMessage" assert message.payload.username == "user" assert message.username == "user" assert message.payload.password == "password" assert message.password == "password" def test_incorrect_fixed_header(): header = MQTTFixedHeader(PUBLISH, 0x00) with pytest.raises(AMQTTError): _ = ConnectPacket(fixed=header) @pytest.mark.parametrize("prop", [ "proto_name", "proto_level", "username_flag", "password_flag", "clean_session_flag", "will_retain_flag", "will_qos", "will_flag", "reserved_flag", "client_id", "client_id_is_random", "will_topic", "will_message", "username", "password", "keep_alive", ]) def test_empty_variable_header(prop): packet = ConnectPacket() with pytest.raises(ValueError): assert getattr(packet, prop) is not None with pytest.raises(ValueError): assert setattr(packet, prop, "a value") Yakifo-amqtt-2637127/tests/mqtt/test_packet.py000066400000000000000000000031721504664204300212730ustar00rootroot00000000000000import asyncio import unittest import pytest from amqtt.adapters import BufferReader from amqtt.errors import MQTTError from amqtt.mqtt.packet import CONNECT, MQTTFixedHeader class TestMQTTFixedHeaderTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_bytes(self): data = b"\x10\x7f" stream = BufferReader(data) header = self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) assert header.packet_type == CONNECT assert not header.flags & 8 assert (header.flags & 6) >> 1 == 0 assert not header.flags & 1 assert header.remaining_length == 127 def test_from_bytes_with_length(self): data = b"\x10\xff\xff\xff\x7f" stream = BufferReader(data) header = self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) assert header.packet_type == CONNECT assert not header.flags & 8 assert (header.flags & 6) >> 1 == 0 assert not header.flags & 1 assert header.remaining_length == 268435455 def test_from_bytes_ko_with_length(self): data = b"\x10\xff\xff\xff\xff\x7f" stream = BufferReader(data) with pytest.raises(MQTTError): self.loop.run_until_complete(MQTTFixedHeader.from_stream(stream)) def test_to_bytes(self): header = MQTTFixedHeader(CONNECT, 0x00, 0) data = header.to_bytes() assert data == b"\x10\x00" def test_to_bytes_2(self): header = MQTTFixedHeader(CONNECT, 0x00, 268435455) data = header.to_bytes() assert data == b"\x10\xff\xff\xff\x7f" Yakifo-amqtt-2637127/tests/mqtt/test_puback.py000066400000000000000000000024141504664204300212670ustar00rootroot00000000000000import asyncio import unittest import pytest from amqtt.adapters import BufferReader from amqtt.errors import AMQTTError from amqtt.mqtt import PUBLISH, MQTTFixedHeader from amqtt.mqtt.puback import PacketIdVariableHeader, PubackPacket class PubackPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b"\x40\x02\x00\x0a" stream = BufferReader(data) message = self.loop.run_until_complete(PubackPacket.from_stream(stream)) assert message.variable_header.packet_id == 10 def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubackPacket(variable_header=variable_header) out = publish.to_bytes() assert out == b"@\x02\x00\n" def test_incorrect_fixed_header(): header = MQTTFixedHeader(PUBLISH, 0x00) with pytest.raises(AMQTTError): connect_packet = PubackPacket(fixed=header) @pytest.mark.parametrize("prop", [ "packet_id", ]) def test_empty_variable_header(prop): connect_packet = PubackPacket() with pytest.raises(ValueError): assert getattr(connect_packet, prop) is not None with pytest.raises(ValueError): assert setattr(connect_packet, prop, "a value") Yakifo-amqtt-2637127/tests/mqtt/test_pubcomp.py000066400000000000000000000023541504664204300214720ustar00rootroot00000000000000import asyncio import unittest import pytest from amqtt.adapters import BufferReader from amqtt.errors import AMQTTError from amqtt.mqtt import MQTTFixedHeader, PUBLISH from amqtt.mqtt.pubcomp import PacketIdVariableHeader, PubcompPacket class PubcompPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b"\x70\x02\x00\x0a" stream = BufferReader(data) message = self.loop.run_until_complete(PubcompPacket.from_stream(stream)) assert message.variable_header.packet_id == 10 def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubcompPacket(variable_header=variable_header) out = publish.to_bytes() assert out == b"p\x02\x00\n" def test_incorrect_fixed_header(): header = MQTTFixedHeader(PUBLISH, 0x00) with pytest.raises(AMQTTError): _ = PubcompPacket(fixed=header) @pytest.mark.parametrize("prop", [ "packet_id" ]) def test_empty_variable_header(prop): packet = PubcompPacket() with pytest.raises(ValueError): assert getattr(packet, prop) is not None with pytest.raises(ValueError): assert setattr(packet, prop, "a value") Yakifo-amqtt-2637127/tests/mqtt/test_publish.py000066400000000000000000000123031504664204300214660ustar00rootroot00000000000000import asyncio import unittest import pytest from amqtt.adapters import BufferReader from amqtt.errors import AMQTTError from amqtt.mqtt.packet import MQTTFixedHeader, CONNECT from amqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 from amqtt.mqtt.publish import PublishPacket, PublishPayload, PublishVariableHeader class PublishPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream_qos_0(self): data = b"\x31\x11\x00\x05topic0123456789" stream = BufferReader(data) message = self.loop.run_until_complete(PublishPacket.from_stream(stream)) assert message.variable_header.topic_name == "topic" assert message.variable_header.packet_id is None assert not message.fixed_header.flags >> 1 & 3 assert message.fixed_header.flags & 0x01 assert message.payload.data, b"0123456789" def test_from_stream_qos_2(self): data = b"\x37\x13\x00\x05topic\x00\x0a0123456789" stream = BufferReader(data) message = self.loop.run_until_complete(PublishPacket.from_stream(stream)) assert message.variable_header.topic_name == "topic" assert message.variable_header.packet_id == 10 assert (message.fixed_header.flags >> 1) & 0x03 assert message.fixed_header.flags & 0x01 assert message.payload.data, b"0123456789" def test_to_stream_no_packet_id(self): variable_header = PublishVariableHeader("topic", None) payload = PublishPayload(b"0123456789") publish = PublishPacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() assert out == b"0\x11\x00\x05topic0123456789" def test_to_stream_packet(self): variable_header = PublishVariableHeader("topic", 10) payload = PublishPayload(b"0123456789") publish = PublishPacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() assert out == b"0\x13\x00\x05topic\x00\n0123456789" def test_build(self): packet = PublishPacket.build("/topic", b"data", 1, False, QOS_0, False) assert packet.packet_id == 1 assert not packet.dup_flag assert packet.qos == QOS_0 assert not packet.retain_flag packet = PublishPacket.build("/topic", b"data", 1, False, QOS_1, False) assert packet.packet_id == 1 assert not packet.dup_flag assert packet.qos == QOS_1 assert not packet.retain_flag packet = PublishPacket.build("/topic", b"data", 1, False, QOS_2, False) assert packet.packet_id == 1 assert not packet.dup_flag assert packet.qos == QOS_2 assert not packet.retain_flag packet = PublishPacket.build("/topic", b"data", 1, True, QOS_0, False) assert packet.packet_id == 1 assert packet.dup_flag assert packet.qos == QOS_0 assert not packet.retain_flag packet = PublishPacket.build("/topic", b"data", 1, True, QOS_1, False) assert packet.packet_id == 1 assert packet.dup_flag assert packet.qos == QOS_1 assert not packet.retain_flag packet = PublishPacket.build("/topic", b"data", 1, True, QOS_2, False) assert packet.packet_id == 1 assert packet.dup_flag assert packet.qos == QOS_2 assert not packet.retain_flag packet = PublishPacket.build("/topic", b"data", 1, False, QOS_0, True) assert packet.packet_id == 1 assert not packet.dup_flag assert packet.qos == QOS_0 assert packet.retain_flag packet = PublishPacket.build("/topic", b"data", 1, False, QOS_1, True) assert packet.packet_id == 1 assert not packet.dup_flag assert packet.qos == QOS_1 assert packet.retain_flag packet = PublishPacket.build("/topic", b"data", 1, False, QOS_2, True) assert packet.packet_id == 1 assert not packet.dup_flag assert packet.qos == QOS_2 assert packet.retain_flag packet = PublishPacket.build("/topic", b"data", 1, True, QOS_0, True) assert packet.packet_id == 1 assert packet.dup_flag assert packet.qos == QOS_0 assert packet.retain_flag packet = PublishPacket.build("/topic", b"data", 1, True, QOS_1, True) assert packet.packet_id == 1 assert packet.dup_flag assert packet.qos == QOS_1 assert packet.retain_flag packet = PublishPacket.build("/topic", b"data", 1, True, QOS_2, True) assert packet.packet_id == 1 assert packet.dup_flag assert packet.qos == QOS_2 assert packet.retain_flag def test_incorrect_fixed_header(): header = MQTTFixedHeader(CONNECT, 0x00) with pytest.raises(AMQTTError): _ = PublishPacket(fixed=header) def test_set_flags(): packet = PublishPacket() packet.set_flags(dup_flag=True, qos=QOS_1, retain_flag=True) @pytest.mark.parametrize("prop", [ "packet_id", "data", "topic_name" ]) def test_empty_variable_header(prop): packet = PublishPacket() with pytest.raises(ValueError): assert getattr(packet, prop) is not None with pytest.raises(ValueError): assert setattr(packet, prop, "a value") Yakifo-amqtt-2637127/tests/mqtt/test_pubrec.py000066400000000000000000000023551504664204300213060ustar00rootroot00000000000000import asyncio import unittest import pytest from amqtt.adapters import BufferReader from amqtt.errors import AMQTTError from amqtt.mqtt.packet import MQTTFixedHeader, PUBLISH from amqtt.mqtt.pubrec import PacketIdVariableHeader, PubrecPacket class PubrecPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b"\x50\x02\x00\x0a" stream = BufferReader(data) message = self.loop.run_until_complete(PubrecPacket.from_stream(stream)) assert message.variable_header.packet_id == 10 def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubrecPacket(variable_header=variable_header) out = publish.to_bytes() assert out == b"P\x02\x00\n" def test_incorrect_fixed_header(): header = MQTTFixedHeader(PUBLISH, 0x00) with pytest.raises(AMQTTError): _ = PubrecPacket(fixed=header) @pytest.mark.parametrize("prop", [ "packet_id" ]) def test_empty_variable_header(prop): packet = PubrecPacket() with pytest.raises(ValueError): assert getattr(packet, prop) is not None with pytest.raises(ValueError): assert setattr(packet, prop, "a value") Yakifo-amqtt-2637127/tests/mqtt/test_pubrel.py000066400000000000000000000023551504664204300213170ustar00rootroot00000000000000import asyncio import unittest import pytest from amqtt.adapters import BufferReader from amqtt.errors import AMQTTError from amqtt.mqtt.packet import MQTTFixedHeader, PUBLISH from amqtt.mqtt.pubrel import PacketIdVariableHeader, PubrelPacket class PubrelPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b"\x60\x02\x00\x0a" stream = BufferReader(data) message = self.loop.run_until_complete(PubrelPacket.from_stream(stream)) assert message.variable_header.packet_id == 10 def test_to_bytes(self): variable_header = PacketIdVariableHeader(10) publish = PubrelPacket(variable_header=variable_header) out = publish.to_bytes() assert out == b"b\x02\x00\n" def test_incorrect_fixed_header(): header = MQTTFixedHeader(PUBLISH, 0x00) with pytest.raises(AMQTTError): _ = PubrelPacket(fixed=header) @pytest.mark.parametrize("prop", [ "packet_id" ]) def test_empty_variable_header(prop): packet = PubrelPacket() with pytest.raises(ValueError): assert getattr(packet, prop) is not None with pytest.raises(ValueError): assert setattr(packet, prop, "a value")Yakifo-amqtt-2637127/tests/mqtt/test_suback.py000066400000000000000000000024471504664204300213000ustar00rootroot00000000000000import asyncio import unittest from amqtt.adapters import BufferReader from amqtt.mqtt.packet import PacketIdVariableHeader from amqtt.mqtt.suback import SubackPacket, SubackPayload class SubackPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b"\x90\x06\x00\x0a\x00\x01\x02\x80" stream = BufferReader(data) message = self.loop.run_until_complete(SubackPacket.from_stream(stream)) assert message.payload.return_codes[0] == SubackPayload.RETURN_CODE_00 assert message.payload.return_codes[1] == SubackPayload.RETURN_CODE_01 assert message.payload.return_codes[2] == SubackPayload.RETURN_CODE_02 assert message.payload.return_codes[3] == SubackPayload.RETURN_CODE_80 def test_to_stream(self): variable_header = PacketIdVariableHeader(10) payload = SubackPayload( [ SubackPayload.RETURN_CODE_00, SubackPayload.RETURN_CODE_01, SubackPayload.RETURN_CODE_02, SubackPayload.RETURN_CODE_80, ], ) suback = SubackPacket(variable_header=variable_header, payload=payload) out = suback.to_bytes() assert out == b"\x90\x06\x00\n\x00\x01\x02\x80" Yakifo-amqtt-2637127/tests/mqtt/test_subscribe.py000066400000000000000000000021441504664204300220030ustar00rootroot00000000000000import asyncio import unittest from amqtt.adapters import BufferReader from amqtt.mqtt.constants import QOS_1, QOS_2 from amqtt.mqtt.packet import PacketIdVariableHeader from amqtt.mqtt.subscribe import SubscribePacket, SubscribePayload class SubscribePacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b"\x80\x0e\x00\x0a\x00\x03a/b\x01\x00\x03c/d\x02" stream = BufferReader(data) message = self.loop.run_until_complete(SubscribePacket.from_stream(stream)) (topic, qos) = message.payload.topics[0] assert topic == "a/b" assert qos == QOS_1 (topic, qos) = message.payload.topics[1] assert topic == "c/d" assert qos == QOS_2 def test_to_stream(self): variable_header = PacketIdVariableHeader(10) payload = SubscribePayload([("a/b", QOS_1), ("c/d", QOS_2)]) publish = SubscribePacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() assert out == b"\x82\x0e\x00\n\x00\x03a/b\x01\x00\x03c/d\x02" Yakifo-amqtt-2637127/tests/mqtt/test_unsuback.py000066400000000000000000000014411504664204300216340ustar00rootroot00000000000000import asyncio import unittest from amqtt.adapters import BufferReader from amqtt.mqtt.packet import PacketIdVariableHeader from amqtt.mqtt.unsuback import UnsubackPacket class UnsubackPacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b"\xb0\x02\x00\x0a" stream = BufferReader(data) message = self.loop.run_until_complete(UnsubackPacket.from_stream(stream)) assert message.variable_header is not None assert message.variable_header.packet_id == 10 def test_to_stream(self): variable_header = PacketIdVariableHeader(10) publish = UnsubackPacket(variable_header=variable_header) out = publish.to_bytes() assert out == b"\xb0\x02\x00\n" Yakifo-amqtt-2637127/tests/mqtt/test_unsubscribe.py000066400000000000000000000016541504664204300223530ustar00rootroot00000000000000import asyncio import unittest from amqtt.adapters import BufferReader from amqtt.mqtt.packet import PacketIdVariableHeader from amqtt.mqtt.unsubscribe import UnsubscribePacket, UnubscribePayload class UnsubscribePacketTest(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_from_stream(self): data = b"\xa2\x0c\x00\n\x00\x03a/b\x00\x03c/d" stream = BufferReader(data) message = self.loop.run_until_complete(UnsubscribePacket.from_stream(stream)) assert message.payload.topics[0] == "a/b" assert message.payload.topics[1] == "c/d" def test_to_stream(self): variable_header = PacketIdVariableHeader(10) payload = UnubscribePayload(["a/b", "c/d"]) publish = UnsubscribePacket(variable_header=variable_header, payload=payload) out = publish.to_bytes() assert out == b"\xa2\x0c\x00\n\x00\x03a/b\x00\x03c/d" Yakifo-amqtt-2637127/tests/plugins/000077500000000000000000000000001504664204300171045ustar00rootroot00000000000000Yakifo-amqtt-2637127/tests/plugins/broker_plugin.yml000066400000000000000000000003071504664204300224710ustar00rootroot00000000000000--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: - test.plugins.plugins.TestSimplePlugin - test.plugins.plugins.TestConfigPlugin: option1: foo option2: bar Yakifo-amqtt-2637127/tests/plugins/mock_plugins.py000066400000000000000000000004521504664204300221510ustar00rootroot00000000000000from amqtt.broker import BrokerContext from amqtt.plugins.base import BasePlugin # intentional import error to test broker response from pathlib import Pat # noqa class MockImportErrorPlugin(BasePlugin): def __init__(self, context: BrokerContext) -> None: super().__init__(context) Yakifo-amqtt-2637127/tests/plugins/mocks.py000066400000000000000000000031571504664204300206000ustar00rootroot00000000000000import logging from dataclasses import dataclass from amqtt.plugins.base import BasePlugin, BaseAuthPlugin, BaseTopicPlugin from amqtt.contexts import BaseContext, Action from amqtt.session import Session logger = logging.getLogger(__name__) class TestSimplePlugin(BasePlugin): def __init__(self, context: BaseContext): super().__init__(context) class TestConfigPlugin(BasePlugin): def __init__(self, context: BaseContext): super().__init__(context) @dataclass class Config: option1: int option2: str option3: int = 20 class TestCoroErrorPlugin(BaseAuthPlugin): def authenticate(self, *, session: Session) -> bool | None: return True class TestAuthPlugin(BaseAuthPlugin): async def authenticate(self, *, session: Session) -> bool | None: return True class TestNoAuthPlugin(BaseAuthPlugin): async def authenticate(self, *, session: Session) -> bool | None: return False class TestAllowTopicPlugin(BaseTopicPlugin): def __init__(self, context: BaseContext): super().__init__(context) async def topic_filtering( self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None ) -> bool: return True class TestBlockTopicPlugin(BaseTopicPlugin): def __init__(self, context: BaseContext): super().__init__(context) async def topic_filtering( self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None ) -> bool: logger.debug("topic filtering plugin is returning false") return False Yakifo-amqtt-2637127/tests/plugins/passwd000066400000000000000000000011461504664204300203320ustar00rootroot00000000000000# Test password file user:$6$1sSXVMBVKMF$uDStq59YfiuFxVF1Gi/.i/Li7Vwf5iTwg8LovLKmCvM5FsRNJM.OPWHhXwI2.4AscLZXSPQVxpIlX6laUl9570 test_user:$6$.c9f9sAzs5YXX2de$GSdOi3iFwHJRCIJn1W63muDFQAL29yoFmU/TXcwDB42F2BZg3zcN5uKBM0.1PjwdMpWHRydbhXWSc3uWKSmKr. # Password for these is "${USER}password" user1:$6$h.fV0zYsXI$8wKblqETpztRKcPD6OLWZc1mU4nW5yQ713R5ECs7EwJa7oas/yrhI2itUdhETI8BvmtfGy65ltAMap9gHkzdc1 user2:$6$bUyF8v0mTo94$IJMa2BlCd6/mAM5N5s6heFB2ewC4j3CDkFb9mYIoarXg4OxiZVnLyh20IWHf6GY8i4sc5m2D9nWLrtbnIO5o8. user3:$6$YOjKg1kYEeGibb$HlVa2EvQbF1ssSQs.lFnS1NhoTBQLG5YF7h0z4komAEvNJw6m4gay81MRp.lt4PSbcVrimcuRbidR9cRZkfBb/ Yakifo-amqtt-2637127/tests/plugins/test_authentication.py000066400000000000000000000110231504664204300235310ustar00rootroot00000000000000import asyncio import logging from pathlib import Path import unittest import pytest from amqtt.plugins.authentication import AnonymousAuthPlugin, FileAuthPlugin from amqtt.contexts import BaseContext from amqtt.plugins.base import BaseAuthPlugin from amqtt.session import Session formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) @pytest.mark.asyncio async def test_base_no_config(logdog): """Check BaseTopicPlugin returns false if no topic-check is present.""" with logdog() as pile: context = BaseContext() context.logger = logging.getLogger("testlog") context.config = {} plugin = BaseAuthPlugin(context) s = Session() authorised = await plugin.authenticate(session=s) assert authorised is False # Warning messages are only generated if using deprecated plugin configuration on initial load log_records = list(pile.drain(name="testlog")) assert len(log_records) == 1 assert log_records[0].levelno == logging.WARNING assert log_records[0].message == "'auth' section not found in context configuration" class TestAnonymousAuthPlugin(unittest.TestCase): def setUp(self) -> None: self.loop: asyncio.AbstractEventLoop = asyncio.new_event_loop() def test_allow_anonymous_dict_config(self) -> None: context = BaseContext() context.logger = logging.getLogger(__name__) context.config = {"auth": {"allow-anonymous": True}} s = Session() s.username = "" auth_plugin = AnonymousAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) assert ret def test_allow_anonymous_dataclass_config(self) -> None: context = BaseContext() context.logger = logging.getLogger(__name__) context.config = AnonymousAuthPlugin.Config(allow_anonymous=True) s = Session() s.username = "" auth_plugin = AnonymousAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) assert ret def test_disallow_anonymous(self) -> None: context = BaseContext() context.logger = logging.getLogger(__name__) context.config = {"auth": {"allow-anonymous": False}} s = Session() s.username = "" auth_plugin = AnonymousAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) assert not ret def test_allow_nonanonymous(self) -> None: context = BaseContext() context.logger = logging.getLogger(__name__) context.config = {"auth": {"allow-anonymous": False}} s = Session() s.username = "test" auth_plugin = AnonymousAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) assert ret class TestFileAuthPlugin(unittest.TestCase): def setUp(self) -> None: self.loop: asyncio.AbstractEventLoop = asyncio.new_event_loop() def test_allow(self) -> None: context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { "auth": { "password-file": Path(__file__).parent / "passwd", }, } s = Session() s.username = "user" s.password = "test" auth_plugin = FileAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) assert ret def test_wrong_password(self) -> None: context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { "auth": { "password-file": Path(__file__).parent / "passwd", }, } s = Session() s.username = "user" s.password = "wrong password" auth_plugin = FileAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) assert not ret def test_unknown_password(self) -> None: context = BaseContext() context.logger = logging.getLogger(__name__) context.config = { "auth": { "password-file": Path(__file__).parent / "passwd", }, } s = Session() s.username = "some user" s.password = "some password" auth_plugin = FileAuthPlugin(context) ret = self.loop.run_until_complete(auth_plugin.authenticate(session=s)) assert not ret Yakifo-amqtt-2637127/tests/plugins/test_config.py000066400000000000000000000172171504664204300217720ustar00rootroot00000000000000import asyncio import logging from dataclasses import dataclass, field from typing import Any import pytest import yaml from amqtt.broker import Broker from yaml import CLoader as Loader from dacite import from_dict, Config, UnexpectedDataError from amqtt.client import MQTTClient from amqtt.errors import PluginLoadError, ConnectError, PluginCoroError from amqtt.mqtt.constants import QOS_0 logger = logging.getLogger(__name__) plugin_config = """--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: - tests.plugins.mocks.TestSimplePlugin: - tests.plugins.mocks.TestConfigPlugin: option1: 1 option2: bar """ plugin_invalid_config_one = """--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: - tests.plugins.mocks.TestSimplePlugin: option1: 1 option2: bar """ plugin_invalid_config_two = """--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: - tests.plugins.mocks.TestConfigPlugin: """ plugin_coro_error_config = """--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: - tests.plugins.mocks.TestCoroErrorPlugin: """ plugin_config_auth = """--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: - tests.plugins.mocks.TestAuthPlugin: """ plugin_config_no_auth = """--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: - tests.plugins.mocks.TestNoAuthPlugin: """ plugin_config_topic = """--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: - amqtt.plugins.authentication.AnonymousAuthPlugin - tests.plugins.mocks.TestAllowTopicPlugin: """ plugin_config_topic_block = """--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: - amqtt.plugins.authentication.AnonymousAuthPlugin - tests.plugins.mocks.TestBlockTopicPlugin: """ @pytest.mark.asyncio async def test_plugin_config_extra_fields(): cfg: dict[str, Any] = yaml.load(plugin_invalid_config_one, Loader=Loader) with pytest.raises(PluginLoadError): _ = Broker(config=cfg) @pytest.mark.asyncio async def test_plugin_config_missing_fields(): cfg: dict[str, Any] = yaml.load(plugin_invalid_config_one, Loader=Loader) with pytest.raises(PluginLoadError): _ = Broker(config=cfg) @pytest.mark.asyncio async def test_alternate_plugin_load(): cfg: dict[str, Any] = yaml.load(plugin_config, Loader=Loader) broker = Broker(config=cfg) await broker.start() await broker.shutdown() @pytest.mark.asyncio async def test_coro_error_plugin_load(): cfg: dict[str, Any] = yaml.load(plugin_coro_error_config, Loader=Loader) with pytest.raises(PluginCoroError): _ = Broker(config=cfg) @pytest.mark.asyncio async def test_auth_plugin_load(): cfg: dict[str, Any] = yaml.load(plugin_config_auth, Loader=Loader) broker = Broker(config=cfg) await broker.start() await asyncio.sleep(0.5) client1 = MQTTClient() await client1.connect() await client1.publish('my/topic', b'my message') await client1.disconnect() await asyncio.sleep(0.5) await broker.shutdown() @pytest.mark.asyncio async def test_no_auth_plugin_load(): cfg: dict[str, Any] = yaml.load(plugin_config_no_auth, Loader=Loader) broker = Broker(config=cfg) await broker.start() await asyncio.sleep(0.5) client1 = MQTTClient(config={'auto_reconnect': False}) with pytest.raises(ConnectError): await client1.connect() await asyncio.sleep(0.5) await broker.shutdown() @pytest.mark.asyncio async def test_allow_topic_plugin_load(): cfg: dict[str, Any] = yaml.load(plugin_config_topic, Loader=Loader) broker = Broker(config=cfg) await broker.start() await asyncio.sleep(0.5) client2 = MQTTClient(config={'auto_reconnect': False}) await client2.connect() await client2.subscribe([ ('my/topic', QOS_0) ]) client1 = MQTTClient(config={'auto_reconnect': True}) await client1.connect() await client1.publish('my/topic', b'my message') message = await client2.deliver_message(timeout_duration=1) assert message.topic == 'my/topic' assert message.data == b'my message' await client2.disconnect() await client1.disconnect() await broker.shutdown() @pytest.mark.asyncio async def test_block_topic_plugin_load(): cfg: dict[str, Any] = yaml.load(plugin_config_topic_block, Loader=Loader) broker = Broker(config=cfg) await broker.start() await asyncio.sleep(0.5) client2 = MQTTClient(config={'auto_reconnect': False}) await client2.connect() await client2.subscribe([ ('my/topic', QOS_0) ]) client1 = MQTTClient(config={'auto_reconnect': True}) await client1.connect() await client1.publish('my/topic', b'my message') with pytest.raises(asyncio.TimeoutError): message = await client2.deliver_message(timeout_duration=1) logger.debug(f"msg received: {message.topic} >> {message.data}") await client2.disconnect() await client1.disconnect() await broker.shutdown() plugin_yaml_list_config_one = """--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: - tests.plugins.mocks.TestSimplePlugin: - tests.plugins.mocks.TestConfigPlugin: option1: 1 option2: bar option3: 3 """ plugin_yaml_list_config_two = """--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: - tests.plugins.mocks.TestSimplePlugin - tests.plugins.mocks.TestConfigPlugin: option1: 1 option2: bar option3: 3 """ plugin_yaml_dict_config = """--- listeners: default: type: tcp bind: 0.0.0.0:1883 plugins: tests.plugins.mocks.TestSimplePlugin: tests.plugins.mocks.TestConfigPlugin: option1: 1 option2: bar option3: 3 """ plugin_empty_dict_config = { 'listeners': {'default': {'type': 'tcp', 'bind': '127.0.0.1'}}, 'plugins': { 'tests.plugins.mocks.TestSimplePlugin': {}, } } plugin_dict_option_config = { 'listeners': {'default': {'type': 'tcp', 'bind': '127.0.0.1'}}, 'plugins': { 'tests.plugins.mocks.TestConfigPlugin': {'option1': 1, 'option2': 'bar', 'option3': 3} } } @pytest.mark.asyncio async def test_plugin_yaml_list_config(): cfg: dict[str, Any] = yaml.load(plugin_yaml_list_config_one, Loader=Loader) broker = Broker(config=cfg) await asyncio.sleep(0.5) plugin = broker.plugins_manager.get_plugin('TestConfigPlugin') assert getattr(plugin.context.config, 'option1', None) == 1 assert getattr(plugin.context.config, 'option3', None) == 3 cfg: dict[str, Any] = yaml.load(plugin_yaml_list_config_two, Loader=Loader) broker = Broker(config=cfg) await asyncio.sleep(0.5) plugin = broker.plugins_manager.get_plugin('TestConfigPlugin') assert getattr(plugin.context.config, 'option1', None) == 1 assert getattr(plugin.context.config, 'option3', None) == 3 @pytest.mark.asyncio async def test_plugin_yaml_dict_config(): cfg: dict[str, Any] = yaml.load(plugin_yaml_dict_config, Loader=Loader) broker = Broker(config=cfg) await asyncio.sleep(0.5) assert broker.plugins_manager.get_plugin('TestSimplePlugin') is not None @pytest.mark.asyncio async def test_plugin_empty_dict_config(): broker = Broker(config=plugin_empty_dict_config) await asyncio.sleep(0.5) assert broker.plugins_manager.get_plugin('TestSimplePlugin') is not None @pytest.mark.asyncio async def test_plugin_option_dict_config(): broker = Broker(config=plugin_dict_option_config) await asyncio.sleep(0.5) plugin = broker.plugins_manager.get_plugin('TestConfigPlugin') assert getattr(plugin.context.config, 'option1', None) == 1 assert getattr(plugin.context.config, 'option3', None) == 3 Yakifo-amqtt-2637127/tests/plugins/test_manager.py000066400000000000000000000075421504664204300221370ustar00rootroot00000000000000import asyncio import logging import unittest from amqtt.events import BrokerEvents from amqtt.plugins.base import BaseAuthPlugin, BaseTopicPlugin from amqtt.plugins.manager import PluginManager from amqtt.contexts import BaseContext, Action from amqtt.session import Session formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.INFO, format=formatter) class EmptyTestPlugin: def __init__(self, context: BaseContext) -> None: self.context = context class EventTestPlugin(BaseAuthPlugin, BaseTopicPlugin): def __init__(self, context: BaseContext) -> None: super().__init__(context) self.test_close_flag = False self.test_auth_flag = False self.test_topic_flag = False self.test_event_flag = False async def on_broker_message_received(self) -> None: self.test_event_flag = True async def authenticate(self, *, session: Session) -> bool | None: self.test_auth_flag = True return True async def topic_filtering( self, *, session: Session | None = None, topic: str | None = None, action: Action | None = None ) -> bool: self.test_topic_flag = True return False async def close(self) -> None: self.test_close_flag = True class TestPluginManager(unittest.TestCase): def setUp(self) -> None: self.loop = asyncio.new_event_loop() def test_load_plugin(self) -> None: manager = PluginManager("amqtt.test.plugins", context=None) assert len(manager._plugins) > 0 def test_fire_event(self) -> None: async def fire_event() -> None: await manager.fire_event(BrokerEvents.MESSAGE_RECEIVED) await asyncio.sleep(1) await manager.close() manager = PluginManager("amqtt.test.plugins", context=None) self.loop.run_until_complete(fire_event()) plugin = manager.get_plugin("EventTestPlugin") assert plugin is not None assert plugin.test_event_flag def test_fire_event_wait(self) -> None: async def fire_event() -> None: await manager.fire_event(BrokerEvents.MESSAGE_RECEIVED, wait=True) await manager.close() manager = PluginManager("amqtt.test.plugins", context=None) self.loop.run_until_complete(fire_event()) plugin = manager.get_plugin("EventTestPlugin") assert plugin is not None assert plugin.test_event_flag def test_plugin_close_coro(self) -> None: manager = PluginManager("amqtt.test.plugins", context=None) self.loop.run_until_complete(manager.map_plugin_close()) self.loop.run_until_complete(asyncio.sleep(0.5)) plugin = manager.get_plugin("EventTestPlugin") assert plugin is not None assert plugin.test_close_flag def test_plugin_auth_coro(self) -> None: # provide context that activates auth plugins context = BaseContext() context.config = {'auth':{}} manager = PluginManager("amqtt.test.plugins", context=context) self.loop.run_until_complete(manager.map_plugin_auth(session=Session())) self.loop.run_until_complete(asyncio.sleep(0.5)) plugin = manager.get_plugin("EventTestPlugin") assert plugin is not None assert plugin.test_auth_flag def test_plugin_topic_coro(self) -> None: # provide context that activates topic check plugins context = BaseContext() context.config = {'topic-check':{}} manager = PluginManager("amqtt.test.plugins", context=context) self.loop.run_until_complete(manager.map_plugin_topic(session=Session(), topic="test", action=Action.PUBLISH)) self.loop.run_until_complete(asyncio.sleep(0.5)) plugin = manager.get_plugin("EventTestPlugin") assert plugin is not None assert plugin.test_topic_flag Yakifo-amqtt-2637127/tests/plugins/test_plugins.py000066400000000000000000000205011504664204300221740ustar00rootroot00000000000000import asyncio import inspect import logging from functools import partial from logging import getLogger from pathlib import Path from types import ModuleType from typing import Any, Callable, Coroutine import pytest import amqtt.plugins from amqtt.broker import Broker, BrokerContext from amqtt.client import MQTTClient from amqtt.errors import PluginInitError, PluginImportError from amqtt.events import MQTTEvents, BrokerEvents from amqtt.mqtt.constants import QOS_0, QOS_1 from amqtt.plugins.base import BasePlugin from amqtt.contexts import BaseContext from amqtt.contrib.persistence import RetainedMessage _INVALID_METHOD: str = "invalid_foo" _PLUGIN: str = "Plugin" logger = logging.getLogger(__name__) class _TestContext(BaseContext): def __init__(self) -> None: super().__init__() self.config: dict[str, Any] = {"auth": {}} self.logger = getLogger(__name__) def _verify_module(module: ModuleType, plugin_module_name: str) -> None: if not module.__name__.startswith(plugin_module_name): return for name, clazz in inspect.getmembers(module, inspect.isclass): if not name.endswith(_PLUGIN) or name == _PLUGIN: continue obj = clazz(_TestContext()) with pytest.raises( AttributeError, match=f"'{name}' object has no attribute '{_INVALID_METHOD}'", ): getattr(obj, _INVALID_METHOD) assert hasattr(obj, _INVALID_METHOD) is False for _, obj in inspect.getmembers(module, inspect.ismodule): _verify_module(obj, plugin_module_name) def removesuffix(self: str, suffix: str) -> str: """Compatibility for Python versions prior to 3.9.""" if suffix and self.endswith(suffix): return self[: -len(suffix)] return self[:] def test_plugins_correct_has_attr() -> None: """Test plugins to ensure they correctly handle the 'has_attr' check.""" module = amqtt.plugins for file in Path(module.__file__).parent.rglob("*.py"): if not Path(file).is_file(): continue name = file.as_posix().replace("/", ".") name = name[name.find(module.__name__) : -3] name = removesuffix(name, ".__init__") __import__(name) _verify_module(module, module.__name__) class MockInitErrorPlugin(BasePlugin): def __init__(self, context: BrokerContext) -> None: super().__init__(context) raise KeyError @pytest.mark.asyncio async def test_plugin_exception_while_init() -> None: config = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10}, }, 'sys_interval': 1, 'plugins':{ 'tests.plugins.test_plugins.MockInitErrorPlugin':{} } } with pytest.raises(PluginInitError): _ = Broker(plugin_namespace='tests.mock_plugins', config=config) @pytest.mark.asyncio async def test_plugin_exception_while_loading() -> None: config = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10}, }, 'sys_interval': 1, 'plugins':{ 'tests.plugins.mock_plugins.MockImportErrorPlugin':{} } } with pytest.raises(PluginImportError): _ = Broker(plugin_namespace='tests.mock_plugins', config=config) class AllEventsPlugin(BasePlugin[BaseContext]): """A plugin to verify all events get sent to plugins.""" def __init__(self, context: BaseContext) -> None: super().__init__(context) self.test_flags = { events:False for events in list(MQTTEvents) + list(BrokerEvents)} async def call_method(self, event_name: str, **kwargs: Any) -> None: assert event_name in self.test_flags self.test_flags[event_name] = True def __getattr__(self, name: str) -> Callable[..., Coroutine[Any, Any, None]]: """Dynamically handle calls to methods starting with 'on_'.""" if name.startswith("on_"): event_name = name.replace('on_', '') return partial(self.call_method, event_name) if name not in ('authenticate', 'topic_filtering'): pytest.fail(f'unexpected method called: {name}') @pytest.mark.asyncio async def test_all_plugin_events(): config = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10}, }, 'sys_interval': 1, 'plugins':{ 'amqtt.plugins.authentication.AnonymousAuthPlugin': {}, 'tests.plugins.test_plugins.AllEventsPlugin': {} } } broker = Broker(plugin_namespace='tests.mock_plugins', config=config) await broker.start() await asyncio.sleep(2) # make sure all expected events get triggered client = MQTTClient() await client.connect("mqtt://127.0.0.1:1883/") await client.subscribe([('my/test/topic', QOS_0),]) await client.publish('test/topic', b'my test message', retain=True) await client.unsubscribe(['my/test/topic',]) await client.disconnect() await asyncio.sleep(1) # get the plugin so it doesn't get gc on shutdown test_plugin = broker.plugins_manager.get_plugin('AllEventsPlugin') await broker.shutdown() await asyncio.sleep(1) assert all(test_plugin.test_flags.values()), f'event not received: {[event for event, value in test_plugin.test_flags.items() if not value]}' class RetainedMessageEventPlugin(BasePlugin[BrokerContext]): """A plugin to verify all events get sent to plugins.""" def __init__(self, context: BaseContext) -> None: super().__init__(context) self.topic_retained_message_flag = False self.session_retained_message_flag = False self.topic_clear_retained_message_flag = False async def on_broker_retained_message(self, *, client_id: str | None, retained_message: RetainedMessage) -> None: """retaining message event handler.""" if client_id: session = self.context.get_session(client_id) assert session.transitions.state != "connected" logger.debug("retained message event fired for offline client") self.session_retained_message_flag = True else: if not retained_message.data: logger.debug("retained message event fired for clearing a topic") self.topic_clear_retained_message_flag = True else: logger.debug("retained message event fired for setting a topic") self.topic_retained_message_flag = True @pytest.mark.asyncio async def test_retained_message_plugin_event(): config = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10}, }, 'sys_interval': 1, 'plugins':[{'amqtt.plugins.authentication.AnonymousAuthPlugin': {'allow_anonymous': False}}, {'tests.plugins.test_plugins.RetainedMessageEventPlugin': {}}] } broker = Broker(plugin_namespace='tests.mock_plugins', config=config) await broker.start() await asyncio.sleep(0.1) # make sure all expected events get triggered client1 = MQTTClient(config={'auto_reconnect': False}) await client1.connect("mqtt://myUsername@127.0.0.1:1883/", cleansession=False) await client1.subscribe([('test/topic', QOS_1),]) await client1.publish('test/retained', b'message should be retained for test/retained', retain=True) await asyncio.sleep(0.1) await client1.disconnect() client2 = MQTTClient(config={'auto_reconnect': False}) await client2.connect("mqtt://myOtherUsername@127.0.0.1:1883/", cleansession=True) await client2.publish('test/topic', b'message should be retained for myUsername since subscription was qos > 0') await client2.publish('test/retained', b'', retain=True) # should clear previously retained message await asyncio.sleep(0.1) await client2.disconnect() await asyncio.sleep(0.1) # get the plugin so it doesn't get gc on shutdown test_plugin = broker.plugins_manager.get_plugin('RetainedMessageEventPlugin') await broker.shutdown() await asyncio.sleep(0.1) assert test_plugin.topic_retained_message_flag, "message to topic wasn't retained" assert test_plugin.session_retained_message_flag, "message to disconnected client wasn't retained" assert test_plugin.topic_clear_retained_message_flag, "message to retained topic wasn't cleared" Yakifo-amqtt-2637127/tests/plugins/test_sys.py000066400000000000000000000123151504664204300213350ustar00rootroot00000000000000import asyncio import logging from importlib.metadata import EntryPoint from logging.config import dictConfig from unittest.mock import patch import pytest from amqtt.broker import Broker from amqtt.client import MQTTClient from amqtt.mqtt.constants import QOS_0 dictConfig({ 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'verbose': { 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s' } }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'level': 'DEBUG', 'formatter': 'verbose', } }, 'loggers': { 'transitions': { 'level': 'WARNING', } } }) # logging.basicConfig(level=logging.DEBUG, format=formatter) logger = logging.getLogger(__name__) all_sys_topics = [ '$SYS/broker/version', '$SYS/broker/load/bytes/received', '$SYS/broker/load/bytes/sent', '$SYS/broker/messages/received', '$SYS/broker/messages/sent', '$SYS/broker/time', '$SYS/broker/uptime', '$SYS/broker/uptime/formatted', '$SYS/broker/clients/connected', '$SYS/broker/clients/disconnected', '$SYS/broker/clients/maximum', '$SYS/broker/clients/total', '$SYS/broker/messages/inflight', '$SYS/broker/messages/inflight/in', '$SYS/broker/messages/inflight/out', '$SYS/broker/messages/inflight/stored', '$SYS/broker/messages/publish/received', '$SYS/broker/messages/publish/sent', '$SYS/broker/messages/retained/count', '$SYS/broker/messages/subscriptions/count', '$SYS/broker/heap/size', '$SYS/broker/heap/maximum', '$SYS/broker/cpu/percent', '$SYS/broker/cpu/maximum', ] # test broker sys @pytest.mark.asyncio async def test_broker_sys_plugin_deprecated_config() -> None: sys_topic_flags = {sys_topic:False for sys_topic in all_sys_topics} class MockEntryPoints: def select(self, group) -> list[EntryPoint]: match group: case 'tests.mock_plugins': return [ EntryPoint(name='broker_sys', group='tests.mock_plugins', value='amqtt.plugins.sys.broker:BrokerSysPlugin'), EntryPoint(name='auth_anonymous', group='test.mock_plugins', value='amqtt.plugins.authentication:AnonymousAuthPlugin'), ] case _: return list() with patch("amqtt.plugins.manager.entry_points", side_effect=MockEntryPoints) as mocked_mqtt_publish: config = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10}, }, 'sys_interval': 1, 'auth': { 'allow_anonymous': True } } broker = Broker(plugin_namespace='tests.mock_plugins', config=config) await broker.start() client = MQTTClient() await client.connect("mqtt://127.0.0.1:1883/") await client.subscribe([("$SYS/#", QOS_0),]) await client.publish('test/topic', b'my test message') await asyncio.sleep(2) sys_msg_count = 0 try: while sys_msg_count < 30: message = await client.deliver_message(timeout_duration=1) if '$SYS' in message.topic: sys_msg_count += 1 assert message.topic in sys_topic_flags sys_topic_flags[message.topic] = True except asyncio.TimeoutError: logger.debug(f"TimeoutError after {sys_msg_count} messages") await client.disconnect() await broker.shutdown() assert sys_msg_count > 1 assert all(sys_topic_flags.values()), f'topic not received: {[ topic for topic, flag in sys_topic_flags.items() if not flag ]}' @pytest.mark.asyncio async def test_broker_sys_plugin_config() -> None: sys_topic_flags = {sys_topic:False for sys_topic in all_sys_topics} config = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10}, }, 'plugins': [ {'amqtt.plugins.authentication.AnonymousAuthPlugin': {'allow_anonymous': True}}, {'amqtt.plugins.sys.broker.BrokerSysPlugin': {'sys_interval': 1}}, ] } broker = Broker(plugin_namespace='tests.mock_plugins', config=config) await broker.start() client = MQTTClient() await client.connect("mqtt://127.0.0.1:1883/") await client.subscribe([("$SYS/#", QOS_0), ]) await client.publish('test/topic', b'my test message') await asyncio.sleep(2) sys_msg_count = 0 try: while sys_msg_count < 30: message = await client.deliver_message(timeout_duration=1) if '$SYS' in message.topic: sys_msg_count += 1 assert message.topic in sys_topic_flags sys_topic_flags[message.topic] = True except asyncio.TimeoutError: logger.debug(f"TimeoutError after {sys_msg_count} messages") await client.disconnect() await broker.shutdown() assert sys_msg_count > 1 assert all( sys_topic_flags.values()), f'topic not received: {[topic for topic, flag in sys_topic_flags.items() if not flag]}' Yakifo-amqtt-2637127/tests/plugins/test_topic_checking.py000066400000000000000000000401231504664204300234660ustar00rootroot00000000000000import logging import pytest from amqtt.broker import BrokerContext, Broker from amqtt.contexts import BaseContext, Action from amqtt.plugins.topic_checking import TopicAccessControlListPlugin, TopicTabooPlugin from amqtt.plugins.base import BaseTopicPlugin from amqtt.session import Session logger = logging.getLogger(__name__) # Base plug-in object @pytest.mark.asyncio async def test_base_no_config(logdog): """Check BaseTopicPlugin returns false if no topic-check is present.""" with logdog() as pile: context = BaseContext() context.logger = logging.getLogger("testlog") context.config = {} plugin = BaseTopicPlugin(context) authorised = await plugin.topic_filtering() assert authorised is False # Warning messages are only generated if using deprecated plugin configuration on initial load log_records = list(pile.drain(name="testlog")) assert len(log_records) == 1 assert log_records[0].levelno == logging.WARNING assert log_records[0].message == "'topic-check' section not found in context configuration" @pytest.mark.asyncio async def test_base_empty_config(logdog): """Check BaseTopicPlugin returns false if topic-check is empty.""" with logdog() as pile: broker = Broker() context = BrokerContext(broker) context.logger = logging.getLogger("testlog") context.config = {"topic-check": {}} plugin = BaseTopicPlugin(context) authorised = await plugin.topic_filtering() assert authorised is False # Warning messages are only generated if using deprecated plugin configuration on initial load log_records = list(pile.drain(name="testlog")) assert len(log_records) == 1 assert log_records[0].levelno == logging.WARNING assert log_records[0].message == "'topic-check' section not found in context configuration" @pytest.mark.asyncio async def test_base_disabled_config(logdog): """Check BaseTopicPlugin returns true if disabled. (it doesn't actually check).""" with logdog() as pile: context = BaseContext() context.logger = logging.getLogger("testlog") context.config = {"topic-check": {"enabled": False}} plugin = BaseTopicPlugin(context) authorised = await plugin.topic_filtering() assert authorised is True # Should NOT have printed warnings log_records = list(pile.drain(name="testlog")) assert len(log_records) == 0 @pytest.mark.asyncio async def test_base_enabled_config(logdog): """Check BaseTopicPlugin returns true if enabled.""" with logdog() as pile: context = BaseContext() context.logger = logging.getLogger("testlog") context.config = {"topic-check": {"enabled": True}} plugin = BaseTopicPlugin(context) authorised = await plugin.topic_filtering() assert authorised is True # Should NOT have printed warnings log_records = list(pile.drain(name="testlog")) assert len(log_records) == 0 # Taboo plug-in @pytest.mark.asyncio async def test_taboo_empty_config(logdog): """Check TopicTabooPlugin returns false if topic-check absent.""" with logdog() as pile: context = BaseContext() context.logger = logging.getLogger("testlog") context.config = {} plugin = TopicTabooPlugin(context) assert (await plugin.topic_filtering()) is False # Warning messages are only generated if using deprecated plugin configuration on initial load log_records = list(pile.drain(name="testlog")) assert len(log_records) == 1 assert log_records[0].levelno == logging.WARNING assert log_records[0].message == "'topic-check' section not found in context configuration" @pytest.mark.asyncio async def test_taboo_disabled(logdog): """Check TopicTabooPlugin returns true if checking disabled.""" with logdog() as pile: context = BaseContext() context.logger = logging.getLogger("testlog") context.config = {"topic-check": {"enabled": False}} session = Session() session.username = "anybody" plugin = TopicTabooPlugin(context) assert (await plugin.topic_filtering(session=session, topic="not/prohibited")) is True # Should NOT have printed warnings log_records = list(pile.drain(name="testlog")) assert len(log_records) == 0 @pytest.mark.parametrize("test_config", [ ({"topic-check": {"enabled": True}}), (TopicTabooPlugin.Config()) ]) @pytest.mark.asyncio async def test_taboo_not_taboo_topic(logdog, test_config): """Check TopicTabooPlugin returns true if topic not taboo.""" with logdog() as pile: context = BaseContext() context.logger = logging.getLogger("testlog") context.config = test_config session = Session() session.username = "anybody" plugin = TopicTabooPlugin(context) assert (await plugin.topic_filtering(session=session, topic="not/prohibited")) is True # Should NOT have printed warnings log_records = list(pile.drain(name="testlog")) assert len(log_records) == 0 @pytest.mark.parametrize("test_config", [ ({"topic-check": {"enabled": True}}), (TopicTabooPlugin.Config()) ]) @pytest.mark.asyncio async def test_taboo_anon_taboo_topic(logdog, test_config): """Check TopicTabooPlugin returns false if topic is taboo and session is anonymous.""" with logdog() as pile: context = BaseContext() context.logger = logging.getLogger("testlog") context.config = test_config session = Session() session.username = "" plugin = TopicTabooPlugin(context) assert (await plugin.topic_filtering(session=session, topic="prohibited")) is False # Should NOT have printed warnings log_records = list(pile.drain(name="testlog")) assert len(log_records) == 0 @pytest.mark.parametrize("test_config", [ ({"topic-check": {"enabled": True}}), (TopicTabooPlugin.Config()) ]) @pytest.mark.asyncio async def test_taboo_notadmin_taboo_topic(logdog, test_config): """Check TopicTabooPlugin returns false if topic is taboo and user is not "admin".""" with logdog() as pile: context = BaseContext() context.logger = logging.getLogger("testlog") context.config = test_config session = Session() session.username = "notadmin" plugin = TopicTabooPlugin(context) assert (await plugin.topic_filtering(session=session, topic="prohibited")) is False # Should NOT have printed warnings log_records = list(pile.drain(name="testlog")) assert len(log_records) == 0 @pytest.mark.parametrize("test_config", [ ({"topic-check": {"enabled": True}}), (TopicTabooPlugin.Config()) ]) @pytest.mark.asyncio async def test_taboo_admin_taboo_topic(logdog, test_config): """Check TopicTabooPlugin returns true if topic is taboo and user is "admin".""" with logdog() as pile: context = BaseContext() context.logger = logging.getLogger("testlog") context.config = test_config session = Session() session.username = "admin" plugin = TopicTabooPlugin(context) assert (await plugin.topic_filtering(session=session, topic="prohibited")) is True # Should NOT have printed warnings log_records = list(pile.drain(name="testlog")) assert len(log_records) == 0 # TopicAccessControlListPlugin tests def test_topic_ac_not_match(): """Test TopicAccessControlListPlugin.topic_ac returns false if topics do not match.""" assert TopicAccessControlListPlugin.topic_ac("a/topic/to/match", "a/topic/to/notmatch") is False def test_topic_ac_not_match_longer_acl(): """Test TopicAccessControlListPlugin.topic_ac returns false if topics do not match and ACL topic is longer.""" assert TopicAccessControlListPlugin.topic_ac("topic", "topic/is/longer") is False def test_topic_ac_not_match_longer_rq(): """Test TopicAccessControlListPlugin.topic_ac returns false if topics do not match and RQ topic is longer.""" assert TopicAccessControlListPlugin.topic_ac("topic/is/longer", "topic") is False def test_topic_ac_match_exact(): """Test TopicAccessControlListPlugin.topic_ac returns true if topics match exactly.""" assert TopicAccessControlListPlugin.topic_ac("exact/topic", "exact/topic") is True def test_topic_ac_match_plus(): """Test TopicAccessControlListPlugin.topic_ac correctly handles '+' wildcard.""" assert ( TopicAccessControlListPlugin.topic_ac( "a/topic/anything/value", "a/topic/+/value", ) is True ) def test_topic_ac_match_hash(): """Test TopicAccessControlListPlugin.topic_ac correctly handles '#' wildcard.""" assert ( TopicAccessControlListPlugin.topic_ac( "topic/prefix/and/suffix", "topic/prefix/#", ) is True ) @pytest.mark.asyncio async def test_taclp_empty_config(logdog): """Check TopicAccessControlListPlugin returns false if topic-check absent.""" with logdog() as pile: context = BaseContext() context.logger = logging.getLogger("testlog") context.config = {} plugin = TopicAccessControlListPlugin(context) assert (await plugin.topic_filtering()) is False # Warning messages are only generated if using deprecated plugin configuration on initial load log_records = list(pile.drain(name="testlog")) assert len(log_records) == 1 assert log_records[0].levelno == logging.WARNING assert log_records[0].message == "'topic-check' section not found in context configuration" @pytest.mark.asyncio async def test_taclp_true_disabled(logdog): """Check TopicAccessControlListPlugin returns true if topic checking is disabled.""" context = BaseContext() context.logger = logging.getLogger("testlog") context.config = {"topic-check": {"enabled": False}} session = Session() session.username = "user" plugin = TopicAccessControlListPlugin(context) authorised = await plugin.topic_filtering( action=Action.PUBLISH, session=session, topic="a/topic", ) assert authorised is True @pytest.mark.parametrize("test_config", [ ({"topic-check": {"enabled": True}}), (TopicAccessControlListPlugin.Config()) ]) @pytest.mark.asyncio async def test_taclp_true_no_pub_acl(logdog, test_config): """Check TopicAccessControlListPlugin returns true if action=publish and no publish-acl given. (This is for backward-compatibility with existing installations.). """ context = BaseContext() context.logger = logging.getLogger("testlog") context.config = test_config session = Session() session.username = "user" plugin = TopicAccessControlListPlugin(context) authorised = await plugin.topic_filtering( action=Action.PUBLISH, session=session, topic="a/topic", ) assert authorised is True @pytest.mark.parametrize("test_config", [ ({ "topic-check": { "enabled": True, "acl": {"anotheruser": ["allowed/topic", "another/allowed/topic/#"]}, }, }), (TopicAccessControlListPlugin.Config( acl={"anotheruser": ["allowed/topic", "another/allowed/topic/#"]} )) ]) @pytest.mark.asyncio async def test_taclp_false_sub_no_topic(logdog, test_config): """Check TopicAccessControlListPlugin returns false user there is no topic.""" context = BaseContext() context.logger = logging.getLogger("testlog") context.config = test_config session = Session() session.username = "user" plugin = TopicAccessControlListPlugin(context) authorised = await plugin.topic_filtering( action=Action.SUBSCRIBE, session=session, topic="", ) assert authorised is False @pytest.mark.parametrize("test_config", [ ({ "topic-check": { "enabled": True, "acl": {"anotheruser": ["allowed/topic", "another/allowed/topic/#"]}, }, }), (TopicAccessControlListPlugin.Config( acl={"anotheruser": ["allowed/topic", "another/allowed/topic/#"]} )) ]) @pytest.mark.asyncio async def test_taclp_false_sub_unknown_user(logdog, test_config): """Check TopicAccessControlListPlugin returns false user is not listed in ACL.""" context = BaseContext() context.logger = logging.getLogger("testlog") context.config = test_config session = Session() session.username = "user" plugin = TopicAccessControlListPlugin(context) authorised = await plugin.topic_filtering( action=Action.SUBSCRIBE, session=session, topic="allowed/topic", ) assert authorised is False @pytest.mark.parametrize("test_config", [ ({ "topic-check": { "enabled": True, "acl": {"user": ["allowed/topic", "another/allowed/topic/#"]}, }, }), (TopicAccessControlListPlugin.Config( acl={"user": ["allowed/topic", "another/allowed/topic/#"]} )) ]) @pytest.mark.asyncio async def test_taclp_false_sub_no_permission(logdog, test_config): """Check TopicAccessControlListPlugin returns false if "acl" does not list allowed topic.""" context = BaseContext() context.logger = logging.getLogger("testlog") context.config = test_config session = Session() session.username = "user" plugin = TopicAccessControlListPlugin(context) authorised = await plugin.topic_filtering( action=Action.SUBSCRIBE, session=session, topic="forbidden/topic", ) assert authorised is False @pytest.mark.parametrize("test_config", [ ({ "topic-check": { "enabled": True, "acl": {"user": ["allowed/topic", "another/allowed/topic/#"]}, }, }), (TopicAccessControlListPlugin.Config( acl={"user": ["allowed/topic", "another/allowed/topic/#"]} )) ]) @pytest.mark.asyncio async def test_taclp_true_sub_permission(logdog, test_config): """Check TopicAccessControlListPlugin returns true if "acl" lists allowed topic.""" context = BaseContext() context.logger = logging.getLogger("testlog") context.config = test_config session = Session() session.username = "user" plugin = TopicAccessControlListPlugin(context) authorised = await plugin.topic_filtering( action=Action.SUBSCRIBE, session=session, topic="allowed/topic", ) assert authorised is True @pytest.mark.parametrize("test_config", [ ({ "topic-check": { "enabled": True, "publish-acl": {"user": ["allowed/topic", "another/allowed/topic/#"]}, }, }), (TopicAccessControlListPlugin.Config( publish_acl={"user": ["allowed/topic", "another/allowed/topic/#"]} )) ]) @pytest.mark.asyncio async def test_taclp_true_pub_permission(logdog, test_config): """Check TopicAccessControlListPlugin returns true if "publish-acl" lists allowed topic for publish action.""" context = BaseContext() context.logger = logging.getLogger("testlog") context.config = test_config session = Session() session.username = "user" plugin = TopicAccessControlListPlugin(context) authorised = await plugin.topic_filtering( action=Action.PUBLISH, session=session, topic="allowed/topic", ) assert authorised is True @pytest.mark.parametrize("test_config", [ ({ "topic-check": { "enabled": True, "acl": {"anonymous": ["allowed/topic", "another/allowed/topic/#"]}, }, }), (TopicAccessControlListPlugin.Config( acl={"anonymous": ["allowed/topic", "another/allowed/topic/#"]} )) ]) @pytest.mark.asyncio async def test_taclp_true_anon_sub_permission(logdog, test_config): """Check TopicAccessControlListPlugin handles anonymous users.""" context = BaseContext() context.logger = logging.getLogger("testlog") context.config = test_config session = Session() session.username = None plugin = TopicAccessControlListPlugin(context) authorised = await plugin.topic_filtering( action=Action.SUBSCRIBE, session=session, topic="allowed/topic", ) assert authorised is True Yakifo-amqtt-2637127/tests/test_adapters.py000066400000000000000000000024621504664204300206430ustar00rootroot00000000000000import ssl import pytest from amqtt.adapters import ReaderAdapter, WriterAdapter class BrokenReaderAdapter(ReaderAdapter): async def read(self, n: int = -1) -> bytes: return await super().read(n) def feed_eof(self) -> None: return super().feed_eof() @pytest.mark.asyncio async def test_abstract_read_raises(): reader = BrokenReaderAdapter() with pytest.raises(NotImplementedError): await reader.read() with pytest.raises(NotImplementedError): reader.feed_eof() class BrokerWriterAdapter(WriterAdapter): def write(self, data: bytes) -> None: super().write(data) async def drain(self) -> None: await super().drain() def get_peer_info(self) -> tuple[str, int] | None: return super().get_peer_info() def get_ssl_info(self) -> ssl.SSLObject | None: return None async def close(self) -> None: await super().close() @pytest.mark.asyncio async def test_abstract_write_raises(): writer = BrokerWriterAdapter() with pytest.raises(NotImplementedError): writer.write(b'') with pytest.raises(NotImplementedError): await writer.drain() with pytest.raises(NotImplementedError): writer.get_peer_info() with pytest.raises(NotImplementedError): await writer.close() Yakifo-amqtt-2637127/tests/test_broker.py000066400000000000000000000753131504664204300203310ustar00rootroot00000000000000import asyncio import logging import logging.config import secrets import socket import string from unittest.mock import MagicMock, call, patch import psutil import pytest from amqtt.events import BrokerEvents from amqtt.adapters import StreamReaderAdapter, StreamWriterAdapter from amqtt.broker import Broker from amqtt.client import MQTTClient from amqtt.errors import ConnectError from amqtt.mqtt.connack import ConnackPacket from amqtt.mqtt.connect import ConnectPacket, ConnectPayload, ConnectVariableHeader from amqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 from amqtt.mqtt.disconnect import DisconnectPacket from amqtt.mqtt.protocol.broker_handler import BrokerProtocolHandler from amqtt.mqtt.pubcomp import PubcompPacket from amqtt.mqtt.publish import PublishPacket from amqtt.mqtt.pubrec import PubrecPacket from amqtt.mqtt.pubrel import PubrelPacket from amqtt.session import OutgoingApplicationMessage # formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" # logging.basicConfig(level=logging.DEBUG, format=formatter) LOGGING_CONFIG = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'default': { 'format': '[%(asctime)s] %(levelname)s %(name)s: %(message)s', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'level': 'DEBUG', 'formatter': 'default', 'stream': 'ext://sys.stdout', } }, 'root': { 'handlers': ['console'], 'level': 'DEBUG', }, 'loggers': { 'transitions': { 'handlers': ['console'], 'level': 'WARNING', 'propagate': False, }, }, } logging.config.dictConfig(LOGGING_CONFIG) log = logging.getLogger(__name__) # monkey patch MagicMock # taken from https://stackoverflow.com/questions/51394411/python-object-magicmock-cant-be-used-in-await-expression async def async_magic(): pass MagicMock.__await__ = lambda _: async_magic().__await__() @pytest.mark.parametrize( "input_str, output_addr, output_port", [ ("1234", None, 1234), (":1234", None, 1234), ("0.0.0.0:1234", "0.0.0.0", 1234), ("[::]:1234", "[::]", 1234), ("0.0.0.0", "0.0.0.0", 5678), ("[::]", "[::]", 5678), ("localhost", "localhost", 5678), ("localhost:1234", "localhost", 1234), ], ) def test_split_bindaddr_port(input_str, output_addr, output_port): assert Broker._split_bindaddr_port(input_str, 5678) == (output_addr, output_port) @pytest.mark.asyncio async def test_start_stop(broker, mock_plugin_manager): mock_plugin_manager.assert_has_calls( [ call().fire_event(BrokerEvents.PRE_START), call().fire_event(BrokerEvents.POST_START), ], any_order=True, ) mock_plugin_manager.reset_mock() await broker.shutdown() mock_plugin_manager.assert_has_calls( [ call().fire_event(BrokerEvents.PRE_SHUTDOWN), call().fire_event(BrokerEvents.POST_SHUTDOWN), ], any_order=True, ) assert broker.transitions.is_stopped() @pytest.mark.asyncio async def test_client_connect(broker, mock_plugin_manager): client = MQTTClient(config={'auto_reconnect':False}) ret = await client.connect("mqtt://127.0.0.1/") assert ret == 0 assert client.session is not None assert client.session.client_id in broker._sessions await client.disconnect() await asyncio.sleep(0.01) broker.plugins_manager.fire_event.assert_called() assert broker.plugins_manager.fire_event.call_count > 2 # double indexing is ugly, but call_args_list returns a tuple of tuples events = [c[0][0] for c in broker.plugins_manager.fire_event.call_args_list] assert BrokerEvents.CLIENT_CONNECTED in events assert BrokerEvents.CLIENT_DISCONNECTED in events @pytest.mark.asyncio async def _connect_tcp(broker): process = psutil.Process() connections_number = 10 # mqtt 3.1 requires a connect packet, otherwise the socket connection is rejected sockets = [] for i in range(connections_number): static_connect_packet = b'\x10\x1b\x00\x04MQTT\x04\x02\x00<\x00\x0ftest-client-12' + f"{i}".encode() s = socket.create_connection(("127.0.0.1", 1883)) s.send(static_connect_packet) sockets.append(s) # Wait for a brief moment to ensure connections are established await asyncio.sleep(0.1) # # Get the current number of TCP connections connections = process.net_connections() # max number of connections on the TCP listener is 10 assert broker._servers["default"].conn_count == connections_number # Ensure connections are only on the TCP listener (port 1883) tcp_connections = [conn for conn in connections if conn.laddr.port == 1883] assert len(tcp_connections) == connections_number + 1 # Including the Broker's listening socket await asyncio.sleep(0.1) for conn in connections: assert conn.status in ("ESTABLISHED", "LISTEN") await asyncio.sleep(0.1) # close all connections for s in sockets: s.close() # Wait a moment for connections to be closed await asyncio.sleep(0.1) # Recheck connections after closing connections = process.net_connections() tcp_connections = [conn for conn in connections if conn.laddr.port == 1883] for conn in tcp_connections: assert conn.status in ("CLOSE_WAIT", "LISTEN") # Ensure no active connections for the default listener assert broker._servers["default"].conn_count == 0 # Add one more connection to the TCP listener s = socket.create_connection(("127.0.0.1", 1883)) s.send(static_connect_packet) open_connections = [] open_connections = [conn for conn in process.net_connections() if conn.status == "ESTABLISHED"] # Ensure that only one TCP connection is active now assert len(open_connections) == 1 await asyncio.sleep(0.1) assert broker._servers["default"].conn_count == 1 @pytest.mark.asyncio async def test_client_connect_will_flag(broker): conn_reader, conn_writer = await asyncio.open_connection("127.0.0.1", 1883) reader = StreamReaderAdapter(conn_reader) writer = StreamWriterAdapter(conn_writer) vh = ConnectVariableHeader() payload = ConnectPayload() vh.keep_alive = 10 vh.clean_session_flag = False vh.will_retain_flag = False vh.will_flag = True vh.will_qos = QOS_0 payload.client_id = "test_id" payload.will_message = b"test" payload.will_topic = "/topic" connect = ConnectPacket(variable_header=vh, payload=payload) await connect.to_stream(writer) await ConnackPacket.from_stream(reader) await asyncio.sleep(0.1) disconnect = DisconnectPacket() await disconnect.to_stream(writer) await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_connect_clean_session_false(broker): client = MQTTClient(client_id="", config={"auto_reconnect": False}) return_code = None try: await client.connect("mqtt://127.0.0.1", cleansession=False) except ConnectError as ce: return_code = ce.return_code assert return_code == 0x02 assert client.session is not None assert client.session.client_id not in broker._sessions await client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_subscribe(broker, mock_plugin_manager): client = MQTTClient() ret = await client.connect("mqtt://127.0.0.1/") assert ret == 0 await client.subscribe([("/topic", QOS_0)]) # Test if the client test client subscription is registered assert "/topic" in broker._subscriptions subs = broker._subscriptions["/topic"] assert len(subs) == 1 (s, qos) = subs[0] assert s == client.session assert qos == QOS_0 await client.disconnect() await asyncio.sleep(0.1) mock_plugin_manager.assert_has_calls( [ call().fire_event( BrokerEvents.CLIENT_SUBSCRIBED, client_id=client.session.client_id, topic="/topic", qos=QOS_0, ), ], any_order=True, ) @pytest.mark.asyncio async def test_client_subscribe_twice(broker, mock_plugin_manager): client = MQTTClient() ret = await client.connect("mqtt://127.0.0.1/") assert ret == 0 await client.subscribe([("/topic", QOS_0)]) # Test if the client test client subscription is registered assert "/topic" in broker._subscriptions subs = broker._subscriptions["/topic"] assert len(subs) == 1 (s, qos) = subs[0] assert s == client.session assert qos == QOS_0 await client.subscribe([("/topic", QOS_0)]) assert len(subs) == 1 (s, qos) = subs[0] assert s == client.session assert qos == QOS_0 await client.disconnect() await asyncio.sleep(0.1) mock_plugin_manager.assert_has_calls( [ call().fire_event( BrokerEvents.CLIENT_SUBSCRIBED, client_id=client.session.client_id, topic="/topic", qos=QOS_0, ), ], any_order=True, ) @pytest.mark.asyncio async def test_client_unsubscribe(broker, mock_plugin_manager): client = MQTTClient() ret = await client.connect("mqtt://127.0.0.1/") assert ret == 0 await client.subscribe([("/topic", QOS_0)]) # Test if the client test client subscription is registered assert "/topic" in broker._subscriptions subs = broker._subscriptions["/topic"] assert len(subs) == 1 (s, qos) = subs[0] assert s == client.session assert qos == QOS_0 await client.unsubscribe(["/topic"]) await asyncio.sleep(0.1) assert broker._subscriptions["/topic"] == [] await client.disconnect() await asyncio.sleep(0.1) mock_plugin_manager.assert_has_calls( [ call().fire_event( BrokerEvents.CLIENT_SUBSCRIBED, client_id=client.session.client_id, topic="/topic", qos=QOS_0, ), call().fire_event( BrokerEvents.CLIENT_UNSUBSCRIBED, client_id=client.session.client_id, topic="/topic", ), ], any_order=True, ) @pytest.mark.asyncio async def test_client_publish(broker, mock_plugin_manager): pub_client = MQTTClient() ret = await pub_client.connect("mqtt://127.0.0.1/") assert ret == 0 ret_message = await pub_client.publish("/topic", b"data", QOS_0) await pub_client.disconnect() assert broker._retained_messages == {} await asyncio.sleep(0.1) assert pub_client.session is not None mock_plugin_manager.assert_has_calls( [ call().fire_event( BrokerEvents.MESSAGE_RECEIVED, client_id=pub_client.session.client_id, message=ret_message, ), ], any_order=True, ) @pytest.mark.asyncio async def test_client_publish_acl_permitted(acl_broker): sub_client = MQTTClient() ret_conn = await sub_client.connect("mqtt://user2:user2password@127.0.0.1:1884/") assert ret_conn == 0 ret_sub = await sub_client.subscribe([("public/subtopic/test", QOS_0)]) assert ret_sub == [QOS_0] pub_client = MQTTClient() ret_conn = await pub_client.connect("mqtt://user1:user1password@127.0.0.1:1884/") assert ret_conn == 0 await pub_client.publish("public/subtopic/test", b"data", QOS_0) message = await sub_client.deliver_message(timeout_duration=1) await pub_client.disconnect() await sub_client.disconnect() assert message is not None assert message.topic == "public/subtopic/test" assert message.data == b"data" assert message.qos == QOS_0 @pytest.mark.asyncio async def test_client_publish_acl_forbidden(acl_broker): sub_client = MQTTClient() ret_conn = await sub_client.connect("mqtt://user2:user2password@127.0.0.1:1884/") assert ret_conn == 0 ret_sub = await sub_client.subscribe([("public/forbidden/test", QOS_0)]) assert ret_sub == [QOS_0] pub_client = MQTTClient() ret_conn = await pub_client.connect("mqtt://user1:user1password@127.0.0.1:1884/") assert ret_conn == 0 await pub_client.publish("public/forbidden/test", b"data", QOS_0) try: await sub_client.deliver_message(timeout_duration=1) msg = "Should not have worked" raise AssertionError(msg) except Exception: pass await pub_client.disconnect() await sub_client.disconnect() @pytest.mark.asyncio async def test_client_publish_acl_permitted_sub_forbidden(acl_broker): sub_client1 = MQTTClient(client_id="sub_client1") ret_conn = await sub_client1.connect("mqtt://user2:user2password@127.0.0.1:1884/") assert ret_conn == 0 sub_client2 = MQTTClient(client_id="sub_client2") ret_conn = await sub_client2.connect("mqtt://user3:user3password@127.0.0.1:1884/") assert ret_conn == 0 ret_sub = await sub_client1.subscribe([("public/subtopic/test", QOS_0)]) assert ret_sub == [QOS_0] ret_sub = await sub_client2.subscribe([("public/subtopic/test", QOS_0)]) assert ret_sub == [128] pub_client = MQTTClient(client_id="pub_client") ret_conn = await pub_client.connect("mqtt://user1:user1password@127.0.0.1:1884/") assert ret_conn == 0 await pub_client.publish("public/subtopic/test", b"data", QOS_0) message = await sub_client1.deliver_message(timeout_duration=1) try: await sub_client2.deliver_message(timeout_duration=1) msg = "Should not have worked" raise AssertionError(msg) except Exception: pass await pub_client.disconnect() await sub_client1.disconnect() await sub_client2.disconnect() assert message is not None assert message.topic == "public/subtopic/test" assert message.data == b"data" assert message.qos == QOS_0 @pytest.mark.asyncio async def test_client_publish_dup(broker): conn_reader, conn_writer = await asyncio.open_connection("127.0.0.1", 1883) reader = StreamReaderAdapter(conn_reader) writer = StreamWriterAdapter(conn_writer) vh = ConnectVariableHeader() payload = ConnectPayload() vh.keep_alive = 10 vh.clean_session_flag = False vh.will_retain_flag = False payload.client_id = "test_id" connect = ConnectPacket(variable_header=vh, payload=payload) await connect.to_stream(writer) await ConnackPacket.from_stream(reader) publish_1 = PublishPacket.build("/test", b"data", 1, False, QOS_2, False) await publish_1.to_stream(writer) # Store the future of PubrecPacket.from_stream() in a variable pubrec_task_1 = asyncio.ensure_future(PubrecPacket.from_stream(reader)) await asyncio.sleep(2) publish_dup = PublishPacket.build("/test", b"data", 1, True, QOS_2, False) await publish_dup.to_stream(writer) await PubrecPacket.from_stream(reader) pubrel = PubrelPacket.build(1) await pubrel.to_stream(writer) await PubcompPacket.from_stream(reader) # Ensure we wait for the Pubrec packets to be processed await pubrec_task_1 disconnect = DisconnectPacket() await disconnect.to_stream(writer) @pytest.mark.asyncio async def test_client_publishing_invalid_topic(broker): assert broker.transitions.is_started() pub_client = MQTTClient(config={'auto_reconnect': False}) ret = await pub_client.connect("mqtt://127.0.0.1/") assert ret == 0 await pub_client.subscribe([ ("my/+/topic", QOS_0) ]) await asyncio.sleep(0.5) # need to build & send packet directly to bypass client's check of invalid topic name # see test_client.py::test_publish_to_incorrect_wildcard for client checks packet = PublishPacket.build(topic_name='my/topic', message=b'messages', packet_id=None, dup_flag=False, qos=QOS_0, retain=False) packet.topic_name = "my/+/topic" await pub_client._handler._send_packet(packet) await asyncio.sleep(0.5) with pytest.raises(asyncio.TimeoutError): msg = await pub_client.deliver_message(timeout_duration=1) assert msg is None await asyncio.sleep(0.1) await pub_client.disconnect() @pytest.mark.asyncio async def test_client_publish_asterisk(broker): """'*' is a valid, non-wildcard character for MQTT.""" assert broker.transitions.is_started() pub_client = MQTTClient(config={'auto_reconnect': False}) ret = await pub_client.connect("mqtt://127.0.0.1/") assert ret == 0 await pub_client.subscribe([ ("my*/topic", QOS_0), ("my/+/topic", QOS_0) ]) await asyncio.sleep(0.1) await pub_client.publish('my*/topic', b'my valid message', QOS_0, retain=False) await asyncio.sleep(0.1) msg = await pub_client.deliver_message(timeout_duration=1) assert msg is not None assert msg.topic == "my*/topic" assert msg.data == b'my valid message' await asyncio.sleep(0.1) msg = await pub_client.publish('my/****/topic', b'my valid message', QOS_0, retain=False) assert msg is not None assert msg.topic == "my/****/topic" assert msg.data == b'my valid message' await pub_client.disconnect() @pytest.mark.asyncio async def test_client_publish_big(broker, mock_plugin_manager): pub_client = MQTTClient() ret = await pub_client.connect("mqtt://127.0.0.1/") assert ret == 0 ret_message = await pub_client.publish( "/topic", bytearray(b"\x99" * 256 * 1024), QOS_2, ) await pub_client.disconnect() assert broker._retained_messages == {} await asyncio.sleep(0.1) assert pub_client.session is not None mock_plugin_manager.assert_has_calls( [ call().fire_event( BrokerEvents.MESSAGE_RECEIVED, client_id=pub_client.session.client_id, message=ret_message, ), ], any_order=True, ) @pytest.mark.asyncio async def test_client_publish_retain(broker): pub_client = MQTTClient() ret = await pub_client.connect("mqtt://127.0.0.1/") assert ret == 0 await pub_client.publish("/topic", b"data", QOS_0, retain=True) await pub_client.disconnect() await asyncio.sleep(0.1) assert "/topic" in broker._retained_messages retained_message = broker._retained_messages["/topic"] assert retained_message.source_session == pub_client.session assert retained_message.topic == "/topic" assert retained_message.data == b"data" assert retained_message.qos == QOS_0 @pytest.mark.asyncio async def test_client_publish_retain_delete(broker): pub_client = MQTTClient() ret = await pub_client.connect("mqtt://127.0.0.1/") assert ret == 0 await pub_client.publish("/topic", b"", QOS_0, retain=True) await pub_client.disconnect() await asyncio.sleep(0.1) assert "/topic" not in broker._retained_messages @pytest.mark.asyncio async def test_client_subscribe_publish(broker): sub_client = MQTTClient() await sub_client.connect("mqtt://127.0.0.1") ret = await sub_client.subscribe( [("/qos0", QOS_0), ("/qos1", QOS_1), ("/qos2", QOS_2)], ) assert ret == [QOS_0, QOS_1, QOS_2] await _client_publish("/qos0", b"data", QOS_0) await _client_publish("/qos1", b"data", QOS_1) await _client_publish("/qos2", b"data", QOS_2) await asyncio.sleep(0.1) for qos in [QOS_0, QOS_1, QOS_2]: message = await sub_client.deliver_message() assert message is not None assert message.topic == f"/qos{qos}" assert message.data == b"data" assert message.qos == qos await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_subscribe_invalid(broker): sub_client = MQTTClient() await sub_client.connect("mqtt://127.0.0.1") ret = await sub_client.subscribe( [ ("+", QOS_0), ("+/tennis/#", QOS_0), ("sport+", QOS_0), ("sport/+/player1", QOS_0), ], ) assert ret == [QOS_0, QOS_0, 0x80, QOS_0] await asyncio.sleep(0.1) await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_subscribe_publish_dollar_topic_1(broker): assert broker.transitions.is_started() sub_client = MQTTClient() await sub_client.connect("mqtt://127.0.0.1") ret = await sub_client.subscribe([("#", QOS_0)]) assert ret == [QOS_0] await _client_publish("/topic", b"data", QOS_0) message = await sub_client.deliver_message() assert message is not None await _client_publish("$topic", b"data", QOS_0) await asyncio.sleep(0.1) message = None try: message = await sub_client.deliver_message(timeout_duration=2) except Exception: pass except RuntimeError as e: # The loop is closed with pending tasks. Needs fine tuning. log.warning(e) assert message is None await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_subscribe_publish_dollar_topic_2(broker): sub_client = MQTTClient() await sub_client.connect("mqtt://127.0.0.1") ret = await sub_client.subscribe([("+/monitor/Clients", QOS_0)]) assert ret == [QOS_0] await _client_publish("test/monitor/Clients", b"data", QOS_0) message = await sub_client.deliver_message() assert message is not None await _client_publish("$SYS/monitor/Clients", b"data", QOS_0) await asyncio.sleep(0.1) message = None try: message = await sub_client.deliver_message(timeout_duration=2) except Exception: pass except RuntimeError as e: # The loop is closed with pending tasks. Needs fine tuning. log.warning(e) assert message is None await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_publish_clean_session_subscribe(broker): sub_client = MQTTClient(client_id='test_client', config={'auto_reconnect': False}) await sub_client.connect("mqtt://127.0.0.1", cleansession=False) ret = await sub_client.subscribe( [("/qos0", QOS_0), ("/qos1", QOS_1), ("/qos2", QOS_2)], ) assert ret == [QOS_0, QOS_1, QOS_2] await sub_client.disconnect() await asyncio.sleep(0.5) await _client_publish("/qos0", b"data0", QOS_0) # should not be retained await _client_publish("/qos1", b"data1", QOS_1) await _client_publish("/qos2", b"data2", QOS_2) await asyncio.sleep(2) await sub_client.reconnect(cleansession=False) for qos in [QOS_1, QOS_2]: log.debug(f"TEST QOS: {qos}") message = await sub_client.deliver_message(timeout_duration=2) log.debug(f"Message: {message.publish_packet if message else None!r}") assert message is not None assert message.topic == f"/qos{qos}" assert message.data == f"data{qos}".encode("utf-8") assert message.qos == qos try: while True: message = await sub_client.deliver_message(timeout_duration=1) assert message is not None, "no other messages should have been retained" except asyncio.TimeoutError: pass await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_publish_retain_with_new_subscribe(broker): await asyncio.sleep(2) sub_client1 = MQTTClient(client_id='test_client1') await sub_client1.connect("mqtt://127.0.0.1") await sub_client1.disconnect() await asyncio.sleep(0.5) await _client_publish("/qos0", b"data0", QOS_0, retain=True) await asyncio.sleep(0.5) sub_client2 = MQTTClient(client_id='test_client2') await sub_client2.connect("mqtt://127.0.0.1") # should receive the retained message on subscription ret = await sub_client2.subscribe( [("/qos0", QOS_0)], ) assert ret == [QOS_0] message = await sub_client2.deliver_message(timeout_duration=1) assert message is not None assert message.topic == "/qos0" assert message.data == b"data0" assert message.qos == QOS_0 await sub_client2.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_publish_retain_latest_with_new_subscribe(broker): await asyncio.sleep(2) sub_client1 = MQTTClient(client_id='test_client1') await sub_client1.connect("mqtt://127.0.0.1") await sub_client1.disconnect() await asyncio.sleep(0.5) await _client_publish("/qos0", b"data a", QOS_0, retain=True) await asyncio.sleep(0.5) sub_client2 = MQTTClient(client_id='test_client2') await sub_client2.connect("mqtt://127.0.0.1") await _client_publish("/qos0", b"data b", QOS_0, retain=True) # should receive the retained message on subscription ret = await sub_client2.subscribe( [("/qos0", QOS_0)], ) assert ret == [QOS_0] message = await sub_client2.deliver_message(timeout_duration=1) assert message is not None assert message.topic == "/qos0" assert message.data == b"data b" assert message.qos == QOS_0 await sub_client2.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_publish_retain_subscribe_on_reconnect(broker): await asyncio.sleep(2) sub_client = MQTTClient(client_id='test_client') await sub_client.connect("mqtt://127.0.0.1", cleansession=False) ret = await sub_client.subscribe( [("/qos0", QOS_0)], ) assert ret == [QOS_0] await sub_client.disconnect() await asyncio.sleep(0.5) await _client_publish("/qos0", b"data0", QOS_0, retain=True) await asyncio.sleep(0.5) await sub_client.reconnect(cleansession=False) message = await sub_client.deliver_message(timeout_duration=1) assert message is not None assert message.topic == "/qos0" assert message.data == b"data0" assert message.qos == QOS_0 await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def _client_publish(topic, data, qos, retain=False) -> int | OutgoingApplicationMessage: gen_id = "pub_" valid_chars = string.ascii_letters + string.digits gen_id += "".join(secrets.choice(valid_chars) for _ in range(16)) pub_client = MQTTClient(client_id=gen_id) ret: int | OutgoingApplicationMessage = await pub_client.connect("mqtt://127.0.0.1/") assert ret == 0 ret = await pub_client.publish(topic, data, qos, retain) await pub_client.disconnect() return ret def test_matches_multi_level_wildcard(broker): test_filter = "sport/tennis/player1/#" for bad_topic in [ "sport/tennis", "sport/tennis/", ]: assert not broker._matches(bad_topic, test_filter) for good_topic in [ "sport/tennis/player1", "sport/tennis/player1/", "sport/tennis/player1/ranking", "sport/tennis/player1/score/wimbledon", ]: assert broker._matches(good_topic, test_filter) def test_matches_single_level_wildcard(broker): test_filter = "sport/tennis/+" for bad_topic in [ "sport/tennis", "sport/tennis/player1/", "sport/tennis/player1/ranking", ]: assert not broker._matches(bad_topic, test_filter) for good_topic in [ "sport/tennis/", "sport/tennis/player1", "sport/tennis/player2", ]: assert broker._matches(good_topic, test_filter) @pytest.mark.asyncio async def test_broker_broadcast_cancellation(broker): topic = "test" data = b"data" qos = QOS_0 sub_client = MQTTClient() await sub_client.connect("mqtt://127.0.0.1") await sub_client.subscribe([(topic, qos)]) with patch.object(BrokerProtocolHandler, "mqtt_publish", side_effect=asyncio.CancelledError) as mocked_mqtt_publish: await _client_publish(topic, data, qos) # Second publish triggers the awaiting of first `mqtt_publish` task await _client_publish(topic, data, qos) await asyncio.sleep(0.01) mocked_mqtt_publish.assert_awaited() # Ensure broadcast loop is still functional and can deliver the message await _client_publish(topic, data, qos) message = await asyncio.wait_for(sub_client.deliver_message(), timeout=1) assert message @pytest.mark.asyncio async def test_broker_socket_open_close(broker): # check that https://github.com/Yakifo/amqtt/issues/86 is fixed # mqtt 3.1 requires a connect packet, otherwise the socket connection is rejected static_connect_packet = b'\x10\x1b\x00\x04MQTT\x04\x02\x00<\x00\x0ftest-client-123' s = socket.create_connection(("127.0.0.1", 1883)) s.send(static_connect_packet) await asyncio.sleep(0.1) s.close() std_legacy_config = { "listeners": { "default": { "type": "tcp", "bind": f"127.0.0.1:1883", } }, "sys_interval": 10, "auth": { "allow-anonymous": True, "plugins": ["auth_anonymous"], }, "topic-check": {"enabled": False}, } @pytest.mark.asyncio async def test_broker_with_legacy_config(): broker = Broker(config=std_legacy_config) await broker.start() await asyncio.sleep(2) mqtt_client = MQTTClient(config={'auto_reconnect': False}) await mqtt_client.connect() await broker.shutdown() legacy_config_empty_auth_plugin_list = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10}, }, 'sys_interval': 0, 'auth':{ 'plugins':[] # explicitly declare no auth plugins } } class_path_config_no_auth = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10}, }, 'plugins':{ 'tests.plugins.test_plugins.AllEventsPlugin': {} } } @pytest.mark.parametrize("test_config", [ legacy_config_empty_auth_plugin_list, class_path_config_no_auth, ]) @pytest.mark.asyncio async def test_broker_without_auth_plugin(test_config): broker = Broker(config=test_config) await broker.start() await asyncio.sleep(2) # make sure all expected events get triggered with pytest.raises(ConnectError): mqtt_client = MQTTClient(config={'auto_reconnect': False}) await mqtt_client.connect() await broker.shutdown() legacy_config_with_absent_auth_plugin_filter = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10}, }, 'sys_interval': 0, 'auth':{ 'allow-anonymous': True } } @pytest.mark.asyncio async def test_broker_with_absent_auth_plugin_filter(): # maintain legacy behavior that if a config is missing the 'auth' > 'plugins' filter, all plugins are active broker = Broker(config=legacy_config_with_absent_auth_plugin_filter) await broker.start() await asyncio.sleep(2) mqtt_client = MQTTClient(config={'auto_reconnect': False}) await mqtt_client.connect() await broker.shutdown() Yakifo-amqtt-2637127/tests/test_broker_config.py000066400000000000000000000017421504664204300216510ustar00rootroot00000000000000import logging from typing import Any try: from enum import Enum, StrEnum except ImportError: # support for python 3.10 from enum import Enum class StrEnum(str, Enum): #type: ignore[no-redef] pass from dacite import from_dict, Config from amqtt.contexts import BrokerConfig, ListenerType logger = logging.getLogger(__name__) def test_entrypoint_broker_config(caplog): test_cfg: dict[str, Any] = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10}, }, 'sys_interval': 1, 'auth': { 'allow_anonymous': True } } if 'plugins' not in test_cfg: test_cfg['plugins'] = None # cfg: dict[str, Any] = yaml.load(config, Loader=Loader) broker_config = from_dict(data_class=BrokerConfig, data=test_cfg, config=Config(cast=[StrEnum, ListenerType])) assert isinstance(broker_config, BrokerConfig) assert broker_config.plugins is None Yakifo-amqtt-2637127/tests/test_cli.py000066400000000000000000000150001504664204300175770ustar00rootroot00000000000000import asyncio import logging import os import signal import subprocess import tempfile from unittest.mock import patch import pytest import yaml from amqtt.broker import Broker from amqtt.mqtt.constants import QOS_0 formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) logger = logging.getLogger(__name__) from amqtt.client import MQTTClient @pytest.fixture def broker_config(): return { "listeners": { "default": { "type": "tcp", "bind": "127.0.0.1:1884", # Use non-standard port for testing }, }, "sys_interval": 0, "auth": { "allow-anonymous": True, "plugins": ["auth_anonymous"], }, "topic-check": {"enabled": False}, } @pytest.fixture def broker_config_file(broker_config, tmp_path): config_path = tmp_path / "broker.yaml" with config_path.open("w") as f: yaml.dump(broker_config, f) return str(config_path) @pytest.fixture async def broker(broker_config_file): proc = subprocess.Popen( ["amqtt", "-c", broker_config_file], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # Give broker time to start await asyncio.sleep(4) yield proc proc.terminate() proc.wait() def test_cli_help_messages(): """Test that help messages are displayed correctly.""" env = os.environ.copy() env["NO_COLOR"] = '1' amqtt_path = "amqtt" output = subprocess.check_output([amqtt_path, "--help"], env=env, text=True) assert "Usage: amqtt" in output amqtt_sub_path = "amqtt_sub" output = subprocess.check_output([amqtt_sub_path, "--help"], env=env, text=True) assert "Usage: amqtt_sub" in output amqtt_pub_path = "amqtt_pub" output = subprocess.check_output([amqtt_pub_path, "--help"], env=env, text=True) assert "Usage: amqtt_pub" in output def test_broker_version(): """Test broker version command.""" output = subprocess.check_output(["amqtt", "--version"]) assert output.strip() @pytest.mark.asyncio async def test_broker_start_stop(broker_config_file): """Test broker start and stop with config file.""" proc = subprocess.Popen( ["amqtt", "-c", broker_config_file], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # Give broker time to start await asyncio.sleep(1) # Verify broker is running by connecting a client client = MQTTClient() await client.connect("mqtt://127.0.0.1:1884") await client.disconnect() # Stop broker proc.terminate() proc.wait() @pytest.mark.asyncio async def test_publish_subscribe(broker): """Test pub/sub CLI tools with running broker.""" # Create a temporary file with test message with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp: tmp.write("test message\n") tmp.write("another message\n") tmp_path = tmp.name # Start subscriber in background sub_proc = subprocess.Popen( [ "amqtt_sub", "--url", "mqtt://127.0.0.1:1884", "-t", "test/topic", "-n", "2", # Exit after 2 messages "--qos", "1", ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # Give subscriber time to connect await asyncio.sleep(0.5) # # # Publish messages from file pub_proc = subprocess.run( [ "amqtt_pub", "--url", "mqtt://127.0.0.1:1884", "-t", "test/topic", "-f", tmp_path, "--qos", "1", ], capture_output=True, ) assert pub_proc.returncode == 0 # # # Wait for subscriber to receive messages stdout, stderr = sub_proc.communicate(timeout=5) # Clean up temp file os.unlink(tmp_path) # Verify messages were received print(stdout.decode("utf-8")) assert "test message" in str(stdout) assert "another message" in str(stdout) assert sub_proc.returncode == 0 @pytest.mark.asyncio async def test_pub_errors(client_config_file): """Test error handling in pub/sub tools.""" # Test connection to non-existent broker cmd = [ "amqtt_pub", "--url", "mqtt://127.0.0.1:9999", # Wrong port "-t", "test/topic", "-m", "test", "-c", client_config_file, ] proc = await asyncio.create_subprocess_shell( " ".join(cmd), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await proc.communicate() logger.debug(f"Command: {cmd}") logger.debug(f"Stdout: {stdout.decode()}") logger.debug(f"Stderr: {stderr.decode()}") assert proc.returncode != 0, f"publisher error code: {proc.returncode}" assert "Connection failed" in str(stderr) @pytest.mark.asyncio async def test_sub_errors(client_config_file): # Test invalid URL format sub_proc = subprocess.run( [ "amqtt_sub", "--url", "invalid://url", "-t", "test/topic", "-c", client_config_file ], capture_output=True, ) assert sub_proc.returncode != 0, f"subscriber error code: {sub_proc.returncode}" @pytest.fixture def client_config(): return { "keep_alive": 10, "ping_delay": 1, "default_qos": 0, "default_retain": False, "auto_reconnect": False, "will": { "topic": "test/will/topic", "message": "client ABC has disconnected", "qos": 0, "retain": False }, "broker": { "uri": "mqtt://localhost:1884" } } @pytest.fixture def client_config_file(client_config, tmp_path): config_path = tmp_path / "client.yaml" with config_path.open("w") as f: yaml.dump(client_config, f) return str(config_path) @pytest.mark.asyncio async def test_pub_client_config(broker, client_config_file): await asyncio.sleep(1) cmd = [ "amqtt_pub", "-t", "test/topic", "-m", "test", "-c", client_config_file ] proc = await asyncio.create_subprocess_shell( " ".join(cmd), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await proc.communicate() logger.debug(f"Command: {cmd}") logger.debug(f"Stdout: {stdout.decode()}") logger.debug(f"Stderr: {stderr.decode()}") assert proc.returncode == 0, f"publisher error code: {proc.returncode}" Yakifo-amqtt-2637127/tests/test_client.py000066400000000000000000000374211504664204300203210ustar00rootroot00000000000000import asyncio import logging from importlib.metadata import EntryPoint from unittest.mock import patch import pytest from amqtt.broker import Broker from amqtt.client import MQTTClient from amqtt.errors import ClientError, ConnectError, MQTTError from amqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.ERROR, format=formatter) log = logging.getLogger(__name__) @pytest.mark.asyncio async def test_connect_tcp(broker_fixture): client = MQTTClient() await client.connect("mqtt://localhost:1883/") assert client.session is not None await client.disconnect() @pytest.mark.asyncio async def test_connect_tcp_secure(rsa_keys, broker_fixture): certfile, _ = rsa_keys client = MQTTClient(config={"check_hostname": False, "auto_reconnect": False}) # since we're using a self-signed certificate, need to provide the server's certificate to verify authenticity await client.connect("mqtts://localhost:1884/", cafile=certfile) assert client.session is not None await client.disconnect() @pytest.mark.asyncio async def test_connect_tcp_failure(): config = {"auto_reconnect": False} client = MQTTClient(config=config) with pytest.raises(ConnectError): await client.connect("mqtt://127.0.0.1/") @pytest.mark.asyncio async def test_connect_ws(broker_fixture): client = MQTTClient() await client.connect("ws://127.0.0.1:8080/") assert client.session is not None await client.disconnect() @pytest.mark.asyncio async def test_reconnect_ws_retain_username_password(broker_fixture): client = MQTTClient() await client.connect("ws://fred:password@127.0.0.1:8080/") assert client.session is not None assert client.session.username is not None assert client.session.password is not None await client.disconnect() await client.reconnect() assert client.session.username is not None assert client.session.password is not None @pytest.mark.asyncio async def test_connect_ws_secure(rsa_keys, broker_fixture): certfile, _ = rsa_keys client = MQTTClient(config={"auto_reconnect": False}) # since we're using a self-signed certificate, need to provide the server's certificate to verify authenticity await client.connect("wss://localhost:8081/", cafile=certfile) assert client.session is not None await client.disconnect() @pytest.mark.asyncio async def test_connect_username_without_password(broker_fixture): client = MQTTClient() await client.connect("mqtt://alice@127.0.0.1/") assert client.session is not None await client.disconnect() @pytest.mark.asyncio async def test_ping(broker_fixture): client = MQTTClient() await client.connect("mqtt://127.0.0.1/") assert client.session is not None await client.ping() await client.disconnect() @pytest.mark.asyncio async def test_subscribe(broker_fixture): client = MQTTClient() await client.connect("mqtt://127.0.0.1/") assert client.session is not None ret = await client.subscribe( [ ("$SYS/broker/uptime", QOS_0), ("$SYS/broker/uptime", QOS_1), ("$SYS/broker/uptime", QOS_2), ], ) assert ret[0] == QOS_0 assert ret[1] == QOS_1 assert ret[2] == QOS_2 await client.disconnect() @pytest.mark.asyncio async def test_unsubscribe(broker_fixture): client = MQTTClient() await client.connect("mqtt://127.0.0.1/") assert client.session is not None ret = await client.subscribe( [ ("$SYS/broker/uptime", QOS_0), ], ) assert ret[0] == QOS_0 await client.unsubscribe(["$SYS/broker/uptime"]) await client.disconnect() @pytest.mark.asyncio async def test_deliver(broker_fixture): data = b"data" client = MQTTClient() await client.connect("mqtt://127.0.0.1/") assert client.session is not None ret = await client.subscribe( [ ("test_topic", QOS_0), ], ) assert ret[0] == QOS_0 client_pub = MQTTClient() await client_pub.connect("mqtt://127.0.0.1/") await client_pub.publish("test_topic", data, QOS_0) await client_pub.disconnect() message = await client.deliver_message() assert message is not None assert message.publish_packet is not None assert message.data == data await client.unsubscribe(["$SYS/broker/uptime"]) await client.disconnect() @pytest.mark.asyncio async def test_deliver_timeout(broker_fixture): client = MQTTClient() await client.connect("mqtt://127.0.0.1/") assert client.session is not None ret = await client.subscribe( [ ("test_topic", QOS_0), ], ) assert ret[0] == QOS_0 with pytest.raises(asyncio.TimeoutError): await client.deliver_message(timeout_duration=2) await client.unsubscribe(["$SYS/broker/uptime"]) await client.disconnect() @pytest.mark.asyncio async def test_cancel_publish_qos1(broker_fixture): """Tests that timeouts on published messages will clean up in-flight messages.""" data = b"data" client_pub = MQTTClient() await client_pub.connect("mqtt://127.0.0.1/") assert client_pub.session is not None assert client_pub._handler is not None assert client_pub.session.inflight_out_count == 0 fut = asyncio.create_task(client_pub.publish("test_topic", data, QOS_1)) assert len(client_pub._handler._puback_waiters) == 0 while len(client_pub._handler._puback_waiters) == 0 and not fut.done(): await asyncio.sleep(0) assert len(client_pub._handler._puback_waiters) == 1 assert client_pub.session.inflight_out_count == 1 fut.cancel() await asyncio.wait([fut]) assert len(client_pub._handler._puback_waiters) == 0 assert client_pub.session.inflight_out_count == 0 await asyncio.sleep(0.1) await client_pub.disconnect() @pytest.mark.asyncio async def test_cancel_publish_qos2_pubrec(broker_fixture): """Tests that timeouts on published messages will clean up in-flight messages.""" data = b"data" client_pub = MQTTClient() await client_pub.connect("mqtt://127.0.0.1/") assert client_pub.session is not None assert client_pub._handler is not None assert client_pub.session.inflight_out_count == 0 fut = asyncio.create_task(client_pub.publish("test_topic", data, QOS_2)) assert len(client_pub._handler._pubrec_waiters) == 0 while len(client_pub._handler._pubrec_waiters) == 0 or fut.done() or fut.cancelled(): await asyncio.sleep(0) assert len(client_pub._handler._pubrec_waiters) == 1 assert client_pub.session.inflight_out_count == 1 fut.cancel() await asyncio.sleep(1) await asyncio.wait([fut]) assert len(client_pub._handler._pubrec_waiters) == 0 assert client_pub.session.inflight_out_count == 0 await asyncio.sleep(0.1) await client_pub.disconnect() @pytest.mark.asyncio async def test_cancel_publish_qos2_pubcomp(broker_fixture): """Tests that timeouts on published messages will clean up in-flight messages.""" data = b"data" client_pub = MQTTClient() await client_pub.connect("mqtt://127.0.0.1/") assert client_pub.session is not None assert client_pub._handler is not None assert client_pub.session.inflight_out_count == 0 fut = asyncio.create_task(client_pub.publish("test_topic", data, QOS_2)) assert len(client_pub._handler._pubcomp_waiters) == 0 while len(client_pub._handler._pubcomp_waiters) == 0 and not fut.done(): await asyncio.sleep(0) assert len(client_pub._handler._pubcomp_waiters) == 1 fut.cancel() await asyncio.wait([fut]) assert len(client_pub._handler._pubcomp_waiters) == 0 assert client_pub.session.inflight_out_count == 0 await asyncio.sleep(0.1) await client_pub.disconnect() @pytest.fixture def client_config(): return { "default_retain": False, "topics": { "test": { "qos": 0 }, "some_topic": { "retain": True, "qos": 2 } }, "keep_alive": 10, "connection": { "uri": "mqtt://localhost:1884" }, "reconnect_max_interval": 5, "will": { "topic": "test/will/topic", "retain": True, "message": "client ABC has disconnected", "qos": 1 }, "ping_delay": 1, "default_qos": 0, "auto_reconnect": True, "reconnect_retries": 10 } @pytest.mark.asyncio async def test_client_will_with_clean_disconnect(broker_fixture): config = { "will": { "topic": "test/will/topic", "retain": False, "message": "client ABC has disconnected", "qos": 1 }, } client1 = MQTTClient(client_id="client1", config=config) await client1.connect("mqtt://localhost:1883") client2 = MQTTClient(client_id="client2") await client2.connect("mqtt://localhost:1883") await client2.subscribe( [ ("test/will/topic", QOS_0), ] ) await client1.disconnect() await asyncio.sleep(1) with pytest.raises(asyncio.TimeoutError): message = await client2.deliver_message(timeout_duration=2) # if we do get a message, make sure it's not a will message assert message.topic != "test/will/topic" await client2.disconnect() @pytest.mark.asyncio async def test_client_will_with_abrupt_disconnect(broker_fixture): config = { "will": { "topic": "test/will/topic", "retain": False, "message": "client ABC has disconnected", "qos": 1 }, } client1 = MQTTClient(client_id="client1", config=config) await client1.connect("mqtt://localhost:1883") client2 = MQTTClient(client_id="client2") await client2.connect("mqtt://localhost:1883") await client2.subscribe( [ ("test/will/topic", QOS_0), ] ) # instead of client.disconnect, call the necessary closing but without sending the disconnect packet await client1.cancel_tasks() if client1._disconnect_task and not client1._disconnect_task.done(): client1._disconnect_task.cancel() client1._connected_state.clear() await client1._handler.stop() client1.session.transitions.disconnect() await asyncio.sleep(1) message = await client2.deliver_message(timeout_duration=1) # make sure we receive the will message assert message.topic == "test/will/topic" assert message.data == b'client ABC has disconnected' await client2.disconnect() @pytest.mark.asyncio async def test_client_retained_will_with_abrupt_disconnect(broker_fixture): # verifying client functionality of retained will topic/message config = { "will": { "topic": "test/will/topic", "retain": True, "message": "client ABC has disconnected", "qos": 1 }, } # first client, connect with retained will message client1 = MQTTClient(client_id="client1", config=config) await client1.connect('mqtt://localhost:1883') client2 = MQTTClient(client_id="client2") await client2.connect('mqtt://localhost:1883') await client2.subscribe([ ("test/will/topic", QOS_0) ]) # let's abruptly disconnect client1 await client1.cancel_tasks() if client1._disconnect_task and not client1._disconnect_task.done(): client1._disconnect_task.cancel() client1._connected_state.clear() await client1._handler.stop() client1.session.transitions.disconnect() await asyncio.sleep(0.5) # make sure the client which is still connected that we get the 'will' message message = await client2.deliver_message(timeout_duration=1) assert message.topic == 'test/will/topic' assert message.data == b'client ABC has disconnected' await client2.disconnect() # make sure a client which is connected after client1 disconnected still receives the 'will' message from client3 = MQTTClient(client_id="client3") await client3.connect('mqtt://localhost:1883') await client3.subscribe([ ("test/will/topic", QOS_0) ]) message3 = await client3.deliver_message(timeout_duration=1) assert message3.topic == 'test/will/topic' assert message3.data == b'client ABC has disconnected' await client3.disconnect() @pytest.mark.asyncio async def test_client_abruptly_disconnecting_with_empty_will_message(broker_fixture): config = { "will": { "topic": "test/will/topic", "retain": True, "message": "", "qos": 1 }, } client1 = MQTTClient(client_id="client1", config=config) await client1.connect('mqtt://localhost:1883') client2 = MQTTClient(client_id="client2") await client2.connect('mqtt://localhost:1883') await client2.subscribe([ ("test/will/topic", QOS_0) ]) # let's abruptly disconnect client1 await client1.cancel_tasks() if client1._disconnect_task and not client1._disconnect_task.done(): client1._disconnect_task.cancel() client1._connected_state.clear() await client1._handler.stop() client1.session.transitions.disconnect() await asyncio.sleep(0.5) message = await client2.deliver_message(timeout_duration=1) assert message.topic == 'test/will/topic' assert message.data == b'' await client2.disconnect() async def test_connect_broken_uri(): config = {"auto_reconnect": False} client = MQTTClient(config=config) with pytest.raises(ClientError): await client.connect('"mqtt://someplace') @pytest.mark.asyncio async def test_connect_incorrect_scheme(): config = {"auto_reconnect": False} client = MQTTClient(config=config) with pytest.raises(ClientError): await client.connect('"mq://someplace') @pytest.mark.asyncio @pytest.mark.timeout(3) async def test_connect_timeout(): config = {"auto_reconnect": False, "connection_timeout": 2} client = MQTTClient(config=config) with pytest.raises(ClientError): await client.connect("mqtt://localhost:8888") async def test_client_no_auth(): class MockEntryPoints: def select(self, group) -> list[EntryPoint]: match group: case 'tests.mock_plugins': return [ EntryPoint(name='auth_plugin', group='tests.mock_plugins', value='tests.plugins.mocks:TestNoAuthPlugin'), ] case _: return list() with patch("amqtt.plugins.manager.entry_points", side_effect=MockEntryPoints) as mocked_mqtt_publish: config = { "listeners": { "default": {"type": "tcp", "bind": "127.0.0.1:1883", "max_connections": 10}, }, 'sys_interval': 1, 'auth': { 'plugins': ['auth_plugin', ] } } client = MQTTClient(client_id="client1", config={'auto_reconnect': False}) with pytest.warns(DeprecationWarning): broker = Broker(plugin_namespace='tests.mock_plugins', config=config) await broker.start() with pytest.raises(ConnectError): await client.connect("mqtt://127.0.0.1:1883/") await broker.shutdown() @pytest.mark.asyncio async def test_publish_to_incorrect_wildcard(broker_fixture): client = MQTTClient(config={'auto_reconnect': False}) await client.connect("mqtt://127.0.0.1/") with pytest.raises(MQTTError): await client.publish("my/+/topic", b'plus-sign wildcard topic invalid publish') with pytest.raises(MQTTError): await client.publish("topic/#", b'hash wildcard topic invalid publish') await client.publish("topic/*", b'asterisk topic normal publish') await client.disconnect() Yakifo-amqtt-2637127/tests/test_codecs.py000066400000000000000000000016341504664204300203000ustar00rootroot00000000000000import asyncio import unittest from amqtt.adapters import StreamReaderAdapter from amqtt.codecs_amqtt import ( bytes_to_hex_str, bytes_to_int, decode_string, encode_string, ) class TestCodecs(unittest.TestCase): def setUp(self): self.loop = asyncio.new_event_loop() def test_bytes_to_hex_str(self): ret = bytes_to_hex_str(b"\x7f") assert ret == "0x7f" def test_bytes_to_int(self): ret = bytes_to_int(b"\x7f") assert ret == 127 ret = bytes_to_int(b"\xff\xff") assert ret == 65535 def test_decode_string(self): stream = asyncio.StreamReader(loop=self.loop) stream.feed_data(b"\x00\x02AA") ret = self.loop.run_until_complete(decode_string(StreamReaderAdapter(stream))) assert ret == "AA" def test_encode_string(self): encoded = encode_string("AA") assert encoded == b"\x00\x02AA" Yakifo-amqtt-2637127/tests/test_dollar_topics.py000066400000000000000000000064711504664204300217020ustar00rootroot00000000000000import asyncio import logging import pytest from amqtt.broker import Broker from amqtt.client import MQTTClient from amqtt.mqtt.constants import QOS_0 logger = logging.getLogger(__name__) @pytest.mark.asyncio async def test_publish_to_dollar_sign_topics(): """Applications cannot use a topic with a leading $ character for their own purposes [MQTT-4.7.2-1].""" cfg = { 'listeners': {'default': {'type': 'tcp', 'bind': '127.0.0.1'}}, 'plugins': {'amqtt.plugins.authentication.AnonymousAuthPlugin': {"allow_anonymous": True}}, } b = Broker(config=cfg) await b.start() await asyncio.sleep(0.1) c = MQTTClient(config={'auto_reconnect': False}) await c.connect() await asyncio.sleep(0.1) await c.subscribe( [('$#', QOS_0), ('#', QOS_0)] ) await asyncio.sleep(0.1) await c.publish('$MY', b'message should be blocked') await asyncio.sleep(0.1) with pytest.raises(asyncio.TimeoutError): # wait long enough for broker sys plugin to run _ = await c.deliver_message(timeout_duration=1) await c.disconnect() await asyncio.sleep(0.1) await b.shutdown() @pytest.mark.asyncio async def test_hash_will_not_receive_dollar(): """A subscription to β€œ#” will not receive any messages published to a topic beginning with a $ [MQTT-4.7.2-1].""" cfg = { 'listeners': {'default': {'type': 'tcp', 'bind': '127.0.0.1'}}, 'plugins': { 'amqtt.plugins.authentication.AnonymousAuthPlugin': {"allow_anonymous": True}, 'amqtt.plugins.sys.broker.BrokerSysPlugin': {"sys_interval": 2} } } b = Broker(config=cfg) await b.start() await asyncio.sleep(0.1) c = MQTTClient(config={'auto_reconnect': False}) await c.connect() await asyncio.sleep(0.1) await c.subscribe( [('#', QOS_0)] ) await asyncio.sleep(0.1) with pytest.raises(asyncio.TimeoutError): # wait long enough for broker sys plugin to run _ = await c.deliver_message(timeout_duration=5) await c.disconnect() await asyncio.sleep(0.1) await b.shutdown() @pytest.mark.asyncio async def test_plus_will_not_receive_dollar(): """A subscription to β€œ+/monitor/Clients” will not receive any messages published to β€œ$SYS/monitor/Clients [MQTT-4.7.2-1]""" # BrokerSysPlugin doesn't use $SYS/monitor/Clients, so this is an equivalent test with $SYS/broker topics cfg = { 'listeners': {'default': {'type': 'tcp', 'bind': '127.0.0.1'}}, 'plugins': { 'amqtt.plugins.authentication.AnonymousAuthPlugin': {"allow_anonymous": True}, 'amqtt.plugins.sys.broker.BrokerSysPlugin': {"sys_interval": 2} } } b = Broker(config=cfg) await b.start() await asyncio.sleep(0.1) c = MQTTClient(config={'auto_reconnect': False}) await c.connect() await asyncio.sleep(0.1) await c.subscribe( [('+/broker/#', QOS_0), ('+/broker/time', QOS_0), ('+/broker/clients/#', QOS_0), ('+/broker/+/maximum', QOS_0) ] ) await asyncio.sleep(0.1) with pytest.raises(asyncio.TimeoutError): # wait long enough for broker sys plugin to run _ = await c.deliver_message(timeout_duration=5) await c.disconnect() await asyncio.sleep(0.1) await b.shutdown() Yakifo-amqtt-2637127/tests/test_paho.py000066400000000000000000000126641504664204300177740ustar00rootroot00000000000000import asyncio import logging import random import threading import time from pathlib import Path from threading import Thread from typing import Any from unittest.mock import MagicMock, call, patch import pytest import yaml from paho.mqtt import client as paho_client from yaml import Loader from amqtt.broker import Broker from amqtt.events import BrokerEvents from amqtt.client import MQTTClient from amqtt.mqtt.constants import QOS_1, QOS_2 from amqtt.session import Session logger = logging.getLogger(__name__) paho_logger = logging.getLogger("paho_client") # monkey patch MagicMock # taken from https://stackoverflow.com/questions/51394411/python-object-magicmock-cant-be-used-in-await-expression async def async_magic(): pass MagicMock.__await__ = lambda _: async_magic().__await__() @pytest.mark.asyncio async def test_paho_connect(broker, mock_plugin_manager): test_complete = asyncio.Event() host = "localhost" port = 1883 client_id = f'python-mqtt-{random.randint(0, 1000)}' def on_connect(client, userdata, flags, rc, properties=None): assert rc == 0, f"Connection failed with result code {rc}" client.disconnect() def on_disconnect(client, userdata, flags, rc, properties=None): assert rc == 0, f"Disconnect failed with result code {rc}" test_complete.set() test_client = paho_client.Client(paho_client.CallbackAPIVersion.VERSION2, client_id=client_id) test_client.enable_logger(paho_logger) test_client.on_connect = on_connect test_client.on_disconnect = on_disconnect test_client.connect(host, port) test_client.loop_start() await asyncio.wait_for(test_complete.wait(), timeout=5) await asyncio.sleep(0.1) broker.plugins_manager.fire_event.assert_called() assert broker.plugins_manager.fire_event.call_count > 2 # double indexing is ugly, but call_args_list returns a tuple of tuples events = [c[0][0] for c in broker.plugins_manager.fire_event.call_args_list] assert BrokerEvents.CLIENT_CONNECTED in events assert BrokerEvents.CLIENT_DISCONNECTED in events test_client.loop_stop() @pytest.mark.asyncio async def test_paho_qos1(broker, mock_plugin_manager): sub_client = MQTTClient() await sub_client.connect("mqtt://127.0.0.1") ret = await sub_client.subscribe( [("/qos1", QOS_1),], ) host = "localhost" port = 1883 client_id = f'python-mqtt-{random.randint(0, 1000)}' test_client = paho_client.Client(paho_client.CallbackAPIVersion.VERSION2, client_id=client_id) test_client.enable_logger(paho_logger) test_client.connect(host, port) test_client.loop_start() await asyncio.sleep(0.1) test_client.publish("/qos1", "test message", qos=1) await asyncio.sleep(0.1) test_client.loop_stop() message = await sub_client.deliver_message() assert message is not None assert message.qos == 1 assert message.topic == "/qos1" assert message.data == b"test message" await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_paho_qos2(broker, mock_plugin_manager): sub_client = MQTTClient() await sub_client.connect("mqtt://127.0.0.1") ret = await sub_client.subscribe( [("/qos2", QOS_2), ], ) host = "localhost" port = 1883 client_id = f'python-mqtt-{random.randint(0, 1000)}' test_client = paho_client.Client(paho_client.CallbackAPIVersion.VERSION2, client_id=client_id) test_client.enable_logger(paho_logger) test_client.connect(host, port) test_client.loop_start() await asyncio.sleep(0.1) test_client.publish("/qos2", "test message", qos=2) await asyncio.sleep(0.1) test_client.loop_stop() message = await sub_client.deliver_message() assert message is not None assert message.qos == 2 assert message.topic == "/qos2" assert message.data == b"test message" await sub_client.disconnect() await asyncio.sleep(0.1) def run_paho_client(flag): client_id = 'websocket_client_1' logging.info("creating paho client") test_client = paho_client.Client(callback_api_version=paho_client.CallbackAPIVersion.VERSION2, transport='websockets', client_id=client_id) test_client.ws_set_options('') logging.info("client connecting...") test_client.connect('127.0.0.1', 8080) logging.info("starting loop") test_client.loop_start() logging.info("client connected") time.sleep(1) logging.info("sending messages") test_client.publish("/qos2", "test message", qos=2) test_client.publish("/qos2", "test message", qos=2) test_client.publish("/qos2", "test message", qos=2) test_client.publish("/qos2", "test message", qos=2) time.sleep(1) test_client.loop_stop() test_client.disconnect() flag.set() @pytest.mark.asyncio async def test_paho_ws(): path = Path('docs_test/test.amqtt.local.yaml') with path.open() as f: cfg: dict[str, Any] = yaml.load(f, Loader=Loader) logger.warning(cfg) broker = Broker(config=cfg) await broker.start() # python websockets and paho mqtt don't play well with each other in the same thread flag = threading.Event() thread = Thread(target=run_paho_client, args=(flag,)) thread.start() await asyncio.sleep(5) thread.join(1) assert flag.is_set(), "paho thread didn't execute completely" logging.info("client disconnected") await broker.shutdown() Yakifo-amqtt-2637127/tests/test_samples.py000066400000000000000000000252021504664204300205010ustar00rootroot00000000000000import asyncio import logging import signal import subprocess from multiprocessing import Process from pathlib import Path from typer.testing import CliRunner from samples.http_server_integration import main as http_server_main from samples.unix_sockets import app as unix_sockets_app import pytest from amqtt.broker import Broker from amqtt.client import MQTTClient from samples.broker_acl import config as broker_acl_config from samples.broker_taboo import config as broker_taboo_config logger = logging.getLogger(__name__) @pytest.mark.asyncio async def test_broker_acl(): broker_acl_script = Path(__file__).parent.parent / "samples/broker_acl.py" process = subprocess.Popen(["python", broker_acl_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Send the interrupt signal await asyncio.sleep(2) process.send_signal(signal.SIGINT) stdout, stderr = process.communicate() logger.debug(stderr.decode("utf-8")) assert "Broker closed" in stderr.decode("utf-8") assert "ERROR" not in stderr.decode("utf-8") assert "Exception" not in stderr.decode("utf-8") @pytest.mark.asyncio async def test_broker_simple(): broker_simple_script = Path(__file__).parent.parent / "samples/broker_simple.py" process = subprocess.Popen(["python", broker_simple_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE) await asyncio.sleep(2) # Send the interrupt signal process.send_signal(signal.SIGINT) stdout, stderr = process.communicate() logger.debug(stderr.decode("utf-8")) has_broker_closed = "Broker closed" in stderr.decode("utf-8") has_loop_stopped = "Broadcast loop stopped by exception" in stderr.decode("utf-8") assert has_broker_closed or has_loop_stopped, "Broker didn't close correctly." @pytest.mark.asyncio async def test_broker_start(): broker_start_script = Path(__file__).parent.parent / "samples/broker_start.py" process = subprocess.Popen(["python", broker_start_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE) await asyncio.sleep(2) # Send the interrupt signal to stop broker process.send_signal(signal.SIGINT) stdout, stderr = process.communicate() logger.debug(stderr.decode("utf-8")) assert "Broker closed" in stderr.decode("utf-8") assert "ERROR" not in stderr.decode("utf-8") assert "Exception" not in stderr.decode("utf-8") @pytest.mark.asyncio async def test_broker_taboo(): broker_taboo_script = Path(__file__).parent.parent / "samples/broker_taboo.py" process = subprocess.Popen(["python", broker_taboo_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE) await asyncio.sleep(2) # Send the interrupt signal to stop broker process.send_signal(signal.SIGINT) stdout, stderr = process.communicate() logger.debug(stderr.decode("utf-8")) assert "INFO :: amqtt.broker :: Broker closed" in stderr.decode("utf-8") assert "ERROR" not in stderr.decode("utf-8") assert "Exception" not in stderr.decode("utf-8") @pytest.mark.timeout(25) @pytest.mark.asyncio async def test_client_keepalive(): broker = Broker() await broker.start() await asyncio.sleep(2) keep_alive_script = Path(__file__).parent.parent / "samples/client_keepalive.py" process = subprocess.Popen(["python", keep_alive_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE) await asyncio.sleep(1) stdout, stderr = process.communicate() assert "ERROR" not in stderr.decode("utf-8") assert "Exception" not in stderr.decode("utf-8") await broker.shutdown() @pytest.mark.asyncio async def test_client_publish(): broker = Broker() await broker.start() await asyncio.sleep(2) client_publish = Path(__file__).parent.parent / "samples/client_publish.py" process = subprocess.Popen(["python", client_publish], stdout=subprocess.PIPE, stderr=subprocess.PIPE) await asyncio.sleep(2) stdout, stderr = process.communicate() assert "ERROR" not in stderr.decode("utf-8") assert "Exception" not in stderr.decode("utf-8") await broker.shutdown() @pytest.fixture def broker_ssl_config(rsa_keys): certfile, keyfile = rsa_keys return { "listeners": { "default": { "type": "tcp", "bind": "0.0.0.0:8883", "ssl": True, "certfile": certfile, "keyfile": keyfile, } }, "auth": { "allow-anonymous": True, "plugins": ["auth_anonymous"] } } @pytest.mark.asyncio async def test_client_publish_ssl(broker_ssl_config, rsa_keys): certfile, _ = rsa_keys # generate a self-signed certificate for this test # start a secure broker broker = Broker(config=broker_ssl_config) await broker.start() await asyncio.sleep(2) # run the sample client_publish_ssl_script = Path(__file__).parent.parent / "samples/client_publish_ssl.py" process = subprocess.Popen(["python", client_publish_ssl_script, '--cert', certfile], stdout=subprocess.PIPE, stderr=subprocess.PIPE) await asyncio.sleep(2) stdout, stderr = process.communicate() assert "ERROR" not in stderr.decode("utf-8") assert "Exception" not in stderr.decode("utf-8") await broker.shutdown() @pytest.mark.asyncio async def test_client_publish_acl(): broker = Broker() await broker.start() await asyncio.sleep(2) broker_simple_script = Path(__file__).parent.parent / "samples/client_publish_acl.py" process = subprocess.Popen(["python", broker_simple_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Send the interrupt signal await asyncio.sleep(2) stdout, stderr = process.communicate() logger.debug(stderr.decode("utf-8")) assert "ERROR" not in stderr.decode("utf-8") assert "Exception" not in stderr.decode("utf-8") await broker.shutdown() broker_ws_config = { "listeners": { "default": { "type": "ws", "bind": "0.0.0.0:8080", } }, "auth": { "allow-anonymous": True, "plugins": ["auth_anonymous"] } } @pytest.mark.asyncio async def test_client_publish_ws(): # start a secure broker broker = Broker(config=broker_ws_config) await broker.start() await asyncio.sleep(2) # run the sample client_publish_ssl_script = Path(__file__).parent.parent / "samples/client_publish_ws.py" process = subprocess.Popen(["python", client_publish_ssl_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE) await asyncio.sleep(2) stdout, stderr = process.communicate() assert "ERROR" not in stderr.decode("utf-8") assert "Exception" not in stderr.decode("utf-8") await broker.shutdown() broker_std_config = { "listeners": { "default": { "type": "tcp", "bind": "0.0.0.0:1883", } }, 'sys_interval':2, "auth": { "allow-anonymous": True, "plugins": ["auth_anonymous"] } } @pytest.mark.asyncio async def test_client_subscribe(): # start a standard broker broker = Broker(config=broker_std_config) await broker.start() await asyncio.sleep(1) # run the sample client_subscribe_script = Path(__file__).parent.parent / "samples/client_subscribe.py" process = await asyncio.create_subprocess_shell( " ".join(["python", str(client_subscribe_script)]), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() assert "ERROR" not in stdout.decode("utf-8") assert "Exception" not in stdout.decode("utf-8") assert "ERROR" not in stderr.decode("utf-8") assert "Exception" not in stderr.decode("utf-8") await broker.shutdown() @pytest.mark.asyncio async def test_client_subscribe_plugin_acl(): broker = Broker(config=broker_acl_config) await broker.start() broker_simple_script = Path(__file__).parent.parent / "samples/client_subscribe_acl.py" process = subprocess.Popen(["python", broker_simple_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Send the interrupt signal await asyncio.sleep(2) process.send_signal(signal.SIGINT) stdout, stderr = process.communicate() logger.debug(stderr.decode("utf-8")) assert "Subscribed results: [128, 1, 128, 1, 128, 1]" in stderr.decode("utf-8") assert "ERROR" not in stderr.decode("utf-8") assert "Exception" not in stderr.decode("utf-8") await broker.shutdown() @pytest.mark.asyncio async def test_client_subscribe_plugin_taboo(): broker = Broker(config=broker_taboo_config) await broker.start() broker_simple_script = Path(__file__).parent.parent / "samples/client_subscribe_acl.py" process = subprocess.Popen(["python", broker_simple_script], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Send the interrupt signal await asyncio.sleep(2) process.send_signal(signal.SIGINT) stdout, stderr = process.communicate() logger.debug(stderr.decode("utf-8")) assert "Subscribed results: [1, 1, 128, 1, 1, 1]" in stderr.decode("utf-8") assert "ERROR" not in stderr.decode("utf-8") assert "Exception" not in stderr.decode("utf-8") await broker.shutdown() @pytest.fixture def external_http_server(): p = Process(target=http_server_main) p.start() yield p p.terminate() @pytest.mark.asyncio async def test_external_http_server(external_http_server): await asyncio.sleep(1) client = MQTTClient(config={'auto_reconnect': False}) await client.connect("ws://127.0.0.1:8080/mqtt") assert client.session is not None await client.publish("my/topic", b'test message') await client.disconnect() # Send the interrupt signal await asyncio.sleep(1) @pytest.mark.asyncio async def test_unix_connection(): unix_socket_script = Path(__file__).parent.parent / "samples/unix_sockets.py" broker_process = subprocess.Popen(["python", unix_socket_script, "broker", "-s", "/tmp/mqtt"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # start the broker await asyncio.sleep(1) # start the client client_process = subprocess.Popen(["python", unix_socket_script, "client", "-s", "/tmp/mqtt"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) await asyncio.sleep(3) # stop the client (ctrl-c) client_process.send_signal(signal.SIGINT) _ = client_process.communicate() # stop the broker (ctrl-c) broker_process.send_signal(signal.SIGINT) broker_stdout, broker_stderr = broker_process.communicate() logger.debug(broker_stderr.decode("utf-8")) # verify that the broker received client connected/disconnected assert "on_broker_client_connected" in broker_stderr.decode("utf-8") assert "on_broker_client_disconnected" in broker_stderr.decode("utf-8") Yakifo-amqtt-2637127/tests/test_session_monitor.py000066400000000000000000000045671504664204300223020ustar00rootroot00000000000000import asyncio import logging import pytest from amqtt.broker import Broker from amqtt.client import MQTTClient from amqtt.contexts import BrokerConfig, ListenerConfig, ConnectionConfig, ClientConfig logger = logging.getLogger(__name__) @pytest.fixture def session_broker_config(): return BrokerConfig( listeners={ 'default': ListenerConfig(bind='127.0.0.1:1883') }, session_expiry_interval=2, plugins={ 'amqtt.plugins.authentication.AnonymousAuthPlugin': {'allow_anonymous': False} }) @pytest.mark.parametrize("username,clean_session,expiration,session_count", [ # session expiration disabled ("", True, None, 0), # anonymous and clean session ("", False, None, 0), # anonymous ("myuser@", True, None, 0), # named user, clean session ("myuser@", False, None, 1), # named user # session expiration enabled ("myuser@", False, 1, 0), # named user, quick expiration ("myuser@", False, 20, 1), # named user, long expiration ]) @pytest.mark.asyncio async def test_clear_session_expiration(caplog, session_broker_config, username, clean_session, session_count, expiration): caplog.set_level(logging.DEBUG) session_broker_config.session_expiry_interval = expiration session_broker_config.plugins = {'amqtt.plugins.authentication.AnonymousAuthPlugin': {'allow_anonymous': username == ""}} broker = Broker(config=session_broker_config) await broker.start() await asyncio.sleep(0.1) assert len(broker._sessions) < 1 c = MQTTClient(config=ClientConfig(cleansession=clean_session, auto_reconnect=False)) await c.connect(f'mqtt://{username}127.0.0.1:1883') await asyncio.sleep(0.1) assert len(broker._sessions) == 1, "client should be connected" await asyncio.sleep(0.1) await c.disconnect() await asyncio.sleep(4) assert len(broker._sessions) == session_count, f"session counts don't match {len(broker._sessions)} v {session_count}" if not session_count: assert any([record for record in caplog.records if "Expired 1 sessions" in record.message]) == (session_count == 0) await broker.shutdown() Yakifo-amqtt-2637127/tests/test_utils.py000066400000000000000000000022061504664204300201740ustar00rootroot00000000000000from pathlib import Path from hypothesis import given, provisional, strategies as st import yaml from amqtt import utils from amqtt.session import Session @given(st.text()) def test_format_client_message(client_id): test_session = Session() test_session.client_id = client_id client_message = utils.format_client_message(session=test_session) assert client_message == f"(client id={client_id})" @given(provisional.urls(), st.integers()) def test_format_client_message_valid(url, port): client_message = utils.format_client_message(address=url, port=port) assert client_message == f"(client @={url}:{port})" def test_format_client_message_unknown(): client_message = utils.format_client_message() assert client_message == "(unknown client)" def test_client_id(): client_id = utils.gen_client_id() assert isinstance(client_id, str) assert client_id.startswith("amqtt/") def test_read_yaml_config(tmpdir): fn = tmpdir / "test.config" with Path(fn).open("w") as f: yaml.dump({"a": "b"}, f) configuration = utils.read_yaml_config(config_file=fn) assert configuration == {"a": "b"} Yakifo-amqtt-2637127/uv.lock000066400000000000000000017560301504664204300156010ustar00rootroot00000000000000version = 1 revision = 2 requires-python = ">=3.10.0" resolution-markers = [ "python_full_version >= '3.12'", "python_full_version == '3.11.*'", "python_full_version < '3.11'", ] [[package]] name = "aiohappyeyeballs" version = "2.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, ] [[package]] name = "aiohttp" version = "3.12.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, { name = "aiosignal" }, { name = "async-timeout", marker = "python_full_version < '3.11'" }, { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, { name = "yarl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e6/0b/e39ad954107ebf213a2325038a3e7a506be3d98e1435e1f82086eec4cde2/aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2", size = 7822921, upload-time = "2025-07-10T13:05:33.968Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/88/f161f429f9de391eee6a5c2cffa54e2ecd5b7122ae99df247f7734dfefcb/aiohttp-3.12.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:906d5075b5ba0dd1c66fcaaf60eb09926a9fef3ca92d912d2a0bbdbecf8b1248", size = 702641, upload-time = "2025-07-10T13:02:38.98Z" }, { url = "https://files.pythonhosted.org/packages/fe/b5/24fa382a69a25d242e2baa3e56d5ea5227d1b68784521aaf3a1a8b34c9a4/aiohttp-3.12.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c875bf6fc2fd1a572aba0e02ef4e7a63694778c5646cdbda346ee24e630d30fb", size = 479005, upload-time = "2025-07-10T13:02:42.714Z" }, { url = "https://files.pythonhosted.org/packages/09/67/fda1bc34adbfaa950d98d934a23900918f9d63594928c70e55045838c943/aiohttp-3.12.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fbb284d15c6a45fab030740049d03c0ecd60edad9cd23b211d7e11d3be8d56fd", size = 466781, upload-time = "2025-07-10T13:02:44.639Z" }, { url = "https://files.pythonhosted.org/packages/36/96/3ce1ea96d3cf6928b87cfb8cdd94650367f5c2f36e686a1f5568f0f13754/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e360381e02e1a05d36b223ecab7bc4a6e7b5ab15760022dc92589ee1d4238c", size = 1648841, upload-time = "2025-07-10T13:02:46.356Z" }, { url = "https://files.pythonhosted.org/packages/be/04/ddea06cb4bc7d8db3745cf95e2c42f310aad485ca075bd685f0e4f0f6b65/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aaf90137b5e5d84a53632ad95ebee5c9e3e7468f0aab92ba3f608adcb914fa95", size = 1622896, upload-time = "2025-07-10T13:02:48.422Z" }, { url = "https://files.pythonhosted.org/packages/73/66/63942f104d33ce6ca7871ac6c1e2ebab48b88f78b2b7680c37de60f5e8cd/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e532a25e4a0a2685fa295a31acf65e027fbe2bea7a4b02cdfbbba8a064577663", size = 1695302, upload-time = "2025-07-10T13:02:50.078Z" }, { url = "https://files.pythonhosted.org/packages/20/00/aab615742b953f04b48cb378ee72ada88555b47b860b98c21c458c030a23/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eab9762c4d1b08ae04a6c77474e6136da722e34fdc0e6d6eab5ee93ac29f35d1", size = 1737617, upload-time = "2025-07-10T13:02:52.123Z" }, { url = "https://files.pythonhosted.org/packages/d6/4f/ef6d9f77225cf27747368c37b3d69fac1f8d6f9d3d5de2d410d155639524/aiohttp-3.12.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abe53c3812b2899889a7fca763cdfaeee725f5be68ea89905e4275476ffd7e61", size = 1642282, upload-time = "2025-07-10T13:02:53.899Z" }, { url = "https://files.pythonhosted.org/packages/37/e1/e98a43c15aa52e9219a842f18c59cbae8bbe2d50c08d298f17e9e8bafa38/aiohttp-3.12.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5760909b7080aa2ec1d320baee90d03b21745573780a072b66ce633eb77a8656", size = 1582406, upload-time = "2025-07-10T13:02:55.515Z" }, { url = "https://files.pythonhosted.org/packages/71/5c/29c6dfb49323bcdb0239bf3fc97ffcf0eaf86d3a60426a3287ec75d67721/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:02fcd3f69051467bbaa7f84d7ec3267478c7df18d68b2e28279116e29d18d4f3", size = 1626255, upload-time = "2025-07-10T13:02:57.343Z" }, { url = "https://files.pythonhosted.org/packages/79/60/ec90782084090c4a6b459790cfd8d17be2c5662c9c4b2d21408b2f2dc36c/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4dcd1172cd6794884c33e504d3da3c35648b8be9bfa946942d353b939d5f1288", size = 1637041, upload-time = "2025-07-10T13:02:59.008Z" }, { url = "https://files.pythonhosted.org/packages/22/89/205d3ad30865c32bc472ac13f94374210745b05bd0f2856996cb34d53396/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:224d0da41355b942b43ad08101b1b41ce633a654128ee07e36d75133443adcda", size = 1612494, upload-time = "2025-07-10T13:03:00.618Z" }, { url = "https://files.pythonhosted.org/packages/48/ae/2f66edaa8bd6db2a4cba0386881eb92002cdc70834e2a93d1d5607132c7e/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e387668724f4d734e865c1776d841ed75b300ee61059aca0b05bce67061dcacc", size = 1692081, upload-time = "2025-07-10T13:03:02.154Z" }, { url = "https://files.pythonhosted.org/packages/08/3a/fa73bfc6e21407ea57f7906a816f0dc73663d9549da703be05dbd76d2dc3/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:dec9cde5b5a24171e0b0a4ca064b1414950904053fb77c707efd876a2da525d8", size = 1715318, upload-time = "2025-07-10T13:03:04.322Z" }, { url = "https://files.pythonhosted.org/packages/e3/b3/751124b8ceb0831c17960d06ee31a4732cb4a6a006fdbfa1153d07c52226/aiohttp-3.12.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bbad68a2af4877cc103cd94af9160e45676fc6f0c14abb88e6e092b945c2c8e3", size = 1643660, upload-time = "2025-07-10T13:03:06.406Z" }, { url = "https://files.pythonhosted.org/packages/81/3c/72477a1d34edb8ab8ce8013086a41526d48b64f77e381c8908d24e1c18f5/aiohttp-3.12.14-cp310-cp310-win32.whl", hash = "sha256:ee580cb7c00bd857b3039ebca03c4448e84700dc1322f860cf7a500a6f62630c", size = 428289, upload-time = "2025-07-10T13:03:08.274Z" }, { url = "https://files.pythonhosted.org/packages/a2/c4/8aec4ccf1b822ec78e7982bd5cf971113ecce5f773f04039c76a083116fc/aiohttp-3.12.14-cp310-cp310-win_amd64.whl", hash = "sha256:cf4f05b8cea571e2ccc3ca744e35ead24992d90a72ca2cf7ab7a2efbac6716db", size = 451328, upload-time = "2025-07-10T13:03:10.146Z" }, { url = "https://files.pythonhosted.org/packages/53/e1/8029b29316971c5fa89cec170274582619a01b3d82dd1036872acc9bc7e8/aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597", size = 709960, upload-time = "2025-07-10T13:03:11.936Z" }, { url = "https://files.pythonhosted.org/packages/96/bd/4f204cf1e282041f7b7e8155f846583b19149e0872752711d0da5e9cc023/aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393", size = 482235, upload-time = "2025-07-10T13:03:14.118Z" }, { url = "https://files.pythonhosted.org/packages/d6/0f/2a580fcdd113fe2197a3b9df30230c7e85bb10bf56f7915457c60e9addd9/aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179", size = 470501, upload-time = "2025-07-10T13:03:16.153Z" }, { url = "https://files.pythonhosted.org/packages/38/78/2c1089f6adca90c3dd74915bafed6d6d8a87df5e3da74200f6b3a8b8906f/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb", size = 1740696, upload-time = "2025-07-10T13:03:18.4Z" }, { url = "https://files.pythonhosted.org/packages/4a/c8/ce6c7a34d9c589f007cfe064da2d943b3dee5aabc64eaecd21faf927ab11/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245", size = 1689365, upload-time = "2025-07-10T13:03:20.629Z" }, { url = "https://files.pythonhosted.org/packages/18/10/431cd3d089de700756a56aa896faf3ea82bee39d22f89db7ddc957580308/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b", size = 1788157, upload-time = "2025-07-10T13:03:22.44Z" }, { url = "https://files.pythonhosted.org/packages/fa/b2/26f4524184e0f7ba46671c512d4b03022633bcf7d32fa0c6f1ef49d55800/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641", size = 1827203, upload-time = "2025-07-10T13:03:24.628Z" }, { url = "https://files.pythonhosted.org/packages/e0/30/aadcdf71b510a718e3d98a7bfeaea2396ac847f218b7e8edb241b09bd99a/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe", size = 1729664, upload-time = "2025-07-10T13:03:26.412Z" }, { url = "https://files.pythonhosted.org/packages/67/7f/7ccf11756ae498fdedc3d689a0c36ace8fc82f9d52d3517da24adf6e9a74/aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7", size = 1666741, upload-time = "2025-07-10T13:03:28.167Z" }, { url = "https://files.pythonhosted.org/packages/6b/4d/35ebc170b1856dd020c92376dbfe4297217625ef4004d56587024dc2289c/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635", size = 1715013, upload-time = "2025-07-10T13:03:30.018Z" }, { url = "https://files.pythonhosted.org/packages/7b/24/46dc0380146f33e2e4aa088b92374b598f5bdcde1718c77e8d1a0094f1a4/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da", size = 1710172, upload-time = "2025-07-10T13:03:31.821Z" }, { url = "https://files.pythonhosted.org/packages/2f/0a/46599d7d19b64f4d0fe1b57bdf96a9a40b5c125f0ae0d8899bc22e91fdce/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419", size = 1690355, upload-time = "2025-07-10T13:03:34.754Z" }, { url = "https://files.pythonhosted.org/packages/08/86/b21b682e33d5ca317ef96bd21294984f72379454e689d7da584df1512a19/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab", size = 1783958, upload-time = "2025-07-10T13:03:36.53Z" }, { url = "https://files.pythonhosted.org/packages/4f/45/f639482530b1396c365f23c5e3b1ae51c9bc02ba2b2248ca0c855a730059/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0", size = 1804423, upload-time = "2025-07-10T13:03:38.504Z" }, { url = "https://files.pythonhosted.org/packages/7e/e5/39635a9e06eed1d73671bd4079a3caf9cf09a49df08490686f45a710b80e/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28", size = 1717479, upload-time = "2025-07-10T13:03:40.158Z" }, { url = "https://files.pythonhosted.org/packages/51/e1/7f1c77515d369b7419c5b501196526dad3e72800946c0099594c1f0c20b4/aiohttp-3.12.14-cp311-cp311-win32.whl", hash = "sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b", size = 427907, upload-time = "2025-07-10T13:03:41.801Z" }, { url = "https://files.pythonhosted.org/packages/06/24/a6bf915c85b7a5b07beba3d42b3282936b51e4578b64a51e8e875643c276/aiohttp-3.12.14-cp311-cp311-win_amd64.whl", hash = "sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced", size = 452334, upload-time = "2025-07-10T13:03:43.485Z" }, { url = "https://files.pythonhosted.org/packages/c3/0d/29026524e9336e33d9767a1e593ae2b24c2b8b09af7c2bd8193762f76b3e/aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22", size = 701055, upload-time = "2025-07-10T13:03:45.59Z" }, { url = "https://files.pythonhosted.org/packages/0a/b8/a5e8e583e6c8c1056f4b012b50a03c77a669c2e9bf012b7cf33d6bc4b141/aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a", size = 475670, upload-time = "2025-07-10T13:03:47.249Z" }, { url = "https://files.pythonhosted.org/packages/29/e8/5202890c9e81a4ec2c2808dd90ffe024952e72c061729e1d49917677952f/aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff", size = 468513, upload-time = "2025-07-10T13:03:49.377Z" }, { url = "https://files.pythonhosted.org/packages/23/e5/d11db8c23d8923d3484a27468a40737d50f05b05eebbb6288bafcb467356/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d", size = 1715309, upload-time = "2025-07-10T13:03:51.556Z" }, { url = "https://files.pythonhosted.org/packages/53/44/af6879ca0eff7a16b1b650b7ea4a827301737a350a464239e58aa7c387ef/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869", size = 1697961, upload-time = "2025-07-10T13:03:53.511Z" }, { url = "https://files.pythonhosted.org/packages/bb/94/18457f043399e1ec0e59ad8674c0372f925363059c276a45a1459e17f423/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c", size = 1753055, upload-time = "2025-07-10T13:03:55.368Z" }, { url = "https://files.pythonhosted.org/packages/26/d9/1d3744dc588fafb50ff8a6226d58f484a2242b5dd93d8038882f55474d41/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7", size = 1799211, upload-time = "2025-07-10T13:03:57.216Z" }, { url = "https://files.pythonhosted.org/packages/73/12/2530fb2b08773f717ab2d249ca7a982ac66e32187c62d49e2c86c9bba9b4/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660", size = 1718649, upload-time = "2025-07-10T13:03:59.469Z" }, { url = "https://files.pythonhosted.org/packages/b9/34/8d6015a729f6571341a311061b578e8b8072ea3656b3d72329fa0faa2c7c/aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088", size = 1634452, upload-time = "2025-07-10T13:04:01.698Z" }, { url = "https://files.pythonhosted.org/packages/ff/4b/08b83ea02595a582447aeb0c1986792d0de35fe7a22fb2125d65091cbaf3/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7", size = 1695511, upload-time = "2025-07-10T13:04:04.165Z" }, { url = "https://files.pythonhosted.org/packages/b5/66/9c7c31037a063eec13ecf1976185c65d1394ded4a5120dd5965e3473cb21/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9", size = 1716967, upload-time = "2025-07-10T13:04:06.132Z" }, { url = "https://files.pythonhosted.org/packages/ba/02/84406e0ad1acb0fb61fd617651ab6de760b2d6a31700904bc0b33bd0894d/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3", size = 1657620, upload-time = "2025-07-10T13:04:07.944Z" }, { url = "https://files.pythonhosted.org/packages/07/53/da018f4013a7a179017b9a274b46b9a12cbeb387570f116964f498a6f211/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb", size = 1737179, upload-time = "2025-07-10T13:04:10.182Z" }, { url = "https://files.pythonhosted.org/packages/49/e8/ca01c5ccfeaafb026d85fa4f43ceb23eb80ea9c1385688db0ef322c751e9/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425", size = 1765156, upload-time = "2025-07-10T13:04:12.029Z" }, { url = "https://files.pythonhosted.org/packages/22/32/5501ab525a47ba23c20613e568174d6c63aa09e2caa22cded5c6ea8e3ada/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0", size = 1724766, upload-time = "2025-07-10T13:04:13.961Z" }, { url = "https://files.pythonhosted.org/packages/06/af/28e24574801fcf1657945347ee10df3892311c2829b41232be6089e461e7/aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729", size = 422641, upload-time = "2025-07-10T13:04:16.018Z" }, { url = "https://files.pythonhosted.org/packages/98/d5/7ac2464aebd2eecac38dbe96148c9eb487679c512449ba5215d233755582/aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338", size = 449316, upload-time = "2025-07-10T13:04:18.289Z" }, { url = "https://files.pythonhosted.org/packages/06/48/e0d2fa8ac778008071e7b79b93ab31ef14ab88804d7ba71b5c964a7c844e/aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767", size = 695471, upload-time = "2025-07-10T13:04:20.124Z" }, { url = "https://files.pythonhosted.org/packages/8d/e7/f73206afa33100804f790b71092888f47df65fd9a4cd0e6800d7c6826441/aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e", size = 473128, upload-time = "2025-07-10T13:04:21.928Z" }, { url = "https://files.pythonhosted.org/packages/df/e2/4dd00180be551a6e7ee979c20fc7c32727f4889ee3fd5b0586e0d47f30e1/aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63", size = 465426, upload-time = "2025-07-10T13:04:24.071Z" }, { url = "https://files.pythonhosted.org/packages/de/dd/525ed198a0bb674a323e93e4d928443a680860802c44fa7922d39436b48b/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d", size = 1704252, upload-time = "2025-07-10T13:04:26.049Z" }, { url = "https://files.pythonhosted.org/packages/d8/b1/01e542aed560a968f692ab4fc4323286e8bc4daae83348cd63588e4f33e3/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab", size = 1685514, upload-time = "2025-07-10T13:04:28.186Z" }, { url = "https://files.pythonhosted.org/packages/b3/06/93669694dc5fdabdc01338791e70452d60ce21ea0946a878715688d5a191/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4", size = 1737586, upload-time = "2025-07-10T13:04:30.195Z" }, { url = "https://files.pythonhosted.org/packages/a5/3a/18991048ffc1407ca51efb49ba8bcc1645961f97f563a6c480cdf0286310/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026", size = 1786958, upload-time = "2025-07-10T13:04:32.482Z" }, { url = "https://files.pythonhosted.org/packages/30/a8/81e237f89a32029f9b4a805af6dffc378f8459c7b9942712c809ff9e76e5/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd", size = 1709287, upload-time = "2025-07-10T13:04:34.493Z" }, { url = "https://files.pythonhosted.org/packages/8c/e3/bd67a11b0fe7fc12c6030473afd9e44223d456f500f7cf526dbaa259ae46/aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88", size = 1622990, upload-time = "2025-07-10T13:04:36.433Z" }, { url = "https://files.pythonhosted.org/packages/83/ba/e0cc8e0f0d9ce0904e3cf2d6fa41904e379e718a013c721b781d53dcbcca/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086", size = 1676015, upload-time = "2025-07-10T13:04:38.958Z" }, { url = "https://files.pythonhosted.org/packages/d8/b3/1e6c960520bda094c48b56de29a3d978254637ace7168dd97ddc273d0d6c/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933", size = 1707678, upload-time = "2025-07-10T13:04:41.275Z" }, { url = "https://files.pythonhosted.org/packages/0a/19/929a3eb8c35b7f9f076a462eaa9830b32c7f27d3395397665caa5e975614/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151", size = 1650274, upload-time = "2025-07-10T13:04:43.483Z" }, { url = "https://files.pythonhosted.org/packages/22/e5/81682a6f20dd1b18ce3d747de8eba11cbef9b270f567426ff7880b096b48/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8", size = 1726408, upload-time = "2025-07-10T13:04:45.577Z" }, { url = "https://files.pythonhosted.org/packages/8c/17/884938dffaa4048302985483f77dfce5ac18339aad9b04ad4aaa5e32b028/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3", size = 1759879, upload-time = "2025-07-10T13:04:47.663Z" }, { url = "https://files.pythonhosted.org/packages/95/78/53b081980f50b5cf874359bde707a6eacd6c4be3f5f5c93937e48c9d0025/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758", size = 1708770, upload-time = "2025-07-10T13:04:49.944Z" }, { url = "https://files.pythonhosted.org/packages/ed/91/228eeddb008ecbe3ffa6c77b440597fdf640307162f0c6488e72c5a2d112/aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5", size = 421688, upload-time = "2025-07-10T13:04:51.993Z" }, { url = "https://files.pythonhosted.org/packages/66/5f/8427618903343402fdafe2850738f735fd1d9409d2a8f9bcaae5e630d3ba/aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa", size = 448098, upload-time = "2025-07-10T13:04:53.999Z" }, ] [[package]] name = "aiosignal" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] name = "aiosqlite" version = "0.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, ] [[package]] name = "amqtt" version = "0.11.3" source = { editable = "." } dependencies = [ { name = "dacite" }, { name = "passlib" }, { name = "psutil" }, { name = "pyyaml" }, { name = "transitions" }, { name = "typer" }, { name = "websockets" }, ] [package.optional-dependencies] ci = [ { name = "coveralls" }, ] contrib = [ { name = "aiohttp" }, { name = "aiosqlite" }, { name = "argon2-cffi" }, { name = "cryptography" }, { name = "greenlet" }, { name = "jsonschema" }, { name = "mergedeep" }, { name = "pyjwt" }, { name = "pyopenssl" }, { name = "python-ldap" }, { name = "sqlalchemy", extra = ["asyncio"] }, ] [package.dev-dependencies] dev = [ { name = "aiosqlite" }, { name = "greenlet" }, { name = "hatch" }, { name = "hypothesis" }, { name = "mypy" }, { name = "paho-mqtt" }, { name = "poethepoet" }, { name = "pre-commit" }, { name = "psutil" }, { name = "pyhamcrest" }, { name = "pylint" }, { name = "pyopenssl" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "pytest-docker" }, { name = "pytest-logdog" }, { name = "pytest-timeout" }, { name = "ruff" }, { name = "setuptools" }, { name = "sqlalchemy", extra = ["mypy"] }, { name = "types-mock" }, { name = "types-pyyaml" }, { name = "types-setuptools" }, ] docs = [ { name = "griffe" }, { name = "markdown-callouts" }, { name = "markdown-exec" }, { name = "mkdocs" }, { name = "mkdocs-coverage" }, { name = "mkdocs-exclude" }, { name = "mkdocs-git-revision-date-localized-plugin" }, { name = "mkdocs-llmstxt" }, { name = "mkdocs-material" }, { name = "mkdocs-minify-plugin" }, { name = "mkdocs-open-in-new-tab" }, { name = "mkdocs-redirects" }, { name = "mkdocs-section-index" }, { name = "mkdocs-typer2" }, { name = "mkdocstrings-python" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] [package.metadata] requires-dist = [ { name = "aiohttp", marker = "extra == 'contrib'", specifier = ">=3.12.13" }, { name = "aiosqlite", marker = "extra == 'contrib'", specifier = ">=0.21.0" }, { name = "argon2-cffi", marker = "extra == 'contrib'", specifier = ">=25.1.0" }, { name = "coveralls", marker = "extra == 'ci'", specifier = "==4.0.1" }, { name = "cryptography", marker = "extra == 'contrib'", specifier = ">=45.0.3" }, { name = "dacite", specifier = ">=1.9.2" }, { name = "greenlet", marker = "extra == 'contrib'", specifier = ">=3.2.3" }, { name = "jsonschema", marker = "extra == 'contrib'", specifier = ">=4.25.0" }, { name = "mergedeep", marker = "extra == 'contrib'", specifier = ">=1.3.4" }, { name = "passlib", specifier = "==1.7.4" }, { name = "psutil", specifier = ">=7.0.0" }, { name = "pyjwt", marker = "extra == 'contrib'", specifier = ">=2.10.1" }, { name = "pyopenssl", marker = "extra == 'contrib'", specifier = ">=25.1.0" }, { name = "python-ldap", marker = "extra == 'contrib'", specifier = ">=3.4.4" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'contrib'", specifier = ">=2.0.41" }, { name = "transitions", specifier = "==0.9.2" }, { name = "typer", specifier = "==0.15.4" }, { name = "websockets", specifier = "==15.0.1" }, ] provides-extras = ["ci", "contrib"] [package.metadata.requires-dev] dev = [ { name = "aiosqlite", specifier = ">=0.21.0" }, { name = "greenlet", specifier = ">=3.2.3" }, { name = "hatch", specifier = ">=1.14.1" }, { name = "hypothesis", specifier = ">=6.130.8" }, { name = "mypy", specifier = ">=1.15.0" }, { name = "paho-mqtt", specifier = ">=2.1.0" }, { name = "poethepoet", specifier = ">=0.34.0" }, { name = "pre-commit", specifier = ">=4.2.0" }, { name = "psutil", specifier = ">=7.0.0" }, { name = "pyhamcrest", specifier = ">=2.1.0" }, { name = "pylint", specifier = ">=3.3.6" }, { name = "pyopenssl", specifier = ">=25.1.0" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-asyncio", specifier = ">=0.26.0" }, { name = "pytest-cov", specifier = ">=6.1.0" }, { name = "pytest-docker", specifier = ">=3.2.3" }, { name = "pytest-logdog", specifier = ">=0.1.0" }, { name = "pytest-timeout", specifier = ">=2.3.1" }, { name = "ruff", specifier = ">=0.11.3" }, { name = "setuptools", specifier = ">=78.1.0" }, { name = "sqlalchemy", extras = ["mypy"], specifier = ">=2.0.41" }, { name = "types-mock", specifier = ">=5.2.0.20250306" }, { name = "types-pyyaml", specifier = ">=6.0.12.20250402" }, { name = "types-setuptools", specifier = ">=78.1.0.20250329" }, ] docs = [ { name = "griffe", specifier = ">=1.11.1" }, { name = "markdown-callouts", specifier = ">=0.4" }, { name = "markdown-exec", specifier = ">=1.8" }, { name = "mkdocs", specifier = ">=1.6" }, { name = "mkdocs-coverage", specifier = ">=1.0" }, { name = "mkdocs-exclude", specifier = ">=1.0.2" }, { name = "mkdocs-git-revision-date-localized-plugin", specifier = ">=1.2" }, { name = "mkdocs-llmstxt", specifier = ">=0.1" }, { name = "mkdocs-material", specifier = ">=9.5" }, { name = "mkdocs-minify-plugin", specifier = ">=0.8" }, { name = "mkdocs-open-in-new-tab", specifier = ">=1.0.8" }, { name = "mkdocs-redirects", specifier = ">=1.2.1" }, { name = "mkdocs-section-index", specifier = ">=0.3" }, { name = "mkdocs-typer2", specifier = ">=0.1.4" }, { name = "mkdocstrings-python", specifier = ">=1.16.2" }, { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] name = "anyio" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, ] [[package]] name = "argon2-cffi" version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argon2-cffi-bindings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, ] [[package]] name = "argon2-cffi-bindings" version = "21.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911, upload-time = "2021-12-01T08:52:55.68Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658, upload-time = "2021-12-01T09:09:17.016Z" }, { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583, upload-time = "2021-12-01T09:09:19.546Z" }, { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168, upload-time = "2021-12-01T09:09:21.445Z" }, { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709, upload-time = "2021-12-01T09:09:18.182Z" }, { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613, upload-time = "2021-12-01T09:09:22.741Z" }, { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583, upload-time = "2021-12-01T09:09:24.177Z" }, { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475, upload-time = "2021-12-01T09:09:26.673Z" }, { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698, upload-time = "2021-12-01T09:09:27.87Z" }, { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817, upload-time = "2021-12-01T09:09:30.267Z" }, { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104, upload-time = "2021-12-01T09:09:31.335Z" }, ] [[package]] name = "astroid" version = "3.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/c2/9b2de9ed027f9fe5734a6c0c0a601289d796b3caaf1e372e23fa88a73047/astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce", size = 398941, upload-time = "2025-05-10T13:33:10.405Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/15/58/5260205b9968c20b6457ed82f48f9e3d6edf2f1f95103161798b73aeccf0/astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb", size = 275388, upload-time = "2025-05-10T13:33:08.391Z" }, ] [[package]] name = "async-timeout" version = "5.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, ] [[package]] name = "attrs" version = "25.3.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] [[package]] name = "babel" version = "2.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] [[package]] name = "backports-tarfile" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, ] [[package]] name = "backrefs" version = "5.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994, upload-time = "2025-02-25T18:15:32.003Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337, upload-time = "2025-02-25T16:53:14.607Z" }, { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142, upload-time = "2025-02-25T16:53:17.266Z" }, { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021, upload-time = "2025-02-25T16:53:26.378Z" }, { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915, upload-time = "2025-02-25T16:53:28.167Z" }, { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336, upload-time = "2025-02-25T16:53:29.858Z" }, ] [[package]] name = "beautifulsoup4" version = "4.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, ] [[package]] name = "certifi" version = "2025.4.26" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, ] [[package]] name = "cffi" version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" }, { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" }, { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, ] [[package]] name = "cfgv" version = "3.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] [[package]] name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.8.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759, upload-time = "2025-05-23T11:39:57.856Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/26/6b/7dd06399a5c0b81007e3a6af0395cd60e6a30f959f8d407d3ee04642e896/coverage-7.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd8ec21e1443fd7a447881332f7ce9d35b8fbd2849e761bb290b584535636b0a", size = 211573, upload-time = "2025-05-23T11:37:47.207Z" }, { url = "https://files.pythonhosted.org/packages/f0/df/2b24090820a0bac1412955fb1a4dade6bc3b8dcef7b899c277ffaf16916d/coverage-7.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26c2396674816deaeae7ded0e2b42c26537280f8fe313335858ffff35019be", size = 212006, upload-time = "2025-05-23T11:37:50.289Z" }, { url = "https://files.pythonhosted.org/packages/c5/c4/e4e3b998e116625562a872a342419652fa6ca73f464d9faf9f52f1aff427/coverage-7.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1aec326ed237e5880bfe69ad41616d333712c7937bcefc1343145e972938f9b3", size = 241128, upload-time = "2025-05-23T11:37:52.229Z" }, { url = "https://files.pythonhosted.org/packages/b1/67/b28904afea3e87a895da850ba587439a61699bf4b73d04d0dfd99bbd33b4/coverage-7.8.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e818796f71702d7a13e50c70de2a1924f729228580bcba1607cccf32eea46e6", size = 239026, upload-time = "2025-05-23T11:37:53.846Z" }, { url = "https://files.pythonhosted.org/packages/8c/0f/47bf7c5630d81bc2cd52b9e13043685dbb7c79372a7f5857279cc442b37c/coverage-7.8.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:546e537d9e24efc765c9c891328f30f826e3e4808e31f5d0f87c4ba12bbd1622", size = 240172, upload-time = "2025-05-23T11:37:55.711Z" }, { url = "https://files.pythonhosted.org/packages/ba/38/af3eb9d36d85abc881f5aaecf8209383dbe0fa4cac2d804c55d05c51cb04/coverage-7.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab9b09a2349f58e73f8ebc06fac546dd623e23b063e5398343c5270072e3201c", size = 240086, upload-time = "2025-05-23T11:37:57.724Z" }, { url = "https://files.pythonhosted.org/packages/9e/64/c40c27c2573adeba0fe16faf39a8aa57368a1f2148865d6bb24c67eadb41/coverage-7.8.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fd51355ab8a372d89fb0e6a31719e825cf8df8b6724bee942fb5b92c3f016ba3", size = 238792, upload-time = "2025-05-23T11:37:59.737Z" }, { url = "https://files.pythonhosted.org/packages/8e/ab/b7c85146f15457671c1412afca7c25a5696d7625e7158002aa017e2d7e3c/coverage-7.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0774df1e093acb6c9e4d58bce7f86656aeed6c132a16e2337692c12786b32404", size = 239096, upload-time = "2025-05-23T11:38:01.693Z" }, { url = "https://files.pythonhosted.org/packages/d3/50/9446dad1310905fb1dc284d60d4320a5b25d4e3e33f9ea08b8d36e244e23/coverage-7.8.2-cp310-cp310-win32.whl", hash = "sha256:00f2e2f2e37f47e5f54423aeefd6c32a7dbcedc033fcd3928a4f4948e8b96af7", size = 214144, upload-time = "2025-05-23T11:38:03.68Z" }, { url = "https://files.pythonhosted.org/packages/23/ed/792e66ad7b8b0df757db8d47af0c23659cdb5a65ef7ace8b111cacdbee89/coverage-7.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:145b07bea229821d51811bf15eeab346c236d523838eda395ea969d120d13347", size = 215043, upload-time = "2025-05-23T11:38:05.217Z" }, { url = "https://files.pythonhosted.org/packages/6a/4d/1ff618ee9f134d0de5cc1661582c21a65e06823f41caf801aadf18811a8e/coverage-7.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b99058eef42e6a8dcd135afb068b3d53aff3921ce699e127602efff9956457a9", size = 211692, upload-time = "2025-05-23T11:38:08.485Z" }, { url = "https://files.pythonhosted.org/packages/96/fa/c3c1b476de96f2bc7a8ca01a9f1fcb51c01c6b60a9d2c3e66194b2bdb4af/coverage-7.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5feb7f2c3e6ea94d3b877def0270dff0947b8d8c04cfa34a17be0a4dc1836879", size = 212115, upload-time = "2025-05-23T11:38:09.989Z" }, { url = "https://files.pythonhosted.org/packages/f7/c2/5414c5a1b286c0f3881ae5adb49be1854ac5b7e99011501f81c8c1453065/coverage-7.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:670a13249b957bb9050fab12d86acef7bf8f6a879b9d1a883799276e0d4c674a", size = 244740, upload-time = "2025-05-23T11:38:11.947Z" }, { url = "https://files.pythonhosted.org/packages/cd/46/1ae01912dfb06a642ef3dd9cf38ed4996fda8fe884dab8952da616f81a2b/coverage-7.8.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bdc8bf760459a4a4187b452213e04d039990211f98644c7292adf1e471162b5", size = 242429, upload-time = "2025-05-23T11:38:13.955Z" }, { url = "https://files.pythonhosted.org/packages/06/58/38c676aec594bfe2a87c7683942e5a30224791d8df99bcc8439fde140377/coverage-7.8.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07a989c867986c2a75f158f03fdb413128aad29aca9d4dbce5fc755672d96f11", size = 244218, upload-time = "2025-05-23T11:38:15.631Z" }, { url = "https://files.pythonhosted.org/packages/80/0c/95b1023e881ce45006d9abc250f76c6cdab7134a1c182d9713878dfefcb2/coverage-7.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2db10dedeb619a771ef0e2949ccba7b75e33905de959c2643a4607bef2f3fb3a", size = 243865, upload-time = "2025-05-23T11:38:17.622Z" }, { url = "https://files.pythonhosted.org/packages/57/37/0ae95989285a39e0839c959fe854a3ae46c06610439350d1ab860bf020ac/coverage-7.8.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e6ea7dba4e92926b7b5f0990634b78ea02f208d04af520c73a7c876d5a8d36cb", size = 242038, upload-time = "2025-05-23T11:38:19.966Z" }, { url = "https://files.pythonhosted.org/packages/4d/82/40e55f7c0eb5e97cc62cbd9d0746fd24e8caf57be5a408b87529416e0c70/coverage-7.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ef2f22795a7aca99fc3c84393a55a53dd18ab8c93fb431004e4d8f0774150f54", size = 242567, upload-time = "2025-05-23T11:38:21.912Z" }, { url = "https://files.pythonhosted.org/packages/f9/35/66a51adc273433a253989f0d9cc7aa6bcdb4855382cf0858200afe578861/coverage-7.8.2-cp311-cp311-win32.whl", hash = "sha256:641988828bc18a6368fe72355df5f1703e44411adbe49bba5644b941ce6f2e3a", size = 214194, upload-time = "2025-05-23T11:38:23.571Z" }, { url = "https://files.pythonhosted.org/packages/f6/8f/a543121f9f5f150eae092b08428cb4e6b6d2d134152c3357b77659d2a605/coverage-7.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:8ab4a51cb39dc1933ba627e0875046d150e88478dbe22ce145a68393e9652975", size = 215109, upload-time = "2025-05-23T11:38:25.137Z" }, { url = "https://files.pythonhosted.org/packages/77/65/6cc84b68d4f35186463cd7ab1da1169e9abb59870c0f6a57ea6aba95f861/coverage-7.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:8966a821e2083c74d88cca5b7dcccc0a3a888a596a04c0b9668a891de3a0cc53", size = 213521, upload-time = "2025-05-23T11:38:27.123Z" }, { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876, upload-time = "2025-05-23T11:38:29.01Z" }, { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130, upload-time = "2025-05-23T11:38:30.675Z" }, { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176, upload-time = "2025-05-23T11:38:32.395Z" }, { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068, upload-time = "2025-05-23T11:38:33.989Z" }, { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328, upload-time = "2025-05-23T11:38:35.568Z" }, { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099, upload-time = "2025-05-23T11:38:37.627Z" }, { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314, upload-time = "2025-05-23T11:38:39.238Z" }, { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489, upload-time = "2025-05-23T11:38:40.845Z" }, { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366, upload-time = "2025-05-23T11:38:43.551Z" }, { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165, upload-time = "2025-05-23T11:38:45.148Z" }, { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548, upload-time = "2025-05-23T11:38:46.74Z" }, { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898, upload-time = "2025-05-23T11:38:49.066Z" }, { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171, upload-time = "2025-05-23T11:38:51.207Z" }, { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564, upload-time = "2025-05-23T11:38:52.857Z" }, { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719, upload-time = "2025-05-23T11:38:54.529Z" }, { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634, upload-time = "2025-05-23T11:38:57.326Z" }, { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824, upload-time = "2025-05-23T11:38:59.421Z" }, { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872, upload-time = "2025-05-23T11:39:01.049Z" }, { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179, upload-time = "2025-05-23T11:39:02.709Z" }, { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393, upload-time = "2025-05-23T11:39:05.457Z" }, { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194, upload-time = "2025-05-23T11:39:07.171Z" }, { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580, upload-time = "2025-05-23T11:39:08.862Z" }, { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734, upload-time = "2025-05-23T11:39:11.109Z" }, { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959, upload-time = "2025-05-23T11:39:12.751Z" }, { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024, upload-time = "2025-05-23T11:39:15.569Z" }, { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867, upload-time = "2025-05-23T11:39:17.64Z" }, { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096, upload-time = "2025-05-23T11:39:19.328Z" }, { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276, upload-time = "2025-05-23T11:39:21.077Z" }, { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478, upload-time = "2025-05-23T11:39:22.838Z" }, { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255, upload-time = "2025-05-23T11:39:24.644Z" }, { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109, upload-time = "2025-05-23T11:39:26.722Z" }, { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268, upload-time = "2025-05-23T11:39:28.429Z" }, { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071, upload-time = "2025-05-23T11:39:30.55Z" }, { url = "https://files.pythonhosted.org/packages/69/2f/572b29496d8234e4a7773200dd835a0d32d9e171f2d974f3fe04a9dbc271/coverage-7.8.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:ec455eedf3ba0bbdf8f5a570012617eb305c63cb9f03428d39bf544cb2b94837", size = 203636, upload-time = "2025-05-23T11:39:52.002Z" }, { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623, upload-time = "2025-05-23T11:39:53.846Z" }, ] [package.optional-dependencies] toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] name = "coveralls" version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "docopt" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/75/a454fb443eb6a053833f61603a432ffbd7dd6ae53a11159bacfadb9d6219/coveralls-4.0.1.tar.gz", hash = "sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69", size = 12419, upload-time = "2024-05-15T12:56:14.297Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/63/e5/6708c75e2a4cfca929302d4d9b53b862c6dc65bd75e6933ea3d20016d41d/coveralls-4.0.1-py3-none-any.whl", hash = "sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809", size = 13599, upload-time = "2024-05-15T12:56:12.342Z" }, ] [[package]] name = "cryptography" version = "45.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/1f/9fa001e74a1993a9cadd2333bb889e50c66327b8594ac538ab8a04f915b7/cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899", size = 744738, upload-time = "2025-05-25T14:17:24.777Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/82/b2/2345dc595998caa6f68adf84e8f8b50d18e9fc4638d32b22ea8daedd4b7a/cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71", size = 7056239, upload-time = "2025-05-25T14:16:12.22Z" }, { url = "https://files.pythonhosted.org/packages/71/3d/ac361649a0bfffc105e2298b720d8b862330a767dab27c06adc2ddbef96a/cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b", size = 4205541, upload-time = "2025-05-25T14:16:14.333Z" }, { url = "https://files.pythonhosted.org/packages/70/3e/c02a043750494d5c445f769e9c9f67e550d65060e0bfce52d91c1362693d/cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f", size = 4433275, upload-time = "2025-05-25T14:16:16.421Z" }, { url = "https://files.pythonhosted.org/packages/40/7a/9af0bfd48784e80eef3eb6fd6fde96fe706b4fc156751ce1b2b965dada70/cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942", size = 4209173, upload-time = "2025-05-25T14:16:18.163Z" }, { url = "https://files.pythonhosted.org/packages/31/5f/d6f8753c8708912df52e67969e80ef70b8e8897306cd9eb8b98201f8c184/cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9", size = 3898150, upload-time = "2025-05-25T14:16:20.34Z" }, { url = "https://files.pythonhosted.org/packages/8b/50/f256ab79c671fb066e47336706dc398c3b1e125f952e07d54ce82cf4011a/cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56", size = 4466473, upload-time = "2025-05-25T14:16:22.605Z" }, { url = "https://files.pythonhosted.org/packages/62/e7/312428336bb2df0848d0768ab5a062e11a32d18139447a76dfc19ada8eed/cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca", size = 4211890, upload-time = "2025-05-25T14:16:24.738Z" }, { url = "https://files.pythonhosted.org/packages/e7/53/8a130e22c1e432b3c14896ec5eb7ac01fb53c6737e1d705df7e0efb647c6/cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1", size = 4466300, upload-time = "2025-05-25T14:16:26.768Z" }, { url = "https://files.pythonhosted.org/packages/ba/75/6bb6579688ef805fd16a053005fce93944cdade465fc92ef32bbc5c40681/cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578", size = 4332483, upload-time = "2025-05-25T14:16:28.316Z" }, { url = "https://files.pythonhosted.org/packages/2f/11/2538f4e1ce05c6c4f81f43c1ef2bd6de7ae5e24ee284460ff6c77e42ca77/cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497", size = 4573714, upload-time = "2025-05-25T14:16:30.474Z" }, { url = "https://files.pythonhosted.org/packages/f5/bb/e86e9cf07f73a98d84a4084e8fd420b0e82330a901d9cac8149f994c3417/cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710", size = 2934752, upload-time = "2025-05-25T14:16:32.204Z" }, { url = "https://files.pythonhosted.org/packages/c7/75/063bc9ddc3d1c73e959054f1fc091b79572e716ef74d6caaa56e945b4af9/cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490", size = 3412465, upload-time = "2025-05-25T14:16:33.888Z" }, { url = "https://files.pythonhosted.org/packages/71/9b/04ead6015229a9396890d7654ee35ef630860fb42dc9ff9ec27f72157952/cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06", size = 7031892, upload-time = "2025-05-25T14:16:36.214Z" }, { url = "https://files.pythonhosted.org/packages/46/c7/c7d05d0e133a09fc677b8a87953815c522697bdf025e5cac13ba419e7240/cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57", size = 4196181, upload-time = "2025-05-25T14:16:37.934Z" }, { url = "https://files.pythonhosted.org/packages/08/7a/6ad3aa796b18a683657cef930a986fac0045417e2dc428fd336cfc45ba52/cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716", size = 4423370, upload-time = "2025-05-25T14:16:39.502Z" }, { url = "https://files.pythonhosted.org/packages/4f/58/ec1461bfcb393525f597ac6a10a63938d18775b7803324072974b41a926b/cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8", size = 4197839, upload-time = "2025-05-25T14:16:41.322Z" }, { url = "https://files.pythonhosted.org/packages/d4/3d/5185b117c32ad4f40846f579369a80e710d6146c2baa8ce09d01612750db/cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc", size = 3886324, upload-time = "2025-05-25T14:16:43.041Z" }, { url = "https://files.pythonhosted.org/packages/67/85/caba91a57d291a2ad46e74016d1f83ac294f08128b26e2a81e9b4f2d2555/cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342", size = 4450447, upload-time = "2025-05-25T14:16:44.759Z" }, { url = "https://files.pythonhosted.org/packages/ae/d1/164e3c9d559133a38279215c712b8ba38e77735d3412f37711b9f8f6f7e0/cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b", size = 4200576, upload-time = "2025-05-25T14:16:46.438Z" }, { url = "https://files.pythonhosted.org/packages/71/7a/e002d5ce624ed46dfc32abe1deff32190f3ac47ede911789ee936f5a4255/cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782", size = 4450308, upload-time = "2025-05-25T14:16:48.228Z" }, { url = "https://files.pythonhosted.org/packages/87/ad/3fbff9c28cf09b0a71e98af57d74f3662dea4a174b12acc493de00ea3f28/cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65", size = 4325125, upload-time = "2025-05-25T14:16:49.844Z" }, { url = "https://files.pythonhosted.org/packages/f5/b4/51417d0cc01802304c1984d76e9592f15e4801abd44ef7ba657060520bf0/cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b", size = 4560038, upload-time = "2025-05-25T14:16:51.398Z" }, { url = "https://files.pythonhosted.org/packages/80/38/d572f6482d45789a7202fb87d052deb7a7b136bf17473ebff33536727a2c/cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab", size = 2924070, upload-time = "2025-05-25T14:16:53.472Z" }, { url = "https://files.pythonhosted.org/packages/91/5a/61f39c0ff4443651cc64e626fa97ad3099249152039952be8f344d6b0c86/cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2", size = 3395005, upload-time = "2025-05-25T14:16:55.134Z" }, { url = "https://files.pythonhosted.org/packages/1b/63/ce30cb7204e8440df2f0b251dc0464a26c55916610d1ba4aa912f838bcc8/cryptography-45.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed43d396f42028c1f47b5fec012e9e12631266e3825e95c00e3cf94d472dac49", size = 3578348, upload-time = "2025-05-25T14:16:56.792Z" }, { url = "https://files.pythonhosted.org/packages/45/0b/87556d3337f5e93c37fda0a0b5d3e7b4f23670777ce8820fce7962a7ed22/cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fed5aaca1750e46db870874c9c273cd5182a9e9deb16f06f7bdffdb5c2bde4b9", size = 4142867, upload-time = "2025-05-25T14:16:58.459Z" }, { url = "https://files.pythonhosted.org/packages/72/ba/21356dd0bcb922b820211336e735989fe2cf0d8eaac206335a0906a5a38c/cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc", size = 4385000, upload-time = "2025-05-25T14:17:00.656Z" }, { url = "https://files.pythonhosted.org/packages/2f/2b/71c78d18b804c317b66283be55e20329de5cd7e1aec28e4c5fbbe21fd046/cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:92d5f428c1a0439b2040435a1d6bc1b26ebf0af88b093c3628913dd464d13fa1", size = 4144195, upload-time = "2025-05-25T14:17:02.782Z" }, { url = "https://files.pythonhosted.org/packages/55/3e/9f9b468ea779b4dbfef6af224804abd93fbcb2c48605d7443b44aea77979/cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:ec64ee375b5aaa354b2b273c921144a660a511f9df8785e6d1c942967106438e", size = 4384540, upload-time = "2025-05-25T14:17:04.49Z" }, { url = "https://files.pythonhosted.org/packages/97/f5/6e62d10cf29c50f8205c0dc9aec986dca40e8e3b41bf1a7878ea7b11e5ee/cryptography-45.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:71320fbefd05454ef2d457c481ba9a5b0e540f3753354fff6f780927c25d19b0", size = 3328796, upload-time = "2025-05-25T14:17:06.174Z" }, { url = "https://files.pythonhosted.org/packages/e7/d4/58a246342093a66af8935d6aa59f790cbb4731adae3937b538d054bdc2f9/cryptography-45.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7", size = 3589802, upload-time = "2025-05-25T14:17:07.792Z" }, { url = "https://files.pythonhosted.org/packages/96/61/751ebea58c87b5be533c429f01996050a72c7283b59eee250275746632ea/cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8", size = 4146964, upload-time = "2025-05-25T14:17:09.538Z" }, { url = "https://files.pythonhosted.org/packages/8d/01/28c90601b199964de383da0b740b5156f5d71a1da25e7194fdf793d373ef/cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4", size = 4388103, upload-time = "2025-05-25T14:17:11.978Z" }, { url = "https://files.pythonhosted.org/packages/3d/ec/cd892180b9e42897446ef35c62442f5b8b039c3d63a05f618aa87ec9ebb5/cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972", size = 4150031, upload-time = "2025-05-25T14:17:14.131Z" }, { url = "https://files.pythonhosted.org/packages/db/d4/22628c2dedd99289960a682439c6d3aa248dff5215123ead94ac2d82f3f5/cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c", size = 4387389, upload-time = "2025-05-25T14:17:17.303Z" }, { url = "https://files.pythonhosted.org/packages/39/ec/ba3961abbf8ecb79a3586a4ff0ee08c9d7a9938b4312fb2ae9b63f48a8ba/cryptography-45.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19", size = 3337432, upload-time = "2025-05-25T14:17:19.507Z" }, ] [[package]] name = "csscompressor" version = "0.9.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/2a/8c3ac3d8bc94e6de8d7ae270bb5bc437b210bb9d6d9e46630c98f4abd20c/csscompressor-0.9.5.tar.gz", hash = "sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05", size = 237808, upload-time = "2017-11-26T21:13:08.238Z" } [[package]] name = "dacite" version = "1.9.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/55/a0/7ca79796e799a3e782045d29bf052b5cde7439a2bbb17f15ff44f7aacc63/dacite-1.9.2.tar.gz", hash = "sha256:6ccc3b299727c7aa17582f0021f6ae14d5de47c7227932c47fec4cdfefd26f09", size = 22420, upload-time = "2025-02-05T09:27:29.757Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/94/35/386550fd60316d1e37eccdda609b074113298f23cef5bddb2049823fe666/dacite-1.9.2-py3-none-any.whl", hash = "sha256:053f7c3f5128ca2e9aceb66892b1a3c8936d02c686e707bee96e19deef4bc4a0", size = 16600, upload-time = "2025-02-05T09:27:24.345Z" }, ] [[package]] name = "dill" version = "0.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, ] [[package]] name = "distlib" version = "0.3.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, ] [[package]] name = "docopt" version = "0.6.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } [[package]] name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] [[package]] name = "filelock" version = "3.18.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] [[package]] name = "frozenlist" version = "1.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/af/36/0da0a49409f6b47cc2d060dc8c9040b897b5902a8a4e37d9bc1deb11f680/frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a", size = 81304, upload-time = "2025-06-09T22:59:46.226Z" }, { url = "https://files.pythonhosted.org/packages/77/f0/77c11d13d39513b298e267b22eb6cb559c103d56f155aa9a49097221f0b6/frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61", size = 47735, upload-time = "2025-06-09T22:59:48.133Z" }, { url = "https://files.pythonhosted.org/packages/37/12/9d07fa18971a44150593de56b2f2947c46604819976784bcf6ea0d5db43b/frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d", size = 46775, upload-time = "2025-06-09T22:59:49.564Z" }, { url = "https://files.pythonhosted.org/packages/70/34/f73539227e06288fcd1f8a76853e755b2b48bca6747e99e283111c18bcd4/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e", size = 224644, upload-time = "2025-06-09T22:59:51.35Z" }, { url = "https://files.pythonhosted.org/packages/fb/68/c1d9c2f4a6e438e14613bad0f2973567586610cc22dcb1e1241da71de9d3/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9", size = 222125, upload-time = "2025-06-09T22:59:52.884Z" }, { url = "https://files.pythonhosted.org/packages/b9/d0/98e8f9a515228d708344d7c6986752be3e3192d1795f748c24bcf154ad99/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c", size = 233455, upload-time = "2025-06-09T22:59:54.74Z" }, { url = "https://files.pythonhosted.org/packages/79/df/8a11bcec5600557f40338407d3e5bea80376ed1c01a6c0910fcfdc4b8993/frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981", size = 227339, upload-time = "2025-06-09T22:59:56.187Z" }, { url = "https://files.pythonhosted.org/packages/50/82/41cb97d9c9a5ff94438c63cc343eb7980dac4187eb625a51bdfdb7707314/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615", size = 212969, upload-time = "2025-06-09T22:59:57.604Z" }, { url = "https://files.pythonhosted.org/packages/13/47/f9179ee5ee4f55629e4f28c660b3fdf2775c8bfde8f9c53f2de2d93f52a9/frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50", size = 222862, upload-time = "2025-06-09T22:59:59.498Z" }, { url = "https://files.pythonhosted.org/packages/1a/52/df81e41ec6b953902c8b7e3a83bee48b195cb0e5ec2eabae5d8330c78038/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa", size = 222492, upload-time = "2025-06-09T23:00:01.026Z" }, { url = "https://files.pythonhosted.org/packages/84/17/30d6ea87fa95a9408245a948604b82c1a4b8b3e153cea596421a2aef2754/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577", size = 238250, upload-time = "2025-06-09T23:00:03.401Z" }, { url = "https://files.pythonhosted.org/packages/8f/00/ecbeb51669e3c3df76cf2ddd66ae3e48345ec213a55e3887d216eb4fbab3/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59", size = 218720, upload-time = "2025-06-09T23:00:05.282Z" }, { url = "https://files.pythonhosted.org/packages/1a/c0/c224ce0e0eb31cc57f67742071bb470ba8246623c1823a7530be0e76164c/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e", size = 232585, upload-time = "2025-06-09T23:00:07.962Z" }, { url = "https://files.pythonhosted.org/packages/55/3c/34cb694abf532f31f365106deebdeac9e45c19304d83cf7d51ebbb4ca4d1/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd", size = 234248, upload-time = "2025-06-09T23:00:09.428Z" }, { url = "https://files.pythonhosted.org/packages/98/c0/2052d8b6cecda2e70bd81299e3512fa332abb6dcd2969b9c80dfcdddbf75/frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718", size = 221621, upload-time = "2025-06-09T23:00:11.32Z" }, { url = "https://files.pythonhosted.org/packages/c5/bf/7dcebae315436903b1d98ffb791a09d674c88480c158aa171958a3ac07f0/frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e", size = 39578, upload-time = "2025-06-09T23:00:13.526Z" }, { url = "https://files.pythonhosted.org/packages/8f/5f/f69818f017fa9a3d24d1ae39763e29b7f60a59e46d5f91b9c6b21622f4cd/frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464", size = 43830, upload-time = "2025-06-09T23:00:14.98Z" }, { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251, upload-time = "2025-06-09T23:00:16.279Z" }, { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183, upload-time = "2025-06-09T23:00:17.698Z" }, { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107, upload-time = "2025-06-09T23:00:18.952Z" }, { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333, upload-time = "2025-06-09T23:00:20.275Z" }, { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724, upload-time = "2025-06-09T23:00:21.705Z" }, { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842, upload-time = "2025-06-09T23:00:23.148Z" }, { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767, upload-time = "2025-06-09T23:00:25.103Z" }, { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130, upload-time = "2025-06-09T23:00:27.061Z" }, { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301, upload-time = "2025-06-09T23:00:29.02Z" }, { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606, upload-time = "2025-06-09T23:00:30.514Z" }, { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372, upload-time = "2025-06-09T23:00:31.966Z" }, { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860, upload-time = "2025-06-09T23:00:33.375Z" }, { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893, upload-time = "2025-06-09T23:00:35.002Z" }, { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323, upload-time = "2025-06-09T23:00:36.468Z" }, { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149, upload-time = "2025-06-09T23:00:37.963Z" }, { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565, upload-time = "2025-06-09T23:00:39.753Z" }, { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019, upload-time = "2025-06-09T23:00:40.988Z" }, { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] [[package]] name = "ghp-import" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] [[package]] name = "gitdb" version = "4.0.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, ] [[package]] name = "gitpython" version = "3.1.44" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, ] [[package]] name = "greenlet" version = "3.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/92/db/b4c12cff13ebac2786f4f217f06588bccd8b53d260453404ef22b121fc3a/greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be", size = 268977, upload-time = "2025-06-05T16:10:24.001Z" }, { url = "https://files.pythonhosted.org/packages/52/61/75b4abd8147f13f70986df2801bf93735c1bd87ea780d70e3b3ecda8c165/greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac", size = 627351, upload-time = "2025-06-05T16:38:50.685Z" }, { url = "https://files.pythonhosted.org/packages/35/aa/6894ae299d059d26254779a5088632874b80ee8cf89a88bca00b0709d22f/greenlet-3.2.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392", size = 638599, upload-time = "2025-06-05T16:41:34.057Z" }, { url = "https://files.pythonhosted.org/packages/30/64/e01a8261d13c47f3c082519a5e9dbf9e143cc0498ed20c911d04e54d526c/greenlet-3.2.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c", size = 634482, upload-time = "2025-06-05T16:48:16.26Z" }, { url = "https://files.pythonhosted.org/packages/47/48/ff9ca8ba9772d083a4f5221f7b4f0ebe8978131a9ae0909cf202f94cd879/greenlet-3.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db", size = 633284, upload-time = "2025-06-05T16:13:01.599Z" }, { url = "https://files.pythonhosted.org/packages/e9/45/626e974948713bc15775b696adb3eb0bd708bec267d6d2d5c47bb47a6119/greenlet-3.2.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b", size = 582206, upload-time = "2025-06-05T16:12:48.51Z" }, { url = "https://files.pythonhosted.org/packages/b1/8e/8b6f42c67d5df7db35b8c55c9a850ea045219741bb14416255616808c690/greenlet-3.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712", size = 1111412, upload-time = "2025-06-05T16:36:45.479Z" }, { url = "https://files.pythonhosted.org/packages/05/46/ab58828217349500a7ebb81159d52ca357da747ff1797c29c6023d79d798/greenlet-3.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:731e154aba8e757aedd0781d4b240f1225b075b4409f1bb83b05ff410582cf00", size = 1135054, upload-time = "2025-06-05T16:12:36.478Z" }, { url = "https://files.pythonhosted.org/packages/68/7f/d1b537be5080721c0f0089a8447d4ef72839039cdb743bdd8ffd23046e9a/greenlet-3.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:96c20252c2f792defe9a115d3287e14811036d51e78b3aaddbee23b69b216302", size = 296573, upload-time = "2025-06-05T16:34:26.521Z" }, { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219, upload-time = "2025-06-05T16:10:10.414Z" }, { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383, upload-time = "2025-06-05T16:38:51.785Z" }, { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422, upload-time = "2025-06-05T16:41:35.259Z" }, { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375, upload-time = "2025-06-05T16:48:18.235Z" }, { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627, upload-time = "2025-06-05T16:13:02.858Z" }, { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502, upload-time = "2025-06-05T16:12:49.642Z" }, { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498, upload-time = "2025-06-05T16:36:46.598Z" }, { url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977, upload-time = "2025-06-05T16:12:38.262Z" }, { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017, upload-time = "2025-06-05T16:25:05.225Z" }, { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" }, { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" }, { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" }, { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" }, { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" }, { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" }, { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" }, { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" }, { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" }, { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" }, { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" }, { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" }, { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" }, { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" }, { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" }, { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" }, { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" }, { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" }, { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" }, { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" }, { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" }, { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" }, { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" }, { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" }, { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" }, ] [[package]] name = "griffe" version = "1.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/0f/9cbd56eb047de77a4b93d8d4674e70cd19a1ff64d7410651b514a1ed93d5/griffe-1.11.1.tar.gz", hash = "sha256:d54ffad1ec4da9658901eb5521e9cddcdb7a496604f67d8ae71077f03f549b7e", size = 410996, upload-time = "2025-08-11T11:38:35.528Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e6/a3/451ffd422ce143758a39c0290aaa7c9727ecc2bcc19debd7a8f3c6075ce9/griffe-1.11.1-py3-none-any.whl", hash = "sha256:5799cf7c513e4b928cfc6107ee6c4bc4a92e001f07022d97fd8dee2f612b6064", size = 138745, upload-time = "2025-08-11T11:38:33.964Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "hatch" version = "1.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "hatchling" }, { name = "httpx" }, { name = "hyperlink" }, { name = "keyring" }, { name = "packaging" }, { name = "pexpect" }, { name = "platformdirs" }, { name = "rich" }, { name = "shellingham" }, { name = "tomli-w" }, { name = "tomlkit" }, { name = "userpath" }, { name = "uv" }, { name = "virtualenv" }, { name = "zstandard" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1f/43/c0b37db0e857a44ce5ffdb7e8a9b8fa6425d0b74dea698fafcd9bddb50d1/hatch-1.14.1.tar.gz", hash = "sha256:ca1aff788f8596b0dd1f8f8dfe776443d2724a86b1976fabaf087406ba3d0713", size = 5188180, upload-time = "2025-04-07T04:16:04.522Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a5/40/19c0935bf9f25808541a0e3144ac459de696c5b6b6d4511a98d456c69604/hatch-1.14.1-py3-none-any.whl", hash = "sha256:39cdaa59e47ce0c5505d88a951f4324a9c5aafa17e4a877e2fde79b36ab66c21", size = 125770, upload-time = "2025-04-07T04:16:02.525Z" }, ] [[package]] name = "hatchling" version = "1.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "pathspec" }, { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "trove-classifiers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8f/8a/cc1debe3514da292094f1c3a700e4ca25442489731ef7c0814358816bb03/hatchling-1.27.0.tar.gz", hash = "sha256:971c296d9819abb3811112fc52c7a9751c8d381898f36533bb16f9791e941fd6", size = 54983, upload-time = "2024-12-15T17:08:11.894Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/08/e7/ae38d7a6dfba0533684e0b2136817d667588ae3ec984c1a4e5df5eb88482/hatchling-1.27.0-py3-none-any.whl", hash = "sha256:d3a2f3567c4f926ea39849cdf924c7e99e6686c9c8e288ae1037c8fa2a5d937b", size = 75794, upload-time = "2024-12-15T17:08:10.364Z" }, ] [[package]] name = "htmlmin2" version = "0.1.13" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/be/31/a76f4bfa885f93b8167cb4c85cf32b54d1f64384d0b897d45bc6d19b7b45/htmlmin2-0.1.13-py3-none-any.whl", hash = "sha256:75609f2a42e64f7ce57dbff28a39890363bde9e7e5885db633317efbdf8c79a2", size = 34486, upload-time = "2023-03-14T21:28:30.388Z" }, ] [[package]] name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "hyperlink" version = "21.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, ] [[package]] name = "hypothesis" version = "6.131.30" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/7f/e1d7a5ee9f96ca73e0fe51d226e2ad15029ff1ff16b6096ced2837c4af2f/hypothesis-6.131.30.tar.gz", hash = "sha256:c04f748c9cb6c3e3d134699258c2d076afebf40e2752572b6f05f86bd3f23fe5", size = 442221, upload-time = "2025-05-27T18:05:40.098Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e3/a5/59fd76d3445e54cfb3982ffbca627aa58cca127e05d6552a6c4302926a6f/hypothesis-6.131.30-py3-none-any.whl", hash = "sha256:1a04a43f282a32bffb21dc4b1ab7e68c9b34db0298b9b91933484eca4682d6b4", size = 506833, upload-time = "2025-05-27T18:05:35.867Z" }, ] [[package]] name = "identify" version = "2.6.12" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "isort" version = "6.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, ] [[package]] name = "jaraco-classes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, ] [[package]] name = "jaraco-context" version = "6.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, ] [[package]] name = "jaraco-functools" version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159, upload-time = "2024-09-27T19:47:09.122Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187, upload-time = "2024-09-27T19:47:07.14Z" }, ] [[package]] name = "jeepney" version = "0.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, ] [[package]] name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "jsmin" version = "3.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5e/73/e01e4c5e11ad0494f4407a3f623ad4d87714909f50b17a06ed121034ff6e/jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc", size = 13925, upload-time = "2022-01-16T20:35:59.13Z" } [[package]] name = "jsonschema" version = "4.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "jsonschema-specifications" }, { name = "referencing" }, { name = "rpds-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, ] [[package]] name = "jsonschema-specifications" version = "2025.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, ] [[package]] name = "keyring" version = "25.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, { name = "jaraco-classes" }, { name = "jaraco-context" }, { name = "jaraco-functools" }, { name = "jeepney", marker = "sys_platform == 'linux'" }, { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, ] [[package]] name = "markdown" version = "3.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906, upload-time = "2025-04-11T14:42:50.928Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210, upload-time = "2025-04-11T14:42:49.178Z" }, ] [[package]] name = "markdown-callouts" version = "0.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/73/ae5aa379f6f7fea9d0bf4cba888f9a31d451d90f80033ae60ae3045770d5/markdown_callouts-0.4.0.tar.gz", hash = "sha256:7ed2c90486967058a73a547781121983839522d67041ae52c4979616f1b2b746", size = 9768, upload-time = "2024-01-22T23:18:18.513Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1d/b5/7b0a0a52c82bfccd830af2a8cc8add1c5bc932e0204922434954a631dd51/markdown_callouts-0.4.0-py3-none-any.whl", hash = "sha256:ed0da38f29158d93116a0d0c6ecaf9df90b37e0d989b5337d678ee6e6d6550b7", size = 7108, upload-time = "2024-01-22T23:18:17.465Z" }, ] [[package]] name = "markdown-exec" version = "1.10.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pymdown-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/70/e8/dafa2b91c60f3cec6a2926851fb20b3c1fcdfd5721d6ea0b65bb6b7dab7b/markdown_exec-1.10.3.tar.gz", hash = "sha256:ddd33996526a54dcc33debc464a9d4c00c1acece092cf1843cbb1264bf6800a6", size = 81050, upload-time = "2025-03-24T21:52:36.357Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/7e/94d6c703d9a1927339d709ca4224c35215dcd1033ee4d756fa7fa0c8bea9/markdown_exec-1.10.3-py3-none-any.whl", hash = "sha256:74bfe5a9063fafab6199847cbef28dd5071a515e8959f326cf16f2ae7a66033b", size = 34404, upload-time = "2025-03-24T21:52:35.145Z" }, ] [[package]] name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markdownify" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/78/c48fed23c7aebc2c16049062e72de1da3220c274de59d28c942acdc9ffb2/markdownify-1.1.0.tar.gz", hash = "sha256:449c0bbbf1401c5112379619524f33b63490a8fa479456d41de9dc9e37560ebd", size = 17127, upload-time = "2025-03-05T11:54:40.574Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/11/b751af7ad41b254a802cf52f7bc1fca7cabe2388132f2ce60a1a6b9b9622/markdownify-1.1.0-py3-none-any.whl", hash = "sha256:32a5a08e9af02c8a6528942224c91b933b4bd2c7d078f9012943776fc313eeef", size = 13901, upload-time = "2025-03-05T11:54:39.454Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] [[package]] name = "mccabe" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] [[package]] name = "mdformat" version = "0.7.22" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/eb/b5cbf2484411af039a3d4aeb53a5160fae25dd8c84af6a4243bc2f3fedb3/mdformat-0.7.22.tar.gz", hash = "sha256:eef84fa8f233d3162734683c2a8a6222227a229b9206872e6139658d99acb1ea", size = 34610, upload-time = "2025-01-30T18:00:51.418Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f2/6f/94a7344f6d634fe3563bea8b33bccedee37f2726f7807e9a58440dc91627/mdformat-0.7.22-py3-none-any.whl", hash = "sha256:61122637c9e1d9be1329054f3fa216559f0d1f722b7919b060a8c2a4ae1850e5", size = 34447, upload-time = "2025-01-30T18:00:48.708Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mergedeep" version = "1.3.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] [[package]] name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mergedeep" }, { name = "mkdocs-get-deps" }, { name = "packaging" }, { name = "pathspec" }, { name = "pyyaml" }, { name = "pyyaml-env-tag" }, { name = "watchdog" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, ] [[package]] name = "mkdocs-autorefs" version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, ] [[package]] name = "mkdocs-coverage" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/64/a9fe7d953d6b02944610a5f7361cdab7532a9f5518ffe890287a9f187f08/mkdocs_coverage-1.1.0.tar.gz", hash = "sha256:a67cc6f6d548b8d6b4b21ecd777f2e3768b49e7a95e54c6df158e7c0f179134c", size = 5514, upload-time = "2024-06-11T18:47:23.773Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/eb/f0/96851f1d9809e1cbd1f1545948d3c4bde7abfa3af5965b7ba8d88ec37c11/mkdocs_coverage-1.1.0-py3-none-any.whl", hash = "sha256:168ca4ebc35ba48309c1f734d0cab0359cd95b205d0d18030d27e73b6a4590d9", size = 6853, upload-time = "2024-06-11T18:47:20.649Z" }, ] [[package]] name = "mkdocs-exclude" version = "1.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/54/b5/3a8e289282c9e8d7003f8a2f53d673d4fdaa81d493dc6966092d9985b6fc/mkdocs-exclude-1.0.2.tar.gz", hash = "sha256:ba6fab3c80ddbe3fd31d3e579861fd3124513708271180a5f81846da8c7e2a51", size = 6751, upload-time = "2019-02-20T23:34:12.81Z" } [[package]] name = "mkdocs-get-deps" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] [[package]] name = "mkdocs-git-revision-date-localized-plugin" version = "1.4.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "gitpython" }, { name = "mkdocs" }, { name = "pytz" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/f8/a17ec39a4fc314d40cc96afdc1d401e393ebd4f42309d454cc940a2cf38a/mkdocs_git_revision_date_localized_plugin-1.4.7.tar.gz", hash = "sha256:10a49eff1e1c3cb766e054b9d8360c904ce4fe8c33ac3f6cc083ac6459c91953", size = 450473, upload-time = "2025-05-28T18:26:20.697Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/53/b6/106fcc15287e7228658fbd0ad9e8b0d775becced0a089cc39984641f4a0f/mkdocs_git_revision_date_localized_plugin-1.4.7-py3-none-any.whl", hash = "sha256:056c0a90242409148f1dc94d5c9d2c25b5b8ddd8de45489fa38f7fa7ccad2bc4", size = 25382, upload-time = "2025-05-28T18:26:18.907Z" }, ] [[package]] name = "mkdocs-llmstxt" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, { name = "markdownify" }, { name = "mdformat" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ee/25/263ea9c16d1f95f30d9eb1b76e63eb50a88a1ec9fad1829281bab7a371eb/mkdocs_llmstxt-0.2.0.tar.gz", hash = "sha256:104f10b8101167d6baf7761942b4743869be3d8f8a8d909f4e9e0b63307f709e", size = 41376, upload-time = "2025-04-08T13:18:48.664Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/24/29/0a33f7d8499a01dd7fd0d90fb163b2d8eefa9c90ac0ecbc1a7770e50614e/mkdocs_llmstxt-0.2.0-py3-none-any.whl", hash = "sha256:907de892e0c8be74002e8b4d553820c2b5bbcf03cc303b95c8bca48fb49c1a29", size = 23244, upload-time = "2025-04-08T13:18:47.516Z" }, ] [[package]] name = "mkdocs-material" version = "9.6.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, { name = "backrefs" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, { name = "mkdocs" }, { name = "mkdocs-material-extensions" }, { name = "paginate" }, { name = "pygments" }, { name = "pymdown-extensions" }, { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fa/0101de32af88f87cf5cc23ad5f2e2030d00995f74e616306513431b8ab4b/mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754", size = 3951707, upload-time = "2025-05-13T13:27:57.173Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3a/a1/7fdb959ad592e013c01558822fd3c22931a95a0f08cf0a7c36da13a5b2b5/mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b", size = 8703767, upload-time = "2025-05-13T13:27:54.089Z" }, ] [[package]] name = "mkdocs-material-extensions" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] [[package]] name = "mkdocs-minify-plugin" version = "0.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "csscompressor" }, { name = "htmlmin2" }, { name = "jsmin" }, { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/52/67/fe4b77e7a8ae7628392e28b14122588beaf6078b53eb91c7ed000fd158ac/mkdocs-minify-plugin-0.8.0.tar.gz", hash = "sha256:bc11b78b8120d79e817308e2b11539d790d21445eb63df831e393f76e52e753d", size = 8366, upload-time = "2024-01-29T16:11:32.982Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/cd/2e8d0d92421916e2ea4ff97f10a544a9bd5588eb747556701c983581df13/mkdocs_minify_plugin-0.8.0-py3-none-any.whl", hash = "sha256:5fba1a3f7bd9a2142c9954a6559a57e946587b21f133165ece30ea145c66aee6", size = 6723, upload-time = "2024-01-29T16:11:31.851Z" }, ] [[package]] name = "mkdocs-open-in-new-tab" version = "1.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0a/0e/f72a506a21bdb27b807124e00c688226848a388d1fd3980b80ae3cc27203/mkdocs_open_in_new_tab-1.0.8.tar.gz", hash = "sha256:3e0dad08cc9938b0b13097be8e0aa435919de1eeb2d1a648e66b5dee8d57e048", size = 5791, upload-time = "2024-11-18T13:15:13.977Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/21/94/44f3c868495481c868d08eea065c82803f1affd8553d3383b782f497613c/mkdocs_open_in_new_tab-1.0.8-py3-none-any.whl", hash = "sha256:051d767a4467b12d89827e1fea0ec660b05b027c726317fe4fceee5456e36ad2", size = 7717, upload-time = "2024-11-18T13:15:12.286Z" }, ] [[package]] name = "mkdocs-redirects" version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/a8/6d44a6cf07e969c7420cb36ab287b0669da636a2044de38a7d2208d5a758/mkdocs_redirects-1.2.2.tar.gz", hash = "sha256:3094981b42ffab29313c2c1b8ac3969861109f58b2dd58c45fc81cd44bfa0095", size = 7162, upload-time = "2024-11-07T14:57:21.109Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c4/ec/38443b1f2a3821bbcb24e46cd8ba979154417794d54baf949fefde1c2146/mkdocs_redirects-1.2.2-py3-none-any.whl", hash = "sha256:7dbfa5647b79a3589da4401403d69494bd1f4ad03b9c15136720367e1f340ed5", size = 6142, upload-time = "2024-11-07T14:57:19.143Z" }, ] [[package]] name = "mkdocs-section-index" version = "0.3.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/40/4aa9d3cfa2ac6528b91048847a35f005b97ec293204c02b179762a85b7f2/mkdocs_section_index-0.3.10.tar.gz", hash = "sha256:a82afbda633c82c5568f0e3b008176b9b365bf4bd8b6f919d6eff09ee146b9f8", size = 14446, upload-time = "2025-04-05T20:56:45.387Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/01/53/76c109e6f822a6d19befb0450c87330b9a6ce52353de6a9dda7892060a1f/mkdocs_section_index-0.3.10-py3-none-any.whl", hash = "sha256:bc27c0d0dc497c0ebaee1fc72839362aed77be7318b5ec0c30628f65918e4776", size = 8796, upload-time = "2025-04-05T20:56:43.975Z" }, ] [[package]] name = "mkdocs-typer2" version = "0.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs" }, { name = "pydantic" }, { name = "typer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4a/ac/0ada9ee8273f9cd379b3f60512059bc7ce2de245a5caa89bb3177d16aa0e/mkdocs_typer2-0.1.4.tar.gz", hash = "sha256:1e974bbe7717c42f747215270323196d6acd214aa383634cf237f746293c3b90", size = 22211, upload-time = "2025-03-09T17:14:41.95Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/4b/c8/076e1fe8dd813434da661bd0782f79f0af8cc06e129b0293c9ae3cb0c1c8/mkdocs_typer2-0.1.4-py3-none-any.whl", hash = "sha256:8d269313647fa5a798a043e12ba5806c4c9253d348f753f5d18a6010d9699346", size = 11582, upload-time = "2025-03-09T17:14:40.856Z" }, ] [[package]] name = "mkdocstrings" version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686, upload-time = "2025-03-31T08:33:11.997Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075, upload-time = "2025-03-31T08:33:09.661Z" }, ] [[package]] name = "mkdocstrings-python" version = "1.16.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/a3/0c7559a355fa21127a174a5aa2d3dca2de6e479ddd9c63ca4082d5f9980c/mkdocstrings_python-1.16.11.tar.gz", hash = "sha256:935f95efa887f99178e4a7becaaa1286fb35adafffd669b04fd611d97c00e5ce", size = 205392, upload-time = "2025-05-24T10:41:32.078Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/c4/ffa32f2c7cdb1728026c7a34aab87796b895767893aaa54611a79b4eef45/mkdocstrings_python-1.16.11-py3-none-any.whl", hash = "sha256:25d96cc9c1f9c272ea1bd8222c900b5f852bf46c984003e9c7c56eaa4696190f", size = 124282, upload-time = "2025-05-24T10:41:30.008Z" }, ] [[package]] name = "more-itertools" version = "10.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" }, ] [[package]] name = "multidict" version = "6.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0b/67/414933982bce2efce7cbcb3169eaaf901e0f25baec69432b4874dfb1f297/multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817", size = 77017, upload-time = "2025-06-30T15:50:58.931Z" }, { url = "https://files.pythonhosted.org/packages/8a/fe/d8a3ee1fad37dc2ef4f75488b0d9d4f25bf204aad8306cbab63d97bff64a/multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140", size = 44897, upload-time = "2025-06-30T15:51:00.999Z" }, { url = "https://files.pythonhosted.org/packages/1f/e0/265d89af8c98240265d82b8cbcf35897f83b76cd59ee3ab3879050fd8c45/multidict-6.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd7793bab517e706c9ed9d7310b06c8672fd0aeee5781bfad612f56b8e0f7d14", size = 44574, upload-time = "2025-06-30T15:51:02.449Z" }, { url = "https://files.pythonhosted.org/packages/e6/05/6b759379f7e8e04ccc97cfb2a5dcc5cdbd44a97f072b2272dc51281e6a40/multidict-6.6.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:72d8815f2cd3cf3df0f83cac3f3ef801d908b2d90409ae28102e0553af85545a", size = 225729, upload-time = "2025-06-30T15:51:03.794Z" }, { url = "https://files.pythonhosted.org/packages/4e/f5/8d5a15488edd9a91fa4aad97228d785df208ed6298580883aa3d9def1959/multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:531e331a2ee53543ab32b16334e2deb26f4e6b9b28e41f8e0c87e99a6c8e2d69", size = 242515, upload-time = "2025-06-30T15:51:05.002Z" }, { url = "https://files.pythonhosted.org/packages/6e/b5/a8f317d47d0ac5bb746d6d8325885c8967c2a8ce0bb57be5399e3642cccb/multidict-6.6.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:42ca5aa9329a63be8dc49040f63817d1ac980e02eeddba763a9ae5b4027b9c9c", size = 222224, upload-time = "2025-06-30T15:51:06.148Z" }, { url = "https://files.pythonhosted.org/packages/76/88/18b2a0d5e80515fa22716556061189c2853ecf2aa2133081ebbe85ebea38/multidict-6.6.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:208b9b9757060b9faa6f11ab4bc52846e4f3c2fb8b14d5680c8aac80af3dc751", size = 253124, upload-time = "2025-06-30T15:51:07.375Z" }, { url = "https://files.pythonhosted.org/packages/62/bf/ebfcfd6b55a1b05ef16d0775ae34c0fe15e8dab570d69ca9941073b969e7/multidict-6.6.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:acf6b97bd0884891af6a8b43d0f586ab2fcf8e717cbd47ab4bdddc09e20652d8", size = 251529, upload-time = "2025-06-30T15:51:08.691Z" }, { url = "https://files.pythonhosted.org/packages/44/11/780615a98fd3775fc309d0234d563941af69ade2df0bb82c91dda6ddaea1/multidict-6.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68e9e12ed00e2089725669bdc88602b0b6f8d23c0c95e52b95f0bc69f7fe9b55", size = 241627, upload-time = "2025-06-30T15:51:10.605Z" }, { url = "https://files.pythonhosted.org/packages/28/3d/35f33045e21034b388686213752cabc3a1b9d03e20969e6fa8f1b1d82db1/multidict-6.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05db2f66c9addb10cfa226e1acb363450fab2ff8a6df73c622fefe2f5af6d4e7", size = 239351, upload-time = "2025-06-30T15:51:12.18Z" }, { url = "https://files.pythonhosted.org/packages/6e/cc/ff84c03b95b430015d2166d9aae775a3985d757b94f6635010d0038d9241/multidict-6.6.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0db58da8eafb514db832a1b44f8fa7906fdd102f7d982025f816a93ba45e3dcb", size = 233429, upload-time = "2025-06-30T15:51:13.533Z" }, { url = "https://files.pythonhosted.org/packages/2e/f0/8cd49a0b37bdea673a4b793c2093f2f4ba8e7c9d6d7c9bd672fd6d38cd11/multidict-6.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14117a41c8fdb3ee19c743b1c027da0736fdb79584d61a766da53d399b71176c", size = 243094, upload-time = "2025-06-30T15:51:14.815Z" }, { url = "https://files.pythonhosted.org/packages/96/19/5d9a0cfdafe65d82b616a45ae950975820289069f885328e8185e64283c2/multidict-6.6.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:877443eaaabcd0b74ff32ebeed6f6176c71850feb7d6a1d2db65945256ea535c", size = 248957, upload-time = "2025-06-30T15:51:16.076Z" }, { url = "https://files.pythonhosted.org/packages/e6/dc/c90066151da87d1e489f147b9b4327927241e65f1876702fafec6729c014/multidict-6.6.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:70b72e749a4f6e7ed8fb334fa8d8496384840319512746a5f42fa0aec79f4d61", size = 243590, upload-time = "2025-06-30T15:51:17.413Z" }, { url = "https://files.pythonhosted.org/packages/ec/39/458afb0cccbb0ee9164365273be3e039efddcfcb94ef35924b7dbdb05db0/multidict-6.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43571f785b86afd02b3855c5ac8e86ec921b760298d6f82ff2a61daf5a35330b", size = 237487, upload-time = "2025-06-30T15:51:19.039Z" }, { url = "https://files.pythonhosted.org/packages/35/38/0016adac3990426610a081787011177e661875546b434f50a26319dc8372/multidict-6.6.3-cp310-cp310-win32.whl", hash = "sha256:20c5a0c3c13a15fd5ea86c42311859f970070e4e24de5a550e99d7c271d76318", size = 41390, upload-time = "2025-06-30T15:51:20.362Z" }, { url = "https://files.pythonhosted.org/packages/f3/d2/17897a8f3f2c5363d969b4c635aa40375fe1f09168dc09a7826780bfb2a4/multidict-6.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab0a34a007704c625e25a9116c6770b4d3617a071c8a7c30cd338dfbadfe6485", size = 45954, upload-time = "2025-06-30T15:51:21.383Z" }, { url = "https://files.pythonhosted.org/packages/2d/5f/d4a717c1e457fe44072e33fa400d2b93eb0f2819c4d669381f925b7cba1f/multidict-6.6.3-cp310-cp310-win_arm64.whl", hash = "sha256:769841d70ca8bdd140a715746199fc6473414bd02efd678d75681d2d6a8986c5", size = 42981, upload-time = "2025-06-30T15:51:22.809Z" }, { url = "https://files.pythonhosted.org/packages/08/f0/1a39863ced51f639c81a5463fbfa9eb4df59c20d1a8769ab9ef4ca57ae04/multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c", size = 76445, upload-time = "2025-06-30T15:51:24.01Z" }, { url = "https://files.pythonhosted.org/packages/c9/0e/a7cfa451c7b0365cd844e90b41e21fab32edaa1e42fc0c9f68461ce44ed7/multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df", size = 44610, upload-time = "2025-06-30T15:51:25.158Z" }, { url = "https://files.pythonhosted.org/packages/c6/bb/a14a4efc5ee748cc1904b0748be278c31b9295ce5f4d2ef66526f410b94d/multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d", size = 44267, upload-time = "2025-06-30T15:51:26.326Z" }, { url = "https://files.pythonhosted.org/packages/c2/f8/410677d563c2d55e063ef74fe578f9d53fe6b0a51649597a5861f83ffa15/multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539", size = 230004, upload-time = "2025-06-30T15:51:27.491Z" }, { url = "https://files.pythonhosted.org/packages/fd/df/2b787f80059314a98e1ec6a4cc7576244986df3e56b3c755e6fc7c99e038/multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462", size = 247196, upload-time = "2025-06-30T15:51:28.762Z" }, { url = "https://files.pythonhosted.org/packages/05/f2/f9117089151b9a8ab39f9019620d10d9718eec2ac89e7ca9d30f3ec78e96/multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9", size = 225337, upload-time = "2025-06-30T15:51:30.025Z" }, { url = "https://files.pythonhosted.org/packages/93/2d/7115300ec5b699faa152c56799b089a53ed69e399c3c2d528251f0aeda1a/multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7", size = 257079, upload-time = "2025-06-30T15:51:31.716Z" }, { url = "https://files.pythonhosted.org/packages/15/ea/ff4bab367623e39c20d3b07637225c7688d79e4f3cc1f3b9f89867677f9a/multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9", size = 255461, upload-time = "2025-06-30T15:51:33.029Z" }, { url = "https://files.pythonhosted.org/packages/74/07/2c9246cda322dfe08be85f1b8739646f2c4c5113a1422d7a407763422ec4/multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821", size = 246611, upload-time = "2025-06-30T15:51:34.47Z" }, { url = "https://files.pythonhosted.org/packages/a8/62/279c13d584207d5697a752a66ffc9bb19355a95f7659140cb1b3cf82180e/multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d", size = 243102, upload-time = "2025-06-30T15:51:36.525Z" }, { url = "https://files.pythonhosted.org/packages/69/cc/e06636f48c6d51e724a8bc8d9e1db5f136fe1df066d7cafe37ef4000f86a/multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6", size = 238693, upload-time = "2025-06-30T15:51:38.278Z" }, { url = "https://files.pythonhosted.org/packages/89/a4/66c9d8fb9acf3b226cdd468ed009537ac65b520aebdc1703dd6908b19d33/multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430", size = 246582, upload-time = "2025-06-30T15:51:39.709Z" }, { url = "https://files.pythonhosted.org/packages/cf/01/c69e0317be556e46257826d5449feb4e6aa0d18573e567a48a2c14156f1f/multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b", size = 253355, upload-time = "2025-06-30T15:51:41.013Z" }, { url = "https://files.pythonhosted.org/packages/c0/da/9cc1da0299762d20e626fe0042e71b5694f9f72d7d3f9678397cbaa71b2b/multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56", size = 247774, upload-time = "2025-06-30T15:51:42.291Z" }, { url = "https://files.pythonhosted.org/packages/e6/91/b22756afec99cc31105ddd4a52f95ab32b1a4a58f4d417979c570c4a922e/multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183", size = 242275, upload-time = "2025-06-30T15:51:43.642Z" }, { url = "https://files.pythonhosted.org/packages/be/f1/adcc185b878036a20399d5be5228f3cbe7f823d78985d101d425af35c800/multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5", size = 41290, upload-time = "2025-06-30T15:51:45.264Z" }, { url = "https://files.pythonhosted.org/packages/e0/d4/27652c1c6526ea6b4f5ddd397e93f4232ff5de42bea71d339bc6a6cc497f/multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2", size = 45942, upload-time = "2025-06-30T15:51:46.377Z" }, { url = "https://files.pythonhosted.org/packages/16/18/23f4932019804e56d3c2413e237f866444b774b0263bcb81df2fdecaf593/multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb", size = 42880, upload-time = "2025-06-30T15:51:47.561Z" }, { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514, upload-time = "2025-06-30T15:51:48.728Z" }, { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394, upload-time = "2025-06-30T15:51:49.986Z" }, { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590, upload-time = "2025-06-30T15:51:51.331Z" }, { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292, upload-time = "2025-06-30T15:51:52.584Z" }, { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385, upload-time = "2025-06-30T15:51:53.913Z" }, { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328, upload-time = "2025-06-30T15:51:55.672Z" }, { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057, upload-time = "2025-06-30T15:51:57.037Z" }, { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341, upload-time = "2025-06-30T15:51:59.111Z" }, { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081, upload-time = "2025-06-30T15:52:00.533Z" }, { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581, upload-time = "2025-06-30T15:52:02.43Z" }, { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750, upload-time = "2025-06-30T15:52:04.26Z" }, { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548, upload-time = "2025-06-30T15:52:06.002Z" }, { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718, upload-time = "2025-06-30T15:52:07.707Z" }, { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603, upload-time = "2025-06-30T15:52:09.58Z" }, { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351, upload-time = "2025-06-30T15:52:10.947Z" }, { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860, upload-time = "2025-06-30T15:52:12.334Z" }, { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982, upload-time = "2025-06-30T15:52:13.6Z" }, { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210, upload-time = "2025-06-30T15:52:14.893Z" }, { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843, upload-time = "2025-06-30T15:52:16.155Z" }, { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053, upload-time = "2025-06-30T15:52:17.429Z" }, { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273, upload-time = "2025-06-30T15:52:19.346Z" }, { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124, upload-time = "2025-06-30T15:52:20.773Z" }, { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892, upload-time = "2025-06-30T15:52:22.242Z" }, { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547, upload-time = "2025-06-30T15:52:23.736Z" }, { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223, upload-time = "2025-06-30T15:52:25.185Z" }, { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262, upload-time = "2025-06-30T15:52:26.969Z" }, { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345, upload-time = "2025-06-30T15:52:28.467Z" }, { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248, upload-time = "2025-06-30T15:52:29.938Z" }, { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115, upload-time = "2025-06-30T15:52:31.416Z" }, { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649, upload-time = "2025-06-30T15:52:32.996Z" }, { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203, upload-time = "2025-06-30T15:52:34.521Z" }, { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051, upload-time = "2025-06-30T15:52:35.999Z" }, { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601, upload-time = "2025-06-30T15:52:37.473Z" }, { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683, upload-time = "2025-06-30T15:52:38.927Z" }, { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811, upload-time = "2025-06-30T15:52:40.207Z" }, { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056, upload-time = "2025-06-30T15:52:41.575Z" }, { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811, upload-time = "2025-06-30T15:52:43.281Z" }, { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304, upload-time = "2025-06-30T15:52:45.026Z" }, { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775, upload-time = "2025-06-30T15:52:46.459Z" }, { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773, upload-time = "2025-06-30T15:52:47.88Z" }, { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083, upload-time = "2025-06-30T15:52:49.366Z" }, { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980, upload-time = "2025-06-30T15:52:50.903Z" }, { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776, upload-time = "2025-06-30T15:52:52.764Z" }, { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882, upload-time = "2025-06-30T15:52:54.596Z" }, { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816, upload-time = "2025-06-30T15:52:56.175Z" }, { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341, upload-time = "2025-06-30T15:52:57.752Z" }, { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854, upload-time = "2025-06-30T15:52:59.74Z" }, { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432, upload-time = "2025-06-30T15:53:01.602Z" }, { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731, upload-time = "2025-06-30T15:53:03.517Z" }, { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086, upload-time = "2025-06-30T15:53:05.48Z" }, { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338, upload-time = "2025-06-30T15:53:07.522Z" }, { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812, upload-time = "2025-06-30T15:53:09.263Z" }, { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011, upload-time = "2025-06-30T15:53:11.038Z" }, { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254, upload-time = "2025-06-30T15:53:12.421Z" }, { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, ] [[package]] name = "mypy" version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416, upload-time = "2025-05-29T13:34:17.783Z" }, { url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654, upload-time = "2025-05-29T13:32:37.878Z" }, { url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192, upload-time = "2025-05-29T13:34:54.281Z" }, { url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939, upload-time = "2025-05-29T13:33:14.766Z" }, { url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719, upload-time = "2025-05-29T13:21:52.09Z" }, { url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053, upload-time = "2025-05-29T13:33:29.797Z" }, { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498, upload-time = "2025-05-29T13:18:54.066Z" }, { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755, upload-time = "2025-05-29T13:34:00.851Z" }, { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138, upload-time = "2025-05-29T13:32:55.082Z" }, { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156, upload-time = "2025-05-29T13:19:12.963Z" }, { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426, upload-time = "2025-05-29T13:20:22.72Z" }, { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319, upload-time = "2025-05-29T13:21:17.582Z" }, { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" }, { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" }, { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" }, { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" }, { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" }, { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" }, { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" }, { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" }, { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" }, { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" }, { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" }, { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" }, { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] name = "paginate" version = "0.5.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] [[package]] name = "paho-mqtt" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" }, ] [[package]] name = "passlib" version = "1.7.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, ] [[package]] name = "pastel" version = "0.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555, upload-time = "2020-09-16T19:21:12.43Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] [[package]] name = "platformdirs" version = "4.3.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "poethepoet" version = "0.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel" }, { name = "pyyaml" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/f2/3853d6a9a0dac08aa680895839eeab8ec0ed63db375e1f782e623c9309b6/poethepoet-0.34.0.tar.gz", hash = "sha256:86203acce555bbfe45cb6ccac61ba8b16a5784264484195874da457ddabf5850", size = 64474, upload-time = "2025-04-21T13:38:20.084Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/da/d1/61431afe22577083fcb50614bc5e5aa73aa0ab35e3fc2ae49708a59ff70b/poethepoet-0.34.0-py3-none-any.whl", hash = "sha256:c472d6f0fdb341b48d346f4ccd49779840c15b30dfd6bc6347a80d6274b5e34e", size = 85851, upload-time = "2025-04-21T13:38:18.257Z" }, ] [[package]] name = "pre-commit" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, { name = "identify" }, { name = "nodeenv" }, { name = "pyyaml" }, { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, ] [[package]] name = "propcache" version = "0.3.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ab/14/510deed325e262afeb8b360043c5d7c960da7d3ecd6d6f9496c9c56dc7f4/propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770", size = 73178, upload-time = "2025-06-09T22:53:40.126Z" }, { url = "https://files.pythonhosted.org/packages/cd/4e/ad52a7925ff01c1325653a730c7ec3175a23f948f08626a534133427dcff/propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3", size = 43133, upload-time = "2025-06-09T22:53:41.965Z" }, { url = "https://files.pythonhosted.org/packages/63/7c/e9399ba5da7780871db4eac178e9c2e204c23dd3e7d32df202092a1ed400/propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3", size = 43039, upload-time = "2025-06-09T22:53:43.268Z" }, { url = "https://files.pythonhosted.org/packages/22/e1/58da211eb8fdc6fc854002387d38f415a6ca5f5c67c1315b204a5d3e9d7a/propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e", size = 201903, upload-time = "2025-06-09T22:53:44.872Z" }, { url = "https://files.pythonhosted.org/packages/c4/0a/550ea0f52aac455cb90111c8bab995208443e46d925e51e2f6ebdf869525/propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220", size = 213362, upload-time = "2025-06-09T22:53:46.707Z" }, { url = "https://files.pythonhosted.org/packages/5a/af/9893b7d878deda9bb69fcf54600b247fba7317761b7db11fede6e0f28bd0/propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb", size = 210525, upload-time = "2025-06-09T22:53:48.547Z" }, { url = "https://files.pythonhosted.org/packages/7c/bb/38fd08b278ca85cde36d848091ad2b45954bc5f15cce494bb300b9285831/propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614", size = 198283, upload-time = "2025-06-09T22:53:50.067Z" }, { url = "https://files.pythonhosted.org/packages/78/8c/9fe55bd01d362bafb413dfe508c48753111a1e269737fa143ba85693592c/propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50", size = 191872, upload-time = "2025-06-09T22:53:51.438Z" }, { url = "https://files.pythonhosted.org/packages/54/14/4701c33852937a22584e08abb531d654c8bcf7948a8f87ad0a4822394147/propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339", size = 199452, upload-time = "2025-06-09T22:53:53.229Z" }, { url = "https://files.pythonhosted.org/packages/16/44/447f2253d859602095356007657ee535e0093215ea0b3d1d6a41d16e5201/propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0", size = 191567, upload-time = "2025-06-09T22:53:54.541Z" }, { url = "https://files.pythonhosted.org/packages/f2/b3/e4756258749bb2d3b46defcff606a2f47410bab82be5824a67e84015b267/propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2", size = 193015, upload-time = "2025-06-09T22:53:56.44Z" }, { url = "https://files.pythonhosted.org/packages/1e/df/e6d3c7574233164b6330b9fd697beeac402afd367280e6dc377bb99b43d9/propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7", size = 204660, upload-time = "2025-06-09T22:53:57.839Z" }, { url = "https://files.pythonhosted.org/packages/b2/53/e4d31dd5170b4a0e2e6b730f2385a96410633b4833dc25fe5dffd1f73294/propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b", size = 206105, upload-time = "2025-06-09T22:53:59.638Z" }, { url = "https://files.pythonhosted.org/packages/7f/fe/74d54cf9fbe2a20ff786e5f7afcfde446588f0cf15fb2daacfbc267b866c/propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c", size = 196980, upload-time = "2025-06-09T22:54:01.071Z" }, { url = "https://files.pythonhosted.org/packages/22/ec/c469c9d59dada8a7679625e0440b544fe72e99311a4679c279562051f6fc/propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70", size = 37679, upload-time = "2025-06-09T22:54:03.003Z" }, { url = "https://files.pythonhosted.org/packages/38/35/07a471371ac89d418f8d0b699c75ea6dca2041fbda360823de21f6a9ce0a/propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9", size = 41459, upload-time = "2025-06-09T22:54:04.134Z" }, { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207, upload-time = "2025-06-09T22:54:05.399Z" }, { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648, upload-time = "2025-06-09T22:54:08.023Z" }, { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496, upload-time = "2025-06-09T22:54:09.228Z" }, { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288, upload-time = "2025-06-09T22:54:10.466Z" }, { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456, upload-time = "2025-06-09T22:54:11.828Z" }, { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429, upload-time = "2025-06-09T22:54:13.823Z" }, { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472, upload-time = "2025-06-09T22:54:15.232Z" }, { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480, upload-time = "2025-06-09T22:54:17.104Z" }, { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530, upload-time = "2025-06-09T22:54:18.512Z" }, { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230, upload-time = "2025-06-09T22:54:19.947Z" }, { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754, upload-time = "2025-06-09T22:54:21.716Z" }, { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430, upload-time = "2025-06-09T22:54:23.17Z" }, { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884, upload-time = "2025-06-09T22:54:25.539Z" }, { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480, upload-time = "2025-06-09T22:54:26.892Z" }, { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757, upload-time = "2025-06-09T22:54:28.241Z" }, { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500, upload-time = "2025-06-09T22:54:29.4Z" }, { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] [[package]] name = "psutil" version = "7.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, ] [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, ] [[package]] name = "pyasn1-modules" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] [[package]] name = "pycparser" version = "2.22" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, ] [[package]] name = "pydantic" version = "2.11.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, ] [[package]] name = "pydantic-core" version = "2.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] [[package]] name = "pyhamcrest" version = "2.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/16/3f/f286caba4e64391a8dc9200e6de6ce0d07471e3f718248c3276843b7793b/pyhamcrest-2.1.0.tar.gz", hash = "sha256:c6acbec0923d0cb7e72c22af1926f3e7c97b8e8d69fc7498eabacaf7c975bd9c", size = 60538, upload-time = "2023-10-22T15:47:28.255Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/71/1b25d3797a24add00f6f8c1bb0ac03a38616e2ec6606f598c1d50b0b0ffb/pyhamcrest-2.1.0-py3-none-any.whl", hash = "sha256:f6913d2f392e30e0375b3ecbd7aee79e5d1faa25d345c8f4ff597665dcac2587", size = 54555, upload-time = "2023-10-22T15:47:25.08Z" }, ] [[package]] name = "pyjwt" version = "2.10.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, ] [[package]] name = "pylint" version = "3.3.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astroid" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "dill" }, { name = "isort" }, { name = "mccabe" }, { name = "platformdirs" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomlkit" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/e4/83e487d3ddd64ab27749b66137b26dc0c5b5c161be680e6beffdc99070b3/pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559", size = 1520709, upload-time = "2025-05-04T17:07:51.089Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e8/83/bff755d09e31b5d25cc7fdc4bf3915d1a404e181f1abf0359af376845c24/pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d", size = 522565, upload-time = "2025-05-04T17:07:48.714Z" }, ] [[package]] name = "pymdown-extensions" version = "10.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/92/a7296491dbf5585b3a987f3f3fc87af0e632121ff3e490c14b5f2d2b4eb5/pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7", size = 852320, upload-time = "2025-04-27T23:48:29.183Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a7/d1/c54e608505776ce4e7966d03358ae635cfd51dff1da6ee421c090dbc797b/pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f", size = 265845, upload-time = "2025-04-27T23:48:27.359Z" }, ] [[package]] name = "pyopenssl" version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/04/8c/cd89ad05804f8e3c17dea8f178c3f40eeab5694c30e0c9f5bcd49f576fc3/pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b", size = 179937, upload-time = "2025-05-17T16:28:31.31Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771, upload-time = "2025-05-17T16:28:29.197Z" }, ] [[package]] name = "pytest" version = "8.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] [[package]] name = "pytest-asyncio" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, ] [[package]] name = "pytest-cov" version = "6.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, ] [[package]] name = "pytest-docker" version = "3.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/75/285187953062ebe38108e77a7919c75e157943fa3513371c88e27d3df7b2/pytest_docker-3.2.3.tar.gz", hash = "sha256:26a1c711d99ef01e86e7c9c007f69641552c1554df4fccb065b35581cca24206", size = 13452, upload-time = "2025-07-04T07:46:17.647Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c5/c7/e057e0d1de611ce1bbb26cccf07ddf56eb30a6f6a03aa512a09dac356e03/pytest_docker-3.2.3-py3-none-any.whl", hash = "sha256:f973c35e6f2b674c8fc87e8b3354b02c15866a21994c0841a338c240a05de1eb", size = 8585, upload-time = "2025-07-04T07:46:16.439Z" }, ] [[package]] name = "pytest-logdog" version = "0.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/11/7a/4c59b0bce4d9a56a570d89038ae771b8dcb38ec691f871fd946141d29bca/pytest-logdog-0.1.0.tar.gz", hash = "sha256:b84aca02b6b609bda8bfcd6d0207a428b146cd706d14c7095a3ba79429ab534b", size = 7179, upload-time = "2021-06-15T18:34:06.57Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/19/86/f3716547f113acc07167baeff445c2c5445cdbdb0411295fc24dd0a4b53e/pytest_logdog-0.1.0-py3-none-any.whl", hash = "sha256:4d5a4c46442ca7da73b1cf6c9ebea144958a0a6258ba19ad7bf877dec22400e8", size = 4562, upload-time = "2021-06-15T18:34:05.024Z" }, ] [[package]] name = "pytest-timeout" version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] [[package]] name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-ldap" version = "3.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, { name = "pyasn1-modules" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fd/8b/1eeb4025dc1d3ac2f16678f38dec9ebdde6271c74955b72db5ce7a4dbfbd/python-ldap-3.4.4.tar.gz", hash = "sha256:7edb0accec4e037797705f3a05cbf36a9fde50d08c8f67f2aef99a2628fab828", size = 377889, upload-time = "2023-11-17T21:14:16.32Z" } [[package]] name = "pytz" version = "2025.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] [[package]] name = "pywin32-ctypes" version = "0.2.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] [[package]] name = "pyyaml-env-tag" version = "1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, ] [[package]] name = "referencing" version = "0.36.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, ] [[package]] name = "requests" version = "2.32.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "charset-normalizer" }, { name = "idna" }, { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] [[package]] name = "rich" version = "14.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, ] [[package]] name = "rpds-py" version = "0.27.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1e/d9/991a0dee12d9fc53ed027e26a26a64b151d77252ac477e22666b9688bc16/rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f", size = 27420, upload-time = "2025-08-07T08:26:39.624Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/75/2d/ad2e37dee3f45580f7fa0066c412a521f9bee53d2718b0e9436d308a1ecd/rpds_py-0.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:130c1ffa5039a333f5926b09e346ab335f0d4ec393b030a18549a7c7e7c2cea4", size = 371511, upload-time = "2025-08-07T08:23:06.205Z" }, { url = "https://files.pythonhosted.org/packages/f5/67/57b4b2479193fde9dd6983a13c2550b5f9c3bcdf8912dffac2068945eb14/rpds_py-0.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a4cf32a26fa744101b67bfd28c55d992cd19438aff611a46cac7f066afca8fd4", size = 354718, upload-time = "2025-08-07T08:23:08.222Z" }, { url = "https://files.pythonhosted.org/packages/a3/be/c2b95ec4b813eb11f3a3c3d22f22bda8d3a48a074a0519cde968c4d102cf/rpds_py-0.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64a0fe3f334a40b989812de70160de6b0ec7e3c9e4a04c0bbc48d97c5d3600ae", size = 381518, upload-time = "2025-08-07T08:23:09.696Z" }, { url = "https://files.pythonhosted.org/packages/a5/d2/5a7279bc2b93b20bd50865a2269016238cee45f7dc3cc33402a7f41bd447/rpds_py-0.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a0ff7ee28583ab30a52f371b40f54e7138c52ca67f8ca17ccb7ccf0b383cb5f", size = 396694, upload-time = "2025-08-07T08:23:11.105Z" }, { url = "https://files.pythonhosted.org/packages/65/e9/bac8b3714bd853c5bcb466e04acfb9a5da030d77e0ddf1dfad9afb791c31/rpds_py-0.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15ea4d2e182345dd1b4286593601d766411b43f868924afe297570658c31a62b", size = 514813, upload-time = "2025-08-07T08:23:12.215Z" }, { url = "https://files.pythonhosted.org/packages/1d/aa/293115e956d7d13b7d2a9e9a4121f74989a427aa125f00ce4426ca8b7b28/rpds_py-0.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36184b44bf60a480863e51021c26aca3dfe8dd2f5eeabb33622b132b9d8b8b54", size = 402246, upload-time = "2025-08-07T08:23:13.699Z" }, { url = "https://files.pythonhosted.org/packages/88/59/2d6789bb898fb3e2f0f7b82b7bcf27f579ebcb6cc36c24f4e208f7f58a5b/rpds_py-0.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b78430703cfcf5f5e86eb74027a1ed03a93509273d7c705babb547f03e60016", size = 383661, upload-time = "2025-08-07T08:23:15.231Z" }, { url = "https://files.pythonhosted.org/packages/0c/55/add13a593a7a81243a9eed56d618d3d427be5dc1214931676e3f695dfdc1/rpds_py-0.27.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:dbd749cff1defbde270ca346b69b3baf5f1297213ef322254bf2a28537f0b046", size = 401691, upload-time = "2025-08-07T08:23:16.681Z" }, { url = "https://files.pythonhosted.org/packages/04/09/3e8b2aad494ffaca571e4e19611a12cc18fcfd756d9274f3871a2d822445/rpds_py-0.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bde37765564cd22a676dd8101b657839a1854cfaa9c382c5abf6ff7accfd4ae", size = 416529, upload-time = "2025-08-07T08:23:17.863Z" }, { url = "https://files.pythonhosted.org/packages/a4/6d/bd899234728f1d8f72c9610f50fdf1c140ecd0a141320e1f1d0f6b20595d/rpds_py-0.27.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1d66f45b9399036e890fb9c04e9f70c33857fd8f58ac8db9f3278cfa835440c3", size = 558673, upload-time = "2025-08-07T08:23:18.99Z" }, { url = "https://files.pythonhosted.org/packages/79/f4/f3e02def5193fb899d797c232f90d6f8f0f2b9eca2faef6f0d34cbc89b2e/rpds_py-0.27.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d85d784c619370d9329bbd670f41ff5f2ae62ea4519761b679d0f57f0f0ee267", size = 588426, upload-time = "2025-08-07T08:23:20.541Z" }, { url = "https://files.pythonhosted.org/packages/e3/0c/88e716cd8fd760e5308835fe298255830de4a1c905fd51760b9bb40aa965/rpds_py-0.27.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5df559e9e7644d9042f626f2c3997b555f347d7a855a15f170b253f6c5bfe358", size = 554552, upload-time = "2025-08-07T08:23:21.714Z" }, { url = "https://files.pythonhosted.org/packages/2b/a9/0a8243c182e7ac59b901083dff7e671feba6676a131bfff3f8d301cd2b36/rpds_py-0.27.0-cp310-cp310-win32.whl", hash = "sha256:b8a4131698b6992b2a56015f51646711ec5d893a0b314a4b985477868e240c87", size = 218081, upload-time = "2025-08-07T08:23:23.273Z" }, { url = "https://files.pythonhosted.org/packages/0f/e7/202ff35852312760148be9e08fe2ba6900aa28e7a46940a313eae473c10c/rpds_py-0.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:cbc619e84a5e3ab2d452de831c88bdcad824414e9c2d28cd101f94dbdf26329c", size = 230077, upload-time = "2025-08-07T08:23:24.308Z" }, { url = "https://files.pythonhosted.org/packages/b4/c1/49d515434c1752e40f5e35b985260cf27af052593378580a2f139a5be6b8/rpds_py-0.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dbc2ab5d10544eb485baa76c63c501303b716a5c405ff2469a1d8ceffaabf622", size = 371577, upload-time = "2025-08-07T08:23:25.379Z" }, { url = "https://files.pythonhosted.org/packages/e1/6d/bf2715b2fee5087fa13b752b5fd573f1a93e4134c74d275f709e38e54fe7/rpds_py-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ec85994f96a58cf7ed288caa344b7fe31fd1d503bdf13d7331ead5f70ab60d5", size = 354959, upload-time = "2025-08-07T08:23:26.767Z" }, { url = "https://files.pythonhosted.org/packages/a3/5c/e7762808c746dd19733a81373c10da43926f6a6adcf4920a21119697a60a/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:190d7285cd3bb6d31d37a0534d7359c1ee191eb194c511c301f32a4afa5a1dd4", size = 381485, upload-time = "2025-08-07T08:23:27.869Z" }, { url = "https://files.pythonhosted.org/packages/40/51/0d308eb0b558309ca0598bcba4243f52c4cd20e15fe991b5bd75824f2e61/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10d92fb6d7fd827e44055fcd932ad93dac6a11e832d51534d77b97d1d85400f", size = 396816, upload-time = "2025-08-07T08:23:29.424Z" }, { url = "https://files.pythonhosted.org/packages/5c/aa/2d585ec911d78f66458b2c91252134ca0c7c70f687a72c87283173dc0c96/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd2c1d27ebfe6a015cfa2005b7fe8c52d5019f7bbdd801bc6f7499aab9ae739e", size = 514950, upload-time = "2025-08-07T08:23:30.576Z" }, { url = "https://files.pythonhosted.org/packages/0b/ef/aced551cc1148179557aed84343073adadf252c91265263ee6203458a186/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4790c9d5dd565ddb3e9f656092f57268951398cef52e364c405ed3112dc7c7c1", size = 402132, upload-time = "2025-08-07T08:23:32.428Z" }, { url = "https://files.pythonhosted.org/packages/4b/ac/cf644803d8d417653fe2b3604186861d62ea6afaef1b2284045741baef17/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4300e15e7d03660f04be84a125d1bdd0e6b2f674bc0723bc0fd0122f1a4585dc", size = 383660, upload-time = "2025-08-07T08:23:33.829Z" }, { url = "https://files.pythonhosted.org/packages/c9/ec/caf47c55ce02b76cbaeeb2d3b36a73da9ca2e14324e3d75cf72b59dcdac5/rpds_py-0.27.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:59195dc244fc183209cf8a93406889cadde47dfd2f0a6b137783aa9c56d67c85", size = 401730, upload-time = "2025-08-07T08:23:34.97Z" }, { url = "https://files.pythonhosted.org/packages/0b/71/c1f355afdcd5b99ffc253422aa4bdcb04ccf1491dcd1bda3688a0c07fd61/rpds_py-0.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fae4a01ef8c4cb2bbe92ef2063149596907dc4a881a8d26743b3f6b304713171", size = 416122, upload-time = "2025-08-07T08:23:36.062Z" }, { url = "https://files.pythonhosted.org/packages/38/0f/f4b5b1eda724ed0e04d2b26d8911cdc131451a7ee4c4c020a1387e5c6ded/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3dc8d4ede2dbae6c0fc2b6c958bf51ce9fd7e9b40c0f5b8835c3fde44f5807d", size = 558771, upload-time = "2025-08-07T08:23:37.478Z" }, { url = "https://files.pythonhosted.org/packages/93/c0/5f8b834db2289ab48d5cffbecbb75e35410103a77ac0b8da36bf9544ec1c/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3782fb753aa825b4ccabc04292e07897e2fd941448eabf666856c5530277626", size = 587876, upload-time = "2025-08-07T08:23:38.662Z" }, { url = "https://files.pythonhosted.org/packages/d2/dd/1a1df02ab8eb970115cff2ae31a6f73916609b900dc86961dc382b8c2e5e/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:887ab1f12b0d227e9260558a4a2320024b20102207ada65c43e1ffc4546df72e", size = 554359, upload-time = "2025-08-07T08:23:39.897Z" }, { url = "https://files.pythonhosted.org/packages/a1/e4/95a014ab0d51ab6e3bebbdb476a42d992d2bbf9c489d24cff9fda998e925/rpds_py-0.27.0-cp311-cp311-win32.whl", hash = "sha256:5d6790ff400254137b81b8053b34417e2c46921e302d655181d55ea46df58cf7", size = 218084, upload-time = "2025-08-07T08:23:41.086Z" }, { url = "https://files.pythonhosted.org/packages/49/78/f8d5b71ec65a0376b0de31efcbb5528ce17a9b7fdd19c3763303ccfdedec/rpds_py-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:e24d8031a2c62f34853756d9208eeafa6b940a1efcbfe36e8f57d99d52bb7261", size = 230085, upload-time = "2025-08-07T08:23:42.143Z" }, { url = "https://files.pythonhosted.org/packages/e7/d3/84429745184091e06b4cc70f8597408e314c2d2f7f5e13249af9ffab9e3d/rpds_py-0.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:08680820d23df1df0a0260f714d12966bc6c42d02e8055a91d61e03f0c47dda0", size = 222112, upload-time = "2025-08-07T08:23:43.233Z" }, { url = "https://files.pythonhosted.org/packages/cd/17/e67309ca1ac993fa1888a0d9b2f5ccc1f67196ace32e76c9f8e1dbbbd50c/rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4", size = 362611, upload-time = "2025-08-07T08:23:44.773Z" }, { url = "https://files.pythonhosted.org/packages/93/2e/28c2fb84aa7aa5d75933d1862d0f7de6198ea22dfd9a0cca06e8a4e7509e/rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b", size = 347680, upload-time = "2025-08-07T08:23:46.014Z" }, { url = "https://files.pythonhosted.org/packages/44/3e/9834b4c8f4f5fe936b479e623832468aa4bd6beb8d014fecaee9eac6cdb1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e", size = 384600, upload-time = "2025-08-07T08:23:48Z" }, { url = "https://files.pythonhosted.org/packages/19/78/744123c7b38865a965cd9e6f691fde7ef989a00a256fa8bf15b75240d12f/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34", size = 400697, upload-time = "2025-08-07T08:23:49.407Z" }, { url = "https://files.pythonhosted.org/packages/32/97/3c3d32fe7daee0a1f1a678b6d4dfb8c4dcf88197fa2441f9da7cb54a8466/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8", size = 517781, upload-time = "2025-08-07T08:23:50.557Z" }, { url = "https://files.pythonhosted.org/packages/b2/be/28f0e3e733680aa13ecec1212fc0f585928a206292f14f89c0b8a684cad1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726", size = 406449, upload-time = "2025-08-07T08:23:51.732Z" }, { url = "https://files.pythonhosted.org/packages/95/ae/5d15c83e337c082d0367053baeb40bfba683f42459f6ebff63a2fd7e5518/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e", size = 386150, upload-time = "2025-08-07T08:23:52.822Z" }, { url = "https://files.pythonhosted.org/packages/bf/65/944e95f95d5931112829e040912b25a77b2e7ed913ea5fe5746aa5c1ce75/rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3", size = 406100, upload-time = "2025-08-07T08:23:54.339Z" }, { url = "https://files.pythonhosted.org/packages/21/a4/1664b83fae02894533cd11dc0b9f91d673797c2185b7be0f7496107ed6c5/rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e", size = 421345, upload-time = "2025-08-07T08:23:55.832Z" }, { url = "https://files.pythonhosted.org/packages/7c/26/b7303941c2b0823bfb34c71378249f8beedce57301f400acb04bb345d025/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f", size = 561891, upload-time = "2025-08-07T08:23:56.951Z" }, { url = "https://files.pythonhosted.org/packages/9b/c8/48623d64d4a5a028fa99576c768a6159db49ab907230edddc0b8468b998b/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03", size = 591756, upload-time = "2025-08-07T08:23:58.146Z" }, { url = "https://files.pythonhosted.org/packages/b3/51/18f62617e8e61cc66334c9fb44b1ad7baae3438662098efbc55fb3fda453/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374", size = 557088, upload-time = "2025-08-07T08:23:59.6Z" }, { url = "https://files.pythonhosted.org/packages/bd/4c/e84c3a276e2496a93d245516be6b49e20499aa8ca1c94d59fada0d79addc/rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97", size = 221926, upload-time = "2025-08-07T08:24:00.695Z" }, { url = "https://files.pythonhosted.org/packages/83/89/9d0fbcef64340db0605eb0a0044f258076f3ae0a3b108983b2c614d96212/rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5", size = 233235, upload-time = "2025-08-07T08:24:01.846Z" }, { url = "https://files.pythonhosted.org/packages/c9/b0/e177aa9f39cbab060f96de4a09df77d494f0279604dc2f509263e21b05f9/rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9", size = 223315, upload-time = "2025-08-07T08:24:03.337Z" }, { url = "https://files.pythonhosted.org/packages/81/d2/dfdfd42565a923b9e5a29f93501664f5b984a802967d48d49200ad71be36/rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff", size = 362133, upload-time = "2025-08-07T08:24:04.508Z" }, { url = "https://files.pythonhosted.org/packages/ac/4a/0a2e2460c4b66021d349ce9f6331df1d6c75d7eea90df9785d333a49df04/rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367", size = 347128, upload-time = "2025-08-07T08:24:05.695Z" }, { url = "https://files.pythonhosted.org/packages/35/8d/7d1e4390dfe09d4213b3175a3f5a817514355cb3524593380733204f20b9/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185", size = 384027, upload-time = "2025-08-07T08:24:06.841Z" }, { url = "https://files.pythonhosted.org/packages/c1/65/78499d1a62172891c8cd45de737b2a4b84a414b6ad8315ab3ac4945a5b61/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc", size = 399973, upload-time = "2025-08-07T08:24:08.143Z" }, { url = "https://files.pythonhosted.org/packages/10/a1/1c67c1d8cc889107b19570bb01f75cf49852068e95e6aee80d22915406fc/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe", size = 515295, upload-time = "2025-08-07T08:24:09.711Z" }, { url = "https://files.pythonhosted.org/packages/df/27/700ec88e748436b6c7c4a2262d66e80f8c21ab585d5e98c45e02f13f21c0/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9", size = 406737, upload-time = "2025-08-07T08:24:11.182Z" }, { url = "https://files.pythonhosted.org/packages/33/cc/6b0ee8f0ba3f2df2daac1beda17fde5cf10897a7d466f252bd184ef20162/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c", size = 385898, upload-time = "2025-08-07T08:24:12.798Z" }, { url = "https://files.pythonhosted.org/packages/e8/7e/c927b37d7d33c0a0ebf249cc268dc2fcec52864c1b6309ecb960497f2285/rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295", size = 405785, upload-time = "2025-08-07T08:24:14.906Z" }, { url = "https://files.pythonhosted.org/packages/5b/d2/8ed50746d909dcf402af3fa58b83d5a590ed43e07251d6b08fad1a535ba6/rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43", size = 419760, upload-time = "2025-08-07T08:24:16.129Z" }, { url = "https://files.pythonhosted.org/packages/d3/60/2b2071aee781cb3bd49f94d5d35686990b925e9b9f3e3d149235a6f5d5c1/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432", size = 561201, upload-time = "2025-08-07T08:24:17.645Z" }, { url = "https://files.pythonhosted.org/packages/98/1f/27b67304272521aaea02be293fecedce13fa351a4e41cdb9290576fc6d81/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b", size = 591021, upload-time = "2025-08-07T08:24:18.999Z" }, { url = "https://files.pythonhosted.org/packages/db/9b/a2fadf823164dd085b1f894be6443b0762a54a7af6f36e98e8fcda69ee50/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d", size = 556368, upload-time = "2025-08-07T08:24:20.54Z" }, { url = "https://files.pythonhosted.org/packages/24/f3/6d135d46a129cda2e3e6d4c5e91e2cc26ea0428c6cf152763f3f10b6dd05/rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd", size = 221236, upload-time = "2025-08-07T08:24:22.144Z" }, { url = "https://files.pythonhosted.org/packages/c5/44/65d7494f5448ecc755b545d78b188440f81da98b50ea0447ab5ebfdf9bd6/rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2", size = 232634, upload-time = "2025-08-07T08:24:23.642Z" }, { url = "https://files.pythonhosted.org/packages/70/d9/23852410fadab2abb611733933401de42a1964ce6600a3badae35fbd573e/rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac", size = 222783, upload-time = "2025-08-07T08:24:25.098Z" }, { url = "https://files.pythonhosted.org/packages/15/75/03447917f78512b34463f4ef11066516067099a0c466545655503bed0c77/rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774", size = 359154, upload-time = "2025-08-07T08:24:26.249Z" }, { url = "https://files.pythonhosted.org/packages/6b/fc/4dac4fa756451f2122ddaf136e2c6aeb758dc6fdbe9ccc4bc95c98451d50/rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b", size = 343909, upload-time = "2025-08-07T08:24:27.405Z" }, { url = "https://files.pythonhosted.org/packages/7b/81/723c1ed8e6f57ed9d8c0c07578747a2d3d554aaefc1ab89f4e42cfeefa07/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd", size = 379340, upload-time = "2025-08-07T08:24:28.714Z" }, { url = "https://files.pythonhosted.org/packages/98/16/7e3740413de71818ce1997df82ba5f94bae9fff90c0a578c0e24658e6201/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb", size = 391655, upload-time = "2025-08-07T08:24:30.223Z" }, { url = "https://files.pythonhosted.org/packages/e0/63/2a9f510e124d80660f60ecce07953f3f2d5f0b96192c1365443859b9c87f/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433", size = 513017, upload-time = "2025-08-07T08:24:31.446Z" }, { url = "https://files.pythonhosted.org/packages/2c/4e/cf6ff311d09776c53ea1b4f2e6700b9d43bb4e99551006817ade4bbd6f78/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615", size = 402058, upload-time = "2025-08-07T08:24:32.613Z" }, { url = "https://files.pythonhosted.org/packages/88/11/5e36096d474cb10f2a2d68b22af60a3bc4164fd8db15078769a568d9d3ac/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8", size = 383474, upload-time = "2025-08-07T08:24:33.767Z" }, { url = "https://files.pythonhosted.org/packages/db/a2/3dff02805b06058760b5eaa6d8cb8db3eb3e46c9e452453ad5fc5b5ad9fe/rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858", size = 400067, upload-time = "2025-08-07T08:24:35.021Z" }, { url = "https://files.pythonhosted.org/packages/67/87/eed7369b0b265518e21ea836456a4ed4a6744c8c12422ce05bce760bb3cf/rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5", size = 412085, upload-time = "2025-08-07T08:24:36.267Z" }, { url = "https://files.pythonhosted.org/packages/8b/48/f50b2ab2fbb422fbb389fe296e70b7a6b5ea31b263ada5c61377e710a924/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9", size = 555928, upload-time = "2025-08-07T08:24:37.573Z" }, { url = "https://files.pythonhosted.org/packages/98/41/b18eb51045d06887666c3560cd4bbb6819127b43d758f5adb82b5f56f7d1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79", size = 585527, upload-time = "2025-08-07T08:24:39.391Z" }, { url = "https://files.pythonhosted.org/packages/be/03/a3dd6470fc76499959b00ae56295b76b4bdf7c6ffc60d62006b1217567e1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c", size = 554211, upload-time = "2025-08-07T08:24:40.6Z" }, { url = "https://files.pythonhosted.org/packages/bf/d1/ee5fd1be395a07423ac4ca0bcc05280bf95db2b155d03adefeb47d5ebf7e/rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23", size = 216624, upload-time = "2025-08-07T08:24:42.204Z" }, { url = "https://files.pythonhosted.org/packages/1c/94/4814c4c858833bf46706f87349c37ca45e154da7dbbec9ff09f1abeb08cc/rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1", size = 230007, upload-time = "2025-08-07T08:24:43.329Z" }, { url = "https://files.pythonhosted.org/packages/0e/a5/8fffe1c7dc7c055aa02df310f9fb71cfc693a4d5ccc5de2d3456ea5fb022/rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb", size = 362595, upload-time = "2025-08-07T08:24:44.478Z" }, { url = "https://files.pythonhosted.org/packages/bc/c7/4e4253fd2d4bb0edbc0b0b10d9f280612ca4f0f990e3c04c599000fe7d71/rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f", size = 347252, upload-time = "2025-08-07T08:24:45.678Z" }, { url = "https://files.pythonhosted.org/packages/f3/c8/3d1a954d30f0174dd6baf18b57c215da03cf7846a9d6e0143304e784cddc/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64", size = 384886, upload-time = "2025-08-07T08:24:46.86Z" }, { url = "https://files.pythonhosted.org/packages/e0/52/3c5835f2df389832b28f9276dd5395b5a965cea34226e7c88c8fbec2093c/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015", size = 399716, upload-time = "2025-08-07T08:24:48.174Z" }, { url = "https://files.pythonhosted.org/packages/40/73/176e46992461a1749686a2a441e24df51ff86b99c2d34bf39f2a5273b987/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0", size = 517030, upload-time = "2025-08-07T08:24:49.52Z" }, { url = "https://files.pythonhosted.org/packages/79/2a/7266c75840e8c6e70effeb0d38922a45720904f2cd695e68a0150e5407e2/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89", size = 408448, upload-time = "2025-08-07T08:24:50.727Z" }, { url = "https://files.pythonhosted.org/packages/e6/5f/a7efc572b8e235093dc6cf39f4dbc8a7f08e65fdbcec7ff4daeb3585eef1/rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d", size = 387320, upload-time = "2025-08-07T08:24:52.004Z" }, { url = "https://files.pythonhosted.org/packages/a2/eb/9ff6bc92efe57cf5a2cb74dee20453ba444b6fdc85275d8c99e0d27239d1/rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51", size = 407414, upload-time = "2025-08-07T08:24:53.664Z" }, { url = "https://files.pythonhosted.org/packages/fb/bd/3b9b19b00d5c6e1bd0f418c229ab0f8d3b110ddf7ec5d9d689ef783d0268/rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c", size = 420766, upload-time = "2025-08-07T08:24:55.917Z" }, { url = "https://files.pythonhosted.org/packages/17/6b/521a7b1079ce16258c70805166e3ac6ec4ee2139d023fe07954dc9b2d568/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4", size = 562409, upload-time = "2025-08-07T08:24:57.17Z" }, { url = "https://files.pythonhosted.org/packages/8b/bf/65db5bfb14ccc55e39de8419a659d05a2a9cd232f0a699a516bb0991da7b/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e", size = 590793, upload-time = "2025-08-07T08:24:58.388Z" }, { url = "https://files.pythonhosted.org/packages/db/b8/82d368b378325191ba7aae8f40f009b78057b598d4394d1f2cdabaf67b3f/rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e", size = 558178, upload-time = "2025-08-07T08:24:59.756Z" }, { url = "https://files.pythonhosted.org/packages/f6/ff/f270bddbfbc3812500f8131b1ebbd97afd014cd554b604a3f73f03133a36/rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6", size = 222355, upload-time = "2025-08-07T08:25:01.027Z" }, { url = "https://files.pythonhosted.org/packages/bf/20/fdab055b1460c02ed356a0e0b0a78c1dd32dc64e82a544f7b31c9ac643dc/rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a", size = 234007, upload-time = "2025-08-07T08:25:02.268Z" }, { url = "https://files.pythonhosted.org/packages/4d/a8/694c060005421797a3be4943dab8347c76c2b429a9bef68fb2c87c9e70c7/rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d", size = 223527, upload-time = "2025-08-07T08:25:03.45Z" }, { url = "https://files.pythonhosted.org/packages/1e/f9/77f4c90f79d2c5ca8ce6ec6a76cb4734ee247de6b3a4f337e289e1f00372/rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828", size = 359469, upload-time = "2025-08-07T08:25:04.648Z" }, { url = "https://files.pythonhosted.org/packages/c0/22/b97878d2f1284286fef4172069e84b0b42b546ea7d053e5fb7adb9ac6494/rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669", size = 343960, upload-time = "2025-08-07T08:25:05.863Z" }, { url = "https://files.pythonhosted.org/packages/b1/b0/dfd55b5bb480eda0578ae94ef256d3061d20b19a0f5e18c482f03e65464f/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd", size = 380201, upload-time = "2025-08-07T08:25:07.513Z" }, { url = "https://files.pythonhosted.org/packages/28/22/e1fa64e50d58ad2b2053077e3ec81a979147c43428de9e6de68ddf6aff4e/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec", size = 392111, upload-time = "2025-08-07T08:25:09.149Z" }, { url = "https://files.pythonhosted.org/packages/49/f9/43ab7a43e97aedf6cea6af70fdcbe18abbbc41d4ae6cdec1bfc23bbad403/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303", size = 515863, upload-time = "2025-08-07T08:25:10.431Z" }, { url = "https://files.pythonhosted.org/packages/38/9b/9bd59dcc636cd04d86a2d20ad967770bf348f5eb5922a8f29b547c074243/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b", size = 402398, upload-time = "2025-08-07T08:25:11.819Z" }, { url = "https://files.pythonhosted.org/packages/71/bf/f099328c6c85667aba6b66fa5c35a8882db06dcd462ea214be72813a0dd2/rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410", size = 384665, upload-time = "2025-08-07T08:25:13.194Z" }, { url = "https://files.pythonhosted.org/packages/a9/c5/9c1f03121ece6634818490bd3c8be2c82a70928a19de03467fb25a3ae2a8/rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156", size = 400405, upload-time = "2025-08-07T08:25:14.417Z" }, { url = "https://files.pythonhosted.org/packages/b5/b8/e25d54af3e63ac94f0c16d8fe143779fe71ff209445a0c00d0f6984b6b2c/rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2", size = 413179, upload-time = "2025-08-07T08:25:15.664Z" }, { url = "https://files.pythonhosted.org/packages/f9/d1/406b3316433fe49c3021546293a04bc33f1478e3ec7950215a7fce1a1208/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1", size = 556895, upload-time = "2025-08-07T08:25:17.061Z" }, { url = "https://files.pythonhosted.org/packages/5f/bc/3697c0c21fcb9a54d46ae3b735eb2365eea0c2be076b8f770f98e07998de/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42", size = 585464, upload-time = "2025-08-07T08:25:18.406Z" }, { url = "https://files.pythonhosted.org/packages/63/09/ee1bb5536f99f42c839b177d552f6114aa3142d82f49cef49261ed28dbe0/rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae", size = 555090, upload-time = "2025-08-07T08:25:20.461Z" }, { url = "https://files.pythonhosted.org/packages/7d/2c/363eada9e89f7059199d3724135a86c47082cbf72790d6ba2f336d146ddb/rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5", size = 218001, upload-time = "2025-08-07T08:25:21.761Z" }, { url = "https://files.pythonhosted.org/packages/e2/3f/d6c216ed5199c9ef79e2a33955601f454ed1e7420a93b89670133bca5ace/rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391", size = 230993, upload-time = "2025-08-07T08:25:23.34Z" }, { url = "https://files.pythonhosted.org/packages/47/55/287068956f9ba1cb40896d291213f09fdd4527630709058b45a592bc09dc/rpds_py-0.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:46f48482c1a4748ab2773f75fffbdd1951eb59794e32788834b945da857c47a8", size = 371566, upload-time = "2025-08-07T08:25:43.95Z" }, { url = "https://files.pythonhosted.org/packages/a2/fb/443af59cbe552e89680bb0f1d1ba47f6387b92083e28a45b8c8863b86c5a/rpds_py-0.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:419dd9c98bcc9fb0242be89e0c6e922df333b975d4268faa90d58499fd9c9ebe", size = 355781, upload-time = "2025-08-07T08:25:45.256Z" }, { url = "https://files.pythonhosted.org/packages/ad/f0/35f48bb073b5ca42b1dcc55cb148f4a3bd4411a3e584f6a18d26f0ea8832/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d42a0ef2bdf6bc81e1cc2d49d12460f63c6ae1423c4f4851b828e454ccf6f1", size = 382575, upload-time = "2025-08-07T08:25:46.524Z" }, { url = "https://files.pythonhosted.org/packages/51/e1/5f5296a21d1189f0f116a938af2e346d83172bf814d373695e54004a936f/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e39169ac6aae06dd79c07c8a69d9da867cef6a6d7883a0186b46bb46ccfb0c3", size = 397435, upload-time = "2025-08-07T08:25:48.204Z" }, { url = "https://files.pythonhosted.org/packages/97/79/3af99b7852b2b55cad8a08863725cbe9dc14781bcf7dc6ecead0c3e1dc54/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:935afcdea4751b0ac918047a2df3f720212892347767aea28f5b3bf7be4f27c0", size = 514861, upload-time = "2025-08-07T08:25:49.814Z" }, { url = "https://files.pythonhosted.org/packages/df/3e/11fd6033708ed3ae0e6947bb94f762f56bb46bf59a1b16eef6944e8a62ee/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de567dec6d451649a781633d36f5c7501711adee329d76c095be2178855b042", size = 402776, upload-time = "2025-08-07T08:25:51.135Z" }, { url = "https://files.pythonhosted.org/packages/b7/89/f9375ceaa996116de9cbc949874804c7874d42fb258c384c037a46d730b8/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:555ed147cbe8c8f76e72a4c6cd3b7b761cbf9987891b9448808148204aed74a5", size = 384665, upload-time = "2025-08-07T08:25:52.82Z" }, { url = "https://files.pythonhosted.org/packages/48/bf/0061e55c6f1f573a63c0f82306b8984ed3b394adafc66854a936d5db3522/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:d2cc2b34f9e1d31ce255174da82902ad75bd7c0d88a33df54a77a22f2ef421ee", size = 402518, upload-time = "2025-08-07T08:25:54.073Z" }, { url = "https://files.pythonhosted.org/packages/ae/dc/8d506676bfe87b3b683332ec8e6ab2b0be118a3d3595ed021e3274a63191/rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cb0702c12983be3b2fab98ead349ac63a98216d28dda6f518f52da5498a27a1b", size = 416247, upload-time = "2025-08-07T08:25:55.433Z" }, { url = "https://files.pythonhosted.org/packages/2e/02/9a89eea1b75c69e81632de7963076e455b1e00e1cfb46dfdabb055fa03e3/rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ba783541be46f27c8faea5a6645e193943c17ea2f0ffe593639d906a327a9bcc", size = 559456, upload-time = "2025-08-07T08:25:56.866Z" }, { url = "https://files.pythonhosted.org/packages/38/4a/0f3ac4351957847c0d322be6ec72f916e43804a2c1d04e9672ea4a67c315/rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:2406d034635d1497c596c40c85f86ecf2bf9611c1df73d14078af8444fe48031", size = 587778, upload-time = "2025-08-07T08:25:58.202Z" }, { url = "https://files.pythonhosted.org/packages/c2/8e/39d0d7401095bed5a5ad5ef304fae96383f9bef40ca3f3a0807ff5b68d9d/rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dea0808153f1fbbad772669d906cddd92100277533a03845de6893cadeffc8be", size = 555247, upload-time = "2025-08-07T08:25:59.707Z" }, { url = "https://files.pythonhosted.org/packages/e0/04/6b8311e811e620b9eaca67cd80a118ff9159558a719201052a7b2abb88bf/rpds_py-0.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d2a81bdcfde4245468f7030a75a37d50400ac2455c3a4819d9d550c937f90ab5", size = 230256, upload-time = "2025-08-07T08:26:01.07Z" }, { url = "https://files.pythonhosted.org/packages/59/64/72ab5b911fdcc48058359b0e786e5363e3fde885156116026f1a2ba9a5b5/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6491658dd2569f05860bad645569145c8626ac231877b0fb2d5f9bcb7054089", size = 371658, upload-time = "2025-08-07T08:26:02.369Z" }, { url = "https://files.pythonhosted.org/packages/6c/4b/90ff04b4da055db53d8fea57640d8d5d55456343a1ec9a866c0ecfe10fd1/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec77545d188f8bdd29d42bccb9191682a46fb2e655e3d1fb446d47c55ac3b8d", size = 355529, upload-time = "2025-08-07T08:26:03.83Z" }, { url = "https://files.pythonhosted.org/packages/a4/be/527491fb1afcd86fc5ce5812eb37bc70428ee017d77fee20de18155c3937/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a4aebf8ca02bbb90a9b3e7a463bbf3bee02ab1c446840ca07b1695a68ce424", size = 382822, upload-time = "2025-08-07T08:26:05.52Z" }, { url = "https://files.pythonhosted.org/packages/e0/a5/dcdb8725ce11e6d0913e6fcf782a13f4b8a517e8acc70946031830b98441/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44524b96481a4c9b8e6c46d6afe43fa1fb485c261e359fbe32b63ff60e3884d8", size = 397233, upload-time = "2025-08-07T08:26:07.179Z" }, { url = "https://files.pythonhosted.org/packages/33/f9/0947920d1927e9f144660590cc38cadb0795d78fe0d9aae0ef71c1513b7c/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45d04a73c54b6a5fd2bab91a4b5bc8b426949586e61340e212a8484919183859", size = 514892, upload-time = "2025-08-07T08:26:08.622Z" }, { url = "https://files.pythonhosted.org/packages/1d/ed/d1343398c1417c68f8daa1afce56ef6ce5cc587daaf98e29347b00a80ff2/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:343cf24de9ed6c728abefc5d5c851d5de06497caa7ac37e5e65dd572921ed1b5", size = 402733, upload-time = "2025-08-07T08:26:10.433Z" }, { url = "https://files.pythonhosted.org/packages/1d/0b/646f55442cd14014fb64d143428f25667a100f82092c90087b9ea7101c74/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aed8118ae20515974650d08eb724150dc2e20c2814bcc307089569995e88a14", size = 384447, upload-time = "2025-08-07T08:26:11.847Z" }, { url = "https://files.pythonhosted.org/packages/4b/15/0596ef7529828e33a6c81ecf5013d1dd33a511a3e0be0561f83079cda227/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:af9d4fd79ee1cc8e7caf693ee02737daabfc0fcf2773ca0a4735b356c8ad6f7c", size = 402502, upload-time = "2025-08-07T08:26:13.537Z" }, { url = "https://files.pythonhosted.org/packages/c3/8d/986af3c42f8454a6cafff8729d99fb178ae9b08a9816325ac7a8fa57c0c0/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f0396e894bd1e66c74ecbc08b4f6a03dc331140942c4b1d345dd131b68574a60", size = 416651, upload-time = "2025-08-07T08:26:14.923Z" }, { url = "https://files.pythonhosted.org/packages/e9/9a/b4ec3629b7b447e896eec574469159b5b60b7781d3711c914748bf32de05/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:59714ab0a5af25d723d8e9816638faf7f4254234decb7d212715c1aa71eee7be", size = 559460, upload-time = "2025-08-07T08:26:16.295Z" }, { url = "https://files.pythonhosted.org/packages/61/63/d1e127b40c3e4733b3a6f26ae7a063cdf2bc1caa5272c89075425c7d397a/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:88051c3b7d5325409f433c5a40328fcb0685fc04e5db49ff936e910901d10114", size = 588072, upload-time = "2025-08-07T08:26:17.776Z" }, { url = "https://files.pythonhosted.org/packages/04/7e/8ffc71a8f6833d9c9fb999f5b0ee736b8b159fd66968e05c7afc2dbcd57e/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:181bc29e59e5e5e6e9d63b143ff4d5191224d355e246b5a48c88ce6b35c4e466", size = 555083, upload-time = "2025-08-07T08:26:19.301Z" }, ] [[package]] name = "ruff" version = "0.11.12" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" }, { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" }, { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" }, { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" }, { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" }, { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" }, { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" }, { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" }, { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" }, { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" }, { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" }, { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" }, { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" }, { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" }, { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" }, { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" }, { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" }, ] [[package]] name = "secretstorage" version = "3.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "jeepney" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, ] [[package]] name = "setuptools" version = "80.9.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "smmap" version = "5.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] name = "sortedcontainers" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] name = "soupsieve" version = "2.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, ] [[package]] name = "sqlalchemy" version = "2.0.41" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e9/12/d7c445b1940276a828efce7331cb0cb09d6e5f049651db22f4ebb0922b77/sqlalchemy-2.0.41-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1f09b6821406ea1f94053f346f28f8215e293344209129a9c0fcc3578598d7b", size = 2117967, upload-time = "2025-05-14T17:48:15.841Z" }, { url = "https://files.pythonhosted.org/packages/6f/b8/cb90f23157e28946b27eb01ef401af80a1fab7553762e87df51507eaed61/sqlalchemy-2.0.41-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1936af879e3db023601196a1684d28e12f19ccf93af01bf3280a3262c4b6b4e5", size = 2107583, upload-time = "2025-05-14T17:48:18.688Z" }, { url = "https://files.pythonhosted.org/packages/9e/c2/eef84283a1c8164a207d898e063edf193d36a24fb6a5bb3ce0634b92a1e8/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2ac41acfc8d965fb0c464eb8f44995770239668956dc4cdf502d1b1ffe0d747", size = 3186025, upload-time = "2025-05-14T17:51:51.226Z" }, { url = "https://files.pythonhosted.org/packages/bd/72/49d52bd3c5e63a1d458fd6d289a1523a8015adedbddf2c07408ff556e772/sqlalchemy-2.0.41-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81c24e0c0fde47a9723c81d5806569cddef103aebbf79dbc9fcbb617153dea30", size = 3186259, upload-time = "2025-05-14T17:55:22.526Z" }, { url = "https://files.pythonhosted.org/packages/4f/9e/e3ffc37d29a3679a50b6bbbba94b115f90e565a2b4545abb17924b94c52d/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23a8825495d8b195c4aa9ff1c430c28f2c821e8c5e2d98089228af887e5d7e29", size = 3126803, upload-time = "2025-05-14T17:51:53.277Z" }, { url = "https://files.pythonhosted.org/packages/8a/76/56b21e363f6039978ae0b72690237b38383e4657281285a09456f313dd77/sqlalchemy-2.0.41-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:60c578c45c949f909a4026b7807044e7e564adf793537fc762b2489d522f3d11", size = 3148566, upload-time = "2025-05-14T17:55:24.398Z" }, { url = "https://files.pythonhosted.org/packages/3b/92/11b8e1b69bf191bc69e300a99badbbb5f2f1102f2b08b39d9eee2e21f565/sqlalchemy-2.0.41-cp310-cp310-win32.whl", hash = "sha256:118c16cd3f1b00c76d69343e38602006c9cfb9998fa4f798606d28d63f23beda", size = 2086696, upload-time = "2025-05-14T17:55:59.136Z" }, { url = "https://files.pythonhosted.org/packages/5c/88/2d706c9cc4502654860f4576cd54f7db70487b66c3b619ba98e0be1a4642/sqlalchemy-2.0.41-cp310-cp310-win_amd64.whl", hash = "sha256:7492967c3386df69f80cf67efd665c0f667cee67032090fe01d7d74b0e19bb08", size = 2110200, upload-time = "2025-05-14T17:56:00.757Z" }, { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232, upload-time = "2025-05-14T17:48:20.444Z" }, { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897, upload-time = "2025-05-14T17:48:21.634Z" }, { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313, upload-time = "2025-05-14T17:51:56.205Z" }, { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807, upload-time = "2025-05-14T17:55:26.928Z" }, { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632, upload-time = "2025-05-14T17:51:59.384Z" }, { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642, upload-time = "2025-05-14T17:55:29.901Z" }, { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475, upload-time = "2025-05-14T17:56:02.095Z" }, { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903, upload-time = "2025-05-14T17:56:03.499Z" }, { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" }, { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" }, { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" }, { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" }, { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" }, { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" }, { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" }, { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, ] [package.optional-dependencies] asyncio = [ { name = "greenlet" }, ] mypy = [ { name = "mypy" }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] [[package]] name = "tomli-w" version = "1.2.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] [[package]] name = "tomlkit" version = "0.13.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885, upload-time = "2024-08-14T08:19:41.488Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955, upload-time = "2024-08-14T08:19:40.05Z" }, ] [[package]] name = "transitions" version = "0.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4a/82/4dfbb3cf62501cb3e8d026cbeb2d5cdeaf5bfe916ea50d3a9435faa2b0e1/transitions-0.9.2.tar.gz", hash = "sha256:2f8490dbdbd419366cef1516032ab06d07ccb5839ef54905e842a472692d4204", size = 1188079, upload-time = "2024-08-06T13:32:49.722Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ec/47/852f96b115425618382472ea06860069da5bb078bdec3e4449f185a40e07/transitions-0.9.2-py2.py3-none-any.whl", hash = "sha256:f7b40c9b4a93869f36c4d1c33809aeb18cdeeb065fd1adba018ee39c3db216f3", size = 111773, upload-time = "2024-08-06T13:32:46.703Z" }, ] [[package]] name = "trove-classifiers" version = "2025.5.9.12" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/38/04/1cd43f72c241fedcf0d9a18d0783953ee301eac9e5d9db1df0f0f089d9af/trove_classifiers-2025.5.9.12.tar.gz", hash = "sha256:7ca7c8a7a76e2cd314468c677c69d12cc2357711fcab4a60f87994c1589e5cb5", size = 16940, upload-time = "2025-05-09T12:04:48.829Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/92/ef/c6deb083748be3bcad6f471b6ae983950c161890bf5ae1b2af80cc56c530/trove_classifiers-2025.5.9.12-py3-none-any.whl", hash = "sha256:e381c05537adac78881c8fa345fd0e9970159f4e4a04fcc42cfd3129cca640ce", size = 14119, upload-time = "2025-05-09T12:04:46.38Z" }, ] [[package]] name = "typer" version = "0.15.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "rich" }, { name = "shellingham" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6c/89/c527e6c848739be8ceb5c44eb8208c52ea3515c6cf6406aa61932887bf58/typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3", size = 101559, upload-time = "2025-05-14T16:34:57.704Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c9/62/d4ba7afe2096d5659ec3db8b15d8665bdcb92a3c6ff0b95e99895b335a9c/typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173", size = 45258, upload-time = "2025-05-14T16:34:55.583Z" }, ] [[package]] name = "types-mock" version = "5.2.0.20250516" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/17/20/7b9e8068bc2db2432389e40683f5d04c1901f72c150332f6749aa37bc5ef/types_mock-5.2.0.20250516.tar.gz", hash = "sha256:aab7d3d9ad3814f2f8da12cc8e42d9be7d38200c5f214e3c0278c38fa01299d7", size = 11220, upload-time = "2025-05-16T03:08:11.623Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/21/80/9e4a5f6dddb4ee046884e06ce6c80bc2266604e3452154a0e383a6f49414/types_mock-5.2.0.20250516-py3-none-any.whl", hash = "sha256:e50fbd0c3be8bcea25c30a47fac0b7a6ca22f630ef2f53416a73b319b39dfde1", size = 10516, upload-time = "2025-05-16T03:08:10.798Z" }, ] [[package]] name = "types-pyyaml" version = "6.0.12.20250516" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378, upload-time = "2025-05-16T03:08:04.897Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312, upload-time = "2025-05-16T03:08:04.019Z" }, ] [[package]] name = "types-setuptools" version = "80.9.0.20250529" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/79/66/1b276526aad4696a9519919e637801f2c103419d2c248a6feb2729e034d1/types_setuptools-80.9.0.20250529.tar.gz", hash = "sha256:79e088ba0cba2186c8d6499cbd3e143abb142d28a44b042c28d3148b1e353c91", size = 41337, upload-time = "2025-05-29T03:07:34.487Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1b/d8/83790d67ec771bf029a45ff1bd1aedbb738d8aa58c09dd0cc3033eea0e69/types_setuptools-80.9.0.20250529-py3-none-any.whl", hash = "sha256:00dfcedd73e333a430e10db096e4d46af93faf9314f832f13b6bbe3d6757e95f", size = 63263, upload-time = "2025-05-29T03:07:33.064Z" }, ] [[package]] name = "typing-extensions" version = "4.13.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, ] [[package]] name = "typing-inspection" version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] [[package]] name = "urllib3" version = "2.4.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, ] [[package]] name = "userpath" version = "1.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140, upload-time = "2024-02-29T21:39:08.742Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065, upload-time = "2024-02-29T21:39:07.551Z" }, ] [[package]] name = "uv" version = "0.7.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/0c/4f/c26b354fc791fb716a990f6b0147c0b5d69351400030654827fb920fd79b/uv-0.7.8.tar.gz", hash = "sha256:a59d6749587946d63d371170d8f69d168ca8f4eade5cf880ad3be2793ea29c77", size = 3258494, upload-time = "2025-05-24T00:28:18.241Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/db/48/dd73c6a9b7b18dc1784b243cd5a93c14db34876c5a5cbb215e00be285e05/uv-0.7.8-py3-none-linux_armv6l.whl", hash = "sha256:ff1b7e4bc8a1d260062782ad34d12ce0df068df01d4a0f61d0ddc20aba1a5688", size = 16741809, upload-time = "2025-05-24T00:27:20.873Z" }, { url = "https://files.pythonhosted.org/packages/b4/bd/0bc26f1f4f476cff93c8ce2d258819b10b9a4e41a9825405788ef25a2300/uv-0.7.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b83866be6a69f680f3d2e36b3befd2661b5596e59e575e266e7446b28efa8319", size = 16836506, upload-time = "2025-05-24T00:27:25.229Z" }, { url = "https://files.pythonhosted.org/packages/26/28/1573e22b5f109f7779ddf64cb11e8e475ac05cf94e6b79ad3a4494c8c39c/uv-0.7.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f749b58a5c348c455083781c92910e49b4ddba85c591eb67e97a8b84db03ef9b", size = 15642479, upload-time = "2025-05-24T00:27:28.866Z" }, { url = "https://files.pythonhosted.org/packages/ad/f1/3d403896ea1edeea9109cab924e6a724ed7f5fbdabe8e5e9f3e3aa2be95a/uv-0.7.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c058ee0f8c20b0942bd9f5c83a67b46577fa79f5691df8867b8e0f2d74cbadb1", size = 16043352, upload-time = "2025-05-24T00:27:31.911Z" }, { url = "https://files.pythonhosted.org/packages/c7/2e/a914e491af320be503db26ff57f1b328738d1d7419cdb690e6e31d87ae16/uv-0.7.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a07bdf9d6aadef40dd4edbe209bca698a3d3244df5285d40d2125f82455519c", size = 16413446, upload-time = "2025-05-24T00:27:35.363Z" }, { url = "https://files.pythonhosted.org/packages/c3/cc/a396870530db7661eac080d276eba25df1b6c930f50c721f8402370acd12/uv-0.7.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13af6b94563f25bdca6bb73e294648af9c0b165af5bb60f0c913ab125ec45e06", size = 17188599, upload-time = "2025-05-24T00:27:38.979Z" }, { url = "https://files.pythonhosted.org/packages/d0/96/299bd3895d630e28593dcc54f4c4dbd72e12b557288c6d153987bbd62f34/uv-0.7.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4acc09c06d6cf7a27e0f1de4edb8c1698b8a3ffe34f322b10f4c145989e434b9", size = 18105049, upload-time = "2025-05-24T00:27:42.194Z" }, { url = "https://files.pythonhosted.org/packages/8f/a4/9fa0b6a4540950fe7fa66d37c44228d6ad7bb6d42f66e16f4f96e20fd50c/uv-0.7.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9221a9679f2ffd031b71b735b84f58d5a2f1adf9bfa59c8e82a5201dad7db466", size = 17777603, upload-time = "2025-05-24T00:27:45.695Z" }, { url = "https://files.pythonhosted.org/packages/d7/62/988cca0f1723406ff22edd6a9fb5e3e1d4dd0af103d8c3a64effadc685fd/uv-0.7.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:409cee21edcaf4a7c714893656ab4dd0814a15659cb4b81c6929cbb75cd2d378", size = 22222113, upload-time = "2025-05-24T00:27:49.172Z" }, { url = "https://files.pythonhosted.org/packages/06/36/0e7943d9415560aa9fdd775d0bb4b9c06b69c543f0647210e5b84776658b/uv-0.7.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81ac0bb371979f48d1293f9c1bee691680ea6a724f16880c8f76718f5ff50049", size = 17454597, upload-time = "2025-05-24T00:27:52.478Z" }, { url = "https://files.pythonhosted.org/packages/bb/70/666be8dbc6a49e1a096f4577d69c4e6f78b3d9228fa2844d1bece21f5cd0/uv-0.7.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:3c620cecd6f3cdab59b316f41c2b1c4d1b709d9d5226cadeec370cfeed56f80c", size = 16335744, upload-time = "2025-05-24T00:27:55.657Z" }, { url = "https://files.pythonhosted.org/packages/24/a5/c1fbffc8b62121c0d07aa66e7e5135065ff881ebb85ba307664125f4c51c/uv-0.7.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0c691090ff631dde788c8f4f1b1ea20f9deb9d805289796dcf10bc4a144a817e", size = 16439468, upload-time = "2025-05-24T00:27:58.599Z" }, { url = "https://files.pythonhosted.org/packages/65/95/a079658721b88d483c97a1765f9fd4f1b8b4fa601f2889d86824244861f2/uv-0.7.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4a117fe3806ba4ebb9c68fdbf91507e515a883dfab73fa863df9bc617d6de7a3", size = 16740156, upload-time = "2025-05-24T00:28:01.657Z" }, { url = "https://files.pythonhosted.org/packages/14/69/a2d110786c4cf093d788cfcde9e99c634af087555f0bf9ceafc009d051ed/uv-0.7.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:91d022235b39e59bab4bce7c4b634dc67e16fa89725cdfb2149a6ef7eaf6d784", size = 17569652, upload-time = "2025-05-24T00:28:04.903Z" }, { url = "https://files.pythonhosted.org/packages/6f/56/db6db0dc20114b76eb48dbd5167a26a2ebe51e8b604b4e84c5ef84ef4103/uv-0.7.8-py3-none-win32.whl", hash = "sha256:6ebe252f34c50b09b7f641f8e603d7b627f579c76f181680c757012b808be456", size = 16958006, upload-time = "2025-05-24T00:28:07.996Z" }, { url = "https://files.pythonhosted.org/packages/4b/80/5c78a9adc50fa3b7cca3a0c1245dff8c74d906ab53c3503b1f8133243930/uv-0.7.8-py3-none-win_amd64.whl", hash = "sha256:b5b62ca8a1bea5fdbf8a6372eabb03376dffddb5d139688bbb488c0719fa52fc", size = 18457129, upload-time = "2025-05-24T00:28:11.844Z" }, { url = "https://files.pythonhosted.org/packages/15/52/fd76b44942ac308e1dbbebea8b23de67a0f891a54d5e51346c3c3564dd9b/uv-0.7.8-py3-none-win_arm64.whl", hash = "sha256:ad79388b0c6eff5383b963d8d5ddcb7fbb24b0b82bf5d0c8b1bdbfbe445cb868", size = 17177058, upload-time = "2025-05-24T00:28:15.561Z" }, ] [[package]] name = "virtualenv" version = "20.31.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, ] [[package]] name = "watchdog" version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] [[package]] name = "websockets" version = "15.0.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] [[package]] name = "yarl" version = "1.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", size = 132910, upload-time = "2025-06-10T00:42:31.108Z" }, { url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", size = 90644, upload-time = "2025-06-10T00:42:33.851Z" }, { url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", size = 89322, upload-time = "2025-06-10T00:42:35.688Z" }, { url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", size = 323786, upload-time = "2025-06-10T00:42:37.817Z" }, { url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", size = 319627, upload-time = "2025-06-10T00:42:39.937Z" }, { url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", size = 339149, upload-time = "2025-06-10T00:42:42.627Z" }, { url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", size = 333327, upload-time = "2025-06-10T00:42:44.842Z" }, { url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", size = 326054, upload-time = "2025-06-10T00:42:47.149Z" }, { url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", size = 315035, upload-time = "2025-06-10T00:42:48.852Z" }, { url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", size = 338962, upload-time = "2025-06-10T00:42:51.024Z" }, { url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", size = 335399, upload-time = "2025-06-10T00:42:53.007Z" }, { url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", size = 338649, upload-time = "2025-06-10T00:42:54.964Z" }, { url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", size = 358563, upload-time = "2025-06-10T00:42:57.28Z" }, { url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", size = 357609, upload-time = "2025-06-10T00:42:59.055Z" }, { url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", size = 350224, upload-time = "2025-06-10T00:43:01.248Z" }, { url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", size = 81753, upload-time = "2025-06-10T00:43:03.486Z" }, { url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", size = 86817, upload-time = "2025-06-10T00:43:05.231Z" }, { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833, upload-time = "2025-06-10T00:43:07.393Z" }, { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070, upload-time = "2025-06-10T00:43:09.538Z" }, { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818, upload-time = "2025-06-10T00:43:11.575Z" }, { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003, upload-time = "2025-06-10T00:43:14.088Z" }, { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537, upload-time = "2025-06-10T00:43:16.431Z" }, { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358, upload-time = "2025-06-10T00:43:18.704Z" }, { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362, upload-time = "2025-06-10T00:43:20.888Z" }, { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979, upload-time = "2025-06-10T00:43:23.169Z" }, { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274, upload-time = "2025-06-10T00:43:27.111Z" }, { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294, upload-time = "2025-06-10T00:43:28.96Z" }, { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169, upload-time = "2025-06-10T00:43:30.701Z" }, { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776, upload-time = "2025-06-10T00:43:32.51Z" }, { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341, upload-time = "2025-06-10T00:43:34.543Z" }, { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988, upload-time = "2025-06-10T00:43:36.489Z" }, { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113, upload-time = "2025-06-10T00:43:38.592Z" }, { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485, upload-time = "2025-06-10T00:43:41.038Z" }, { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686, upload-time = "2025-06-10T00:43:42.692Z" }, { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, ] [[package]] name = "zipp" version = "3.22.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/12/b6/7b3d16792fdf94f146bed92be90b4eb4563569eca91513c8609aebf0c167/zipp-3.22.0.tar.gz", hash = "sha256:dd2f28c3ce4bc67507bfd3781d21b7bb2be31103b51a4553ad7d90b84e57ace5", size = 25257, upload-time = "2025-05-26T14:46:32.217Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ad/da/f64669af4cae46f17b90798a827519ce3737d31dbafad65d391e49643dc4/zipp-3.22.0-py3-none-any.whl", hash = "sha256:fe208f65f2aca48b81f9e6fd8cf7b8b32c26375266b009b413d45306b6148343", size = 9796, upload-time = "2025-05-26T14:46:30.775Z" }, ] [[package]] name = "zstandard" version = "0.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload-time = "2024-07-15T00:18:06.141Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2a/55/bd0487e86679db1823fc9ee0d8c9c78ae2413d34c0b461193b5f4c31d22f/zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9", size = 788701, upload-time = "2024-07-15T00:13:27.351Z" }, { url = "https://files.pythonhosted.org/packages/e1/8a/ccb516b684f3ad987dfee27570d635822e3038645b1a950c5e8022df1145/zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880", size = 633678, upload-time = "2024-07-15T00:13:30.24Z" }, { url = "https://files.pythonhosted.org/packages/12/89/75e633d0611c028e0d9af6df199423bf43f54bea5007e6718ab7132e234c/zstandard-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc", size = 4941098, upload-time = "2024-07-15T00:13:32.526Z" }, { url = "https://files.pythonhosted.org/packages/4a/7a/bd7f6a21802de358b63f1ee636ab823711c25ce043a3e9f043b4fcb5ba32/zstandard-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573", size = 5308798, upload-time = "2024-07-15T00:13:34.925Z" }, { url = "https://files.pythonhosted.org/packages/79/3b/775f851a4a65013e88ca559c8ae42ac1352db6fcd96b028d0df4d7d1d7b4/zstandard-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391", size = 5341840, upload-time = "2024-07-15T00:13:37.376Z" }, { url = "https://files.pythonhosted.org/packages/09/4f/0cc49570141dd72d4d95dd6fcf09328d1b702c47a6ec12fbed3b8aed18a5/zstandard-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e", size = 5440337, upload-time = "2024-07-15T00:13:39.772Z" }, { url = "https://files.pythonhosted.org/packages/e7/7c/aaa7cd27148bae2dc095191529c0570d16058c54c4597a7d118de4b21676/zstandard-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd", size = 4861182, upload-time = "2024-07-15T00:13:42.495Z" }, { url = "https://files.pythonhosted.org/packages/ac/eb/4b58b5c071d177f7dc027129d20bd2a44161faca6592a67f8fcb0b88b3ae/zstandard-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4", size = 4932936, upload-time = "2024-07-15T00:13:44.234Z" }, { url = "https://files.pythonhosted.org/packages/44/f9/21a5fb9bb7c9a274b05ad700a82ad22ce82f7ef0f485980a1e98ed6e8c5f/zstandard-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea", size = 5464705, upload-time = "2024-07-15T00:13:46.822Z" }, { url = "https://files.pythonhosted.org/packages/49/74/b7b3e61db3f88632776b78b1db597af3f44c91ce17d533e14a25ce6a2816/zstandard-0.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2", size = 4857882, upload-time = "2024-07-15T00:13:49.297Z" }, { url = "https://files.pythonhosted.org/packages/4a/7f/d8eb1cb123d8e4c541d4465167080bec88481ab54cd0b31eb4013ba04b95/zstandard-0.23.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9", size = 4697672, upload-time = "2024-07-15T00:13:51.447Z" }, { url = "https://files.pythonhosted.org/packages/5e/05/f7dccdf3d121309b60342da454d3e706453a31073e2c4dac8e1581861e44/zstandard-0.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a", size = 5206043, upload-time = "2024-07-15T00:13:53.587Z" }, { url = "https://files.pythonhosted.org/packages/86/9d/3677a02e172dccd8dd3a941307621c0cbd7691d77cb435ac3c75ab6a3105/zstandard-0.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0", size = 5667390, upload-time = "2024-07-15T00:13:56.137Z" }, { url = "https://files.pythonhosted.org/packages/41/7e/0012a02458e74a7ba122cd9cafe491facc602c9a17f590367da369929498/zstandard-0.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c", size = 5198901, upload-time = "2024-07-15T00:13:58.584Z" }, { url = "https://files.pythonhosted.org/packages/65/3a/8f715b97bd7bcfc7342d8adcd99a026cb2fb550e44866a3b6c348e1b0f02/zstandard-0.23.0-cp310-cp310-win32.whl", hash = "sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813", size = 430596, upload-time = "2024-07-15T00:14:00.693Z" }, { url = "https://files.pythonhosted.org/packages/19/b7/b2b9eca5e5a01111e4fe8a8ffb56bdcdf56b12448a24effe6cfe4a252034/zstandard-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4", size = 495498, upload-time = "2024-07-15T00:14:02.741Z" }, { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699, upload-time = "2024-07-15T00:14:04.909Z" }, { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681, upload-time = "2024-07-15T00:14:13.99Z" }, { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328, upload-time = "2024-07-15T00:14:16.588Z" }, { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955, upload-time = "2024-07-15T00:14:19.389Z" }, { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944, upload-time = "2024-07-15T00:14:22.173Z" }, { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927, upload-time = "2024-07-15T00:14:24.825Z" }, { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910, upload-time = "2024-07-15T00:14:26.982Z" }, { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544, upload-time = "2024-07-15T00:14:29.582Z" }, { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094, upload-time = "2024-07-15T00:14:40.126Z" }, { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440, upload-time = "2024-07-15T00:14:42.786Z" }, { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091, upload-time = "2024-07-15T00:14:45.184Z" }, { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682, upload-time = "2024-07-15T00:14:47.407Z" }, { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707, upload-time = "2024-07-15T00:15:03.529Z" }, { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792, upload-time = "2024-07-15T00:15:28.372Z" }, { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586, upload-time = "2024-07-15T00:15:32.26Z" }, { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420, upload-time = "2024-07-15T00:15:34.004Z" }, { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713, upload-time = "2024-07-15T00:15:35.815Z" }, { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459, upload-time = "2024-07-15T00:15:37.995Z" }, { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707, upload-time = "2024-07-15T00:15:39.872Z" }, { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545, upload-time = "2024-07-15T00:15:41.75Z" }, { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533, upload-time = "2024-07-15T00:15:44.114Z" }, { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510, upload-time = "2024-07-15T00:15:46.509Z" }, { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973, upload-time = "2024-07-15T00:15:49.939Z" }, { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968, upload-time = "2024-07-15T00:15:52.025Z" }, { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179, upload-time = "2024-07-15T00:15:54.971Z" }, { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577, upload-time = "2024-07-15T00:15:57.634Z" }, { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899, upload-time = "2024-07-15T00:16:00.811Z" }, { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964, upload-time = "2024-07-15T00:16:03.669Z" }, { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398, upload-time = "2024-07-15T00:16:06.694Z" }, { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313, upload-time = "2024-07-15T00:16:09.758Z" }, { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877, upload-time = "2024-07-15T00:16:11.758Z" }, { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595, upload-time = "2024-07-15T00:16:13.731Z" }, { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975, upload-time = "2024-07-15T00:16:16.005Z" }, { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448, upload-time = "2024-07-15T00:16:17.897Z" }, { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269, upload-time = "2024-07-15T00:16:20.136Z" }, { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228, upload-time = "2024-07-15T00:16:23.398Z" }, { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891, upload-time = "2024-07-15T00:16:26.391Z" }, { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310, upload-time = "2024-07-15T00:16:29.018Z" }, { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912, upload-time = "2024-07-15T00:16:31.871Z" }, { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946, upload-time = "2024-07-15T00:16:34.593Z" }, { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994, upload-time = "2024-07-15T00:16:36.887Z" }, { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681, upload-time = "2024-07-15T00:16:39.709Z" }, { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239, upload-time = "2024-07-15T00:16:41.83Z" }, { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149, upload-time = "2024-07-15T00:16:44.287Z" }, { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392, upload-time = "2024-07-15T00:16:46.423Z" }, { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299, upload-time = "2024-07-15T00:16:49.053Z" }, { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862, upload-time = "2024-07-15T00:16:51.003Z" }, { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578, upload-time = "2024-07-15T00:16:53.135Z" }, ]