diff --git a/.coverage 2 b/.coverage 2 deleted file mode 100644 index c17903d0..00000000 Binary files a/.coverage 2 and /dev/null differ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..25e74e42 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/python +{ + "name": "opensourceleg", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", + "features": { + "ghcr.io/devcontainers-contrib/features/poetry:2": {} + }, + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "./.devcontainer/postCreateCommand.sh", + + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": ["ms-python.python", "editorconfig.editorconfig"], + "settings": { + "python.testing.pytestArgs": ["tests"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.defaultInterpreterPath": "/workspaces/opensourceleg/.venv/bin/python", + "python.testing.pytestPath": "/workspaces/opensourceleg/.venv/bin/pytest" + } + } + } +} diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh new file mode 100755 index 00000000..38dca893 --- /dev/null +++ b/.devcontainer/postCreateCommand.sh @@ -0,0 +1,7 @@ +#! /usr/bin/env bash + +# Install Dependencies +poetry install --with dev + +# Install pre-commit hooks +poetry run pre-commit install --install-hooks diff --git a/.editorconfig b/.editorconfig index c02f5b71..9395b543 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,22 +1,5 @@ -# Check http://editorconfig.org for more information -# This is the main config file for this project: -root = true +max_line_length = 120 -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -indent_style = tab -trim_trailing_whitespace = true - -[*.{py, pyi}] -indent_style = tab - -[Makefile] -indent_style = tab - -[*.md] -trim_trailing_whitespace = false - -[*.{diff,patch}] -trim_trailing_whitespace = false +[*.json] +indent_style = space +indent_size = 4 diff --git a/.github/.stale.yml b/.github/.stale.yml deleted file mode 100644 index dc90e5a1..00000000 --- a/.github/.stale.yml +++ /dev/null @@ -1,17 +0,0 @@ -# Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 -# Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 -# Issues with these labels will never be considered stale -exemptLabels: - - pinned - - security -# Label to use when marking an issue as stale -staleLabel: wontfix -# Comment to post when marking an issue as stale. Set to `false` to disable -markComment: > - This issue has been automatically marked as stale because it has not had - recent activity. It will be closed if no further activity occurs. Thank you - for your contributions. -# Comment to post when closing a stale issue. Set to `false` to disable -closeComment: false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index bdd76779..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: 🐛 Bug report -about: If something isn't working 🔧 -title: '' -labels: bug -assignees: ---- - -## 🐛 Bug Report - - - -## 🔬 How To Reproduce - -Steps to reproduce the behavior: - -1. ... - -### Code sample - - - -### Environment - -* OS: [e.g. Linux / Windows / macOS] -* Python version, get it with: - -```bash -python --version -``` - -### Screenshots - - - -## 📈 Expected behavior - - - -## 📎 Additional context - - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 8f2da548..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,3 +0,0 @@ -# Configuration: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository - -blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index c387120f..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: 🚀 Feature request -about: Suggest an idea for this project 🏖 -title: '' -labels: enhancement -assignees: ---- - -## 🚀 Feature Request - - - -## 🔈 Motivation - - - -## 🛰 Alternatives - - - -## 📎 Additional context - - diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 9bdb0d73..00000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: ❓ Question -about: Ask a question about this project 🎓 -title: '' -labels: question -assignees: ---- - -## Checklist - - - -- [ ] I've searched the project's [`issues`](https://github.com/imsenthur/opensourceleg/issues?q=is%3Aissue). - -## ❓ Question - - - -How can I [...]? - -Is it possible to [...]? - -## 📎 Additional context - - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index ee09bfcf..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,28 +0,0 @@ -## Description - - - -## Related Issue - - - -## Type of Change - - - -- [ ] 📚 Examples / docs / tutorials / dependencies update -- [ ] 🔧 Bug fix (non-breaking change which fixes an issue) -- [ ] 🥂 Improvement (non-breaking change which improves an existing feature) -- [ ] 🚀 New feature (non-breaking change which adds functionality) -- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to change) -- [ ] 🔐 Security fix - -## Checklist - - - -- [ ] I've read the [`CODE_OF_CONDUCT.md`](https://github.com/imsenthur/opensourceleg/blob/master/CODE_OF_CONDUCT.md) document. -- [ ] I've read the [`CONTRIBUTING.md`](https://github.com/imsenthur/opensourceleg/blob/master/CONTRIBUTING.md) guide. -- [ ] I've updated the code style using `make codestyle`. -- [ ] I've written tests for all new methods and classes that I created. -- [ ] I've written the docstring in Google format for all the methods and classes that I used. diff --git a/.github/actions/setup-poetry-env/action.yml b/.github/actions/setup-poetry-env/action.yml new file mode 100644 index 00000000..18738aea --- /dev/null +++ b/.github/actions/setup-poetry-env/action.yml @@ -0,0 +1,42 @@ +name: "setup-poetry-env" +description: "Composite action to setup the Python and poetry environment." + +inputs: + python-version: + required: false + description: "The python version to use" + default: "3.11" + +runs: + using: "composite" + steps: + - name: Set up python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Install Poetry + env: + POETRY_VERSION: "1.7.1" + run: curl -sSL https://install.python-poetry.org | python - -y + shell: bash + + - name: Add Poetry to Path + run: echo "$HOME/.local/bin" >> $GITHUB_PATH + shell: bash + + - name: Configure Poetry virtual environment in project + run: poetry config virtualenvs.in-project true + shell: bash + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv-${{ runner.os }}-${{ inputs.python-version }}-${{ hashFiles('poetry.lock') }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction + shell: bash diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index a95f5e32..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,35 +0,0 @@ -# Configuration: https://dependabot.com/docs/config-file/ -# Docs: https://docs.github.com/en/github/administering-a-repository/keeping-your-dependencies-updated-automatically - -version: 2 - -updates: - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "monthly" - allow: - - dependency-type: "all" - commit-message: - prefix: ":arrow_up:" - open-pull-requests-limit: 3 - - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" - allow: - - dependency-type: "all" - commit-message: - prefix: ":arrow_up:" - open-pull-requests-limit: 3 - - - package-ecosystem: "docker" - directory: "/docker" - schedule: - interval: "monthly" - allow: - - dependency-type: "all" - commit-message: - prefix: ":arrow_up:" - open-pull-requests-limit: 3 diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index 0ce0984f..00000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,28 +0,0 @@ -# Release drafter configuration https://github.com/release-drafter/release-drafter#configuration -# Emojis were chosen to match the https://gitmoji.carloscuesta.me/ - -name-template: "v$NEXT_PATCH_VERSION" -tag-template: "v$NEXT_PATCH_VERSION" - -categories: - - title: ":rocket: Features" - labels: [enhancement, feature] - - title: ":wrench: Fixes & Refactoring" - labels: [bug, refactoring, bugfix, fix] - - title: ":package: Build System & CI/CD" - labels: [build, ci, testing] - - title: ":boom: Breaking Changes" - labels: [breaking] - - title: ":pencil: Documentation" - labels: [documentation] - - title: ":arrow_up: Dependencies updates" - labels: [dependencies] - -template: | - ## What’s Changed - - $CHANGES - - ## :busts_in_silhouette: List of contributors - - $CONTRIBUTORS diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 9d41ac2b..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: build - -on: [push, pull_request] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.9"] - - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5.3.0 - with: - python-version: ${{ matrix.python-version }} - - - name: Install poetry - run: make poetry-download - - - name: Set up cache - uses: actions/cache@v4.1.2 - with: - path: .venv - key: venv-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('poetry.lock') }} - - name: Install dependencies - run: | - poetry config virtualenvs.in-project true - poetry install - - - name: Run style checks - run: | - make check-codestyle - - - name: Run lint checks - run: | - make lint - - - name: Run tests - run: | - make test - - - name: Run safety checks - run: | - make check-safety diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml deleted file mode 100644 index a1f6e89d..00000000 --- a/.github/workflows/greetings.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Greetings - -on: [pull_request, issues] - -jobs: - greeting: - runs-on: ubuntu-latest - steps: - - uses: actions/first-interaction@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - pr-message: 'Hello @${{ github.actor }}, thank you for submitting a PR! We will respond as soon as possible.' - issue-message: | - Hello @${{ github.actor }}, thank you for your interest in our work! - - If this is a bug report, please provide screenshots and **minimum viable code to reproduce your issue**, otherwise we can not help you. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..9ed7ab89 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,80 @@ +name: Main + +on: + push: + branches: + - main + - "feature/**" + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + + - name: Set up the environment + uses: ./.github/actions/setup-poetry-env + + - name: Run checks + run: make check + + tests-and-type-check: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + fail-fast: false + defaults: + run: + shell: bash + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up the environment + uses: ./.github/actions/setup-poetry-env + with: + python-version: ${{ matrix.python-version }} + + - name: Run tests + run: poetry run pytest tests --cov --cov-config=pyproject.toml --cov-report=xml + + - name: Check typing + run: poetry run mypy + + - name: Upload coverage reports to Codecov with GitHub Action on Python 3.11 + uses: codecov/codecov-action@v4 + if: ${{ matrix.python-version == '3.11' }} + + check-docs: + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up the environment + uses: ./.github/actions/setup-poetry-env + + - name: Check if documentation can be built + run: poetry run mkdocs build -s + + deploy-docs: + runs-on: ubuntu-latest + needs: check-docs + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up the environment + uses: ./.github/actions/setup-poetry-env + + - name: Deploy documentation + run: poetry run mkdocs gh-deploy diff --git a/.github/workflows/on-release-main.yml b/.github/workflows/on-release-main.yml new file mode 100644 index 00000000..080026d4 --- /dev/null +++ b/.github/workflows/on-release-main.yml @@ -0,0 +1,41 @@ +name: release-main + +on: + release: + types: [published] + branches: [main] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up the environment + uses: ./.github/actions/setup-poetry-env + + - name: Export tag + id: vars + run: echo tag=${GITHUB_REF#refs/*/} >> $GITHUB_OUTPUT + + - name: Build and publish + run: | + source .venv/bin/activate + poetry version $RELEASE_VERSION + make build-and-publish + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + RELEASE_VERSION: ${{ steps.vars.outputs.tag }} + deploy-docs: + needs: publish + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up the environment + uses: ./.github/actions/setup-poetry-env + + - name: Deploy documentation + run: poetry run mkdocs gh-deploy --force diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml deleted file mode 100644 index 7fb0ef13..00000000 --- a/.github/workflows/release-drafter.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Release Drafter - -on: - push: - # branches to consider in the event; optional, defaults to all - branches: - - master - -jobs: - update_release_draft: - runs-on: ubuntu-latest - steps: - # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: release-drafter/release-drafter@v6.0.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/validate-codecov-config.yml b/.github/workflows/validate-codecov-config.yml new file mode 100644 index 00000000..2a8fd11a --- /dev/null +++ b/.github/workflows/validate-codecov-config.yml @@ -0,0 +1,15 @@ +name: validate-codecov-config + +on: + pull_request: + paths: [codecov.yaml] + push: + branches: [main] + +jobs: + validate-codecov-config: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Validate codecov configuration + run: curl -sSL --fail-with-body --data-binary @codecov.yaml https://codecov.io/validate diff --git a/.gitignore b/.gitignore index 6bd7836e..683fafdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,124 +1,7 @@ +docs/source + +# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore -# Created by https://www.gitignore.io/api/osx,python,pycharm,windows,visualstudio,visualstudiocode -# Edit at https://www.gitignore.io/?templates=osx,python,pycharm,windows,visualstudio,visualstudiocode - -### OSX ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### PyCharm ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### PyCharm Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -# *.iml -# modules.xml -# .idea/misc.xml -# *.ipr - -# Sonarlint plugin -.idea/**/sonarlint/ - -# SonarQube Plugin -.idea/**/sonarIssues.xml - -# Markdown Navigator plugin -.idea/**/markdown-navigator.xml -.idea/**/markdown-navigator/ - -### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -126,7 +9,6 @@ __pycache__/ # C extensions *.so -*.o # Distribution / packaging .Python @@ -142,7 +24,6 @@ parts/ sdist/ var/ wheels/ -pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg @@ -169,13 +50,25 @@ htmlcov/ nosetests.xml coverage.xml *.cover +*.py,cover .hypothesis/ .pytest_cache/ +cover/ # Translations *.mo *.pot +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + # Scrapy stuff: .scrapy @@ -183,13 +76,20 @@ coverage.xml docs/_build/ # PyBuilder +.pybuilder/ target/ -# pyenv -.python-version +# Jupyter Notebook +.ipynb_checkpoints -# poetry -.venv +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. @@ -198,12 +98,40 @@ target/ # install all needed dependencies. #Pipfile.lock -# celery beat schedule file +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff celerybeat-schedule +celerybeat.pid # SageMath parsed files *.sage.py +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + # Spyder project settings .spyderproject .spyproject @@ -211,11 +139,6 @@ celerybeat-schedule # Rope project settings .ropeproject -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - # mkdocs documentation /site @@ -227,399 +150,21 @@ dmypy.json # Pyre type checker .pyre/ -# Plugins -.secrets.baseline - -### VisualStudioCode ### -.vscode/* -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -Thumbs.db:encryptable -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -### VisualStudio ### -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.npy -*.csv -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser +# pytype static type analyzer +.pytype/ -# MFractors (Xamarin productivity tool) working folder -.mfractor/ +# Cython debug symbols +cython_debug/ -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ +# Vscode config files +.vscode/ -# End of https://www.gitignore.io/api/osx,python,pycharm,windows,visualstudio,visualstudiocode -opensourceleg/knee_encoder_map.npy -opensourceleg/ankle_encoder_map.npy +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ -.vscode/ -poetry.lock +*.csv +*.log.* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aea4fbe0..9f008c8b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,36 +1,22 @@ -default_language_version: - python: python3 - -default_stages: [commit, push] - repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.5.0 + rev: "v4.4.0" hooks: + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml - id: check-yaml - id: end-of-file-fixer - exclude: LICENSE - - - repo: local - hooks: - - id: pyupgrade - name: pyupgrade - entry: poetry run pyupgrade --py39-plus - types: [python] - language: system + - id: trailing-whitespace - - repo: local + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: "v0.5.2" hooks: - - id: isort - name: isort - entry: poetry run isort --settings-path pyproject.toml - types: [python] - language: system + - id: ruff + args: [--exit-non-zero-on-fix, --unsafe-fixes] + - id: ruff-format - - repo: local + - repo: https://github.com/pre-commit/mirrors-prettier + rev: "v3.0.3" hooks: - - id: black - name: black - entry: poetry run black --config pyproject.toml - types: [python] - language: system + - id: prettier diff --git a/.readthedocs.yaml b/.readthedocs.yaml deleted file mode 100644 index 9a93c166..00000000 --- a/.readthedocs.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# .readthedocs.yaml -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required -version: 2 - -# Set the version of Python and other tools you might need -build: - os: ubuntu-20.04 - tools: - python: "3.9" - # You can also specify other tool versions: - # nodejs: "16" - # rust: "1.55" - # golang: "1.17" - -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: docs/conf.py - -# If using Sphinx, optionally build your docs in additional formats such as PDF -formats: - - pdf - -# Optionally declare the Python requirements required to build your docs -python: - install: - - requirements: requirements.txt - - requirements: docs/requirements.txt diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 01915f14..2fc8e2e7 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation. 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 +- 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 +- 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6729ed26..d64688ed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,41 +1,133 @@ -# How to contribute +# Contributing to `opensourceleg` -## Installing dev dependencies +Contributions are welcome, and they are greatly appreciated! +Every little bit helps, and credit will always be given. -To get started you would need to install [`python3.9`](https://www.python.org/downloads/) and [`poetry`](https://python-poetry.org/docs/#installing-with-the-official-installer). Please follow the instructions on their official websites. +You can contribute in many ways: -* We use *poetry* to manage our python [dependencies](https://github.com/python-poetry/poetry). Please make sure that *poetry* is added to your **PATH** variable after installation and you can run `poetry` command in your terminal or command prompt. +# Types of Contributions -* After cloning the *opensourceleg* repository, please activate your *virtualenv* by running `poetry shell` command. This will create an isolated virtual environment for development. +## Report Bugs -* To install dependencies and prepare [`pre-commit`](https://pre-commit.com/) hooks you would need to run these commands from the root of the repository: +Report bugs at https://github.com/neurobionics/opensourceleg/issues - ```bash - make install - make pre-commit-install - ``` +If you are reporting a bug, please include: -## Submitting your code +- Your operating system name and version. +- Any details about your local setup that might be helpful in troubleshooting. +- Detailed steps to reproduce the bug. -Many checks are configured for this project. -* Command `make check-codestyle` will check black, isort and darglint. -* Command `make lint` will check types, docstrings and security using [Mypy](https://pypi.org/project/mypy/), [Darglint](https://pypi.org/project/darglint/), [Pydocstyle](https://pypi.org/project/pydocstyle/) -* Command `make check-safety` will look at the security of your code and dependencies using [Safety](https://pypi.org/project/safety/) and [Bandit](https://pypi.org/project/bandit/). +## Fix Bugs -Before submitting your code please do the following steps: +Look through the GitHub issues for bugs. +Anything tagged with "bug" and "help wanted" is open to whoever wants to implement a fix for it. -1. Add any changes you want -1. Add tests for the new changes -1. Edit documentation if you have changed something significant -1. Run `make codestyle` to format your changes. -1. Run `make lint` to ensure that types, security and docstrings are okay. -1. Run `make check-safety` to ensure that your code is secure. +## Implement Features -Your code will be checked by our CI/CD pipeline once you submit a pull request. Happy coding! +Look through the GitHub issues for features. +Anything tagged with "enhancement" and "help wanted" is open to whoever wants to implement it. -## Other ways you can help +## Write Documentation -You can contribute by spreading a word about this library. -It would also be a huge contribution to write -a short article on how you are using this project. -You can also share your best practices with us. +Cookiecutter PyPackage could always use more documentation, whether as part of the official docs, in docstrings, or even on the web in blog posts, articles, and such. + +## Submit Feedback + +The best way to send feedback is to file an issue at https://github.com/neurobionics/opensourceleg/issues. + +If you are proposing a new feature: + +- Explain in detail how it would work. +- Keep the scope as narrow as possible, to make it easier to implement. +- Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +# Get Started! + +Ready to contribute? Here's how to set up `opensourceleg` for local development. +Please note this documentation assumes you already have `poetry` and `Git` installed and ready to go. + +1. Fork the `opensourceleg` repo on GitHub. + +2. Clone your fork locally: + +```bash +cd +git clone git@github.com:YOUR_NAME/opensourceleg.git +``` + +3. Now we need to install the environment. Navigate into the directory + +```bash +cd opensourceleg +``` + +If you are using `pyenv`, select a version to use locally. (See installed versions with `pyenv versions`) + +```bash +pyenv local +``` + +Then, install and activate the environment with: + +```bash +poetry install +poetry shell +``` + +4. Install pre-commit to run linters/formatters at commit time: + +```bash +poetry run pre-commit install +``` + +5. Create a branch for local development: + +```bash +git checkout -b name-of-your-bugfix-or-feature +``` + +Now you can make your changes locally. + +6. Don't forget to add test cases for your added functionality to the `tests` directory. + +7. When you're done making changes, check that your changes pass the formatting tests. + +```bash +make check +``` + +Now, validate that all unit tests are passing: + +```bash +make test +``` + +9. Before raising a pull request you should also run tox. + This will run the tests across different versions of Python: + +```bash +tox +``` + +This requires you to have multiple versions of python installed. +This step is also triggered in the CI/CD pipeline, so you could also choose to skip this step locally. + +10. Commit your changes and push your branch to GitHub: + +```bash +git add . +git commit -m "Your detailed description of your changes." +git push origin name-of-your-bugfix-or-feature +``` + +11. Submit a pull request through the GitHub website. + +# Pull Request Guidelines + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. + +2. If the pull request adds functionality, the docs should be updated. + Put your new functionality into a function with a docstring, and add the feature to the list in `README.md`. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..66c024d0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.9-slim-buster + +ENV POETRY_VERSION=1.4 \ + POETRY_VIRTUALENVS_CREATE=false + +# Install poetry +RUN pip install "poetry==$POETRY_VERSION" + +# Copy only requirements to cache them in docker layer +WORKDIR /code +COPY poetry.lock pyproject.toml /code/ + +# Project initialization: +RUN poetry install --no-interaction --no-ansi --no-root --no-dev + +# Copy Python code to the Docker image +COPY opensourceleg /code/opensourceleg/ + +CMD [ "python", "opensourceleg/foo.py"] diff --git a/Makefile b/Makefile index 4a9d624b..9bba8578 100644 --- a/Makefile +++ b/Makefile @@ -1,114 +1,60 @@ -#* Variables -SHELL := /usr/bin/env bash -PYTHON := python -PYTHONPATH := `pwd` - -#* Docker variables -IMAGE := opensourceleg -VERSION := latest - -#* Poetry -.PHONY: poetry-download -poetry-download: - curl -sSL https://install.python-poetry.org | $(PYTHON) - - -.PHONY: poetry-remove -poetry-remove: - curl -sSL https://install.python-poetry.org | $(PYTHON) - --uninstall - -#* Installation .PHONY: install -install: - poetry lock -n && poetry export --without-hashes > requirements.txt - poetry install -n - -poetry run mypy --install-types --non-interactive ./ --explicit-package-bases - -.PHONY: pre-commit-install -pre-commit-install: - poetry run pre-commit install - -#* Formatters -.PHONY: codestyle -codestyle: - poetry run pyupgrade --exit-zero-even-if-changed --py39-plus **/*.py - poetry run isort --settings-path pyproject.toml ./ - poetry run black --config pyproject.toml ./ +install: ## Install the poetry environment and install the pre-commit hooks + @echo "🚀 Creating virtual environment using pyenv and poetry" + @poetry install + @poetry run pre-commit install + @poetry shell + +.PHONY: check +check: ## Run code quality tools. + @echo "🚀 Checking Poetry lock file consistency with 'pyproject.toml': Running poetry check --lock" + @poetry check --lock + @echo "🚀 Linting code: Running pre-commit" + @poetry run pre-commit run -a + @echo "🚀 Static type checking: Running mypy" + @poetry run mypy + @echo "🚀 Checking for obsolete dependencies: Running deptry" + @poetry run deptry . --ignore DEP002,DEP001 -.PHONY: formatting -formatting: codestyle - -#* Linting .PHONY: test -test: - PYTHONPATH=$(PYTHONPATH) poetry run pytest -c pyproject.toml --cov-report=html --cov=opensourceleg tests/ - poetry run coverage-badge -o assets/images/coverage.svg -f - -.PHONY: check-codestyle -check-codestyle: - poetry run isort --diff --check-only --settings-path pyproject.toml ./ - poetry run black --diff --check --config pyproject.toml ./ - poetry run darglint --verbosity 2 opensourceleg tests - -.PHONY: mypy -mypy: - poetry run mypy --config-file pyproject.toml ./ --explicit-package-bases -#* --check-untyped-defs - -.PHONY: check-safety -check-safety: - poetry check - poetry run safety check --full-report --ignore 70612 - poetry run bandit --recursive -c pyproject.toml opensourceleg tests - -.PHONY: lint -lint: mypy - -.PHONY: update-dev-deps -update-dev-deps: - poetry add -D bandit@latest darglint@latest "isort[colors]@latest" mypy@latest pre-commit@latest pydocstyle@latest pylint@latest pytest@latest pyupgrade@latest safety@latest coverage@latest coverage-badge@latest pytest-html@latest pytest-cov@latest - poetry add -D --allow-prereleases black@latest - -#* Docker -# Example: make docker-build VERSION=latest -# Example: make docker-build IMAGE=some_name VERSION=0.1.0 -.PHONY: docker-build -docker-build: - @echo Building docker $(IMAGE):$(VERSION) ... - docker build \ - -t $(IMAGE):$(VERSION) . \ - -f ./docker/Dockerfile --no-cache - -# Example: make docker-remove VERSION=latest -# Example: make docker-remove IMAGE=some_name VERSION=0.1.0 -.PHONY: docker-remove -docker-remove: - @echo Removing docker $(IMAGE):$(VERSION) ... - docker rmi -f $(IMAGE):$(VERSION) - -#* Cleaning -.PHONY: pycache-remove -pycache-remove: - find . | grep -E "(__pycache__|\.pyc|\.pyo$$)" | xargs rm -rf - -.PHONY: dsstore-remove -dsstore-remove: - find . | grep -E ".DS_Store" | xargs rm -rf - -.PHONY: mypycache-remove -mypycache-remove: - find . | grep -E ".mypy_cache" | xargs rm -rf - -.PHONY: ipynbcheckpoints-remove -ipynbcheckpoints-remove: - find . | grep -E ".ipynb_checkpoints" | xargs rm -rf - -.PHONY: pytestcache-remove -pytestcache-remove: - find . | grep -E ".pytest_cache" | xargs rm -rf - -.PHONY: build-remove -build-remove: - rm -rf build/ - -.PHONY: cleanup -cleanup: pycache-remove dsstore-remove mypycache-remove ipynbcheckpoints-remove pytestcache-remove +test: ## Test the code with pytest + @echo "🚀 Testing code: Running pytest" + @poetry run pytest --cov --cov-config=pyproject.toml --cov-report=xml + +.PHONY: build +build: clean-build ## Build wheel file using poetry + @echo "🚀 Creating wheel file" + @poetry build + +.PHONY: clean-build +clean-build: ## clean build artifacts + @rm -rf dist + +.PHONY: publish +publish: ## publish a release to pypi. + @echo "🚀 Publishing: Dry run." + @poetry config pypi-token.pypi $(PYPI_TOKEN) + @poetry publish --dry-run + @echo "🚀 Publishing." + @poetry publish + +.PHONY: build-and-publish +build-and-publish: build publish ## Build and publish. + +.PHONY: docs-test +docs-test: ## Test if documentation can be built without warnings or errors + @poetry run mkdocs build -s + +.PHONY: docs +docs: ## Build and serve the documentation + @poetry run mkdocs serve + +.PHONY: docs-deploy +docs-deploy: ## Deploy the documentation to GitHub pages + @poetry run mkdocs gh-deploy --force + +.PHONY: help +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md index b0ac7efe..3e26d547 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,26 @@ poetry install poetry shell ``` -## Documentation +Finally, install the environment and the pre-commit hooks with -You can find tutorials and API documentation at [opensourceleg.readthedocs.io](https://opensourceleg.readthedocs.io/en/latest/). +```bash +make install +``` + +You are now ready to start development on your project! +The CI/CD pipeline will be triggered when you open a pull request, merge to main, or when you create a new release. + +To finalize the set-up for publishing to PyPI or Artifactory, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/publishing/#set-up-for-pypi). +For activating the automatic documentation with MkDocs, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/mkdocs/#enabling-the-documentation-on-github). +To enable the code coverage reports, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/codecov/). + +## Releasing a new version + +- Create an API Token on [PyPI](https://pypi.org/). +- Add the API Token to your projects secrets with the name `PYPI_TOKEN` by visiting [this page](https://github.com/neurobionics/opensourceleg/settings/secrets/actions/new). +- Create a [new release](https://github.com/neurobionics/opensourceleg/releases/new) on Github. +- Create a new tag in the form `*.*.*`. +- For more details, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/cicd/#how-to-trigger-a-release). ## License @@ -60,4 +77,4 @@ The GPL license ensures that all these freedoms are protected, now and in the fu ## Contributing -Contributions are welcome, and they are greatly appreciated! For more details, read our [contribution guidelines](https://github.com/neurobionics/opensourceleg/blob/11765f7f7dd94e5d8699675149d5ff3596ea01b8/CONTRIBUTING.md). +Contributions are welcome, and they are greatly appreciated! For more details, read our [contribution guidelines](CONTRIBUTING.md). diff --git a/codecov.yaml b/codecov.yaml new file mode 100644 index 00000000..058cfb76 --- /dev/null +++ b/codecov.yaml @@ -0,0 +1,9 @@ +coverage: + range: 70..100 + round: down + precision: 1 + status: + project: + default: + target: 90% + threshold: 0.5% diff --git a/cookiecutter-config-file.yml b/cookiecutter-config-file.yml deleted file mode 100644 index 1d8727f0..00000000 --- a/cookiecutter-config-file.yml +++ /dev/null @@ -1,13 +0,0 @@ -# This file contains values from Cookiecutter - -default_context: - project_name: "opensourceleg" - project_description: "An open-source software library for numerical computation, data acquisition, and control of lower-limb robotic prostheses." - organization: "Neurobionics Laboratory" - license: "GNU LGPL v2.1" - minimal_python_version: 3.9 - github_name: "neurobionics" - email: "opensourceleg@gmail.com" - version: "0.1.0" - line_length: "88" - create_example_template: "none" diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 08feb0dd..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM python:3.11.4-slim-buster - -ENV LANG=C.UTF-8 \ - LC_ALL=C.UTF-8 \ - PATH="${PATH}:/root/.poetry/bin" - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - curl \ - && rm -rf /var/lib/apt/lists/* - -COPY pyproject.toml ./ - -# Install Poetry -RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | POETRY_HOME=/opt/poetry python && \ - cd /usr/local/bin && \ - ln -s /opt/poetry/bin/poetry && \ - poetry config virtualenvs.create false - -# Allow installing dev dependencies to run tests -ARG INSTALL_DEV=false -RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --no-dev ; fi" - -CMD mkdir -p /workspace -WORKDIR /workspace diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index 01b40b45..00000000 --- a/docker/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Docker for opensourceleg - -## Installation - -To create Docker you need to run: - -```bash -make docker-build -``` - -which is equivalent to: - -```bash -make docker-build VERSION=latest -``` - -You may provide name and version for the image. -Default name is `IMAGE := opensourceleg`. -Default version is `VERSION := latest`. - -```bash -make docker-build IMAGE=some_name VERSION=0.1.0 -``` - -## Usage - -```bash -docker run -it --rm \ - -v $(pwd):/workspace \ - opensourceleg bash -``` - -## How to clean up - -To uninstall docker image run `make docker-remove` with `VERSION`: - -```bash -make docker-remove VERSION=0.1.0 -``` - -you may also choose the image name - -```bash -make docker-remove IMAGE=some_name VERSION=latest -``` - -If you want to clean all, including `build` and `pycache` run `make cleanup` diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d4bb2cbb..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css deleted file mode 100644 index f030cb8f..00000000 --- a/docs/_static/css/custom.css +++ /dev/null @@ -1,16 +0,0 @@ -/* Newlines (\a) and spaces (\20) before each parameter */ -.sig-param::before { - content: "\a\20\20\20\20\20\20\20\20\20\20\20\20\20\20\20\20"; - white-space: pre; -} - -/* Newline after the last parameter (so the closing bracket is on a new line) */ -dt em.sig-param:last-of-type::after { - content: "\a"; - white-space: pre; -} - -/* To have blue background of width of the block (instead of width of content) */ -dl.class > dt:first-of-type { - display: block !important; -} diff --git a/docs/api/control.rst b/docs/api/control.rst deleted file mode 100644 index c16e25b6..00000000 --- a/docs/api/control.rst +++ /dev/null @@ -1,13 +0,0 @@ -======= -Control -======= - -Compiled Controller -------------------- -.. automodule:: opensourceleg.control.compiled_controller - :members: - -State Machine -------------- -.. automodule:: opensourceleg.control.state_machine - :members: diff --git a/docs/api/hardware.rst b/docs/api/hardware.rst deleted file mode 100644 index 11ca8f95..00000000 --- a/docs/api/hardware.rst +++ /dev/null @@ -1,44 +0,0 @@ -======== -Hardware -======== - -Actuators ---------- - -.. automodule:: opensourceleg.hardware.actuators - :members: - - * ``PI`` **float** = 3.14159 - * ``MOTOR_COUNT_PER_REV`` **float** = 16384 - * ``NM_PER_AMP`` **float** = 0.1133 - * ``NM_PER_MILLIAMP`` **float** = ``NM_PER_AMP`` / 1000 - * ``RAD_PER_COUNT`` **float** = 2 * ``PI`` / ``MOTOR_COUNT_PER_REV`` - * ``RAD_PER_DEG`` **float** = ``PI`` / 180 - * ``RAD_PER_SEC_GYROLSB`` **float** = ``PI`` / 180 / 32.8 - * ``M_PER_SEC_SQUARED_ACCLSB`` **float** = 9.80665 / 8192 - * ``IMPEDANCE_A`` **float** = 0.00028444 - * ``IMPEDANCE_C`` **float** = 0.0007812 - * ``NM_PER_RAD_TO_K`` **float** = ``RAD_PER_COUNT`` / ``IMPEDANCE_C`` * 1e3 / ``NM_PER_AMP`` - * ``NM_S_PER_RAD_TO_B`` **float** = ``RAD_PER_DEG`` / ``IMPEDANCE_A`` * 1e3 / ``NM_PER_AMP`` - * ``MAX_CASE_TEMPERATURE`` **float** = 80.0 - - -Joints ------- - -.. automodule:: opensourceleg.hardware.joints - :members: - :show-inheritance: - -Sensors -------- - -.. automodule:: opensourceleg.hardware.sensors - :members: - - -Thermal -------- - -.. automodule:: opensourceleg.hardware.thermal - :members: diff --git a/docs/api/osl.rst b/docs/api/osl.rst deleted file mode 100644 index f536eb55..00000000 --- a/docs/api/osl.rst +++ /dev/null @@ -1,7 +0,0 @@ -=============== -Open-Source Leg -=============== - -.. automodule:: opensourceleg.osl - :members: - :show-inheritance: diff --git a/docs/api/tools.rst b/docs/api/tools.rst deleted file mode 100644 index 01cacad6..00000000 --- a/docs/api/tools.rst +++ /dev/null @@ -1,22 +0,0 @@ -===== -Tools -===== - -Units ------ - -.. automodule:: opensourceleg.tools.units - :members: - -Utilities ---------- - -.. automodule:: opensourceleg.tools.utilities - :members: - -Logger ------- - -.. automodule:: opensourceleg.tools.logger - :members: - :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 8e8d05de..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,67 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys - -sys.path.insert(0, os.path.abspath("..")) -sys.path.insert(0, os.path.abspath("../opensourceleg")) - - -# -- Project information ----------------------------------------------------- - -project = "Open-Source Leg" -copyright = ( - "2022, Open-Source Leg Project (https://opensourceleg.com), Michigan Robotics" -) -author = "Senthur Ayyappan, Kevin Best, Jace Derosia, and Prof. Elliott Rouse" - -# The full version, including alpha/beta/rc tags -release = "2.0.0" - - -# -- General configuration --------------------------------------------------- - -master_doc = "index" - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphinx"] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "sphinx_book_theme" -html_title = "Open-Source Leg" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] - -# These paths are either relative to html_static_path -# or fully qualified paths (eg. https://...) -html_css_files = [ - "css/custom.css", -] diff --git a/docs/contributing/code_of_conduct.rst b/docs/contributing/code_of_conduct.rst deleted file mode 100644 index bd473207..00000000 --- a/docs/contributing/code_of_conduct.rst +++ /dev/null @@ -1,53 +0,0 @@ -================ -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, sex characteristics, 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 opensourceleg@gmail.com. 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 `_, version 1.4, available `here `_. - -For answers to common questions about this code of conduct, read `more `_. diff --git a/docs/contributing/contributing_code.rst b/docs/contributing/contributing_code.rst deleted file mode 100644 index dc08bb89..00000000 --- a/docs/contributing/contributing_code.rst +++ /dev/null @@ -1,30 +0,0 @@ -================= -Contributing Code -================= - -If you want to contribute code to **opensourceleg**, please follow these steps: - -1. Fork the **opensourceleg** repository on GitHub. -2. Clone your forked repository to your local machine. -3. Create a new branch for your changes. -4. Make your changes and commit them with clear and descriptive commit messages. -5. Push your changes to your forked repository. -6. Open a pull request on the **opensourceleg** repository. - -When submitting a pull request, please include: - -- A clear and descriptive title -- A detailed description of the changes you made -- Any relevant issue numbers (if applicable) - -We will review your pull request as soon as possible and provide feedback if necessary. - -Code Style -========== - -Please follow the ``PEP-8`` style guide when contributing code to **opensourceleg**. We also recommend using an automated code formatter like ``black`` to ensure consistent formatting. - -Testing -======= - -Please ensure that your code changes include tests to cover the new functionality or bug fix. We use ``pytest`` for testing, and you can run the tests locally by running ``pytest`` in the root directory of the repository. diff --git a/docs/contributing/reporting_bugs.rst b/docs/contributing/reporting_bugs.rst deleted file mode 100644 index 179b83de..00000000 --- a/docs/contributing/reporting_bugs.rst +++ /dev/null @@ -1,13 +0,0 @@ -============== -Reporting Bugs -============== - -We welcome contributions to the **opensourceleg** library! Whether you want to report a bug, request a feature, or submit a pull request, we appreciate your help in making this library better. - -If you encounter a bug or have a feature request, please open an issue on the **opensourceleg** GitHub repository. When reporting an issue, please include as much detail as possible, including: - -- A clear and descriptive title -- A detailed description of the issue or feature request -- Steps to reproduce the issue (if applicable) -- Any error messages or stack traces (if applicable) -- Your operating system and Python version diff --git a/docs/examples/basic_motion.rst b/docs/examples/basic_motion.rst deleted file mode 100644 index 90d0d7ff..00000000 --- a/docs/examples/basic_motion.rst +++ /dev/null @@ -1,106 +0,0 @@ -Basic Motion Test Script -================================ -In this example, we'll write a basic script that commands the OSL to move the joints in simple, periodic motions. -This script can be a helpful first step when comissioning a new OSL and making sure that the actuation systems -are configured appropriately. - -We'll assume that this is the first example someone looks at, and therefore we'll go through each set of commands in detail. - -Imports ----------- -For this example, we'll import `numpy`, the `OpenSourceLeg` class, and the `units` class from the `tools` module. - -.. literalinclude:: ../../examples/basic_motion.py - :language: python - :linenos: - :lines: 12-15 - -Making an OSL object ------------------------- - -We'll then create an instance of the `OpenSourceLeg` class and name it `osl`. -We pass the desired control loop frequency (200 Hz in our case) as an argument. -This sets the rate of data streaming for the actuators, as well as the frequency of the loop in ``osl.clock``. -We'll see more on this below. - -.. literalinclude:: ../../examples/basic_motion.py - :language: python - :linenos: - :lines: 17 - -Adding Joints --------------- - -Next, we add the knee and the ankle joints to the OSL via the ``add_joint`` method. -We supply the name of the joint and the gear ratio. -Our gear ratio is ``9*83/18=41.5``, which accounts for the internal ActPack ratio as well as the belt transmission. - -.. literalinclude:: ../../examples/basic_motion.py - :language: python - :linenos: - :lines: 18-19 - -.. note:: - If you're only using one joint, you can modify this script by simply commenting out method calls for the joint you're not using. - For example, if you're using ankle-only, you can comment out ``osl.add_joint("knee"...)`` and the subsequent ``knee`` related methods. - - -Generating Reference Trajectories --------------------------------------- - -Next, we define a funciton that returns a function handle for a simple harmonic trajectory. -Given a time input ``t``, instances of these functions return a reference position in degrees. -We'll make a function for both the knee and ankle joints with a period of 30 seconds. -We'll command the ankle to oscillate between -20 and 20 deg and the knee between 10 and 90 deg. - -.. literalinclude:: ../../examples/basic_motion.py - :language: python - :linenos: - :lines: 22-29 - -Configuring the OSL ------------------------ - -Now that the trajectories are configured, we're ready to start sending commands to the OSL. -We start with the syntax ``with osl:``. This syntax allows the library to call certain functions on the entrance and exit of the block. -This way, the library can ensure that the OSL turns off (returns to zero voltage mode) in the event of an error. - -We then home the joints using ``osl.home()``. -The homing routine drives the joints towards their hardstops in order to initialize the encoders. - -.. literalinclude:: ../../examples/basic_motion.py - :language: python - :linenos: - :lines: 31-32 - -After homing, we wait for user input to begin moving the joints. Then we put both joints into position control mode and -set the proportional gains to a low value for this basic example. - -.. literalinclude:: ../../examples/basic_motion.py - :language: python - :linenos: - :lines: 33-37 - -Main Loop ------------ - -Next, we can enter the main loop. The `OpenSourceLeg` class includes a built-in instance of a `SoftRealTimeLoop`. -This loop, which is called `clock`, will execute the contents of a ``for`` loop at the frequency specified when creating the `OpenSourceLeg` object using the syntax ``for t in osl.clock:``. -The loop will run until either ``osl.clock.stop()`` is called, an exception is raised, or the user presses ``ctrl+c`` on the keyboard. - -.. literalinclude:: ../../examples/basic_motion.py - :language: python - :linenos: - :lines: 39-53 - -Within the loop, we first call ``osl.update()`` to query the actuators and other sensors for their latest values. -We then update the knee and ankle position setpoints by calling our trajectory functions. -Note that we convert the setpoint to default OSL units (radians in this case), e.g., ``units.convert_to_default(knee_traj(t), units.position.deg)``. -Finally, we use the ``set_output_position()`` method to command the OSL joints to move to the new reference value. -We also we print values to the screen for debugging. - -Full Code for This Example --------------------------------------- -.. literalinclude:: ../../examples/basic_motion.py - :language: python - :linenos: \ No newline at end of file diff --git a/docs/examples/finite_state_machine.rst b/docs/examples/finite_state_machine.rst deleted file mode 100644 index 86029661..00000000 --- a/docs/examples/finite_state_machine.rst +++ /dev/null @@ -1,256 +0,0 @@ -Finite State Machine Controller -================================ - -Overview ---------------- - -The library ships with three example implementations of the same finite state machine walking controller. - -.. image:: ./images/FSM_Diagram.svg - :width: 400 - :align: center - :alt: A diagram of the finite state machine - -The first implementation is all in Python, and it uses the ``state_machine`` modules from the controls subpackage of this library. -If you plan to write your controllers exclusively in Python, this example would be a good place to start. - -The library also provides support for using compiled `C` and `C++` library functions via the ``compiled_controller`` modules. -You can see a very basic example usage of this module on the tutorials page, which may be helpful to walk through before starting with this example (:ref:`compiled_controller_tutorial_doc`). -The source code for the compiled controllers (C++ and MATLAB implementations) is available in `this repository -`_. -Please refer to the documentation in that repository for information on how to compile both the C++ and MATLAB source code. - -Python Implementation ------------------------ - -Setup and Configuraiton -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -First, we'll perform some standard imports. - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 11-17 - -Next, we'll define all of the tunable FSM parameters. -These include the impedance parameters for each state as well as the transitions between states. - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 19-60 - -.. note:: - These parameters were roughly tuned for a moderately paced walking gait. - You may want to tune them to better suit your intended use case. - -Next, we enter our main function for this script, `run_FSM_controller()`. -We first instantiate an OSL object, add the joints, and add a loadcell. - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 63-87 - -.. note:: - If instantiating the OSL object is unfamiliar, check out the :ref:`adding_joints_tutorial` and :ref:`adding_loadcell_tutorial` tutorial pages. - -Then, we create a `StateMachine` instance. -We've written a helper function to do this just to keep the code tidy, -and we'll get to that function later on. - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 89 - -Next, we configure the OSL log: - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 91-113 - -Main Loop -^^^^^^^^^^^ -Now that everything is set up, we will home the OSL and enter the main loop. -During each iteration of the main loop, we call the update method for both the OSL and the FSM. -We then write the current impedance parameters for each joint to the hardware. -A print statement is also included for debugging. - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 115-169 - -.. note:: - The OSL library provides sensor values in default units. If your library expects other units, - you need to convert the values prior to assigning them. You can use the ``units`` module in the ``tools`` subpackage to do this. - For example you can convert from radians (the default) to degrees using ``ankle_angle_in_deg = units.convert_from_default(osl.ankle.output_position, units.position.deg)``. - -Building the State Machine -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The function ``build_4_state_FSM()`` uses the ``StateMachine`` functionality of the ``opensourceleg.control`` module to make a state machine with 4 states. -First, we create the states using the ``State`` class. - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 175-188 - -Then, we assign impedance values for each state. - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 190-224 - -After the states have been defined, we define transition functions. These functions take the ``osl`` instance as arguments and return a boolean when transition criteria are met. -For example, we first define the transition from early stance to late stance based on the loadcell `z` force and the ankle angle as: - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 226-235 - -The remaining transition functions are defined similarly. - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 226-267 - -Next, we define events corresponding to the state transtions using the ``Event`` class. - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 269-273 - -Finally, we make an instance of the ``StateMachine`` class and add the states, events, and transitions that we've created. -The ``add_transition()`` method takes arguments of a source state, a destination state, an event, and the callback function defining when that transition occurs. -After that, the FSM is fully built and can be returned. - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 275-311 - -Finally, we call our main function: - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :lines: 314-315 - -Full Code for This Example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. literalinclude:: ../../examples/fsm_walking_controller.py - :language: python - :linenos: - -C++ and MATLAB Implementation ------------------------------------ -To get started, make sure you have compiled either the `C++` or the `MATLAB` source code and have a `FSMController.so` library. -If not, please see the source repository for compilation instructions. -To run this example as is, copy the library you generated to the `examples` directory. -Alternatively, you can modify the search path for the library when loading the controller (see below). - -Load Compiled Library -^^^^^^^^^^^^^^^^^^^^^ - -First, we'll perform standard imports, handle some paths, and setup our OSL object. - -.. literalinclude:: ../../examples/fsm_walking_compiled_controller.py - :language: python - :linenos: - :lines: 11-36 - -.. note:: - If instantiating the OSL object is unfamiliar, check out the :ref:`adding_joints_tutorial` and :ref:`adding_loadcell_tutorial` tutorial pages. - -Next, we'll instantiate a ``CompiledController`` wrapper object. This takes arguments of -the name of the library (without extension), the path at which it is located (which in this case is the current directory), -and the names of the main, initializaiton, and cleanup functions. - -.. literalinclude:: ../../examples/fsm_walking_compiled_controller.py - :language: python - :linenos: - :lines: 38-45 - -Define Custom Datatypes -^^^^^^^^^^^^^^^^^^^^^^^^ - -Next, we define the data structures used in the controller. These must exactly match (size and order) what was used to create the library. -First, we define a type for a single set of impedance parameters called ``impedance_param_type``, where `stiffness`, `damping`, and `eq_angle` are each doubles. -The ``CompiledController`` object provides building block types within its ``types`` attribute. - -.. literalinclude:: ../../examples/fsm_walking_compiled_controller.py - :language: python - :linenos: - :lines: 47-54 - -We then define the type ``joint_impedance_set`` which contains a set of impedance parameters for each of the four states. -Because we have already defined the ``impedance_param_type`` in the previous lines, we can now use it to define additional types. - -.. literalinclude:: ../../examples/fsm_walking_compiled_controller.py - :language: python - :linenos: - :lines: 55-63 - -We then similarly define the ``transition_parameters`` type as well as the overall ``UserParameters`` type. - -.. literalinclude:: ../../examples/fsm_walking_compiled_controller.py - :language: python - :linenos: - :lines: 64-85 - -We also define a sensors type. We're using the default sensors, which the ``CompiledController`` class provides in list form already setup correctly. - -.. literalinclude:: ../../examples/fsm_walking_compiled_controller.py - :language: python - :linenos: - :lines: 86 - -The final type definitions are for the input and output types. - -.. literalinclude:: ../../examples/fsm_walking_compiled_controller.py - :language: python - :linenos: - :lines: 88-102 - -Configure Impedance and Transition Parameters -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The next section of code configures the impedance and transition paramters based on a pre-defined tuning. -Feel free to play with these values to get the behavior you want. - -.. literalinclude:: ../../examples/fsm_walking_compiled_controller.py - :language: python - :linenos: - :lines: 105-140 - -Main Loop -^^^^^^^^^^^ - -Now that the controller is configured, we can home the osl, calibrate the loadcell, set both joints to impedance mode, and begin running. -Note that within the main loop after calling ``osl.update()``, we assign the relevant sensor values from the ``osl`` object to the controller inputs. -We also write to any other inputs that change every loop, such as the current time ``t``. -Once all inputs are assigned, we call the ``run()`` method, which calls our compiled library with the configured inputs. -The run method returns the outputs object, which is populated by the compiled library function. - - -.. literalinclude:: ../../examples/fsm_walking_compiled_controller.py - :language: python - :linenos: - :lines: 142-176 - -.. note:: - The OSL library provides sensor values in default units. If your library expects other units, - you need to convert the values prior to assigning them. You can use the ``units`` module in the ``tools`` subpackage to do this. - For example you can convert from radians (the default) to degrees using ``ankle_angle_in_deg = units.convert_from_default(osl.ankle.output_position, units.position.deg)``. - -Finally, we write from the outputs structure to the hardware. -Be careful with units at this step as well, making sure your values are either in default -units, or that you call appropriate conversion functions. As our library was in degrees, -we convert to radians. - -.. literalinclude:: ../../examples/fsm_walking_compiled_controller.py - :language: python - :linenos: - :lines: 179-204 - -Full Code for This Example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. literalinclude:: ../../examples/fsm_walking_compiled_controller.py - :language: python - :linenos: \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..d4e64381 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,64 @@ +# opensourceleg + +An open-source software library for numerical computation, data acquisition,
and control of lower-limb robotic prostheses. + +[![Release](https://img.shields.io/github/v/release/neurobionics/opensourceleg)](https://img.shields.io/github/v/release/neurobionics/opensourceleg) +[![Build status](https://img.shields.io/github/actions/workflow/status/neurobionics/opensourceleg/main.yml?branch=main)](https://github.com/neurobionics/opensourceleg/actions/workflows/main.yml?query=branch%3Amain) +[![Commit activity](https://img.shields.io/github/commit-activity/m/neurobionics/opensourceleg)](https://img.shields.io/github/commit-activity/m/neurobionics/opensourceleg) +[![License](https://img.shields.io/github/license/neurobionics/opensourceleg)](https://img.shields.io/github/license/neurobionics/opensourceleg) + + + +## Installation + +The easiest and quickest way to install the _opensourceleg_ library is via [pip](https://pip.pypa.io/en/stable/): + +```bash +pip install opensourceleg +``` + +> If you plan on installing the _opensourceleg_ library on a Raspberry Pi, we recommend using [opensourcelegpi](https://github.com/neurobionics/opensourcelegpi) tool, which is a cloud-based CI tool used to build an up-to-date OS for a [Raspberry Pi](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/) that can be used headless/GUI-less to control autonomous/remote robotic systems. This tool bundles the _opensourceleg_ library and its dependencies into a single OS image, which can be flashed onto a microSD card and used to boot a Raspberry Pi. For more information, click [here](https://github.com/neurobionics/opensourcelegpi/blob/main/README.md). + +### Developing + +To modify, develop, or contribute to the [opensourceleg](https://pypi.org/project/opensourceleg/) library, we encourage you to install [Poetry](https://python-poetry.org), which is a python packaging and dependency management tool. Once you have Poetry installed on your local machine, you can clone the repository and install the _opensourceleg_ library by running the following commands: + +```bash +git clone https://github.com/neurobionics/opensourceleg.git +cd opensourceleg + +poetry install +poetry shell +``` + +Finally, install the environment and the pre-commit hooks with + +```bash +make install +``` + +You are now ready to start development on your project! +The CI/CD pipeline will be triggered when you open a pull request, merge to main, or when you create a new release. + +To finalize the set-up for publishing to PyPI or Artifactory, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/publishing/#set-up-for-pypi). +For activating the automatic documentation with MkDocs, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/mkdocs/#enabling-the-documentation-on-github). +To enable the code coverage reports, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/codecov/). + +## Releasing a new version + +- Create an API Token on [PyPI](https://pypi.org/). +- Add the API Token to your projects secrets with the name `PYPI_TOKEN` by visiting [this page](https://github.com/neurobionics/opensourceleg/settings/secrets/actions/new). +- Create a [new release](https://github.com/neurobionics/opensourceleg/releases/new) on Github. +- Create a new tag in the form `*.*.*`. +- For more details, see [here](https://fpgmaas.github.io/cookiecutter-poetry/features/cicd/#how-to-trigger-a-release). + +## License + +The _opensourceleg_ library is licensed under the terms of the [LGPL-v2.1 license](https://github.com/neurobionics/opensourceleg/raw/main/LICENSE). This license grants users a number of freedoms: + +- You are free to use the _opensourceleg_ library for any purpose. +- You are free to modify the _opensourceleg_ library to suit your needs. +- You can study how the _opensourceleg_ library works and change it. +- You can distribute modified versions of the _opensourceleg_ library. + +The GPL license ensures that all these freedoms are protected, now and in the future, requiring everyone to share their modifications when they also share the library in public. diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index d549607f..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,111 +0,0 @@ -=========================================================================================================================== -An open-source software library for numerical computation, data acquisition, and control of lower-limb robotic prostheses. -=========================================================================================================================== - -|build| |docs| |python| |license| - -.. |build| image:: https://github.com/imsenthur/opensourceleg/workflows/build/badge.svg?branch=master&event=push - :target: https://github.com/imsenthur/opensourceleg/actions?query=workflow%3Abuild - :alt: Build Status -.. |docs| image:: https://readthedocs.org/projects/opensourceleg/badge/?version=latest - :target: https://opensourceleg.readthedocs.io/en/latest/?badge=latest - :alt: Docs Status -.. |python| image:: https://img.shields.io/pypi/pyversions/opensourceleg.svg - :target: https://pypi.org/project/opensourceleg/ - :alt: Python Versions -.. |license| image:: https://img.shields.io/github/license/imsenthur/opensourceleg - :target: https://github.com/imsenthur/opensourceleg/blob/main/LICENSE - :alt: License - -| - -.. image:: ../assets/images/banner.gif - :width: 80% - -Prerequisites -============= - -Before installing **opensourceleg** library, you should ensure that you have the following prerequisites installed: - -* ``Python 3.9`` or later -* ``pip`` package manager - -Installation -============ - -The easiest and quickest way to install the *opensourceleg* library is via `pip `_: - -.. code-block:: bash - - pip install opensourceleg - -If you plan on installing the *opensourceleg* library on a Raspberry Pi, we recommend using `opensourcelegpi `_ tool, which is a cloud-based CI tool used to build an up-to-date OS for a `Raspberry Pi `_ that can be used headless/GUI-less to control autonomous/remote robotic systems. This tool bundles the *opensourceleg* library and its dependencies into a single OS image, which can be flashed onto a microSD card and used to boot a Raspberry Pi. For more information, click `here `_. - -Getting Started -================= - -For new users, we recommend visiting the :ref:`getting_started` page for an overview of the library and its documentation. - -Developing -========== - -To modify, develop, or contribute to the `opensourceleg `_ library, we encourage you to install `Poetry `_, which is a python packaging and dependency management tool. Once you have Poetry installed on your local machine, you can clone the repository and install the *opensourceleg* library by running the following commands: - -.. code-block:: bash - - git clone https://github.com/neurobionics/opensourceleg.git - cd opensourceleg - - poetry install - poetry shell - -License -======= - -The *opensourceleg* library is licensed under the terms of the `LGPL-v2.1 license `_. This license grants users a number of freedoms: - -* You are free to use the *opensourceleg* library for any purpose. -* You are free to modify the *opensourceleg* library to suit your needs. -* You can study how the *opensourceleg* library works and change it. -* You can distribute modified versions of the *opensourceleg* library. - -The GPL license ensures that all these freedoms are protected, now and in the future, requiring everyone to share their modifications when they also share the library in public. - -.. toctree:: - :hidden: - :caption: Tutorials - - /tutorials/getting_started - /tutorials/adding_joints - /tutorials/adding_loadcell - /tutorials/voltage_mode - /tutorials/current_mode - /tutorials/position_mode - /tutorials/impedance_mode - /tutorials/compiled_control - /tutorials/osl_clock - /tutorials/reading_from_sensors - -.. toctree:: - :hidden: - :caption: Examples - - /examples/basic_motion - /examples/finite_state_machine - -.. toctree:: - :hidden: - :caption: API Reference - - /api/osl - /api/hardware - /api/control - /api/tools - -.. toctree:: - :hidden: - :caption: Contributing - - /contributing/reporting_bugs - /contributing/contributing_code - /contributing/code_of_conduct diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 32bb2452..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/modules.md b/docs/modules.md new file mode 100644 index 00000000..673c84e0 --- /dev/null +++ b/docs/modules.md @@ -0,0 +1 @@ +::: opensourceleg.actuators diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 234ec0f2..00000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,964 +0,0 @@ -accessible-pygments==0.0.4 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:416c6d8c1ea1c5ad8701903a20fcedf953c6e720d64f33dc47bfb2d3f2fa4e8d \ - --hash=sha256:e7b57a9b15958e9601c7e9eb07a440c813283545a20973f2574a5f453d0e953e -alabaster==0.7.13 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \ - --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2 -astroid==3.0.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca \ - --hash=sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e -atomicwrites==1.4.1 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "win32" \ - --hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11 -attrs==23.1.0 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ - --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 -babel==2.13.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210 \ - --hash=sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec -bandit==1.7.5 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:75665181dc1e0096369112541a056c59d1c5f66f9bb74a8d686c3c362b83f549 \ - --hash=sha256:bdfc739baa03b880c2d15d0431b31c658ffc348e907fe197e54e0389dd59e11e -beautifulsoup4==4.12.2 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da \ - --hash=sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a -black==22.12.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320 \ - --hash=sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351 \ - --hash=sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350 \ - --hash=sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f \ - --hash=sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf \ - --hash=sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148 \ - --hash=sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4 \ - --hash=sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d \ - --hash=sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc \ - --hash=sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d \ - --hash=sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2 \ - --hash=sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f -certifi==2023.7.22 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ - --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 -cfgv==3.4.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \ - --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560 -charset-normalizer==3.3.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:06cf46bdff72f58645434d467bf5228080801298fbba19fe268a01b4534467f5 \ - --hash=sha256:0c8c61fb505c7dad1d251c284e712d4e0372cef3b067f7ddf82a7fa82e1e9a93 \ - --hash=sha256:10b8dd31e10f32410751b3430996f9807fc4d1587ca69772e2aa940a82ab571a \ - --hash=sha256:1171ef1fc5ab4693c5d151ae0fdad7f7349920eabbaca6271f95969fa0756c2d \ - --hash=sha256:17a866d61259c7de1bdadef418a37755050ddb4b922df8b356503234fff7932c \ - --hash=sha256:1d6bfc32a68bc0933819cfdfe45f9abc3cae3877e1d90aac7259d57e6e0f85b1 \ - --hash=sha256:1ec937546cad86d0dce5396748bf392bb7b62a9eeb8c66efac60e947697f0e58 \ - --hash=sha256:223b4d54561c01048f657fa6ce41461d5ad8ff128b9678cfe8b2ecd951e3f8a2 \ - --hash=sha256:2465aa50c9299d615d757c1c888bc6fef384b7c4aec81c05a0172b4400f98557 \ - --hash=sha256:28f512b9a33235545fbbdac6a330a510b63be278a50071a336afc1b78781b147 \ - --hash=sha256:2c092be3885a1b7899cd85ce24acedc1034199d6fca1483fa2c3a35c86e43041 \ - --hash=sha256:2c4c99f98fc3a1835af8179dcc9013f93594d0670e2fa80c83aa36346ee763d2 \ - --hash=sha256:31445f38053476a0c4e6d12b047b08ced81e2c7c712e5a1ad97bc913256f91b2 \ - --hash=sha256:31bbaba7218904d2eabecf4feec0d07469284e952a27400f23b6628439439fa7 \ - --hash=sha256:34d95638ff3613849f473afc33f65c401a89f3b9528d0d213c7037c398a51296 \ - --hash=sha256:352a88c3df0d1fa886562384b86f9a9e27563d4704ee0e9d56ec6fcd270ea690 \ - --hash=sha256:39b70a6f88eebe239fa775190796d55a33cfb6d36b9ffdd37843f7c4c1b5dc67 \ - --hash=sha256:3c66df3f41abee950d6638adc7eac4730a306b022570f71dd0bd6ba53503ab57 \ - --hash=sha256:3f70fd716855cd3b855316b226a1ac8bdb3caf4f7ea96edcccc6f484217c9597 \ - --hash=sha256:3f9bc2ce123637a60ebe819f9fccc614da1bcc05798bbbaf2dd4ec91f3e08846 \ - --hash=sha256:3fb765362688821404ad6cf86772fc54993ec11577cd5a92ac44b4c2ba52155b \ - --hash=sha256:45f053a0ece92c734d874861ffe6e3cc92150e32136dd59ab1fb070575189c97 \ - --hash=sha256:46fb9970aa5eeca547d7aa0de5d4b124a288b42eaefac677bde805013c95725c \ - --hash=sha256:4cb50a0335382aac15c31b61d8531bc9bb657cfd848b1d7158009472189f3d62 \ - --hash=sha256:4e12f8ee80aa35e746230a2af83e81bd6b52daa92a8afaef4fea4a2ce9b9f4fa \ - --hash=sha256:4f3100d86dcd03c03f7e9c3fdb23d92e32abbca07e7c13ebd7ddfbcb06f5991f \ - --hash=sha256:4f6e2a839f83a6a76854d12dbebde50e4b1afa63e27761549d006fa53e9aa80e \ - --hash=sha256:4f861d94c2a450b974b86093c6c027888627b8082f1299dfd5a4bae8e2292821 \ - --hash=sha256:501adc5eb6cd5f40a6f77fbd90e5ab915c8fd6e8c614af2db5561e16c600d6f3 \ - --hash=sha256:520b7a142d2524f999447b3a0cf95115df81c4f33003c51a6ab637cbda9d0bf4 \ - --hash=sha256:548eefad783ed787b38cb6f9a574bd8664468cc76d1538215d510a3cd41406cb \ - --hash=sha256:555fe186da0068d3354cdf4bbcbc609b0ecae4d04c921cc13e209eece7720727 \ - --hash=sha256:55602981b2dbf8184c098bc10287e8c245e351cd4fdcad050bd7199d5a8bf514 \ - --hash=sha256:58e875eb7016fd014c0eea46c6fa92b87b62c0cb31b9feae25cbbe62c919f54d \ - --hash=sha256:5a3580a4fdc4ac05f9e53c57f965e3594b2f99796231380adb2baaab96e22761 \ - --hash=sha256:5b70bab78accbc672f50e878a5b73ca692f45f5b5e25c8066d748c09405e6a55 \ - --hash=sha256:5ceca5876032362ae73b83347be8b5dbd2d1faf3358deb38c9c88776779b2e2f \ - --hash=sha256:61f1e3fb621f5420523abb71f5771a204b33c21d31e7d9d86881b2cffe92c47c \ - --hash=sha256:633968254f8d421e70f91c6ebe71ed0ab140220469cf87a9857e21c16687c034 \ - --hash=sha256:63a6f59e2d01310f754c270e4a257426fe5a591dc487f1983b3bbe793cf6bac6 \ - --hash=sha256:63accd11149c0f9a99e3bc095bbdb5a464862d77a7e309ad5938fbc8721235ae \ - --hash=sha256:6db3cfb9b4fcecb4390db154e75b49578c87a3b9979b40cdf90d7e4b945656e1 \ - --hash=sha256:71ef3b9be10070360f289aea4838c784f8b851be3ba58cf796262b57775c2f14 \ - --hash=sha256:7ae8e5142dcc7a49168f4055255dbcced01dc1714a90a21f87448dc8d90617d1 \ - --hash=sha256:7b6cefa579e1237ce198619b76eaa148b71894fb0d6bcf9024460f9bf30fd228 \ - --hash=sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708 \ - --hash=sha256:82ca51ff0fc5b641a2d4e1cc8c5ff108699b7a56d7f3ad6f6da9dbb6f0145b48 \ - --hash=sha256:851cf693fb3aaef71031237cd68699dded198657ec1e76a76eb8be58c03a5d1f \ - --hash=sha256:854cc74367180beb327ab9d00f964f6d91da06450b0855cbbb09187bcdb02de5 \ - --hash=sha256:87071618d3d8ec8b186d53cb6e66955ef2a0e4fa63ccd3709c0c90ac5a43520f \ - --hash=sha256:871d045d6ccc181fd863a3cd66ee8e395523ebfbc57f85f91f035f50cee8e3d4 \ - --hash=sha256:8aee051c89e13565c6bd366813c386939f8e928af93c29fda4af86d25b73d8f8 \ - --hash=sha256:8af5a8917b8af42295e86b64903156b4f110a30dca5f3b5aedea123fbd638bff \ - --hash=sha256:8ec8ef42c6cd5856a7613dcd1eaf21e5573b2185263d87d27c8edcae33b62a61 \ - --hash=sha256:91e43805ccafa0a91831f9cd5443aa34528c0c3f2cc48c4cb3d9a7721053874b \ - --hash=sha256:9505dc359edb6a330efcd2be825fdb73ee3e628d9010597aa1aee5aa63442e97 \ - --hash=sha256:985c7965f62f6f32bf432e2681173db41336a9c2611693247069288bcb0c7f8b \ - --hash=sha256:9a74041ba0bfa9bc9b9bb2cd3238a6ab3b7618e759b41bd15b5f6ad958d17605 \ - --hash=sha256:9edbe6a5bf8b56a4a84533ba2b2f489d0046e755c29616ef8830f9e7d9cf5728 \ - --hash=sha256:a15c1fe6d26e83fd2e5972425a772cca158eae58b05d4a25a4e474c221053e2d \ - --hash=sha256:a66bcdf19c1a523e41b8e9d53d0cedbfbac2e93c649a2e9502cb26c014d0980c \ - --hash=sha256:ae4070f741f8d809075ef697877fd350ecf0b7c5837ed68738607ee0a2c572cf \ - --hash=sha256:ae55d592b02c4349525b6ed8f74c692509e5adffa842e582c0f861751701a673 \ - --hash=sha256:b578cbe580e3b41ad17b1c428f382c814b32a6ce90f2d8e39e2e635d49e498d1 \ - --hash=sha256:b891a2f68e09c5ef989007fac11476ed33c5c9994449a4e2c3386529d703dc8b \ - --hash=sha256:baec8148d6b8bd5cee1ae138ba658c71f5b03e0d69d5907703e3e1df96db5e41 \ - --hash=sha256:bb06098d019766ca16fc915ecaa455c1f1cd594204e7f840cd6258237b5079a8 \ - --hash=sha256:bc791ec3fd0c4309a753f95bb6c749ef0d8ea3aea91f07ee1cf06b7b02118f2f \ - --hash=sha256:bd28b31730f0e982ace8663d108e01199098432a30a4c410d06fe08fdb9e93f4 \ - --hash=sha256:be4d9c2770044a59715eb57c1144dedea7c5d5ae80c68fb9959515037cde2008 \ - --hash=sha256:c0c72d34e7de5604df0fde3644cc079feee5e55464967d10b24b1de268deceb9 \ - --hash=sha256:c0e842112fe3f1a4ffcf64b06dc4c61a88441c2f02f373367f7b4c1aa9be2ad5 \ - --hash=sha256:c15070ebf11b8b7fd1bfff7217e9324963c82dbdf6182ff7050519e350e7ad9f \ - --hash=sha256:c2000c54c395d9e5e44c99dc7c20a64dc371f777faf8bae4919ad3e99ce5253e \ - --hash=sha256:c30187840d36d0ba2893bc3271a36a517a717f9fd383a98e2697ee890a37c273 \ - --hash=sha256:cb7cd68814308aade9d0c93c5bd2ade9f9441666f8ba5aa9c2d4b389cb5e2a45 \ - --hash=sha256:cd805513198304026bd379d1d516afbf6c3c13f4382134a2c526b8b854da1c2e \ - --hash=sha256:d0bf89afcbcf4d1bb2652f6580e5e55a840fdf87384f6063c4a4f0c95e378656 \ - --hash=sha256:d9137a876020661972ca6eec0766d81aef8a5627df628b664b234b73396e727e \ - --hash=sha256:dbd95e300367aa0827496fe75a1766d198d34385a58f97683fe6e07f89ca3e3c \ - --hash=sha256:dced27917823df984fe0c80a5c4ad75cf58df0fbfae890bc08004cd3888922a2 \ - --hash=sha256:de0b4caa1c8a21394e8ce971997614a17648f94e1cd0640fbd6b4d14cab13a72 \ - --hash=sha256:debb633f3f7856f95ad957d9b9c781f8e2c6303ef21724ec94bea2ce2fcbd056 \ - --hash=sha256:e372d7dfd154009142631de2d316adad3cc1c36c32a38b16a4751ba78da2a397 \ - --hash=sha256:ecd26be9f112c4f96718290c10f4caea6cc798459a3a76636b817a0ed7874e42 \ - --hash=sha256:edc0202099ea1d82844316604e17d2b175044f9bcb6b398aab781eba957224bd \ - --hash=sha256:f194cce575e59ffe442c10a360182a986535fd90b57f7debfaa5c845c409ecc3 \ - --hash=sha256:f5fb672c396d826ca16a022ac04c9dce74e00a1c344f6ad1a0fdc1ba1f332213 \ - --hash=sha256:f6a02a3c7950cafaadcd46a226ad9e12fc9744652cc69f9e5534f98b47f3bbcf \ - --hash=sha256:fe81b35c33772e56f4b6cf62cf4aedc1762ef7162a31e6ac7fe5e40d0149eb67 -click==8.1.3 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ - --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 -colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 -contourpy==1.1.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:059c3d2a94b930f4dafe8105bcdc1b21de99b30b51b5bce74c753686de858cb6 \ - --hash=sha256:0683e1ae20dc038075d92e0e0148f09ffcefab120e57f6b4c9c0f477ec171f33 \ - --hash=sha256:07d6f11dfaf80a84c97f1a5ba50d129d9303c5b4206f776e94037332e298dda8 \ - --hash=sha256:081f3c0880712e40effc5f4c3b08feca6d064cb8cfbb372ca548105b86fd6c3d \ - --hash=sha256:0e48694d6a9c5a26ee85b10130c77a011a4fedf50a7279fa0bdaf44bafb4299d \ - --hash=sha256:11b836b7dbfb74e049c302bbf74b4b8f6cb9d0b6ca1bf86cfa8ba144aedadd9c \ - --hash=sha256:19557fa407e70f20bfaba7d55b4d97b14f9480856c4fb65812e8a05fe1c6f9bf \ - --hash=sha256:229a25f68046c5cf8067d6d6351c8b99e40da11b04d8416bf8d2b1d75922521e \ - --hash=sha256:24216552104ae8f3b34120ef84825400b16eb6133af2e27a190fdc13529f023e \ - --hash=sha256:3b53d5769aa1f2d4ea407c65f2d1d08002952fac1d9e9d307aa2e1023554a163 \ - --hash=sha256:3de23ca4f381c3770dee6d10ead6fff524d540c0f662e763ad1530bde5112532 \ - --hash=sha256:407d864db716a067cc696d61fa1ef6637fedf03606e8417fe2aeed20a061e6b2 \ - --hash=sha256:41339b24471c58dc1499e56783fedc1afa4bb018bcd035cfb0ee2ad2a7501ef8 \ - --hash=sha256:462c59914dc6d81e0b11f37e560b8a7c2dbab6aca4f38be31519d442d6cde1a1 \ - --hash=sha256:46e24f5412c948d81736509377e255f6040e94216bf1a9b5ea1eaa9d29f6ec1b \ - --hash=sha256:498e53573e8b94b1caeb9e62d7c2d053c263ebb6aa259c81050766beb50ff8d9 \ - --hash=sha256:4ebf42695f75ee1a952f98ce9775c873e4971732a87334b099dde90b6af6a916 \ - --hash=sha256:4f9147051cb8fdb29a51dc2482d792b3b23e50f8f57e3720ca2e3d438b7adf23 \ - --hash=sha256:549174b0713d49871c6dee90a4b499d3f12f5e5f69641cd23c50a4542e2ca1eb \ - --hash=sha256:560f1d68a33e89c62da5da4077ba98137a5e4d3a271b29f2f195d0fba2adcb6a \ - --hash=sha256:566f0e41df06dfef2431defcfaa155f0acfa1ca4acbf8fd80895b1e7e2ada40e \ - --hash=sha256:56de98a2fb23025882a18b60c7f0ea2d2d70bbbcfcf878f9067234b1c4818442 \ - --hash=sha256:66544f853bfa85c0d07a68f6c648b2ec81dafd30f272565c37ab47a33b220684 \ - --hash=sha256:6c06e4c6e234fcc65435223c7b2a90f286b7f1b2733058bdf1345d218cc59e34 \ - --hash=sha256:6d0a8efc258659edc5299f9ef32d8d81de8b53b45d67bf4bfa3067f31366764d \ - --hash=sha256:70e5a10f8093d228bb2b552beeb318b8928b8a94763ef03b858ef3612b29395d \ - --hash=sha256:8394e652925a18ef0091115e3cc191fef350ab6dc3cc417f06da66bf98071ae9 \ - --hash=sha256:8636cd2fc5da0fb102a2504fa2c4bea3cbc149533b345d72cdf0e7a924decc45 \ - --hash=sha256:93df44ab351119d14cd1e6b52a5063d3336f0754b72736cc63db59307dabb718 \ - --hash=sha256:96ba37c2e24b7212a77da85004c38e7c4d155d3e72a45eeaf22c1f03f607e8ab \ - --hash=sha256:a10dab5ea1bd4401c9483450b5b0ba5416be799bbd50fc7a6cc5e2a15e03e8a3 \ - --hash=sha256:a66045af6cf00e19d02191ab578a50cb93b2028c3eefed999793698e9ea768ae \ - --hash=sha256:a75cc163a5f4531a256f2c523bd80db509a49fc23721b36dd1ef2f60ff41c3cb \ - --hash=sha256:b04c2f0adaf255bf756cf08ebef1be132d3c7a06fe6f9877d55640c5e60c72c5 \ - --hash=sha256:ba42e3810999a0ddd0439e6e5dbf6d034055cdc72b7c5c839f37a7c274cb4eba \ - --hash=sha256:bfc8a5e9238232a45ebc5cb3bfee71f1167064c8d382cadd6076f0d51cff1da0 \ - --hash=sha256:c5bd5680f844c3ff0008523a71949a3ff5e4953eb7701b28760805bc9bcff217 \ - --hash=sha256:c84fdf3da00c2827d634de4fcf17e3e067490c4aea82833625c4c8e6cdea0887 \ - --hash=sha256:ca6fab080484e419528e98624fb5c4282148b847e3602dc8dbe0cb0669469887 \ - --hash=sha256:d0c188ae66b772d9d61d43c6030500344c13e3f73a00d1dc241da896f379bb62 \ - --hash=sha256:d6ab42f223e58b7dac1bb0af32194a7b9311065583cc75ff59dcf301afd8a431 \ - --hash=sha256:dfe80c017973e6a4c367e037cb31601044dd55e6bfacd57370674867d15a899b \ - --hash=sha256:e0c02b75acfea5cab07585d25069207e478d12309557f90a61b5a3b4f77f46ce \ - --hash=sha256:e30aaf2b8a2bac57eb7e1650df1b3a4130e8d0c66fc2f861039d507a11760e1b \ - --hash=sha256:eafbef886566dc1047d7b3d4b14db0d5b7deb99638d8e1be4e23a7c7ac59ff0f \ - --hash=sha256:efe0fab26d598e1ec07d72cf03eaeeba8e42b4ecf6b9ccb5a356fde60ff08b85 \ - --hash=sha256:f08e469821a5e4751c97fcd34bcb586bc243c39c2e39321822060ba902eac49e \ - --hash=sha256:f1eaac5257a8f8a047248d60e8f9315c6cff58f7803971170d952555ef6344a7 \ - --hash=sha256:f29fb0b3f1217dfe9362ec55440d0743fe868497359f2cf93293f4b2701b8251 \ - --hash=sha256:f44d78b61740e4e8c71db1cf1fd56d9050a4747681c59ec1094750a658ceb970 \ - --hash=sha256:f6aec19457617ef468ff091669cca01fa7ea557b12b59a7908b9474bb9674cf0 \ - --hash=sha256:f9dc7f933975367251c1b34da882c4f0e0b2e24bb35dc906d2f598a40b72bfc7 -coverage-badge==1.1.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:c824a106503e981c02821e7d32f008fb3984b2338aa8c3800ec9357e33345b78 \ - --hash=sha256:e365d56e5202e923d1b237f82defd628a02d1d645a147f867ac85c58c81d7997 -coverage==6.5.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79 \ - --hash=sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a \ - --hash=sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f \ - --hash=sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a \ - --hash=sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa \ - --hash=sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398 \ - --hash=sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba \ - --hash=sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d \ - --hash=sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf \ - --hash=sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b \ - --hash=sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518 \ - --hash=sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d \ - --hash=sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795 \ - --hash=sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2 \ - --hash=sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e \ - --hash=sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32 \ - --hash=sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745 \ - --hash=sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b \ - --hash=sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e \ - --hash=sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d \ - --hash=sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f \ - --hash=sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660 \ - --hash=sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62 \ - --hash=sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6 \ - --hash=sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04 \ - --hash=sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c \ - --hash=sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5 \ - --hash=sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef \ - --hash=sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc \ - --hash=sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae \ - --hash=sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578 \ - --hash=sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466 \ - --hash=sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4 \ - --hash=sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91 \ - --hash=sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0 \ - --hash=sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4 \ - --hash=sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b \ - --hash=sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe \ - --hash=sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b \ - --hash=sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75 \ - --hash=sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b \ - --hash=sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c \ - --hash=sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72 \ - --hash=sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b \ - --hash=sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f \ - --hash=sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e \ - --hash=sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53 \ - --hash=sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3 \ - --hash=sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84 \ - --hash=sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987 -coverage[toml]==6.5.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79 \ - --hash=sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a \ - --hash=sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f \ - --hash=sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a \ - --hash=sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa \ - --hash=sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398 \ - --hash=sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba \ - --hash=sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d \ - --hash=sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf \ - --hash=sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b \ - --hash=sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518 \ - --hash=sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d \ - --hash=sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795 \ - --hash=sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2 \ - --hash=sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e \ - --hash=sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32 \ - --hash=sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745 \ - --hash=sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b \ - --hash=sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e \ - --hash=sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d \ - --hash=sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f \ - --hash=sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660 \ - --hash=sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62 \ - --hash=sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6 \ - --hash=sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04 \ - --hash=sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c \ - --hash=sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5 \ - --hash=sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef \ - --hash=sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc \ - --hash=sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae \ - --hash=sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578 \ - --hash=sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466 \ - --hash=sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4 \ - --hash=sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91 \ - --hash=sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0 \ - --hash=sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4 \ - --hash=sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b \ - --hash=sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe \ - --hash=sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b \ - --hash=sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75 \ - --hash=sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b \ - --hash=sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c \ - --hash=sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72 \ - --hash=sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b \ - --hash=sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f \ - --hash=sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e \ - --hash=sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53 \ - --hash=sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3 \ - --hash=sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84 \ - --hash=sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987 -cycler==0.12.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \ - --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c -darglint==1.8.1 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da \ - --hash=sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d -dill==0.3.7 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e \ - --hash=sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03 -distlib==0.3.7 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 \ - --hash=sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8 -docutils==0.17.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125 \ - --hash=sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61 -dparse==0.6.3 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:0d8fe18714056ca632d98b24fbfc4e9791d4e47065285ab486182288813a5318 \ - --hash=sha256:27bb8b4bcaefec3997697ba3f6e06b2447200ba273c0b085c3d012a04571b528 -filelock==3.12.4 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4 \ - --hash=sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd -flexsea==8.0.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:6b728a7cd8c74b1184cea4dad13138f8e843a2f58d4f2291ff24f14ca0d0e7aa -fonttools==4.43.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:10003ebd81fec0192c889e63a9c8c63f88c7d72ae0460b7ba0cd2a1db246e5ad \ - --hash=sha256:10b3922875ffcba636674f406f9ab9a559564fdbaa253d66222019d569db869c \ - --hash=sha256:13a9a185259ed144def3682f74fdcf6596f2294e56fe62dfd2be736674500dba \ - --hash=sha256:17dbc2eeafb38d5d0e865dcce16e313c58265a6d2d20081c435f84dc5a9d8212 \ - --hash=sha256:18a2477c62a728f4d6e88c45ee9ee0229405e7267d7d79ce1f5ce0f3e9f8ab86 \ - --hash=sha256:18eefac1b247049a3a44bcd6e8c8fd8b97f3cad6f728173b5d81dced12d6c477 \ - --hash=sha256:1952c89a45caceedf2ab2506d9a95756e12b235c7182a7a0fff4f5e52227204f \ - --hash=sha256:1cf9e974f63b1080b1d2686180fc1fbfd3bfcfa3e1128695b5de337eb9075cef \ - --hash=sha256:1e09da7e8519e336239fbd375156488a4c4945f11c4c5792ee086dd84f784d02 \ - --hash=sha256:2062542a7565091cea4cc14dd99feff473268b5b8afdee564f7067dd9fff5860 \ - --hash=sha256:25d3da8a01442cbc1106490eddb6d31d7dffb38c1edbfabbcc8db371b3386d72 \ - --hash=sha256:34f713dad41aa21c637b4e04fe507c36b986a40f7179dcc86402237e2d39dcd3 \ - --hash=sha256:360201d46165fc0753229afe785900bc9596ee6974833124f4e5e9f98d0f592b \ - --hash=sha256:3b7ad05b2beeebafb86aa01982e9768d61c2232f16470f9d0d8e385798e37184 \ - --hash=sha256:4c54466f642d2116686268c3e5f35ebb10e49b0d48d41a847f0e171c785f7ac7 \ - --hash=sha256:4d9740e3783c748521e77d3c397dc0662062c88fd93600a3c2087d3d627cd5e5 \ - --hash=sha256:4f88cae635bfe4bbbdc29d479a297bb525a94889184bb69fa9560c2d4834ddb9 \ - --hash=sha256:51669b60ee2a4ad6c7fc17539a43ffffc8ef69fd5dbed186a38a79c0ac1f5db7 \ - --hash=sha256:5db46659cfe4e321158de74c6f71617e65dc92e54980086823a207f1c1c0e24b \ - --hash=sha256:5f37e31291bf99a63328668bb83b0669f2688f329c4c0d80643acee6e63cd933 \ - --hash=sha256:6bb5ea9076e0e39defa2c325fc086593ae582088e91c0746bee7a5a197be3da0 \ - --hash=sha256:748015d6f28f704e7d95cd3c808b483c5fb87fd3eefe172a9da54746ad56bfb6 \ - --hash=sha256:7bbbf8174501285049e64d174e29f9578495e1b3b16c07c31910d55ad57683d8 \ - --hash=sha256:884ef38a5a2fd47b0c1291647b15f4e88b9de5338ffa24ee52c77d52b4dfd09c \ - --hash=sha256:8da417431bfc9885a505e86ba706f03f598c85f5a9c54f67d63e84b9948ce590 \ - --hash=sha256:95e974d70238fc2be5f444fa91f6347191d0e914d5d8ae002c9aa189572cc215 \ - --hash=sha256:9648518ef687ba818db3fcc5d9aae27a369253ac09a81ed25c3867e8657a0680 \ - --hash=sha256:9a2f0aa6ca7c9bc1058a9d0b35483d4216e0c1bbe3962bc62ce112749954c7b8 \ - --hash=sha256:9c36da88422e0270fbc7fd959dc9749d31a958506c1d000e16703c2fce43e3d0 \ - --hash=sha256:9c60ecfa62839f7184f741d0509b5c039d391c3aff71dc5bc57b87cc305cff3b \ - --hash=sha256:9f727c3e3d08fd25352ed76cc3cb61486f8ed3f46109edf39e5a60fc9fecf6ca \ - --hash=sha256:a7a06f8d95b7496e53af80d974d63516ffb263a468e614978f3899a6df52d4b3 \ - --hash=sha256:ad0b3f6342cfa14be996971ea2b28b125ad681c6277c4cd0fbdb50340220dfb6 \ - --hash=sha256:b2adca1b46d69dce4a37eecc096fe01a65d81a2f5c13b25ad54d5430ae430b13 \ - --hash=sha256:b84a1c00f832feb9d0585ca8432fba104c819e42ff685fcce83537e2e7e91204 \ - --hash=sha256:bb6d2f8ef81ea076877d76acfb6f9534a9c5f31dc94ba70ad001267ac3a8e56f \ - --hash=sha256:bf11e2cca121df35e295bd34b309046c29476ee739753bc6bc9d5050de319273 \ - --hash=sha256:d21099b411e2006d3c3e1f9aaf339e12037dbf7bf9337faf0e93ec915991f43b \ - --hash=sha256:d4071bd1c183b8d0b368cc9ed3c07a0f6eb1bdfc4941c4c024c49a35429ac7cd \ - --hash=sha256:e117a92b07407a061cde48158c03587ab97e74e7d73cb65e6aadb17af191162a \ - --hash=sha256:f7a58eb5e736d7cf198eee94844b81c9573102ae5989ebcaa1d1a37acd04b33d \ - --hash=sha256:fe9b1ec799b6086460a7480e0f55c447b1aca0a4eecc53e444f639e967348896 -gitdb==4.0.11 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4 \ - --hash=sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b -gitpython==3.1.40 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:22b126e9ffb671fdd0c129796343a02bf67bf2994b35449ffc9321aa755e18a4 \ - --hash=sha256:cf14627d5a8049ffbf49915732e5eddbe8134c3bdb9d476e6182b676fc573f8a -identify==2.5.30 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54 \ - --hash=sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d -idna==3.4 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ - --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 -imagesize==1.4.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \ - --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a -importlib-metadata==6.8.0 ; python_version >= "3.9" and python_version < "3.10" \ - --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \ - --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743 -importlib-resources==6.1.0 ; python_version >= "3.9" and python_version < "3.10" \ - --hash=sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9 \ - --hash=sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83 -iniconfig==2.0.0 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ - --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 -isort==5.12.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504 \ - --hash=sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6 -isort[colors]==5.12.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504 \ - --hash=sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6 -jinja2==3.1.2 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ - --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 -kiwisolver==1.4.5 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf \ - --hash=sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e \ - --hash=sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af \ - --hash=sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f \ - --hash=sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046 \ - --hash=sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3 \ - --hash=sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5 \ - --hash=sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71 \ - --hash=sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee \ - --hash=sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3 \ - --hash=sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9 \ - --hash=sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b \ - --hash=sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985 \ - --hash=sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea \ - --hash=sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16 \ - --hash=sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89 \ - --hash=sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c \ - --hash=sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9 \ - --hash=sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712 \ - --hash=sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342 \ - --hash=sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a \ - --hash=sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958 \ - --hash=sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d \ - --hash=sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a \ - --hash=sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130 \ - --hash=sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff \ - --hash=sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898 \ - --hash=sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b \ - --hash=sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f \ - --hash=sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265 \ - --hash=sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93 \ - --hash=sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929 \ - --hash=sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635 \ - --hash=sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709 \ - --hash=sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b \ - --hash=sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb \ - --hash=sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a \ - --hash=sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920 \ - --hash=sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e \ - --hash=sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544 \ - --hash=sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45 \ - --hash=sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390 \ - --hash=sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77 \ - --hash=sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355 \ - --hash=sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff \ - --hash=sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4 \ - --hash=sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7 \ - --hash=sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20 \ - --hash=sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c \ - --hash=sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162 \ - --hash=sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228 \ - --hash=sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437 \ - --hash=sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc \ - --hash=sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a \ - --hash=sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901 \ - --hash=sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4 \ - --hash=sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770 \ - --hash=sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525 \ - --hash=sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad \ - --hash=sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a \ - --hash=sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29 \ - --hash=sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90 \ - --hash=sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250 \ - --hash=sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d \ - --hash=sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3 \ - --hash=sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54 \ - --hash=sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f \ - --hash=sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1 \ - --hash=sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da \ - --hash=sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238 \ - --hash=sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa \ - --hash=sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523 \ - --hash=sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0 \ - --hash=sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205 \ - --hash=sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3 \ - --hash=sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4 \ - --hash=sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac \ - --hash=sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9 \ - --hash=sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb \ - --hash=sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced \ - --hash=sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd \ - --hash=sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0 \ - --hash=sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da \ - --hash=sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18 \ - --hash=sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9 \ - --hash=sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276 \ - --hash=sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333 \ - --hash=sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b \ - --hash=sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db \ - --hash=sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126 \ - --hash=sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9 \ - --hash=sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09 \ - --hash=sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0 \ - --hash=sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec \ - --hash=sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7 \ - --hash=sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff \ - --hash=sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9 \ - --hash=sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192 \ - --hash=sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8 \ - --hash=sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d \ - --hash=sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6 \ - --hash=sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797 \ - --hash=sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892 \ - --hash=sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f -markdown-it-py==3.0.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ - --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb -markupsafe==2.1.3 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ - --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ - --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ - --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ - --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \ - --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ - --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ - --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \ - --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \ - --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ - --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ - --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ - --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ - --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ - --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ - --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ - --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \ - --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ - --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ - --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ - --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ - --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ - --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ - --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ - --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ - --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \ - --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ - --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ - --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ - --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ - --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ - --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ - --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ - --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \ - --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ - --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ - --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \ - --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ - --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \ - --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ - --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ - --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ - --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ - --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ - --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ - --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ - --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ - --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ - --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ - --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ - --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ - --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ - --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ - --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ - --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ - --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ - --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ - --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \ - --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \ - --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11 -matplotlib==3.8.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:061ee58facb3580cd2d046a6d227fb77e9295599c5ec6ad069f06b5821ad1cfc \ - --hash=sha256:0b11f354aae62a2aa53ec5bb09946f5f06fc41793e351a04ff60223ea9162955 \ - --hash=sha256:0d5ee602ef517a89d1f2c508ca189cfc395dd0b4a08284fb1b97a78eec354644 \ - --hash=sha256:0e723f5b96f3cd4aad99103dc93e9e3cdc4f18afdcc76951f4857b46f8e39d2d \ - --hash=sha256:23ed11654fc83cd6cfdf6170b453e437674a050a452133a064d47f2f1371f8d3 \ - --hash=sha256:2ea6886e93401c22e534bbfd39201ce8931b75502895cfb115cbdbbe2d31f287 \ - --hash=sha256:31e793c8bd4ea268cc5d3a695c27b30650ec35238626961d73085d5e94b6ab68 \ - --hash=sha256:36eafe2128772195b373e1242df28d1b7ec6c04c15b090b8d9e335d55a323900 \ - --hash=sha256:3cc3776836d0f4f22654a7f2d2ec2004618d5cf86b7185318381f73b80fd8a2d \ - --hash=sha256:5dc945a9cb2deb7d197ba23eb4c210e591d52d77bf0ba27c35fc82dec9fa78d4 \ - --hash=sha256:5de39dc61ca35342cf409e031f70f18219f2c48380d3886c1cf5ad9f17898e06 \ - --hash=sha256:60a6e04dfd77c0d3bcfee61c3cd335fff1b917c2f303b32524cd1235e194ef99 \ - --hash=sha256:6c49a2bd6981264bddcb8c317b6bd25febcece9e2ebfcbc34e7f4c0c867c09dc \ - --hash=sha256:6f25ffb6ad972cdffa7df8e5be4b1e3cadd2f8d43fc72085feb1518006178394 \ - --hash=sha256:7b37b74f00c4cb6af908cb9a00779d97d294e89fd2145ad43f0cdc23f635760c \ - --hash=sha256:7f54b9fb87ca5acbcdd0f286021bedc162e1425fa5555ebf3b3dfc167b955ad9 \ - --hash=sha256:87df75f528020a6299f76a1d986c0ed4406e3b2bd44bc5e306e46bca7d45e53e \ - --hash=sha256:90d74a95fe055f73a6cd737beecc1b81c26f2893b7a3751d52b53ff06ca53f36 \ - --hash=sha256:a33bd3045c7452ca1fa65676d88ba940867880e13e2546abb143035fa9072a9d \ - --hash=sha256:c3499c312f5def8f362a2bf761d04fa2d452b333f3a9a3f58805273719bf20d9 \ - --hash=sha256:c4940bad88a932ddc69734274f6fb047207e008389489f2b6f77d9ca485f0e7a \ - --hash=sha256:d670b9348e712ec176de225d425f150dc8e37b13010d85233c539b547da0be39 \ - --hash=sha256:dae97fdd6996b3a25da8ee43e3fc734fff502f396801063c6b76c20b56683196 \ - --hash=sha256:dd386c80a98b5f51571b9484bf6c6976de383cd2a8cd972b6a9562d85c6d2087 \ - --hash=sha256:df8505e1c19d5c2c26aff3497a7cbd3ccfc2e97043d1e4db3e76afa399164b69 \ - --hash=sha256:eee482731c8c17d86d9ddb5194d38621f9b0f0d53c99006275a12523ab021732 \ - --hash=sha256:f691b4ef47c7384d0936b2e8ebdeb5d526c81d004ad9403dfb9d4c76b9979a93 \ - --hash=sha256:f8b5a1bf27d078453aa7b5b27f52580e16360d02df6d3dc9504f3d2ce11f6309 -mccabe==0.7.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ - --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e -mdurl==0.1.2 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ - --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba -mypy-extensions==0.4.4 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:c8b707883a96efe9b4bb3aaf0dcc07e7e217d7d8368eec4db4049ee9e142f4fd -mypy==0.991 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d \ - --hash=sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6 \ - --hash=sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf \ - --hash=sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f \ - --hash=sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813 \ - --hash=sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33 \ - --hash=sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad \ - --hash=sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05 \ - --hash=sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297 \ - --hash=sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06 \ - --hash=sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd \ - --hash=sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243 \ - --hash=sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305 \ - --hash=sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476 \ - --hash=sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711 \ - --hash=sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70 \ - --hash=sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5 \ - --hash=sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461 \ - --hash=sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab \ - --hash=sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c \ - --hash=sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d \ - --hash=sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135 \ - --hash=sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93 \ - --hash=sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648 \ - --hash=sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a \ - --hash=sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb \ - --hash=sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3 \ - --hash=sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372 \ - --hash=sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb \ - --hash=sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef -nodeenv==1.8.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2 \ - --hash=sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec -numpy==1.25.2 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2 \ - --hash=sha256:1a1329e26f46230bf77b02cc19e900db9b52f398d6722ca853349a782d4cff55 \ - --hash=sha256:1b9735c27cea5d995496f46a8b1cd7b408b3f34b6d50459d9ac8fe3a20cc17bf \ - --hash=sha256:2792d23d62ec51e50ce4d4b7d73de8f67a2fd3ea710dcbc8563a51a03fb07b01 \ - --hash=sha256:3e0746410e73384e70d286f93abf2520035250aad8c5714240b0492a7302fdca \ - --hash=sha256:4c3abc71e8b6edba80a01a52e66d83c5d14433cbcd26a40c329ec7ed09f37901 \ - --hash=sha256:5883c06bb92f2e6c8181df7b39971a5fb436288db58b5a1c3967702d4278691d \ - --hash=sha256:5c97325a0ba6f9d041feb9390924614b60b99209a71a69c876f71052521d42a4 \ - --hash=sha256:60e7f0f7f6d0eee8364b9a6304c2845b9c491ac706048c7e8cf47b83123b8dbf \ - --hash=sha256:76b4115d42a7dfc5d485d358728cdd8719be33cc5ec6ec08632a5d6fca2ed380 \ - --hash=sha256:7dc869c0c75988e1c693d0e2d5b26034644399dd929bc049db55395b1379e044 \ - --hash=sha256:834b386f2b8210dca38c71a6e0f4fd6922f7d3fcff935dbe3a570945acb1b545 \ - --hash=sha256:8b77775f4b7df768967a7c8b3567e309f617dd5e99aeb886fa14dc1a0791141f \ - --hash=sha256:90319e4f002795ccfc9050110bbbaa16c944b1c37c0baeea43c5fb881693ae1f \ - --hash=sha256:b79e513d7aac42ae918db3ad1341a015488530d0bb2a6abcbdd10a3a829ccfd3 \ - --hash=sha256:bb33d5a1cf360304754913a350edda36d5b8c5331a8237268c48f91253c3a364 \ - --hash=sha256:bec1e7213c7cb00d67093247f8c4db156fd03075f49876957dca4711306d39c9 \ - --hash=sha256:c5462d19336db4560041517dbb7759c21d181a67cb01b36ca109b2ae37d32418 \ - --hash=sha256:c5652ea24d33585ea39eb6a6a15dac87a1206a692719ff45d53c5282e66d4a8f \ - --hash=sha256:d7806500e4f5bdd04095e849265e55de20d8cc4b661b038957354327f6d9b295 \ - --hash=sha256:db3ccc4e37a6873045580d413fe79b68e47a681af8db2e046f1dacfa11f86eb3 \ - --hash=sha256:dfe4a913e29b418d096e696ddd422d8a5d13ffba4ea91f9f60440a3b759b0187 \ - --hash=sha256:eb942bfb6f84df5ce05dbf4b46673ffed0d3da59f13635ea9b926af3deb76926 \ - --hash=sha256:f08f2e037bba04e707eebf4bc934f1972a315c883a9e0ebfa8a7756eabf9e357 \ - --hash=sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760 -packaging==23.2 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ - --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 -pathspec==0.11.2 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20 \ - --hash=sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3 -pbr==5.11.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b \ - --hash=sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3 -pillow==10.1.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d \ - --hash=sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de \ - --hash=sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616 \ - --hash=sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839 \ - --hash=sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099 \ - --hash=sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a \ - --hash=sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219 \ - --hash=sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106 \ - --hash=sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b \ - --hash=sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412 \ - --hash=sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b \ - --hash=sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7 \ - --hash=sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2 \ - --hash=sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7 \ - --hash=sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14 \ - --hash=sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f \ - --hash=sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27 \ - --hash=sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57 \ - --hash=sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262 \ - --hash=sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28 \ - --hash=sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610 \ - --hash=sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172 \ - --hash=sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273 \ - --hash=sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e \ - --hash=sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d \ - --hash=sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818 \ - --hash=sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f \ - --hash=sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9 \ - --hash=sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01 \ - --hash=sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7 \ - --hash=sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651 \ - --hash=sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312 \ - --hash=sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80 \ - --hash=sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666 \ - --hash=sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061 \ - --hash=sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b \ - --hash=sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992 \ - --hash=sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593 \ - --hash=sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4 \ - --hash=sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db \ - --hash=sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba \ - --hash=sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd \ - --hash=sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e \ - --hash=sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212 \ - --hash=sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb \ - --hash=sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2 \ - --hash=sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34 \ - --hash=sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256 \ - --hash=sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f \ - --hash=sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2 \ - --hash=sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38 \ - --hash=sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996 \ - --hash=sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a \ - --hash=sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793 -platformdirs==3.11.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \ - --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e -pluggy==1.3.0 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12 \ - --hash=sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7 -pre-commit==2.21.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658 \ - --hash=sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad -py==1.11.0 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ - --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 -pydata-sphinx-theme==0.13.3 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:827f16b065c4fd97e847c11c108bf632b7f2ff53a3bca3272f63f3f3ff782ecc \ - --hash=sha256:bf41ca6c1c6216e929e28834e404bfc90e080b51915bbe7563b5e6fda70354f0 -pydocstyle==6.3.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019 \ - --hash=sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1 -pygments==2.16.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \ - --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29 -pylint==3.0.2 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496 \ - --hash=sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda -pyparsing==3.1.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb \ - --hash=sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db -pyserial==3.5 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \ - --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0 -pytest-cov==3.0.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6 \ - --hash=sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470 -pytest-html==3.2.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:868c08564a68d8b2c26866f1e33178419bb35b1e127c33784a28622eb827f3f3 \ - --hash=sha256:c4e2f4bb0bffc437f51ad2174a8a3e71df81bbc2f6894604e604af18fbe687c3 -pytest-metadata==2.0.4 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:acb739f89fabb3d798c099e9e0c035003062367a441910aaaf2281bc1972ee14 \ - --hash=sha256:fcc653f65fe3035b478820b5284fbf0f52803622ee3f60a2faed7a7d3ba1f41e -pytest-mock==3.12.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f \ - --hash=sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9 -pytest==6.2.5 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89 \ - --hash=sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134 -python-dateutil==2.8.2 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ - --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 -pyupgrade==2.38.4 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:1eb43a49f416752929741ba4d706bf3f33593d3cac9bdc217fc1ef55c047c1f4 \ - --hash=sha256:944ff993c396ddc2b9012eb3de4cda138eb4c149b22c6c560d4c8bfd0e180982 -pyyaml==6.0.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ - --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ - --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ - --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ - --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ - --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ - --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ - --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ - --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ - --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ - --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ - --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ - --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ - --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ - --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ - --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ - --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ - --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ - --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ - --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ - --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ - --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ - --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ - --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ - --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ - --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ - --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ - --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ - --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ - --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ - --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ - --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ - --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ - --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ - --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ - --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ - --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ - --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ - --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ - --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ - --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ - --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ - --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ - --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ - --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ - --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ - --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ - --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ - --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ - --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f -requests==2.31.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 -rich==13.6.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245 \ - --hash=sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef -ruamel-yaml-clib==0.2.8 ; platform_python_implementation == "CPython" and python_version < "3.13" and python_version >= "3.9" \ - --hash=sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001 \ - --hash=sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462 \ - --hash=sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615 \ - --hash=sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15 \ - --hash=sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b \ - --hash=sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675 \ - --hash=sha256:3fcc54cb0c8b811ff66082de1680b4b14cf8a81dce0d4fbf665c2265a81e07a1 \ - --hash=sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7 \ - --hash=sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312 \ - --hash=sha256:665f58bfd29b167039f714c6998178d27ccd83984084c286110ef26b230f259f \ - --hash=sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91 \ - --hash=sha256:7048c338b6c86627afb27faecf418768acb6331fc24cfa56c93e8c9780f815fa \ - --hash=sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b \ - --hash=sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3 \ - --hash=sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5 \ - --hash=sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe \ - --hash=sha256:9eb5dee2772b0f704ca2e45b1713e4e5198c18f515b52743576d196348f374d3 \ - --hash=sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed \ - --hash=sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337 \ - --hash=sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248 \ - --hash=sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d \ - --hash=sha256:b5edda50e5e9e15e54a6a8a0070302b00c518a9d32accc2346ad6c984aacd279 \ - --hash=sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf \ - --hash=sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512 \ - --hash=sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069 \ - --hash=sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb \ - --hash=sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942 \ - --hash=sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d \ - --hash=sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31 \ - --hash=sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92 \ - --hash=sha256:d92f81886165cb14d7b067ef37e142256f1c6a90a65cd156b063a43da1708cfd \ - --hash=sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5 \ - --hash=sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1 \ - --hash=sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2 \ - --hash=sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875 \ - --hash=sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412 -ruamel-yaml==0.17.40 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:6024b986f06765d482b5b07e086cc4b4cd05dd22ddcbc758fa23d54873cf313d \ - --hash=sha256:b16b6c3816dff0a93dca12acf5e70afd089fa5acb80604afd1ffa8b465b7722c -safety==2.3.4 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:6224dcd9b20986a2b2c5e7acfdfba6bca42bb11b2783b24ed04f32317e5167ea \ - --hash=sha256:b9e74e794e82f54d11f4091c5d820c4d2d81de9f953bf0b4f33ac8bc402ae72c -setuptools-scm==8.0.4 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:b47844cd2a84b83b3187a5782c71128c28b4c94cad8bfb871da2784a5cb54c4f \ - --hash=sha256:b5f43ff6800669595193fd09891564ee9d1d7dcb196cab4b2506d53a2e1c95c7 -setuptools==68.2.2 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87 \ - --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a -six==1.16.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 -smbus2==0.4.3 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:36f2288a8e1a363cb7a7b2244ec98d880eb5a728a2494ac9c71e9de7bf6a803a \ - --hash=sha256:a2fc29cfda4081ead2ed61ef2c4fc041d71dd40a8d917e85216f44786fca2d1d -smmap==5.0.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62 \ - --hash=sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da -snowballstemmer==2.2.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ - --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a -soupsieve==2.5 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690 \ - --hash=sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7 -sphinx-book-theme==1.0.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:927b399a6906be067e49c11ef1a87472f1b1964075c9eea30fb82c64b20aedee \ - --hash=sha256:d15f8248b3718a9a6be0ba617a32d1591f9fa39c614469bface777ba06a73b75 -sphinx==4.5.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6 \ - --hash=sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226 -sphinxcontrib-applehelp==1.0.4 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \ - --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e -sphinxcontrib-devhelp==1.0.2 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \ - --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4 -sphinxcontrib-htmlhelp==2.0.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \ - --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903 -sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \ - --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8 -sphinxcontrib-qthelp==1.0.3 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \ - --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6 -sphinxcontrib-serializinghtml==1.1.5 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \ - --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952 -stevedore==5.1.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:8cc040628f3cea5d7128f2e76cf486b2251a4e543c7b938f58d9a377f6694a2d \ - --hash=sha256:a54534acf9b89bc7ed264807013b505bf07f74dbe4bcfa37d32bd063870b087c -tokenize-rt==4.2.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:08a27fa032a81cf45e8858d0ac706004fcd523e8463415ddf1442be38e204ea8 \ - --hash=sha256:0d4f69026fed520f8a1e0103aa36c406ef4661417f20ca643f913e33531b3b94 -toml==0.10.2 ; python_version >= "3.9" and python_version < "4.0" \ - --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ - --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f -tomli==2.0.1 ; python_version >= "3.9" and python_full_version < "3.11.0a7" \ - --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \ - --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f -tomlkit==0.12.1 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86 \ - --hash=sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899 -tornado==6.3.3 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:1bd19ca6c16882e4d37368e0152f99c099bad93e0950ce55e71daed74045908f \ - --hash=sha256:22d3c2fa10b5793da13c807e6fc38ff49a4f6e1e3868b0a6f4164768bb8e20f5 \ - --hash=sha256:502fba735c84450974fec147340016ad928d29f1e91f49be168c0a4c18181e1d \ - --hash=sha256:65ceca9500383fbdf33a98c0087cb975b2ef3bfb874cb35b8de8740cf7f41bd3 \ - --hash=sha256:71a8db65160a3c55d61839b7302a9a400074c9c753040455494e2af74e2501f2 \ - --hash=sha256:7ac51f42808cca9b3613f51ffe2a965c8525cb1b00b7b2d56828b8045354f76a \ - --hash=sha256:7d01abc57ea0dbb51ddfed477dfe22719d376119844e33c661d873bf9c0e4a16 \ - --hash=sha256:805d507b1f588320c26f7f097108eb4023bbaa984d63176d1652e184ba24270a \ - --hash=sha256:9dc4444c0defcd3929d5c1eb5706cbe1b116e762ff3e0deca8b715d14bf6ec17 \ - --hash=sha256:ceb917a50cd35882b57600709dd5421a418c29ddc852da8bcdab1f0db33406b0 \ - --hash=sha256:e7d8db41c0181c80d76c982aacc442c0783a2c54d6400fe028954201a2e032fe -typing-extensions==4.8.0 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ - --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef -urllib3==2.0.7 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \ - --hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e -virtualenv==20.24.5 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b \ - --hash=sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752 -wheel==0.41.2 ; python_version >= "3.9" and python_full_version < "4.0.0" \ - --hash=sha256:0c5ac5ff2afb79ac23ab82bab027a0be7b5dbcf2e54dc50efe4bf507de1f7985 \ - --hash=sha256:75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8 -zipp==3.17.0 ; python_version >= "3.9" and python_version < "3.10" \ - --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \ - --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0 diff --git a/docs/tutorials/adding_joints.rst b/docs/tutorials/adding_joints.rst deleted file mode 100644 index 3e7899e1..00000000 --- a/docs/tutorials/adding_joints.rst +++ /dev/null @@ -1,60 +0,0 @@ -.. _adding_joints_tutorial: - -Adding an Actuator -================== -In this tutorial, we'll show you how to add a joint to your open-source leg using the ``opensourceleg`` library. - -Import the OpenSourceLeg Class ------------------------------- - -To get started, we need to import the ``OpenSourceLeg`` class, which provides an interface for controlling the open-source leg. - -.. code-block:: python - - from opensourceleg.osl import OpenSourceLeg - -Create an instance of the OpenSourceLeg Class ---------------------------------------------- - -Next, we need to create an ``OpenSourceLeg`` object. This object represents the open-source leg, provides methods for controlling its joint, and provides methods for a variety of other tasks. - -.. code-block:: python - - osl = OpenSourceLeg(frequency=200, file_name="getting_started.log") - -This will create an ``OpenSourceLeg`` object with a `frequency of 200 Hz` and a log file named ``getting_started.log``. - -Add a Joint Object -------------------- - -To add a joint to the open-source leg, we can use the ``add_joint`` method of the ``OpenSourceLeg`` object. - -.. code-block:: python - - osl.add_joint(name="knee", gear_ratio=41.99, has_loadcell=False) - -This will add a joint object named ``knee`` with a gear ratio of ``41.99`` to the `osl` object. You can also specify the **port** the joint is connected to using the ``port`` parameter -of the ``add_joint`` method. If you don't specify a port, the joint will be connected to the first available port. - - -.. Note:: - - The ``has_loadcell`` parameter indicates whether or not the actuator has a load cell connected to it via an FFC cable. This feature is only supported by the Dephy actuators. If you - are using a TMotor actuator, this parameter should always be set to ``False``. We'll discuss the loadcell in more detail in a later tutorial. - -You can also add the ankle joint to the open-source leg by calling the ``add_joint`` method again. If the ``port`` parameter is not specified, the joint will use the next available port. - -.. code-block:: python - - osl.add_joint(name="ankle", gear_ratio=41.99, has_loadcell=False) - -.. Warning:: - Please ensure that you are powering-on the actuators in the order of initialization, i.e. if you are initializing the knee joint first, then the knee joint should be powered-on first. - - -Code for this tutorial ----------------------- - -.. literalinclude:: ../../tutorials/adding_joints.py - :language: python - :linenos: diff --git a/docs/tutorials/adding_loadcell.rst b/docs/tutorials/adding_loadcell.rst deleted file mode 100644 index 4c5e3439..00000000 --- a/docs/tutorials/adding_loadcell.rst +++ /dev/null @@ -1,69 +0,0 @@ -.. _adding_loadcell_tutorial: - -Adding a Loadcell -================= -In this tutorial, we'll show you how to add a load cell to your open-source leg using the ``opensourceleg`` library. - -Create an OpenSourceLeg Object ------------------------------- - -First, we need to create an ``OpenSourceLeg`` object. This object represents your open-source leg. - -.. code-block:: python - - osl = OpenSourceLeg(frequency=200, file_name="getting_started.log") - -This will create an ``OpenSourceLeg`` object with a `frequency of 200 Hz` and a log file named ``getting_started.log``. - -Add a Load Cell Object ----------------------- - -To add a load cell to the ``osl`` object, we can use the ``add_loadcell`` method. - -.. code-block:: python - - LOADCELL_MATRIX = np.array( - [ - (-38.72600, -1817.74700, 9.84900, 43.37400, -44.54000, 1824.67000), - (-8.61600, 1041.14900, 18.86100, -2098.82200, 31.79400, 1058.6230), - ( - -1047.16800, - 8.63900, - -1047.28200, - -20.70000, - -1073.08800, - -8.92300, - ), - (20.57600, -0.04000, -0.24600, 0.55400, -21.40800, -0.47600), - (-12.13400, -1.10800, 24.36100, 0.02300, -12.14100, 0.79200), - (-0.65100, -28.28700, 0.02200, -25.23000, 0.47300, -27.3070), - ] - ) - - osl.add_loadcell(dephy_mode=False, loadcell_matrix=LOADCELL_MATRIX) - - -This will add a loadcell to the ``osl`` object with the specified ``loadcell_matrix``, which is the calibration matrix for the loadcell. This calibration matrix is unique to each load cell and can be found in the loadcell's datasheet. -The ``dephy_mode`` argument is set to ``False``, which means that the loadcell is connected to the Raspberry Pi via the GPIO pins and not to the Dephy actuator using an FFC cable. If you are using a loadcell connected to the Dephy actuator, you should set this argument to ``True``. - -Here is an example of how you would add a load cell to the `osl` object if you were using a loadcell connected to the Dephy actuator: - -.. code-block:: python - - osl.add_joint(name="knee", gear_ratio=41.99, has_loadcell=True) - osl.add_loadcell(dephy_mode=True, joint=osl.knee, loadcell_matrix=LOADCELL_MATRIX) - -This method requires a joint to be added to the `osl` object first. This is because the loadcell is connected to the Dephy actuator, which reads the loadcell data and streams it to the Raspberry Pi. This joint object is passed to the ``add_loadcell`` method so that the loadcell data can be read from the joint object. - -.. Note:: - If you are using a different loadcell amplifier, your amplifier gain and excitation voltage may be different. You can change these values by passing the ``amp_gain`` and ``exc`` arguments to the ``add_loadcell`` method. The default values for these arguments are ``amp_gain=125`` and ``exc=5``. - -Code for this tutorial ----------------------- - -.. literalinclude:: ../../tutorials/adding_loadcell.py - :language: python - :linenos: - - -That's it! You've now added a loadcell to your open-source leg using the ``opensourceleg`` library. diff --git a/docs/tutorials/compiled_control.rst b/docs/tutorials/compiled_control.rst deleted file mode 100644 index e8926b15..00000000 --- a/docs/tutorials/compiled_control.rst +++ /dev/null @@ -1,127 +0,0 @@ -.. _compiled_controller_tutorial_doc: - -Compiled Controllers -======================= -The ``opensourceleg.control`` module provides functionality for using controllers written in languages other than python via the ``CompiledController`` class. -This class wraps a function from a compiled dynamic library and allows it to be called from your main python code. -Using a compiled controller written in another language can be beneficial, especially when speed is a concern. - -Make an Example library -------------------------- -In this tutorial, we will write an example linear algebra library in ``c++`` that provides a function to compute the dot product between two 3D vectors. -This very simple function will allow us to show how to define input and output structures and call the controller. More complex examples using actual controllers in both ``c++`` and MATLAB implementations are provided in the examples folder. - -The ``CompiledController`` class assumes that you have a compiled dynamic library (with extension ``*.so`` on linux) with the function prototype ``myFunction(Inputs* inputs, Outputs* outputs)``, where ``Inputs`` and ``Outputs`` are structures holding all of the input and output data. Thus, we first need to write and compile our library. - -.. literalinclude:: ../../tutorials/compiled_control/dot_product_3d.cpp - :language: cpp - :linenos: - -.. note:: - - The ``extern "C"`` linkage-specification is important to prevent the `C++` compiler from name mangling. - Under the hood, our library uses a `C` style calling convention, which expects to be able to find the library functions with their standard names. - - -First, navigate to the directory ``opensourceleg/tutorials/compiled_control/``. Then run ``make`` to build the library. If succesful, a new library named ``lin_alg.so`` should be created. - -Load the Example Library -------------------------- -Next, we need to write a python script to call our newly compiled library. First, we import the library. We also import ``os`` to get the path of the current directory. - -.. code-block:: python - - import os - - from opensourceleg.control.compiled_controller import CompiledController - -Then we'll make an instance of the ``CompiledController`` wrapper and have it load our linear algebra library. -We need to pass it both the name of the library (without an extension) and the directory in which to find the library. -We also give it the name of our main function as well as any initialization and cleanup functions. - -.. code-block:: python - - my_linalg = CompiledController( - library_name="lin_alg.so", - library_path=os.path.dirname(__file__), - main_function_name="dot_product_3d", - initialization_function_name=None, - cleanup_function_name=None, - ) - -.. Note:: - - If your library provides initialization and cleanup functions, they will be called upon loading and cleanup, respectively. - If your library does not need these functions, pass the default argument of `None`. - -Define Custom Datatypes ---------------------------- -Our library uses a `Vector3D` structure, which we need to define so that the python code can pass the data to the library -in the right format. Every structure is built using basic types from ``my_linalg.types``, -such as `c_double`, `c_bool`, `c_int16`, etc. -We therefore can add `Vector3D` to the list of known types using the ``define_type()`` method, which -takes two arguments: (1) a name of the new type definition, and (2) a list of tuples where the first entry is the name -of the field and the second entry is the type. For example, the code to define `Vector3D` is - -.. code-block:: python - - my_linalg.define_type( - "Vector3D", - [ - ("x", my_linalg.types.c_double), - ("y", my_linalg.types.c_double), - ("z", my_linalg.types.c_double), - ], - ) - -Now the wrapper knows how `Vector3D` is defined, we can use it in other type definitions, the same way as any other basic type. -After all necessary types are defined, we need to define the input and output structures using the ``define_inputs()`` and ``define_outputs()`` methods. -These methods are similar to ``define_type()``, but are special because they tell the wrapper which objects to pass to and from -the compiled library. We define the inputs as two `Vector3D` objects and the output as one double titled result. - -.. code-block:: python - - my_linalg.define_inputs([("vector1", my_linalg.types.Vector3D), - ("vector2", my_linalg.types.Vector3D)]) - my_linalg.define_outputs([("result", my_linalg.types.c_double)]) - -Populate Inputs and Test the Function ---------------------------------------- -Now that the input structure has been defined, we can write to the inputs structure at ``my_linalg.inputs``. -First, we declare two vectors and populate their fields with the appropriate values. - -.. code-block:: python - - vector1 = my_linalg.types.Vector3D() - vector2 = my_linalg.types.Vector3D() - vector1.x = 0.6651 - vector1.y = 0.7395 - vector1.z = 0.1037 - vector2.x = -0.7395 - vector2.y = 0.6716 - vector2.z = -0.0460 - -Then we can assign those vectors to the input structure. - -.. code-block:: python - - my_linalg.inputs.vector1 = vector1 - my_linalg.inputs.vector2 = vector2 - -Finally, we can run the dot product function and print the result from the output structure. -As our input vectors were orthogonal, we get the expected result of zero. - -.. code-block:: python - - outputs = my_linalg.run() - - print(f"Dot product: {outputs.result}") - - Dot product: 3.6549999999971154e-05 - -Code for this tutorial ----------------------- - -.. literalinclude:: ../../tutorials/compiled_control/compiled_control.py - :language: python - :linenos: diff --git a/docs/tutorials/current_mode.rst b/docs/tutorials/current_mode.rst deleted file mode 100644 index cf8af4ec..00000000 --- a/docs/tutorials/current_mode.rst +++ /dev/null @@ -1,54 +0,0 @@ -.. _commanding_current_tutorial: - -Commanding Current -================== - -In this tutorial, we'll show you how to use the ``opensourceleg`` library to control the current of a joint. - -Import the OpenSourceLeg Class ------------------------------- - -To use the ``OpenSourceLeg`` class, we first need to import it from the ``opensourceleg.osl`` module: - -.. code-block:: python - - from opensourceleg.osl import OpenSourceLeg - -Add a Joint Object -------------------- - -Once we have imported the ``OpenSourceLeg`` class, we can create an instance of the class with the desired frequency and joint configuration: - -.. code-block:: python - - osl = OpenSourceLeg(frequency=200) # 200 Hz - osl.add_joint(gear_ratio=9.0) - -In this code, we create an ``OpenSourceLeg`` object named `osl` with a frequency of 200 Hz and a joint with a gear ratio of 9.0. - -Controlling Joint Current --------------------------- - -To control the current of a joint, we can use a ``with`` block to ensure that the ``OpenSourceLeg`` object is properly opened and cleaned up after use: - -.. code-block:: python - - with osl: - osl.knee.set_mode(osl.knee.control_modes.current) - - for t in osl.clock: - osl.knee.set_current(400) # 400 mA - osl.log.info(osl.knee.motor_position) - osl.update() - -In this code, we enter a ``with`` block that sets the mode of the ``knee`` joint to "current". We then loop over the ``osl.clock``, which generates a sequence of timestamps at the specified frequency, and set the current of the ``knee`` joint to 400 mA. We then log the motor position of the ``knee`` joint to the console at each timestamp. We then call the ``osl.update`` method to update the state of the ``OpenSourceLeg`` object. - -.. warning:: - This code assumes that the ``OpenSourceLeg`` object is properly configured and calibrated, and that the joint is properly connected and functioning. - -Code for this tutorial: ------------------------ - -.. literalinclude:: ../../tutorials/current_mode.py - :language: python - :linenos: diff --git a/docs/tutorials/getting_started.rst b/docs/tutorials/getting_started.rst deleted file mode 100644 index ce6a98a3..00000000 --- a/docs/tutorials/getting_started.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. _getting_started: - -Getting Started -===================== - -There are a few concepts that almost every OSL script will reqiure. -Before anything else, we suggest going through the following basic tutorials: - -- :ref:`adding_joints_tutorial` -- :ref:`adding_loadcell_tutorial` -- :ref:`reading_from_sensors_tutorial` -- :ref:`using_osl_clock_tutorial` -- :ref:`commanding_current_tutorial` - -Once you're comfortable with these, you should be ready to tackle the rest of the Tutorials and the Examples. - diff --git a/docs/tutorials/impedance_mode.rst b/docs/tutorials/impedance_mode.rst deleted file mode 100644 index 78e52b17..00000000 --- a/docs/tutorials/impedance_mode.rst +++ /dev/null @@ -1,61 +0,0 @@ -Commanding Impedance -==================== - -In this tutorial, we'll show you how to use the ``opensourceleg`` library to control the impedance of a joint. - -Import the OpenSourceLeg Class ------------------------------- - -To use the ``OpenSourceLeg`` class, we first need to import it from the ``opensourceleg.osl`` module: - -.. code-block:: python - - from opensourceleg.osl import OpenSourceLeg - -Add a Joint Object -------------------- - -Once we have imported the ``OpenSourceLeg`` class, we can create an instance of the class with the desired frequency and joint configuration: - -.. code-block:: python - - osl = OpenSourceLeg(frequency=200) # 200 Hz - osl.add_joint(gear_ratio=9.0) - -Controlling Joint Impedance ---------------------------- - -To control the impedance of a joint, we can use a ``with`` block to ensure that the ``OpenSourceLeg`` object is properly opened and cleaned up after use: - -.. code-block:: python - - test_stiffness_value = 20 # Nm/rad - test_damping_value = 20 # Nm/rad/s - set_point = 50 # motor ticks - - with osl: - osl.knee.set_mode(osl.knee.control_modes.impedance) - osl.knee.set_output_impedance(K=test_stiffness_value, B=test_damping_value) - osl.knee.set_motor_position(osl.knee.motor_position + set_point) - - for t in osl.clock: - osl.log.info(osl.knee.motor_position) - osl.update() - -In this code, we enter a ``with`` block that sets the mode of the ``knee`` joint to `impedance` and set the motor position of the ``knee`` joint to the current position plus a set point of 50 degrees. We then set the impedance of the ``knee`` joint using the ``set_output_impedance`` method, with a stiffness of 20 Nm/rad and a damping of 20 Nm/rad/s. We then loop over the ``osl.clock`` generator, which generates a sequence of timestamps at the specified frequency, and log the motor position of the ``knee`` joint to the console at each timestamp. We then call the ``osl.update`` method to update the state of the ``OpenSourceLeg`` object. - -.. Note:: - When setting the impedance gains, we can also use the ``set_motor_impedance`` method to set the impedance of the motor instead of the joint. The difference between the two methods is that the ``set_output_impedance`` method first divides the initial value by the gear ratio squared then sets the gains, while the ``set_motor_impedance`` method simply sets the gains. The impedance of the joint is the impedance of the motor plus the impedance of the joint. - -.. Note:: - We can also set the gains directly using "convert" methods. We can set the impedance gains using the ``set_impedance_gains`` method and the ``convert_to_joint_impedance`` method for joint stiffness and damping using real units. Alternatively, we can set impedance gains using the ``set_impedance_gains`` method and the ``convert_to_motor_impedance`` method for setting the motor stiffness and damping values with real units. Finally, we can set the impedance gains using the ``set_impedance_gains`` method and the ``convert_to_pid_impedance`` method for setting the motor sitffness and damping using the built-in PID controller. - -.. Warning:: - This code assumes that the ``OpenSourceLeg`` object is properly configured and calibrated, and that the joint is properly connected and functioning. - -Code for this tutorial: ------------------------ - -.. literalinclude:: ../../tutorials/impedance_mode.py - :language: python - :linenos: diff --git a/docs/tutorials/logger_tutorial.rst b/docs/tutorials/logger_tutorial.rst deleted file mode 100644 index 5d54ae25..00000000 --- a/docs/tutorials/logger_tutorial.rst +++ /dev/null @@ -1,97 +0,0 @@ -Logger Tutorial -==================== - -This tutorial demonstrates the usage of the ``Logger`` class from the ``opensourceleg.tools.logger`` module to log attributes from class instances to a CSV file. - -Import the Logger Class ------------------------ - -To use the ``Logger`` class, import it from the ``opensourceleg.tools.logger`` module: - -.. code-block:: python - - from opensourceleg.tools.logger import Logger - -Initialization --------------- - -To begin logging, create an instance of the ``Logger`` class: - -.. code-block:: python - - local_logger = Logger(file_path="./test_log") - -- `file_path` (optional): Specify the path and filename for the log file. The default is "./test_log". - -Setting Logging Levels ----------------------- - -Set different logging levels for the file and stream handlers. Available levels are "DEBUG," "INFO," "WARNING," "ERROR," and "CRITICAL." - -.. code-block:: python - - local_logger.set_file_level(level="DEBUG") - local_logger.set_stream_level(level="INFO") - -- ``set_file_level``: Set the logging level for the file handler. -- ``set_stream_level``: Set the logging level for the stream handler. - -Creating a Sample Class ------------------------ - -Define a sample class with attributes to be logged: - -.. code-block:: python - - class SimpleClass: - def __init__(self): - self.a = 1 - self.b = 2 - self.c = 3 - - simple_class = SimpleClass() - -Adding Attributes for Logging ------------------------------- - -Use the ``add_attributes`` method to specify the class instance and attributes to log: - -.. code-block:: python - - local_logger.add_attributes(container=simple_class, attributes=["a", "b", "c"]) - -- ``container``: Pass the object (instance of a class) or a dictionary containing the attributes to be logged. -- ``attributes``: Provide a list of attributes to log. - -Logging Data ------------- - -Once attributes are added, log data using the ``data`` method: - -.. code-block:: python - - local_logger.data() - -- ``data``: Logs the attributes of the class instance to the CSV file. - -Logging a Debug Message ------------------------ - -Log a debug message using the ``debug`` method: - -.. code-block:: python - - local_logger.debug("message") - -- ``debug``: Logs a debug message. - -Closing the Logger ------------------- - -After logging is complete, close the CSV file using the ``close`` method: - -.. code-block:: python - - local_logger.close() - -- ``close``: Closes the CSV file. diff --git a/docs/tutorials/osl_clock.rst b/docs/tutorials/osl_clock.rst deleted file mode 100644 index 6e9580b0..00000000 --- a/docs/tutorials/osl_clock.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. _using_osl_clock_tutorial: - -Using the OSL Clock -====================== - -The ``OpenSourceLeg`` class contains a ``clock`` object that can be used to control the execution of infinite loops. -The ``clock`` is an iterable object that returns a new value (the time since instantiation in seconds) at a specified rate. -Using ``osl.clock`` in a ``for`` loop will create an infinite loop that executes at a fixed frequency. That is, - -.. code-block:: python - - for t in osl.clock: - # this code will execute repeatedly at a fixed rate - -.. note:: - The frequency that ``osl.clock`` updates at is the same as the frequency selected when instantiating the ``OpenSourceLeg`` object. - -The clock will continue ticking until one of three conditions are met: - -1. You call ``osl.clock.stop`` -2. An exception occurs -3. You press ctrl+c on the keyboard. - -We normally use option 3 to end our scripts, allowing the ctrl+c keyboard combination to basically act as a stop button. -In general, we suggest using the following overall structure for your code: - -.. code-block:: python - - # Imports - - # Configuration/Initialization Code - osl = OpenSourceLeg(frequency=loop_frequency) - - with osl: - osl.home() - - for t in osl.clock: - # Main loop code - osl.update() - - # Cleanup code - diff --git a/docs/tutorials/position_mode.rst b/docs/tutorials/position_mode.rst deleted file mode 100644 index fab62b48..00000000 --- a/docs/tutorials/position_mode.rst +++ /dev/null @@ -1,54 +0,0 @@ -Commanding Position -=================== -In this tutorial, we'll show you how to control your open-source leg in position mode using the ``opensourceleg`` library. - -Import the OpenSourceLeg Class ------------------------------- - -To use the ``OpenSourceLeg`` class, we first need to import it from the ``opensourceleg.osl`` module: - -.. code-block:: python - - import opensourceleg.tools.units as units - from opensourceleg.osl import OpenSourceLeg - -Add a Joint Object -------------------- - -Once we have imported the ``OpenSourceLeg`` class, we can create an instance of the class with the desired frequency and joint configuration: - -.. code-block:: python - - osl = OpenSourceLeg(frequency=200) # 200 Hz - osl.add_joint(gear_ratio=9.0) - -In this code, we create an ``OpenSourceLeg`` object named ``osl`` with a frequency of 200 Hz and a joint with a gear ratio of 9.0. - -Controlling Joint Position --------------------------- - -To control the position of a joint, we can use a ``with`` block to ensure that the ``OpenSourceLeg`` object is properly opened and cleaned up after use: - -.. code-block:: python - - set_point = units.convert_to_default(45, units.position.deg) - - with osl: - osl.knee.set_mode(osl.knee.control_modes.position) - osl.knee.set_motor_position(osl.knee.motor_position + set_point) - - for t in osl.clock: - osl.log.info(osl.knee.motor_position) - osl.update() - -In this code, we enter a ``with`` block that sets the mode of the ``knee`` joint to "position", and set the motor position of the ``knee`` joint to the current position plus the `set_point` value. We then loop over the ``osl.clock`` generator, which generates a sequence of timestamps at the specified frequency, and log the motor position of the ``knee`` joint to the console at each timestamp. We then call the ``osl.update`` method to update the state of the ``OpenSourceLeg`` object. - -.. warning:: - This code assumes that the ``OpenSourceLeg`` object is properly configured and calibrated, and that the joint is properly connected and functioning. - -Code for this tutorial: ------------------------ - -.. literalinclude:: ../../tutorials/position_mode.py - :language: python - :linenos: diff --git a/docs/tutorials/reading_from_sensors.rst b/docs/tutorials/reading_from_sensors.rst deleted file mode 100644 index 0fe06bfe..00000000 --- a/docs/tutorials/reading_from_sensors.rst +++ /dev/null @@ -1,64 +0,0 @@ -.. _reading_from_sensors_tutorial: - -Reading from Sensors -==================== - -In this tutorial, we'll show you how to read from the sensors of your open-source leg using the ``opensourceleg`` library. - -Import the OpenSourceLeg Class ------------------------------- - -To use the ``OpenSourceLeg`` class, we first need to import it from the ``opensourceleg.osl`` module: - -.. code-block:: python - - from opensourceleg.osl import OpenSourceLeg - -Add a Joint Object ------------------- - -Once we have imported the ``OpenSourceLeg`` class, we can create an instance of the class with the desired frequency and joint configuration: - -.. code-block:: python - - osl = OpenSourceLeg(frequency=200) - osl.add_joint(gear_ratio=9.0) - -In this code, we create an ``OpenSourceLeg`` object named ``osl`` with a frequency of 200 and a joint with a gear ratio of 9.0. - -.. rubric:: Step 3: Setting Units for the position Attribute - -We can set the units for the ``position`` attribute of the ``osl`` object using the ``units`` dictionary: - -.. code-block:: python - - osl.units["position"] = "deg" - osl.log.info(osl.units) - -In this code, we set the units for the ``position`` attribute to "deg" and log the units to the console. - -Reading from Sensors --------------------- - -To read from sensors, we can use a ``with`` block to ensure that the ``OpenSourceLeg`` object is properly opened and cleaned up after use: - -.. code-block:: python - - with osl: - osl.knee.set_mode(osl.knee.control_modes.voltage) - - for t in osl.clock: - osl.log.info(osl.knee.motor_position) - osl.update() - -In this code, we enter a ``with`` block that sets the mode of the ``knee`` joint to "voltage". We then loop over the ``osl.clock`` generator, which generates a sequence of timestamps at the specified frequency, and log the motor position of the ``knee`` joint to the console at each timestamp. We then call the ``osl.update`` method to update the state of the ``OpenSourceLeg`` object. - -.. warning:: - This code assumes that the ``OpenSourceLeg`` object is properly configured and calibrated, and that the sensors are properly connected and functioning. - -Code for this tutorial ----------------------- - -.. literalinclude:: ../../tutorials/reading_from_sensors.py - :language: python - :linenos: diff --git a/docs/tutorials/voltage_mode.rst b/docs/tutorials/voltage_mode.rst deleted file mode 100644 index 508a823a..00000000 --- a/docs/tutorials/voltage_mode.rst +++ /dev/null @@ -1,52 +0,0 @@ -Commanding Voltage -================== - -In this tutorial, we'll show you how to use the ``opensourceleg`` library to control the voltage of a joint. - -Import the OpenSourceLeg Class ------------------------------- - -To use the ``OpenSourceLeg`` class, we first need to import it from the ``opensourceleg.osl`` module: - -.. code-block:: python - - from opensourceleg.osl import OpenSourceLeg - -Add a Joint object ------------------- - -Once we have imported the ``OpenSourceLeg`` class, we can create an instance of the class with the desired frequency and joint configuration: - -.. code-block:: python - - osl = OpenSourceLeg(frequency=200) # 200 Hz - osl.add_joint(gear_ratio=9.0) - -In this code, we create an ``OpenSourceLeg`` object named ``osl`` with a frequency of 200 Hz and a joint with a gear ratio of 9.0. - -Commanding Joint Voltage -------------------------- - -To control the voltage of a joint, we can use a ``with`` block to ensure that the ``OpenSourceLeg`` object is properly opened and cleaned up after use: - -.. code-block:: python - - with osl: - osl.knee.set_mode(osl.knee.control_modes.voltage) - - for t in osl.clock: - osl.knee.set_voltage(1000) # mV - osl.log.info(osl.knee.motor_position) - osl.update() - -In this code, we enter a ``with`` block that sets the mode of the ``knee`` joint to "voltage". We then loop over the ``osl.clock`` generator, which generates a sequence of timestamps at the specified frequency, and set the voltage of the ``knee`` joint to 1000 mV. We then log the motor position of the ``knee`` joint to the console at each timestamp. We then call the ``osl.update`` method to update the state of the ``OpenSourceLeg`` object. - -.. warning:: - This code assumes that the ``OpenSourceLeg`` object is properly configured and calibrated, and that the joint is properly connected and functioning. - -Code for this tutorial: ------------------------ - -.. literalinclude:: ../../tutorials/voltage_mode.py - :language: python - :linenos: diff --git a/examples/basic_motion.py b/examples/basic_motion.py index 2044bb83..4d6d0106 100644 --- a/examples/basic_motion.py +++ b/examples/basic_motion.py @@ -11,6 +11,7 @@ import numpy as np +from opensourceleg.actuators.base import CONTROL_MODES from opensourceleg.actuators.dephy import DephyActuator from opensourceleg.time import SoftRealtimeLoop from opensourceleg.units import units @@ -53,8 +54,8 @@ def make_periodic_trajectory(period, minimum, maximum): input("Homing complete: Press enter to continue") - knee.set_control_mode(knee.CONTROL_MODES.POSITION) - ankle.set_control_mode(ankle.CONTROL_MODES.POSITION) + knee.set_control_mode(CONTROL_MODES.POSITION) + ankle.set_control_mode(CONTROL_MODES.POSITION) knee.set_position_gains(kp=5) ankle.set_position_gains(kp=5) @@ -62,19 +63,15 @@ def make_periodic_trajectory(period, minimum, maximum): knee.update() ankle.update() - knee_setpoint = units.convert_to_default(knee_traj(t), units.position.deg) - ankle_setpoint = units.convert_to_default(ankle_traj(t), units.position.deg) + knee_setpoint = units.convert_to_default(knee_traj(t), units.Position.deg) + ankle_setpoint = units.convert_to_default(ankle_traj(t), units.Position.deg) knee.set_output_position(knee_setpoint) ankle.set_output_position(ankle_setpoint) print( - "Ankle Desired {:+.2f} rad, Ankle Actual {:+.2f} rad, Knee Desired {:+.2f} rad, Ankle Desired {:+.2f} rad".format( - ankle_setpoint, - ankle.output_position, - knee_setpoint, - knee.output_position, - ), + f"Ankle Desired {ankle_setpoint:+.2f} rad, Ankle Actual {ankle.output_position:+.2f} rad, \ + Knee Desired {knee_setpoint:+.2f} rad, Ankle Desired {knee.output_position:+.2f} rad", end="\r", ) diff --git a/examples/fsm_walking_compiled_controller.py b/examples/fsm_walking_compiled_controller.py index 9768d2f3..3c5050ba 100644 --- a/examples/fsm_walking_compiled_controller.py +++ b/examples/fsm_walking_compiled_controller.py @@ -42,16 +42,14 @@ actuators = [knee, ankle] -LOADCELL_MATRIX = np.array( - [ - (-38.72600, -1817.74700, 9.84900, 43.37400, -44.54000, 1824.67000), - (-8.61600, 1041.14900, 18.86100, -2098.82200, 31.79400, 1058.6230), - (-1047.16800, 8.63900, -1047.28200, -20.70000, -1073.08800, -8.92300), - (20.57600, -0.04000, -0.24600, 0.55400, -21.40800, -0.47600), - (-12.13400, -1.10800, 24.36100, 0.02300, -12.14100, 0.79200), - (-0.65100, -28.28700, 0.02200, -25.23000, 0.47300, -27.3070), - ] -) +LOADCELL_MATRIX = np.array([ + (-38.72600, -1817.74700, 9.84900, 43.37400, -44.54000, 1824.67000), + (-8.61600, 1041.14900, 18.86100, -2098.82200, 31.79400, 1058.6230), + (-1047.16800, 8.63900, -1047.28200, -20.70000, -1073.08800, -8.92300), + (20.57600, -0.04000, -0.24600, 0.55400, -21.40800, -0.47600), + (-12.13400, -1.10800, 24.36100, 0.02300, -12.14100, 0.79200), + (-0.65100, -28.28700, 0.02200, -25.23000, 0.47300, -27.3070), +]) loadcell = SRILoadcell( calibration_matrix=LOADCELL_MATRIX, @@ -60,7 +58,7 @@ sensors = [loadcell] -currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) # type: ignore +currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) controller = CompiledController( library_name="FSMController", library_path=currentdir, @@ -110,59 +108,55 @@ ) controller.define_type("sensors", controller.DEFAULT_SENSOR_LIST) -controller.define_inputs( - [ - ("parameters", controller.types.UserParameters), - ("sensors", controller.types.sensors), - ("time", controller.types.c_double), - ] -) -controller.define_outputs( - [ - ("current_state", controller.types.c_int), - ("time_in_current_state", controller.types.c_double), - ("knee_impedance", controller.types.impedance_param_type), - ("ankle_impedance", controller.types.impedance_param_type), - ] -) +controller.define_inputs([ + ("parameters", controller.types.UserParameters), + ("sensors", controller.types.sensors), + ("time", controller.types.c_double), +]) +controller.define_outputs([ + ("current_state", controller.types.c_int), + ("time_in_current_state", controller.types.c_double), + ("knee_impedance", controller.types.impedance_param_type), + ("ankle_impedance", controller.types.impedance_param_type), +]) # Populate Controller inputs as needed -controller.inputs.parameters.knee_impedance.early_stance.stiffness = 99.372 # type: ignore -controller.inputs.parameters.knee_impedance.early_stance.damping = 3.180 # type: ignore -controller.inputs.parameters.knee_impedance.early_stance.eq_angle = 5 # type: ignore -controller.inputs.parameters.knee_impedance.late_stance.stiffness = 99.372 # type: ignore -controller.inputs.parameters.knee_impedance.late_stance.damping = 1.272 # type: ignore -controller.inputs.parameters.knee_impedance.late_stance.eq_angle = 8 # type: ignore -controller.inputs.parameters.knee_impedance.early_swing.stiffness = 39.746 # type: ignore -controller.inputs.parameters.knee_impedance.early_swing.damping = 0.063 # type: ignore -controller.inputs.parameters.knee_impedance.early_swing.eq_angle = 60 # type: ignore -controller.inputs.parameters.knee_impedance.late_swing.stiffness = 15.899 # type: ignore -controller.inputs.parameters.knee_impedance.late_swing.damping = 3.186 # type: ignore -controller.inputs.parameters.knee_impedance.late_swing.eq_angle = 5 # type: ignore -controller.inputs.parameters.ankle_impedance.early_stance.stiffness = 19.874 # type: ignore -controller.inputs.parameters.ankle_impedance.early_stance.damping = 0 # type: ignore -controller.inputs.parameters.ankle_impedance.early_stance.eq_angle = -2 # type: ignore -controller.inputs.parameters.ankle_impedance.late_stance.stiffness = 79.498 # type: ignore -controller.inputs.parameters.ankle_impedance.late_stance.damping = 0.063 # type: ignore -controller.inputs.parameters.ankle_impedance.late_stance.eq_angle = -20 # type: ignore -controller.inputs.parameters.ankle_impedance.early_swing.stiffness = 7.949 # type: ignore -controller.inputs.parameters.ankle_impedance.early_swing.damping = 0 # type: ignore -controller.inputs.parameters.ankle_impedance.early_swing.eq_angle = 25 # type: ignore -controller.inputs.parameters.ankle_impedance.late_swing.stiffness = 7.949 # type: ignore -controller.inputs.parameters.ankle_impedance.late_swing.damping = 0.0 # type: ignore -controller.inputs.parameters.ankle_impedance.late_swing.eq_angle = 15 # type: ignore +controller.inputs.parameters.knee_impedance.early_stance.stiffness = 99.372 +controller.inputs.parameters.knee_impedance.early_stance.damping = 3.180 +controller.inputs.parameters.knee_impedance.early_stance.eq_angle = 5 +controller.inputs.parameters.knee_impedance.late_stance.stiffness = 99.372 +controller.inputs.parameters.knee_impedance.late_stance.damping = 1.272 +controller.inputs.parameters.knee_impedance.late_stance.eq_angle = 8 +controller.inputs.parameters.knee_impedance.early_swing.stiffness = 39.746 +controller.inputs.parameters.knee_impedance.early_swing.damping = 0.063 +controller.inputs.parameters.knee_impedance.early_swing.eq_angle = 60 +controller.inputs.parameters.knee_impedance.late_swing.stiffness = 15.899 +controller.inputs.parameters.knee_impedance.late_swing.damping = 3.186 +controller.inputs.parameters.knee_impedance.late_swing.eq_angle = 5 +controller.inputs.parameters.ankle_impedance.early_stance.stiffness = 19.874 +controller.inputs.parameters.ankle_impedance.early_stance.damping = 0 +controller.inputs.parameters.ankle_impedance.early_stance.eq_angle = -2 +controller.inputs.parameters.ankle_impedance.late_stance.stiffness = 79.498 +controller.inputs.parameters.ankle_impedance.late_stance.damping = 0.063 +controller.inputs.parameters.ankle_impedance.late_stance.eq_angle = -20 +controller.inputs.parameters.ankle_impedance.early_swing.stiffness = 7.949 +controller.inputs.parameters.ankle_impedance.early_swing.damping = 0 +controller.inputs.parameters.ankle_impedance.early_swing.eq_angle = 25 +controller.inputs.parameters.ankle_impedance.late_swing.stiffness = 7.949 +controller.inputs.parameters.ankle_impedance.late_swing.damping = 0.0 +controller.inputs.parameters.ankle_impedance.late_swing.eq_angle = 15 # Configure state machine body_weight = 82 # kg -controller.inputs.parameters.body_weight = body_weight # type: ignore -controller.inputs.parameters.transition_parameters.min_time_in_state = 0.20 # type: ignore -controller.inputs.parameters.transition_parameters.loadLStance = -body_weight * 0.25 # type: ignore -controller.inputs.parameters.transition_parameters.ankleThetaEstanceToLstance = 6.0 # type: ignore -controller.inputs.parameters.transition_parameters.loadESwing = -body_weight * 0.15 # type: ignore -controller.inputs.parameters.transition_parameters.kneeThetaESwingToLSwing = 50 # type: ignore -controller.inputs.parameters.transition_parameters.kneeDthetaESwingToLSwing = 3 # type: ignore -controller.inputs.parameters.transition_parameters.loadEStance = -body_weight * 0.4 # type: ignore -controller.inputs.parameters.transition_parameters.kneeThetaLSwingToEStance = 30 # type: ignore +controller.inputs.parameters.body_weight = body_weight +controller.inputs.parameters.transition_parameters.min_time_in_state = 0.20 +controller.inputs.parameters.transition_parameters.loadLStance = -body_weight * 0.25 +controller.inputs.parameters.transition_parameters.ankleThetaEstanceToLstance = 6.0 +controller.inputs.parameters.transition_parameters.loadESwing = -body_weight * 0.15 +controller.inputs.parameters.transition_parameters.kneeThetaESwingToLSwing = 50 +controller.inputs.parameters.transition_parameters.kneeDthetaESwingToLSwing = 3 +controller.inputs.parameters.transition_parameters.loadEStance = -body_weight * 0.4 +controller.inputs.parameters.transition_parameters.kneeThetaLSwingToEStance = 30 with knee, ankle, loadcell: knee.home() @@ -177,58 +171,41 @@ ankle.update() loadcell.update() - controller.inputs.sensors.knee_angle = ( # type: ignore - units.convert_from_default(knee.output_position, units.position.deg) + controller.inputs.sensors.knee_angle = units.convert_from_default(knee.output_position, units.position.deg) + controller.inputs.sensors.ankle_angle = units.convert_from_default(ankle.output_position, units.position.deg) + controller.inputs.sensors.knee_velocity = units.convert_from_default( + knee.output_velocity, units.velocity.deg_per_s + ) + controller.inputs.sensors.ankle_velocity = units.convert_from_default( + ankle.output_velocity, units.velocity.deg_per_s ) - controller.inputs.sensors.ankle_angle = units.convert_from_default(ankle.output_position, units.position.deg) # type: ignore - controller.inputs.sensors.knee_velocity = units.convert_from_default(knee.output_velocity, units.velocity.deg_per_s) # type: ignore - controller.inputs.sensors.ankle_velocity = units.convert_from_default(ankle.output_velocity, units.velocity.deg_per_s) # type: ignore - controller.inputs.sensors.Fz = loadcell.fz # type: ignore + controller.inputs.sensors.Fz = loadcell.fz # Update any control inputs that change every loop - controller.inputs.time = t # type: ignore + controller.inputs.time = t # Call the controller outputs = controller.run() # Test print to ensure external library call works print( - "Current time in state {}: {:.2f} seconds, Knee Eq {:.2f}, Ankle Eq {:.2f}, Fz {:.2f}".format( - outputs.current_state, - outputs.time_in_current_state, - outputs.knee_impedance.eq_angle, - outputs.ankle_impedance.eq_angle, - loadcell.fz, - ), + f"Current time in state {outputs.current_state}: {outputs.time_in_current_state:.2f} seconds, \ + Knee Eq {outputs.knee_impedance.eq_angle:.2f}, \ + Ankle Eq {outputs.ankle_impedance.eq_angle:.2f}, \ + Fz {loadcell.fz:.2f}", end="\r", ) # Write to the hardware knee.set_output_impedance( - k=units.convert_to_default( - outputs.knee_impedance.stiffness, units.stiffness.N_m_per_rad - ), - b=units.convert_to_default( - outputs.knee_impedance.damping, units.damping.N_m_per_rad_per_s - ), - ) - knee.set_output_position( - value=units.convert_to_default( - outputs.knee_impedance.eq_angle, units.position.deg - ) + k=units.convert_to_default(outputs.knee_impedance.stiffness, units.stiffness.N_m_per_rad), + b=units.convert_to_default(outputs.knee_impedance.damping, units.damping.N_m_per_rad_per_s), ) + knee.set_output_position(value=units.convert_to_default(outputs.knee_impedance.eq_angle, units.position.deg)) ankle.set_output_impedance( - k=units.convert_to_default( - outputs.ankle_impedance.stiffness, units.stiffness.N_m_per_rad - ), - b=units.convert_to_default( - outputs.ankle_impedance.damping, units.damping.N_m_per_rad_per_s - ), - ) - ankle.set_output_position( - value=units.convert_to_default( - outputs.ankle_impedance.eq_angle, units.position.deg - ) + k=units.convert_to_default(outputs.ankle_impedance.stiffness, units.stiffness.N_m_per_rad), + b=units.convert_to_default(outputs.ankle_impedance.damping, units.damping.N_m_per_rad_per_s), ) + ankle.set_output_position(value=units.convert_to_default(outputs.ankle_impedance.eq_angle, units.position.deg)) print("\n") diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..cbb96a6d --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,54 @@ +site_name: opensourceleg +repo_url: https://github.com/neurobionics/opensourceleg +site_url: https://neurobionics.github.io/opensourceleg +site_description: An open-source software library for numerical computation, data acquisition, and control of lower-limb robotic prostheses. +site_author: Open-Source Leg +edit_uri: edit/main/docs/ +repo_name: neurobionics/opensourceleg +copyright: Maintained by Senthur Ayyappan. + +nav: + - Overview: index.md + - Modules: modules.md +plugins: + - search + - mkdocstrings: + handlers: + python: + setup_commands: + - import sys + - sys.path.append('../') +theme: + name: material + feature: + tabs: true + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: white + accent: deep orange + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: deep orange + toggle: + icon: material/brightness-4 + name: Switch to light mode + icon: + repo: fontawesome/brands/github + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/neurobionics/opensourceleg + - icon: fontawesome/brands/python + link: https://pypi.org/project/opensourceleg + +markdown_extensions: + - toc: + permalink: true + - pymdownx.arithmatex: + generic: true diff --git a/opensourceleg/__init__.py b/opensourceleg/__init__.py index 34c80411..12e3b6e6 100644 --- a/opensourceleg/__init__.py +++ b/opensourceleg/__init__.py @@ -1,6 +1,8 @@ -"""An open-source software library for numerical computation, data acquisition, and control of lower-limb robotic prostheses.""" +""" +An open-source software library for numerical computation, data acquisition, +and control of lower-limb robotic prostheses. +""" -import sys from importlib import metadata as importlib_metadata diff --git a/opensourceleg/actuators/README.md b/opensourceleg/actuators/README.md index fcfb8199..49ded55e 100644 --- a/opensourceleg/actuators/README.md +++ b/opensourceleg/actuators/README.md @@ -48,22 +48,22 @@ The documentation mainly focus on how to develop a custom **actuator module**. S pass ``` - Index from `class ControlModesBase` is needed if constructing the Actuator Object from `base.ActuatorBase`. For example: - ```Python - @dataclass(init=False) - class ActuatorControlModes(ControlModesBase): + Index from `class ControlModesBase` is needed if constructing the Actuator Object from `base.ActuatorBase`. For example: - def __init__(self, actuator: "Actuator") -> None: + ```Python + @dataclass(init=False) + class ActuatorControlModes(ControlModesBase): + + def __init__(self, actuator: "Actuator") -> None: - self.VOLTAGE = VoltageMode(actuator=actuator) - self.CURRENT = CurrentMode(actuator=actuator) - self.POSITION = PositionMode(actuator=actuator) - self.IMPEDANCE = ImpedanceMode(actuator=actuator) + self.VOLTAGE = VoltageMode(actuator=actuator) + self.CURRENT = CurrentMode(actuator=actuator) + self.POSITION = PositionMode(actuator=actuator) + self.IMPEDANCE = ImpedanceMode(actuator=actuator) ``` 3. Customize the actuator object from `base.ActuatorBase` - ```Python class Motor(base.ActuatorBase): @@ -132,7 +132,6 @@ The documentation mainly focus on how to develop a custom **actuator module**. S ![](./images/Class%20Diagram%20Base%20Lib.svg) - ### [DephyActpack](./dephy.py) Support #### Known Issues @@ -141,7 +140,7 @@ The documentation mainly focus on how to develop a custom **actuator module**. S ### [Moteus Controller](./moteus.py) Support -#### Known Issues [Check [Issue Page](https://github.com/neurobionics/opensourceleg/issues) for Details] +#### Known Issues [Check [Issue Page](https://github.com/neurobionics/opensourceleg/issues) for Details] - Missing Joint Properties (e.g. `homing`) diff --git a/opensourceleg/actuators/__init__.py b/opensourceleg/actuators/__init__.py index e69de29b..7806a20b 100644 --- a/opensourceleg/actuators/__init__.py +++ b/opensourceleg/actuators/__init__.py @@ -0,0 +1,3 @@ +from .base import * # noqa: F403 +from .decorators import * # noqa: F403 +from .dephy import * # noqa: F403 diff --git a/opensourceleg/actuators/base.py b/opensourceleg/actuators/base.py index 248554e8..5efd2e62 100644 --- a/opensourceleg/actuators/base.py +++ b/opensourceleg/actuators/base.py @@ -1,23 +1,19 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from enum import Enum +from functools import partial from typing import ( Any, Callable, - Dict, - List, NamedTuple, Optional, Protocol, - Set, TypeVar, Union, cast, runtime_checkable, ) -from abc import ABC, abstractmethod -from dataclasses import dataclass -from enum import Enum -from functools import partial - import numpy as np from opensourceleg.logging.logger import LOGGER @@ -34,11 +30,9 @@ class MOTOR_CONSTANTS: MAX_CASE_TEMPERATURE: float MAX_WINDING_TEMPERATURE: float - def __post_init__(self): + def __post_init__(self) -> None: if any(x <= 0 for x in self.__dict__.values()): - raise ValueError( - "All values in MOTOR_CONSTANTS must be non-zero and positive." - ) + raise ValueError("All values in MOTOR_CONSTANTS must be non-zero and positive.") @property def RAD_PER_COUNT(self) -> float: @@ -113,15 +107,15 @@ class MethodWithRequiredModes(Protocol): _required_modes: set[CONTROL_MODES] -def requires(*modes: CONTROL_MODES): +def requires(*modes: CONTROL_MODES) -> Callable[[T], T]: def decorator(func: T) -> T: if not all(isinstance(mode, CONTROL_MODES) for mode in modes): raise TypeError("All arguments to 'requires' must be of type CONTROL_MODES") if not hasattr(func, "_required_modes"): - setattr(func, "_required_modes", set(modes)) + func._required_modes = set(modes) # type: ignore[attr-defined] else: - getattr(func, "_required_modes").update(modes) + func._required_modes.update(modes) return func @@ -136,8 +130,8 @@ def __init__( motor_constants: MOTOR_CONSTANTS, frequency: int = 1000, offline: bool = False, - *args, - **kwargs, + *args: Any, + **kwargs: Any, ) -> None: self._MOTOR_CONSTANTS: MOTOR_CONSTANTS = motor_constants self._gear_ratio: float = gear_ratio @@ -164,36 +158,32 @@ def __init__( self._set_original_methods() self._set_mutated_methods() - def __enter__(self): + def __enter__(self) -> "ActuatorBase": self.start() return self - def __exit__(self, exc_type, exc_value, exc_traceback): + def __exit__(self, exc_type: Any, exc_value: Any, exc_traceback: Any) -> None: self.stop() - def _restricted_method(self, method_name: str, *args, **kwargs): + def _restricted_method(self, method_name: str, *args: Any, **kwargs: Any) -> None: LOGGER.error(f"{method_name}() is not available in {self._mode.name} mode.") return None - def _set_original_methods(self): + def _set_original_methods(self) -> None: for method_name in CONTROL_MODE_METHODS: try: method = getattr(self, method_name) if callable(method) and hasattr(method, "_required_modes"): self._original_methods[method_name] = method except AttributeError: - LOGGER.debug( - msg=f"[{self.tag}] {method_name}() is not implemented in {self.tag}." - ) + LOGGER.debug(msg=f"[{self.tag}] {method_name}() is not implemented in {self.tag}.") - def _set_mutated_methods(self): + def _set_mutated_methods(self) -> None: for method_name, method in self._original_methods.items(): if self._mode in method._required_modes: setattr(self, method_name, method) else: - setattr( - self, method_name, partial(self._restricted_method, method_name) - ) + setattr(self, method_name, partial(self._restricted_method, method_name)) @property @abstractmethod @@ -212,16 +202,13 @@ def stop(self) -> None: def update(self) -> None: pass - def _get_control_mode_config( - self, mode: CONTROL_MODES - ) -> Optional[ControlModeConfig]: + def _get_control_mode_config(self, mode: CONTROL_MODES) -> Optional[ControlModeConfig]: return cast( Optional[ControlModeConfig], getattr(self._CONTROL_MODE_CONFIGS, mode.name), ) def set_control_mode(self, mode: CONTROL_MODES) -> None: - if self.mode == mode: LOGGER.debug(msg=f"[{self.tag}] Already in {self.mode.name} control mode.") return @@ -281,9 +268,7 @@ def set_position_gains(self, kp: float, ki: float, kd: float, ff: float) -> None @abstractmethod @requires(CONTROL_MODES.IMPEDANCE) - def set_impedance_gains( - self, kp: float, ki: float, kd: float, k: float, b: float, ff: float - ) -> None: + def set_impedance_gains(self, kp: float, ki: float, kd: float, k: float, b: float, ff: float) -> None: pass @abstractmethod diff --git a/opensourceleg/actuators/decorators.py b/opensourceleg/actuators/decorators.py index 19a716d8..df0de834 100644 --- a/opensourceleg/actuators/decorators.py +++ b/opensourceleg/actuators/decorators.py @@ -1,4 +1,5 @@ from functools import wraps +from typing import Any, Callable from opensourceleg.actuators.base import ActuatorBase from opensourceleg.logging.exceptions import ( @@ -7,9 +8,9 @@ ) -def check_actuator_connection(func): +def check_actuator_connection(func: Callable) -> Callable: @wraps(func) - def wrapper(self: ActuatorBase, *args, **kwargs): + def wrapper(self: ActuatorBase, *args: Any, **kwargs: Any) -> Any: if self.is_offline: raise ActuatorConnectionException(tag=self.tag) @@ -18,9 +19,9 @@ def wrapper(self: ActuatorBase, *args, **kwargs): return wrapper -def check_actuator_open(func): +def check_actuator_open(func: Callable) -> Callable: @wraps(func) - def wrapper(self: ActuatorBase, *args, **kwargs): + def wrapper(self: ActuatorBase, *args: Any, **kwargs: Any) -> Any: if not self.is_open: raise ActuatorConnectionException(tag=self.tag) @@ -29,9 +30,9 @@ def wrapper(self: ActuatorBase, *args, **kwargs): return wrapper -def check_actuator_stream(func): +def check_actuator_stream(func: Callable) -> Callable: @wraps(func) - def wrapper(self: ActuatorBase, *args, **kwargs): + def wrapper(self: ActuatorBase, *args: Any, **kwargs: Any) -> Any: if not self.is_streaming: raise ActuatorStreamException(tag=self.tag) diff --git a/opensourceleg/actuators/dephy.py b/opensourceleg/actuators/dephy.py index a194522c..30fd18e5 100644 --- a/opensourceleg/actuators/dephy.py +++ b/opensourceleg/actuators/dephy.py @@ -1,10 +1,7 @@ -from typing import Any, Callable, Union, overload - import os import time from ctypes import c_int -from dataclasses import dataclass -from unittest.mock import Mock +from typing import Optional import numpy as np from flexsea.device import Device @@ -24,9 +21,7 @@ ) from opensourceleg.logging import LOGGER from opensourceleg.logging.decorators import ( - deprecated, deprecated_with_routing, - deprecated_with_suggestion, ) from opensourceleg.math import ThermalModel from opensourceleg.safety import ThermalLimitException @@ -124,7 +119,7 @@ def _dephy_impedance_mode_exit(dephy_actuator: "DephyActuator") -> None: ) -class DephyActuator(Device, ActuatorBase): +class DephyActuator(Device, ActuatorBase): # type: ignore[no-any-unimported] def __init__( self, tag: str = "DephyActuator", @@ -185,10 +180,11 @@ def start(self) -> None: try: self.open() self._is_open = True - except OSError as e: + except OSError: print("\n") LOGGER.error( - msg=f"[{self.__repr__()}] Need admin previleges to open the port '{self.port}'. \n\nPlease run the script with 'sudo' command or add the user to the dialout group.\n" + msg=f"[{self.__repr__()}] Need admin previleges to open the port '{self.port}'. \n\n \ + Please run the script with 'sudo' command or add the user to the dialout group.\n" ) os._exit(status=1) @@ -202,7 +198,6 @@ def start(self) -> None: @check_actuator_stream @check_actuator_open def stop(self) -> None: - self.stop_motor() self.set_control_mode(mode=CONTROL_MODES.IDLE) self._is_streaming = False @@ -210,7 +205,6 @@ def stop(self) -> None: self.close() def update(self) -> None: - self._data = self.read() self._thermal_model.T_c = self.case_temperature @@ -220,14 +214,16 @@ def update(self) -> None: ) if self.case_temperature >= self.max_case_temperature: LOGGER.error( - msg=f"[{str.upper(self.tag)}] Case thermal limit {self.max_case_temperature} reached. Stopping motor." + msg=f"[{str.upper(self.tag)}] Case thermal limit {self.max_case_temperature} reached. \ + Stopping motor." ) # self.stop() raise ThermalLimitException() if self.winding_temperature >= self.max_winding_temperature: LOGGER.error( - msg=f"[{str.upper(self.tag)}] Winding thermal limit {self.max_winding_temperature} reached. Stopping motor." + msg=f"[{str.upper(self.tag)}] Winding thermal limit {self.max_winding_temperature} reached. \ + Stopping motor." ) raise ThermalLimitException() # Check for thermal fault, bit 2 of the execute status byte @@ -238,7 +234,7 @@ def update(self) -> None: def home( self, homing_voltage: int = 2000, - homing_frequency: int = None, + homing_frequency: Optional[int] = None, homing_direction: int = -1, joint_direction: int = -1, joint_position_offset: float = 0.0, @@ -259,20 +255,18 @@ def home( joint_direction (int): Direction to move the joint during homing. Default is -1. joint_position_offset (float): Offset in radians to add to the joint position. Default is 0.0. motor_position_offset (float): Offset in radians to add to the motor position. Default is 0.0. - current_threshold (int): Current threshold in mA to stop homing the joint or actuator. This is used to detect if the actuator or joint has hit a hard stop. Default is 5000 mA. - velocity_threshold (float): Velocity threshold in rad/s to stop homing the joint or actuator. This is also used to detect if the actuator or joint has hit a hard stop. Default is 0.001 rad/s. + current_threshold (int): Current threshold in mA to stop homing the joint or actuator. + This is used to detect if the actuator or joint has hit a hard stop. Default is 5000 mA. + velocity_threshold (float): Velocity threshold in rad/s to stop homing the joint or actuator. + This is also used to detect if the actuator or joint has hit a hard stop. Default is 0.001 rad/s. """ is_homing = True - homing_frequency = ( - homing_frequency if homing_frequency is not None else self.frequency - ) + homing_frequency = homing_frequency if homing_frequency is not None else self.frequency self.set_control_mode(mode=CONTROL_MODES.VOLTAGE) - self.set_motor_voltage( - value=homing_direction * homing_voltage - ) # mV, negative for counterclockwise + self.set_motor_voltage(value=homing_direction * homing_voltage) # mV, negative for counterclockwise _motor_encoder_array = [] _joint_encoder_array = [] @@ -287,10 +281,7 @@ def home( _motor_encoder_array.append(self.motor_position) _joint_encoder_array.append(self.joint_position) - if ( - abs(self.output_velocity) <= velocity_threshold - or abs(self.motor_current) >= current_threshold - ): + if abs(self.output_velocity) <= velocity_threshold or abs(self.motor_current) >= current_threshold: self.set_motor_voltage(value=0) is_homing = False @@ -319,10 +310,11 @@ def home( self.set_encoder_map(np.polynomial.polynomial.Polynomial(coef=coefficients)) else: LOGGER.debug( - msg=f"[{self.__repr__()}] No encoder map found. Please call the make_encoder_map method to create one. The encoder map is used to estimate joint position more accurately." + msg=f"[{self.__repr__()}] No encoder map found. Please call the make_encoder_map method \ + to create one. The encoder map is used to estimate joint position more accurately." ) - def make_encoder_map(self, overwrite=False) -> None: + def make_encoder_map(self, overwrite: bool = False) -> None: """ This method makes a lookup table to calculate the position measured by the joint encoder. This is necessary because the magnetic output encoders are nonlinear. @@ -339,16 +331,12 @@ def make_encoder_map(self, overwrite=False) -> None: """ if not self.is_homed: - LOGGER.warning( - msg=f"[{self.__repr__()}] Please home the {self.tag} joint before making the encoder map." - ) - return + LOGGER.warning(msg=f"[{self.__repr__()}] Please home the {self.tag} joint before making the encoder map.") + return None if os.path.exists(f"./{self.tag}_encoder_map.npy") and not overwrite: - LOGGER.info( - msg=f"[{self.__repr__()}] Encoder map exists. Skipping encoder map creation." - ) - return + LOGGER.info(msg=f"[{self.__repr__()}] Encoder map exists. Skipping encoder map creation.") + return None self.set_control_mode(mode=CONTROL_MODES.CURRENT) self.set_current_gains() @@ -363,7 +351,8 @@ def make_encoder_map(self, overwrite=False) -> None: _output_position_array = [] LOGGER.info( - msg=f"[{self.__repr__()}] Please manually move the {self.tag} joint numerous times through its full range of motion for 10 seconds. \n{input('Press any key when you are ready to start.')}" + msg=f"[{self.__repr__()}] Please manually move the {self.tag} joint numerous times through \ + its full range of motion for 10 seconds. \n{input('Press any key when you are ready to start.')}" ) _start_time: float = time.time() @@ -371,7 +360,8 @@ def make_encoder_map(self, overwrite=False) -> None: try: while time.time() - _start_time < 10: LOGGER.info( - msg=f"[{self.__repr__()}] Mapping the {self.tag} joint encoder: {10 - time.time() + _start_time} seconds left." + msg=f"[{self.__repr__()}] Mapping the {self.tag} \ + joint encoder: {10 - time.time() + _start_time} seconds left." ) self.update() _joint_encoder_array.append(self.joint_encoder_counts) @@ -380,11 +370,9 @@ def make_encoder_map(self, overwrite=False) -> None: except KeyboardInterrupt: LOGGER.warning(msg="Encoder map interrupted.") - return + return None - LOGGER.info( - msg=f"[{self.__repr__()}] You may now stop moving the {self.tag} joint." - ) + LOGGER.info(msg=f"[{self.__repr__()}] You may now stop moving the {self.tag} joint.") _power = np.arange(4.0) _a_mat = np.array(_joint_encoder_array).reshape(-1, 1) ** _power @@ -394,9 +382,7 @@ def make_encoder_map(self, overwrite=False) -> None: self.set_encoder_map(np.polynomial.polynomial.Polynomial(coef=_coeffs)) np.save(file=f"./{self.tag}_encoder_map.npy", arr=_coeffs) - LOGGER.info( - msg=f"[{self.__repr__()}] Encoder map saved to './{self.tag}_encoder_map.npy'." - ) + LOGGER.info(msg=f"[{self.__repr__()}] Encoder map saved to './{self.tag}_encoder_map.npy'.") def set_motor_torque(self, value: float) -> None: """ @@ -432,7 +418,7 @@ def set_output_torque(self, value: float) -> None: def set_motor_current( self, value: float, - ): + ) -> None: """ Sets the motor current in mA. @@ -456,7 +442,6 @@ def set_motor_voltage(self, value: float) -> None: @deprecated_with_routing(alternative_func=set_motor_voltage) def set_voltage(self, value: float) -> None: - self.command_motor_voltage(value=int(value)) def set_motor_position(self, value: float) -> None: @@ -469,8 +454,7 @@ def set_motor_position(self, value: float) -> None: """ self.command_motor_position( value=int( - (value + self.motor_zero_position + self.motor_position_offset) - / self.MOTOR_CONSTANTS.RAD_PER_COUNT + (value + self.motor_zero_position + self.motor_position_offset) / self.MOTOR_CONSTANTS.RAD_PER_COUNT ), ) @@ -617,19 +601,17 @@ def set_motor_impedance( ff=ff, ) - def set_encoder_map(self, encoder_map) -> None: + def set_encoder_map(self, encoder_map: np.polynomial.polynomial.Polynomial) -> None: """Sets the joint encoder map""" - self._encoder_map = encoder_map + self._encoder_map: np.polynomial.polynomial.Polynomial = encoder_map @property - def encoder_map(self): + def encoder_map(self) -> Optional[np.polynomial.polynomial.Polynomial]: """Polynomial coefficients defining the joint encoder map from counts to radians.""" if getattr(self, "_encoder_map", None) is not None: return self._encoder_map else: - LOGGER.warning( - msg="Encoder map is not set. Please call the make_encoder_map method to create one." - ) + LOGGER.warning(msg="Encoder map is not set. Please call the make_encoder_map method to create one.") return None @property @@ -795,7 +777,7 @@ def winding_temperature(self) -> float: return 0.0 @property - def genvars(self): + def genvars(self) -> np.ndarray: """Dephy's 'genvars' object.""" if self._data is not None: return np.array( @@ -902,58 +884,42 @@ def gyroz(self) -> float: def thermal_scaling_factor(self) -> float: """ Scale factor to use in torque control, in [0,1]. - If you scale the torque command by this factor, the motor temperature will never exceed max allowable temperature. - For a proof, see paper referenced in thermal model. + If you scale the torque command by this factor, the motor temperature will never + exceed max allowable temperature. For a proof, see paper referenced in thermal model. """ return self._thermal_scale def _dephy_legacy_voltage_mode_entry(dephy_actuator: "DephyActuator") -> None: - LOGGER.debug( - msg=f"[{dephy_actuator.tag}] Entering {CONTROL_MODES.VOLTAGE.name} control mode." - ) + LOGGER.debug(msg=f"[{dephy_actuator.tag}] Entering {CONTROL_MODES.VOLTAGE.name} control mode.") def _dephy_legacy_current_mode_entry(dephy_actuator: "DephyActuator") -> None: - LOGGER.debug( - msg=f"[{dephy_actuator.tag}] Entering {CONTROL_MODES.CURRENT.name} control mode." - ) + LOGGER.debug(msg=f"[{dephy_actuator.tag}] Entering {CONTROL_MODES.CURRENT.name} control mode.") def _dephy_legacy_position_mode_entry(dephy_actuator: "DephyActuator") -> None: - LOGGER.debug( - msg=f"[{dephy_actuator.tag}] Entering {CONTROL_MODES.POSITION.name} control mode." - ) + LOGGER.debug(msg=f"[{dephy_actuator.tag}] Entering {CONTROL_MODES.POSITION.name} control mode.") def _dephy_legacy_impedance_mode_entry(dephy_actuator: "DephyActuator") -> None: - LOGGER.debug( - msg=f"[{dephy_actuator.tag}] Entering {CONTROL_MODES.IMPEDANCE.name} control mode." - ) + LOGGER.debug(msg=f"[{dephy_actuator.tag}] Entering {CONTROL_MODES.IMPEDANCE.name} control mode.") def _dephy_legacy_voltage_mode_exit(dephy_actuator: "DephyActuator") -> None: - LOGGER.debug( - msg=f"[{dephy_actuator.tag}] Exiting {CONTROL_MODES.VOLTAGE.name} control mode." - ) + LOGGER.debug(msg=f"[{dephy_actuator.tag}] Exiting {CONTROL_MODES.VOLTAGE.name} control mode.") def _dephy_legacy_current_mode_exit(dephy_actuator: "DephyActuator") -> None: - LOGGER.debug( - msg=f"[{dephy_actuator.tag}] Exiting {CONTROL_MODES.CURRENT.name} control mode." - ) + LOGGER.debug(msg=f"[{dephy_actuator.tag}] Exiting {CONTROL_MODES.CURRENT.name} control mode.") def _dephy_legacy_position_mode_exit(dephy_actuator: "DephyActuator") -> None: - LOGGER.debug( - msg=f"[{dephy_actuator.tag}] Exiting {CONTROL_MODES.POSITION.name} control mode." - ) + LOGGER.debug(msg=f"[{dephy_actuator.tag}] Exiting {CONTROL_MODES.POSITION.name} control mode.") def _dephy_legacy_impedance_mode_exit(dephy_actuator: "DephyActuator") -> None: - LOGGER.debug( - msg=f"[{dephy_actuator.tag}] Exiting {CONTROL_MODES.IMPEDANCE.name} control mode." - ) + LOGGER.debug(msg=f"[{dephy_actuator.tag}] Exiting {CONTROL_MODES.IMPEDANCE.name} control mode.") DEPHY_LEGACY_CONTROL_MODE_CONFIGS = CONTROL_MODE_CONFIGS( @@ -1013,7 +979,6 @@ def __init__( self._is_streaming: bool = False self._is_open: bool = False else: - # def set_is_streaming(self, value): # self._is_streaming = value @@ -1050,10 +1015,11 @@ def start(self) -> None: log_level=self._debug_level, log_enabled=self._dephy_log, ) - except OSError as e: + except OSError: print("\n") LOGGER.error( - msg=f"[{self.__repr__()}] Need admin previleges to open the port '{self.port}'. \n\nPlease run the script with 'sudo' command or add the user to the dialout group.\n" + msg=f"[{self.__repr__()}] Need admin previleges to open the port '{self.port}'. \n\n \ + Please run the script with 'sudo' command or add the user to the dialout group.\n" ) os._exit(status=1) @@ -1074,7 +1040,6 @@ def stop(self) -> None: self.close() def update(self) -> None: - self._data = self.read() self._thermal_model.T_c = self.case_temperature @@ -1084,27 +1049,30 @@ def update(self) -> None: ) if self.case_temperature >= self.max_case_temperature: LOGGER.error( - msg=f"[{str.upper(self.tag)}] Case thermal limit {self.max_case_temperature} reached. Stopping motor." + msg=f"[{str.upper(self.tag)}] Case thermal limit {self.max_case_temperature} reached. \ + Stopping motor." ) raise ThermalLimitException() if self.winding_temperature >= self.max_winding_temperature: LOGGER.error( - msg=f"[{str.upper(self.tag)}] Winding thermal limit {self.max_winding_temperature} reached. Stopping motor." + msg=f"[{str.upper(self.tag)}] Winding thermal limit {self.max_winding_temperature} reached. \ + Stopping motor." ) raise ThermalLimitException() # Check for thermal fault, bit 2 of the execute status byte if self._data.status_ex & 0b00000010 == 0b00000010: LOGGER.error( - msg=f"[{str.upper(self.tag)}] Thermal Fault: Winding temperature: {self.winding_temperature}; Case temperature: {self.case_temperature}." + msg=f"[{str.upper(self.tag)}] Thermal Fault: Winding temperature: {self.winding_temperature}; \ + Case temperature: {self.case_temperature}." ) raise ThermalLimitException("Internal thermal limit tripped.") def set_motor_current( self, value: float, - ): + ) -> None: """ Sets the motor current in mA. @@ -1128,7 +1096,6 @@ def set_motor_voltage(self, value: float) -> None: @deprecated_with_routing(alternative_func=set_motor_voltage) def set_voltage(self, value: float) -> None: - self.send_motor_command(ctrl_mode=c_int(self.mode.value), value=int(value)) def set_motor_position(self, value: float) -> None: @@ -1142,8 +1109,7 @@ def set_motor_position(self, value: float) -> None: self.send_motor_command( ctrl_mode=c_int(self.mode.value), value=int( - (value + self.motor_zero_position + self.motor_position_offset) - / self.MOTOR_CONSTANTS.RAD_PER_COUNT + (value + self.motor_zero_position + self.motor_position_offset) / self.MOTOR_CONSTANTS.RAD_PER_COUNT ), ) @@ -1331,7 +1297,7 @@ def winding_temperature(self) -> float: return 0.0 @property - def genvars(self): + def genvars(self) -> np.ndarray: """Dephy's 'genvars' object.""" if self._data is not None: return np.array( @@ -1421,19 +1387,19 @@ def gyroy(self) -> float: return 0.0 @property - def is_streaming(self): + def is_streaming(self) -> bool: return self._is_streaming @is_streaming.setter - def is_streaming(self, value: bool): + def is_streaming(self, value: bool) -> None: self._is_streaming = value @property - def is_open(self): + def is_open(self) -> bool: return self._is_open @is_open.setter - def is_open(self, value: bool): + def is_open(self, value: bool) -> None: self._is_open = value @property diff --git a/opensourceleg/actuators/images/Class Diagram Base Lib.svg b/opensourceleg/actuators/images/Class Diagram Base Lib.svg index 18f3613a..a41de5b3 100644 --- a/opensourceleg/actuators/images/Class Diagram Base Lib.svg +++ b/opensourceleg/actuators/images/Class Diagram Base Lib.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/opensourceleg/actuators/moteus.py b/opensourceleg/actuators/moteus.py index be253123..efff6c24 100644 --- a/opensourceleg/actuators/moteus.py +++ b/opensourceleg/actuators/moteus.py @@ -1,14 +1,10 @@ -from typing import Any, Union - import math import os -import time -from dataclasses import dataclass +from typing import ClassVar, Optional import numpy as np -from moteus import Command, Controller +from moteus import Command, Controller, Stream from moteus import Register as MoteusRegister -from moteus import Stream from moteus import multiplex as mp from opensourceleg.actuators.base import ( @@ -56,7 +52,6 @@ class MoteusQueryResolution: - mode = mp.INT8 position = mp.F32 velocity = mp.F32 @@ -79,7 +74,7 @@ class MoteusQueryResolution: aux1_gpio = mp.IGNORE aux2_gpio = mp.IGNORE - _extra = { + _extra: ClassVar = { MoteusRegister.COMMAND_POSITION: mp.F32, MoteusRegister.COMMAND_VELOCITY: mp.F32, MoteusRegister.COMMAND_FEEDFORWARD_TORQUE: mp.F32, @@ -139,11 +134,10 @@ def __init__(self): pass def __repr__(self): - return f"MoteusInterface" + return "MoteusInterface" def _add2map(self, servo_id, bus_id) -> None: - - if bus_id in self.bus_map.keys(): + if bus_id in self.bus_map: self.bus_map[bus_id].append(servo_id) else: self.bus_map[bus_id] = [servo_id] @@ -173,8 +167,11 @@ def __init__( gear_ratio: float = 1.0, frequency: int = 500, offline: bool = False, - query: MoteusQueryResolution = MoteusQueryResolution(), + query: Optional[MoteusQueryResolution] = None, ) -> None: + if query is None: + query = MoteusQueryResolution() + self._servo_id = servo_id self._bus_id = bus_id super().__init__( @@ -227,10 +224,11 @@ async def start(self) -> None: self._is_open = True self._is_streaming = True - except OSError as e: + except OSError: print("\n") LOGGER.error( - msg=f"[{self.__repr__()}] Need admin previleges to open the port. \n\nPlease run the script with 'sudo' command or add the user to the dialout group.\n" + msg=f"[{self.__repr__()}] Need admin previleges to open the port. \n\n \ + Please run the script with 'sudo' command or add the user to the dialout group.\n" ) os._exit(status=1) @@ -239,9 +237,7 @@ async def start(self) -> None: default_mode_config.entry_callback(self) if (await self._interface.transport.cycle([self.make_stop(query=True)])) == []: - LOGGER.error( - msg=f"[{self.__repr__()}] Could not start the actuator. Please check the connection." - ) + LOGGER.error(msg=f"[{self.__repr__()}] Could not start the actuator. Please check the connection.") self._is_streaming = False self._is_open = False # Keep the default command as query -- reading sensor data @@ -256,7 +252,6 @@ async def stop(self) -> None: self._command = self.make_query() async def update(self): - self._data = await self._interface.transport.cycle([self._command]) self._thermal_model.T_c = self.case_temperature @@ -272,13 +267,14 @@ async def update(self): if self.winding_temperature >= self.max_winding_temperature: LOGGER.error( - msg=f"[{str.upper(self.tag)}] Winding thermal limit {self.max_winding_temperature} reached. Stopping motor." + msg=f"[{str.upper(self.tag)}] Winding thermal limit {self.max_winding_temperature} reached. \ + Stopping motor." ) raise ThermalLimitException() self._command = self.make_query() - def home(self): + def home(self) -> None: # TODO: implement homing LOGGER.info(msg=f"[{self.__repr__()}] Homing not implemented.") @@ -313,16 +309,13 @@ def set_joint_torque(self, value: float) -> None: def set_motor_current( self, value: float, - ): - LOGGER.info(f"Current Mode Not Implemented") + ) -> None: + LOGGER.info("Current Mode Not Implemented") def set_motor_velocity(self, value: float) -> None: self._command = self.make_position( position=math.nan, - velocity=value - / ( - np.pi * 2 - ), # TODO: Verify this conversion, are we converting from rad/s to rev/s? + velocity=value / (np.pi * 2), # TODO: Verify this conversion, are we converting from rad/s to rev/s? query=True, watchdog_timeout=math.nan, ) @@ -334,7 +327,7 @@ def set_motor_voltage(self, value: float) -> None: Args: value (float): The voltage to set in mV. """ - LOGGER.info(f"Voltage Mode Not Implemented") + LOGGER.info("Voltage Mode Not Implemented") def set_motor_position(self, value: float) -> None: """ @@ -345,9 +338,7 @@ def set_motor_position(self, value: float) -> None: value (float): The position to set """ self._command = self.make_position( - position=float( - (value) / (2 * np.pi) - ), # TODO: Verify this conversion, are we converting from rad to rev? + position=float((value) / (2 * np.pi)), # TODO: Verify this conversion, are we converting from rad to rev? query=True, watchdog_timeout=math.nan, ) @@ -460,10 +451,7 @@ def motor_current(self) -> float: @property def motor_torque(self) -> float: if self._data is not None: - return ( - float(self.motor_current * self.MOTOR_CONSTANTS.NM_PER_MILLIAMP) - / self.gear_ratio - ) + return float(self.motor_current * self.MOTOR_CONSTANTS.NM_PER_MILLIAMP) / self.gear_ratio else: LOGGER.warning( msg="Actuator data is none, please ensure that the actuator is connected and streaming. Returning 0.0." @@ -549,7 +537,8 @@ def winding_temperature(self) -> float: def thermal_scaling_factor(self) -> float: """ Scale factor to use in torque control, in [0,1]. - If you scale the torque command by this factor, the motor temperature will never exceed max allowable temperature. + If you scale the torque command by this factor, the motor temperature will + never exceed max allowable temperature. For a proof, see paper referenced in thermal model. """ return self._thermal_scale diff --git a/opensourceleg/actuators/tmotor.py b/opensourceleg/actuators/tmotor.py index 3482906b..b349279d 100644 --- a/opensourceleg/actuators/tmotor.py +++ b/opensourceleg/actuators/tmotor.py @@ -17,7 +17,6 @@ CONTROL_MODES, MOTOR_CONSTANTS, ActuatorBase, - ControlGains, ControlModeConfig, ) from opensourceleg.actuators.decorators import ( @@ -25,7 +24,6 @@ check_actuator_open, check_actuator_stream, ) -from opensourceleg.logging import LOGGER from opensourceleg.math import ThermalModel from opensourceleg.time import SoftRealtimeLoop @@ -179,9 +177,7 @@ def start(self): self._entered = True self.is_streaming = True if not self.check_can_connection(): - raise RuntimeError( - "Device not connected: " + str(self.device_info_string()) - ) + raise RuntimeError("Device not connected: " + str(self.device_info_string())) return self @check_actuator_stream @@ -196,9 +192,7 @@ def stop(self): def home(self): pass - # this method is called by the user to synchronize the current state used by the controller - # with the most recent message recieved - def update(self): + def update(self): # noqa: C901 """ This method is called by the user to synchronize the current state used by the controller with the most recent message recieved, as well as to send the current command. @@ -207,37 +201,29 @@ def update(self): # check that the motor is safely turned on if not self._entered: raise RuntimeError( - "Tried to update motor state before safely powering on for device: " - + self.device_info_string() + "Tried to update motor state before safely powering on for device: " + self.device_info_string() ) if self.case_temperature > self.max_temp: - raise RuntimeError( - "Temperature greater than {}C for device: {}".format( - self.max_temp, self.device_info_string() - ) - ) + raise RuntimeError(f"Temperature greater than {self.max_temp}C for device: {self.device_info_string()}") # check that the motor data is recent # print(self._command_sent) now = time.time() - if (now - self._last_command_time) < 0.25 and ( - (now - self._last_update_time) > 0.1 - ): - # print("State update requested but no data recieved from motor. Delay longer after zeroing, decrease frequency, or check connection.") + if (now - self._last_command_time) < 0.25 and ((now - self._last_update_time) > 0.1): warnings.warn( - "State update requested but no data from motor. Delay longer after zeroing, decrease frequency, or check connection. " + "State update requested but no data from motor. Delay longer after zeroing, \ + decrease frequency, or check connection. " + self.device_info_string(), RuntimeWarning, + stacklevel=2, ) else: self._command_sent = False # artificially extending the range of the position, current, and velocity that we track P_max = MIT_Params[self.type]["P_max"] + 0.01 - I_max = ( - self.TMotor_current_to_qaxis_current(MIT_Params[self.type]["T_max"]) + 1.0 - ) + I_max = self.TMotor_current_to_qaxis_current(MIT_Params[self.type]["T_max"]) + 1.0 V_max = MIT_Params[self.type]["V_max"] + 0.01 if self._old_pos is None: @@ -259,64 +245,42 @@ def update(self): actual_current = new_curr # The TMotor will wrap around to -max at the limits for all values it returns!! Account for this - if (thresh_pos <= new_pos and new_pos <= P_max) and ( - -P_max <= old_pos and old_pos <= -thresh_pos - ): + if (thresh_pos <= new_pos and new_pos <= P_max) and (-P_max <= old_pos and old_pos <= -thresh_pos): self._times_past_position_limit -= 1 - elif (thresh_pos <= old_pos and old_pos <= P_max) and ( - -P_max <= new_pos and new_pos <= -thresh_pos - ): + elif (thresh_pos <= old_pos and old_pos <= P_max) and (-P_max <= new_pos and new_pos <= -thresh_pos): self._times_past_position_limit += 1 - # current is basically the same as position, but if you instantly command a switch it can actually change fast enough - # to throw this off, so that is accounted for too. We just put a hard limit on the current to solve current jitter problems. - if (thresh_curr <= new_curr and new_curr <= I_max) and ( - -I_max <= old_curr and old_curr <= -thresh_curr - ): + # current is basically the same as position, but if you instantly + # command a switch it can actually change fast enough + # to throw this off, so that is accounted for too. We just put a hard limit on the current + # to solve current jitter problems. + if (thresh_curr <= new_curr and new_curr <= I_max) and (-I_max <= old_curr and old_curr <= -thresh_curr): # self._old_current_zone = -1 # if (thresh_curr <= curr_command and curr_command <= I_max): # self._times_past_current_limit -= 1 if curr_command > 0: - actual_current = self.TMotor_current_to_qaxis_current( - MIT_Params[self.type]["T_max"] - ) + actual_current = self.TMotor_current_to_qaxis_current(MIT_Params[self.type]["T_max"]) elif curr_command < 0: - actual_current = -self.TMotor_current_to_qaxis_current( - MIT_Params[self.type]["T_max"] - ) + actual_current = -self.TMotor_current_to_qaxis_current(MIT_Params[self.type]["T_max"]) else: - actual_current = -self.TMotor_current_to_qaxis_current( - MIT_Params[self.type]["T_max"] - ) + actual_current = -self.TMotor_current_to_qaxis_current(MIT_Params[self.type]["T_max"]) new_curr = actual_current - elif (thresh_curr <= old_curr and old_curr <= I_max) and ( - -I_max <= new_curr and new_curr <= -thresh_curr - ): + elif (thresh_curr <= old_curr and old_curr <= I_max) and (-I_max <= new_curr and new_curr <= -thresh_curr): # self._old_current_zone = 1 # if not (-I_max <= curr_command and curr_command <= -thresh_curr): # self._times_past_current_limit += 1 if curr_command > 0: - actual_current = self.TMotor_current_to_qaxis_current( - MIT_Params[self.type]["T_max"] - ) + actual_current = self.TMotor_current_to_qaxis_current(MIT_Params[self.type]["T_max"]) elif curr_command < 0: - actual_current = -self.TMotor_current_to_qaxis_current( - MIT_Params[self.type]["T_max"] - ) + actual_current = -self.TMotor_current_to_qaxis_current(MIT_Params[self.type]["T_max"]) else: - actual_current = self.TMotor_current_to_qaxis_current( - MIT_Params[self.type]["T_max"] - ) + actual_current = self.TMotor_current_to_qaxis_current(MIT_Params[self.type]["T_max"]) new_curr = actual_current # velocity should work the same as position - if (thresh_vel <= new_vel and new_vel <= V_max) and ( - -V_max <= old_vel and old_vel <= -thresh_vel - ): + if (thresh_vel <= new_vel and new_vel <= V_max) and (-V_max <= old_vel and old_vel <= -thresh_vel): self._times_past_velocity_limit -= 1 - elif (thresh_vel <= old_vel and old_vel <= V_max) and ( - -V_max <= new_vel and new_vel <= -thresh_vel - ): + elif (thresh_vel <= old_vel and old_vel <= V_max) and (-V_max <= new_vel and new_vel <= -thresh_vel): self._times_past_velocity_limit += 1 # update expanded state variables @@ -325,21 +289,12 @@ def update(self): self._old_vel = new_vel self._motor_state.set_state_obj(self._motor_state_async) - self._motor_state.position += ( - self._times_past_position_limit * 2 * MIT_Params[self.type]["P_max"] - ) + self._motor_state.position += self._times_past_position_limit * 2 * MIT_Params[self.type]["P_max"] self._motor_state.current = actual_current - self._motor_state.velocity += ( - self._times_past_velocity_limit * 2 * MIT_Params[self.type]["V_max"] - ) + self._motor_state.velocity += self._times_past_velocity_limit * 2 * MIT_Params[self.type]["V_max"] # send current motor command self._send_command() - - # # writing to log file - # if self.csv_file_name is not None: - # self.csv_writer.writerow([self._last_update_time - self._start_time] + [self.LOG_FUNCTIONS[var]() for var in self.log_vars]) - self._updated = False # sends a command to the motor depending on whats controlm mode the motor is in @@ -352,8 +307,6 @@ def _send_command(self): This allows control based on actual q-axis current, rather than estimated torque, which doesn't account for friction losses. """ - # if self._control_state == _TMotorManState.FULL_STATE: - # self._canman.MIT_controller(self.ID,self.type, self._command.position, self._command.velocity, self._command.kp, self._command.kd, self.qaxis_current_to_TMotor_current(self._command.current)) if self.mode == CONTROL_MODES.IMPEDANCE: self._canman.MIT_controller( self.ID, @@ -387,19 +340,17 @@ def _send_command(self): 0.0, ) else: - raise RuntimeError( - "UNDEFINED STATE for device " + self.device_info_string() - ) + raise RuntimeError("UNDEFINED STATE for device " + self.device_info_string()) self._last_command_time = time.time() # getters for motor state @property - def case_temperature(self): + def case_temperature(self) -> float: """ Returns: The most recently updated motor temperature in degrees C. """ - return self._motor_state.temperature + return float(self._motor_state.temperature) @property def winding_temperature(self) -> float: @@ -413,56 +364,52 @@ def winding_temperature(self) -> float: return 0.0 @property - def motor_current(self): + def motor_current(self) -> float: """ Returns: The most recently updated qaxis current in amps """ - return self._motor_state.current + return float(self._motor_state.current) @property - def motor_voltage(self): + def motor_voltage(self) -> float: # Not implemented return 0.0 @property - def output_position(self): + def output_position(self) -> float: """ Returns: The most recently updated output angle in radians """ - return self._motor_state.position + return float(self._motor_state.position) @property - def output_velocity(self): + def output_velocity(self) -> float: """ Returns: The most recently updated output velocity in radians per second """ - return self._motor_state.velocity + return float(self._motor_state.velocity) @property - def output_acceleration(self): + def output_acceleration(self) -> float: """ Returns: The most recently updated output acceleration in radians per second per second """ - return self._motor_state.acceleration + return float(self._motor_state.acceleration) @property - def output_torque(self): + def output_torque(self) -> float: """ Returns: the most recently updated output torque in Nm """ - return ( - self.motor_current - * MIT_Params[self.type]["Kt_actual"] - * MIT_Params[self.type]["GEAR_RATIO"] - ) + return float(self.motor_current * MIT_Params[self.type]["Kt_actual"] * MIT_Params[self.type]["GEAR_RATIO"]) # uses plain impedance mode, will send 0.0 for current command. - def set_impedance_gains(self, kp=0, ki=0, K=0.08922, B=0.0038070, ff=0): + def set_impedance_gains(self, kp=0, ki=0, K=0.08922, B=0.0038070, ff=0) -> None: """ Uses plain impedance mode, will send 0.0 for current command in addition to position request. @@ -473,21 +420,23 @@ def set_impedance_gains(self, kp=0, ki=0, K=0.08922, B=0.0038070, ff=0): B: The damping in Nm/(rad/s) ff: A dummy argument for backward compatibility with the dephy library. """ - assert ( - isfinite(K) - and MIT_Params[self.type]["Kp_min"] <= K - and K <= MIT_Params[self.type]["Kp_max"] - ) - assert ( - isfinite(B) - and MIT_Params[self.type]["Kd_min"] <= B - and B <= MIT_Params[self.type]["Kd_max"] - ) + if not (isfinite(K) and MIT_Params[self.type]["Kp_min"] <= K and MIT_Params[self.type]["Kp_max"] >= K): + raise ValueError( + f"K must be finite and between \ + {MIT_Params[self.type]['Kp_min']} and {MIT_Params[self.type]['Kp_max']}" + ) + + if not (isfinite(B) and MIT_Params[self.type]["Kd_min"] <= B and MIT_Params[self.type]["Kd_max"] >= B): + raise ValueError( + f"B must be finite and between \ + {MIT_Params[self.type]['Kd_min']} and {MIT_Params[self.type]['Kd_max']}" + ) + self._command.kp = K self._command.kd = B self._command.velocity = 0.0 - def set_current_gains(self, kp=40, ki=400, ff=128, spoof=False): + def set_current_gains(self, kp=40, ki=400, ff=128, spoof=False) -> None: """ Uses plain current mode, will send 0.0 for position gains in addition to requested current. @@ -499,7 +448,7 @@ def set_current_gains(self, kp=40, ki=400, ff=128, spoof=False): """ pass - def set_velocity_gains(self, kd=1.0): + def set_velocity_gains(self, kd=1.0) -> None: """ Uses plain speed mode, will send 0.0 for position gain and for feed forward current. @@ -508,12 +457,12 @@ def set_velocity_gains(self, kd=1.0): """ self._command.kd = kd - def set_position_gains(self): + def set_position_gains(self) -> None: # Not implemented pass # used for either impedance or MIT mode to set output angle - def set_output_position(self, value): + def set_output_position(self, value: float) -> None: """ Used for either impedance or full state feedback mode to set output angle command. Note, this does not send a command, it updates the TMotorManager's saved command, @@ -525,9 +474,6 @@ def set_output_position(self, value): Raises: RuntimeError: If the position command is outside the range of the motor. """ - # position commands must be within a certain range :/ - # pos = (np.abs(pos) % MIT_Params[self.type]["P_max"])*np.sign(pos) # this doesn't work because it will unwind itself! - # CANNOT Control using impedance mode for angles greater than 12.5 rad!! if np.abs(value) >= MIT_Params[self.type]["P_max"]: raise RuntimeError( "Cannot control using impedance mode for angles with magnitude greater than " @@ -535,11 +481,9 @@ def set_output_position(self, value): + "rad!" ) - # if self._control_state not in [_TMotorManState.IMPEDANCE, _TMotorManState.FULL_STATE]: - # raise RuntimeError("Attempted to send position command without gains for device " + self.device_info_string()) self._command.position = value - def set_output_velocity(self, value): + def set_output_velocity(self, value: float) -> None: """ Used for either speed or full state feedback mode to set output velocity command. Note, this does not send a command, it updates the TMotorManager's saved command, @@ -558,13 +502,11 @@ def set_output_velocity(self, value): + "rad/s!" ) - # if self._control_state not in [_TMotorManState.SPEED, _TMotorManState.FULL_STATE]: - # raise RuntimeError("Attempted to send speed command without gains for device " + self.device_info_string()) self._command.velocity = value # used for either current MIT mode to set current - def set_motor_current(self, value): + def set_motor_current(self, value: float) -> None: """ Used for either current or full state feedback mode to set current command. Note, this does not send a command, it updates the TMotorManager's saved command, @@ -573,12 +515,10 @@ def set_motor_current(self, value): Args: value: the desired current in amps. """ - # if self._control_state not in [_TMotorManState.CURRENT, _TMotorManState.FULL_STATE]: - # raise RuntimeError("Attempted to send current command before entering current mode for device " + self.device_info_string()) self._command.current = value # used for either current or MIT Mode to set current, based on desired torque - def set_joint_torque(self, value): + def set_joint_torque(self, value: float) -> None: """ Used for either current or MIT Mode to set current, based on desired torque. If a more complicated torque model is available for the motor, that will be used. @@ -587,14 +527,10 @@ def set_joint_torque(self, value): Args: value: The desired output torque in Nm. """ - self.set_motor_current( - value - / MIT_Params[self.type]["Kt_actual"] - / MIT_Params[self.type]["GEAR_RATIO"] - ) + self.set_motor_current(value / MIT_Params[self.type]["Kt_actual"] / MIT_Params[self.type]["GEAR_RATIO"]) # motor-side functions to account for the gear ratio - def set_motor_torque(self, value): + def set_motor_torque(self, value: float) -> None: """ Version of set_output_torque that accounts for gear ratio to control motor-side torque @@ -603,7 +539,7 @@ def set_motor_torque(self, value): """ self.set_output_torque(value * MIT_Params[self.type]["Kt_actual"]) - def set_motor_position(self, value): + def set_motor_position(self, value: float) -> None: """ Wrapper for set_output_angle that accounts for gear ratio to control motor-side angle @@ -612,7 +548,7 @@ def set_motor_position(self, value): """ self.set_output_position(value / (MIT_Params[self.type]["GEAR_RATIO"])) - def set_motor_velocity(self, value): + def set_motor_velocity(self, value: float) -> None: """ Wrapper for set_output_velocity that accounts for gear ratio to control motor-side velocity @@ -621,52 +557,52 @@ def set_motor_velocity(self, value): """ self.set_output_velocity(value / (MIT_Params[self.type]["GEAR_RATIO"])) - def set_motor_voltage(self, value): + def set_motor_voltage(self, value: float) -> float: # Not implemented pass @property - def motor_position(self): + def motor_position(self) -> float: """ Wrapper for get_output_angle that accounts for gear ratio to get motor-side angle Returns: The most recently updated motor-side angle in rad. """ - return self._motor_state.position * MIT_Params[self.type]["GEAR_RATIO"] + return float(self._motor_state.position * MIT_Params[self.type]["GEAR_RATIO"]) @property - def motor_velocity(self): + def motor_velocity(self) -> float: """ Wrapper for get_output_velocity that accounts for gear ratio to get motor-side velocity Returns: The most recently updated motor-side velocity in rad/s. """ - return self._motor_state.velocity * MIT_Params[self.type]["GEAR_RATIO"] + return float(self._motor_state.velocity * MIT_Params[self.type]["GEAR_RATIO"]) @property - def motor_acceleration(self): + def motor_acceleration(self) -> float: """ Wrapper for get_output_acceleration that accounts for gear ratio to get motor-side acceleration Returns: The most recently updated motor-side acceleration in rad/s/s. """ - return self._motor_state.acceleration * MIT_Params[self.type]["GEAR_RATIO"] + return float(self._motor_state.acceleration * MIT_Params[self.type]["GEAR_RATIO"]) @property - def motor_torque(self): + def motor_torque(self) -> float: """ Wrapper for get_output_torque that accounts for gear ratio to get motor-side torque Returns: The most recently updated motor-side torque in Nm. """ - return self.output_torque * MIT_Params[self.type]["GEAR_RATIO"] + return float(self.output_torque * MIT_Params[self.type]["GEAR_RATIO"]) # Pretty stuff - def __str__(self): + def __str__(self) -> str: """Prints the motor's device info and current""" return ( self.device_info_string() @@ -695,17 +631,18 @@ def check_can_connection(self) -> bool: """ if not self._entered: raise RuntimeError( - "Tried to check_can_connection before entering motor control! Enter control using the __enter__ method, or instantiating the TMotorManager in a with block." + "Tried to check_can_connection before entering motor control! \ + Enter control using the __enter__ method, or instantiating the TMotorManager in a with block." ) Listener = can.BufferedReader() self._canman.notifier.add_listener(Listener) - for i in range(10): + for _i in range(10): self.power_on() time.sleep(0.001) success = True self._is_open = True time.sleep(0.1) - for i in range(10): + for _i in range(10): if Listener.get_message(timeout=0.1) is None: success = False self._is_open = False diff --git a/opensourceleg/benchmarks/decorators.py b/opensourceleg/benchmarks/decorators.py index ee670565..1fbdbf80 100644 --- a/opensourceleg/benchmarks/decorators.py +++ b/opensourceleg/benchmarks/decorators.py @@ -1,16 +1,15 @@ import timeit from functools import wraps +from typing import Any, Callable -def profile_time(iterations): - def decorator(func): +def profile_time(iterations: int) -> Callable: + def decorator(func: Callable) -> Callable: @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> None: timer = timeit.Timer(lambda: func(*args, **kwargs)) execution_time = timer.timeit(number=iterations) - print( - f"Execution time for {func.__name__} over {iterations} iterations: {execution_time} seconds" - ) + print(f"Execution time for {func.__name__} over {iterations} iterations: {execution_time} seconds") return None return wrapper diff --git a/opensourceleg/benchmarks/threads.py b/opensourceleg/benchmarks/threads.py index 1b7197c5..5c0a7e59 100644 --- a/opensourceleg/benchmarks/threads.py +++ b/opensourceleg/benchmarks/threads.py @@ -1,4 +1,3 @@ -import asyncio import threading import time @@ -9,7 +8,7 @@ FREQ = 1000 -def core_function(): +def core_function() -> None: global counter counter += 1 # print("Counter: ", counter) @@ -17,13 +16,13 @@ def core_function(): @profile_time(iterations=PROFILING_ITERATIONS) -def basic_counter(): +def basic_counter() -> None: for _ in range(10): core_function() @profile_time(iterations=PROFILING_ITERATIONS) -def threaded_counter(): +def threaded_counter() -> None: threads = [] for _ in range(10): thread = threading.Thread(target=core_function) @@ -34,7 +33,7 @@ def threaded_counter(): thread.join() -def main(): +def main() -> None: basic_counter() threaded_counter() diff --git a/opensourceleg/collections/validators.py b/opensourceleg/collections/validators.py index 31d171d4..a5e00fb6 100644 --- a/opensourceleg/collections/validators.py +++ b/opensourceleg/collections/validators.py @@ -1,31 +1,33 @@ from abc import ABC, abstractmethod -from dataclasses import dataclass +from typing import Any, Optional, Union class Validator(ABC): - def __set_name__(self, owner, name): + def __set_name__(self, name: str) -> None: self.private_name = f"_{name}" - def __get__(self, object, objtype=None): - return getattr(object, self.private_name) + def __get__(self, instance: Any, objtype: Any = None) -> Any: + return getattr(instance, self.private_name) - def __set__(self, object, value): + def __set__(self, instance: Any, value: Any) -> None: self.validate(value) - setattr(object, self.private_name, value) + setattr(instance, self.private_name, value) @abstractmethod - def validate(self, value): + def validate(self, value: Any) -> None: pass class Number(Validator): - def __init__(self, min_value=None, max_value=None) -> None: - self.min_value = min_value - self.max_value = max_value + def __init__( + self, min_value: Optional[Union[int, float]] = None, max_value: Optional[Union[int, float]] = None + ) -> None: + self.min_value: Optional[Union[int, float]] = min_value + self.max_value: Optional[Union[int, float]] = max_value - def validate(self, value): + def validate(self, value: Union[int, float]) -> None: if not isinstance(value, (int, float)): - raise ValueError("Value must be an int or float") + raise TypeError("Value must be an int or float") if self.min_value is not None and value < self.min_value: raise ValueError(f"Value must be at least {self.min_value}") @@ -39,7 +41,7 @@ def validate(self, value): class Gains: kp = Number(0, 100) - def __init__(self, price): + def __init__(self, price: int) -> None: self.kp = price g = Gains(200) diff --git a/opensourceleg/control/compiled_controller.py b/opensourceleg/control/compiled_controller.py index ffc48085..fcc169ce 100644 --- a/opensourceleg/control/compiled_controller.py +++ b/opensourceleg/control/compiled_controller.py @@ -1,9 +1,10 @@ -from typing import Any - import ctypes +from typing import Any, Callable, Optional import numpy.ctypeslib as ctl +from opensourceleg.logging.logger import LOGGER + class CompiledController: """ @@ -17,10 +18,15 @@ class CompiledController: Parameters: ----------- library_name (string): The name of the compiled library file, without the *.so - library_path (string): The path to the directory containing the library. See examples for how to get working directory of parent script. - main_function_name (string): Name of the main function to call within the library. This is the function that will get called via the run() method - initialization_function_name (string): Name of an initialization function for your library. This gets called only once when the library is loaded. If you don't have an initialization function, pass None. - cleanup_function_name (string): Name of a cleanup function for your library. This gets called when the CompiledController class has gone out of scope and is garbage collected. Again, pass None if you don't need this functionality. + library_path (string): The path to the directory containing the library. + See examples for how to get working directory of parent script. + main_function_name (string): Name of the main function to call within the library. + This is the function that will get called via the run() method + initialization_function_name (string): Name of an initialization function for your library. + This gets called only once when the library is loaded. If you don't have an initialization function, pass None. + cleanup_function_name (string): Name of a cleanup function for your library. + This gets called when the CompiledController class has gone out of scope and is garbage collected. + Again, pass None if you don't need this functionality. Authors: -------- @@ -33,20 +39,19 @@ class CompiledController: def __init__( self, - library_name, - library_path, - main_function_name, - initialization_function_name=None, - cleanup_function_name=None, + library_name: str, + library_path: str, + main_function_name: str, + initialization_function_name: Optional[str] = None, + cleanup_function_name: Optional[str] = None, ) -> None: - self.cleanup_func = None - self.lib = ctl.load_library(library_name, library_path) - self.cleanup_func = self._load_function(cleanup_function_name) - self.main_function = self._load_function(main_function_name) - self.init_function = self._load_function(initialization_function_name) + self.lib: Any = ctl.load_library(library_name, library_path) + self.cleanup_func: Callable = self._load_function(cleanup_function_name) + self.main_function: Callable = self._load_function(main_function_name) + self.init_function: Callable = self._load_function(initialization_function_name) # Note if requested function name is None, returned handle is also none - if not self.init_function == None: + if self.init_function is not None: self.init_function() # This alias makes defining types from top script easier without second import @@ -65,23 +70,21 @@ def __init__( self._output_type = None self.outputs = None - def __del__(self): - if not self.cleanup_func == None: + def __del__(self) -> None: + if hasattr(self, "cleanup_func") and self.cleanup_func is not None: self.cleanup_func() - def __repr__(self): - return f"CompiledController" + def __repr__(self) -> str: + return "CompiledController" - def _load_function(self, function_name): - if function_name == None: + def _load_function(self, function_name: Optional[str]) -> Any: + if function_name is None: return None else: try: function_handle = getattr(self.lib, function_name) except AttributeError: - raise AttributeError( - f"Function {function_name} not found in library {self.lib}" - ) + LOGGER.warning(f"Function {function_name} not found in library {self.lib}") return function_handle def define_inputs(self, input_list: list[Any]) -> None: @@ -98,7 +101,11 @@ def define_inputs(self, input_list: list[Any]) -> None: All types can be accessed as CompiledController.types.(type_name) """ self._input_type = self.define_type("inputs", input_list) - self.inputs = self._input_type() # type: ignore + + if self._input_type is None: + raise ValueError("Input type not defined properly. Check define_type() method.") + + self.inputs = self._input_type() def define_outputs(self, output_list: list[Any]) -> None: """ @@ -114,9 +121,13 @@ def define_outputs(self, output_list: list[Any]) -> None: All types can be accessed as CompiledController.types.(type_name) """ self._output_type = self.define_type("outputs", output_list) - self.outputs = self._output_type() # type: ignore - def define_type(self, type_name: str, parameter_list: list[Any]): + if self._output_type is None: + raise ValueError("Output type not defined properly. Check define_type() method.") + + self.outputs = self._output_type() + + def define_type(self, type_name: str, parameter_list: list[Any]) -> Any: """ This method defines a new type to be used in the compiled controller. After calling this method, the datatype with name type_name will be @@ -149,7 +160,7 @@ class CustomStructure(ctypes.Structure): setattr(self.types, type_name, CustomStructure) return getattr(self.types, type_name) - def run(self): + def run(self) -> Any: """ This method calls the main controller function of the library. Under the hood, it calls library_name.main_function_name(*inputs, *outputs), @@ -164,13 +175,9 @@ def run(self): ValueError: If define_inputs() or define_outputs() have not been called. """ if self.inputs is None: - raise ValueError( - "Must define input type before calling controller.run(). Use define_inputs() method." - ) + raise ValueError("Must define input type before calling controller.run(). Use define_inputs() method.") if self.outputs is None: - raise ValueError( - "Must define output type before calling controller.run(). Use define_outputs() method." - ) + raise ValueError("Must define output type before calling controller.run(). Use define_outputs() method.") self.main_function(ctypes.byref(self.inputs), ctypes.byref(self.outputs)) return self.outputs diff --git a/opensourceleg/control/state_machine.py b/opensourceleg/control/state_machine.py index 78516755..341e4fbd 100644 --- a/opensourceleg/control/state_machine.py +++ b/opensourceleg/control/state_machine.py @@ -1,10 +1,10 @@ #!/usr/bin/python3 # A simple and scalable Finite State Machine module -from typing import Any, Callable, List, Optional - import time -from dataclasses import dataclass, field +from typing import Any, Callable, Optional + +from opensourceleg.logging.logger import LOGGER """ The state_machine module provides classes for implementing a finite state machine (FSM). @@ -56,7 +56,6 @@ def __init__( ankle_equilibrium_angle: float = 0.0, minimum_time_in_state: float = 2.0, ) -> None: - self._name: str = name self._is_knee_active: bool = is_knee_active @@ -79,13 +78,10 @@ def __init__( self._entry_callbacks: list[Callable[[Any], None]] = [] self._exit_callbacks: list[Callable[[Any], None]] = [] - def __eq__(self, __o) -> bool: - if __o.name == self._name: - return True - else: - return False + def __eq__(self, __o: Any) -> bool: + return bool(__o.name == self._name) - def __ne__(self, __o) -> bool: + def __ne__(self, __o: Any) -> bool: return not self.__eq__(__o) def __call__(self, data: Any) -> Any: @@ -103,7 +99,7 @@ def set_minimum_time_spent_in_state(self, time: float) -> None: """ self._min_time_in_state = time - def set_knee_impedance_paramters(self, theta, k, b) -> None: + def set_knee_impedance_paramters(self, theta: float, k: float, b: float) -> None: """ Set the knee impedance parameters @@ -121,7 +117,7 @@ def set_knee_impedance_paramters(self, theta, k, b) -> None: self._knee_stiffness = k self._knee_damping = b - def set_ankle_impedance_paramters(self, theta, k, b) -> None: + def set_ankle_impedance_paramters(self, theta: float, k: float, b: float) -> None: """ Set the ankle impedance parameters @@ -193,7 +189,7 @@ def stop(self, data: Any) -> None: for c in self._exit_callbacks: c(data) - def make_knee_active(self): + def make_knee_active(self) -> None: """ Make the knee active @@ -203,7 +199,7 @@ def make_knee_active(self): """ self._is_knee_active = True - def make_ankle_active(self): + def make_ankle_active(self) -> None: """ Make the ankle active @@ -277,29 +273,26 @@ class Event: Event class """ - def __init__(self, name) -> None: + def __init__(self, name: str) -> None: """ Parameters ---------- name : str The name of the event. """ - self._name = name + self._name: str = name - def __eq__(self, __o) -> bool: - if __o.name == self._name: - return True - else: - return False + def __eq__(self, __o: Any) -> bool: + return bool(__o.name == self._name) - def __ne__(self, __o) -> bool: + def __ne__(self, __o: Any) -> bool: return not self.__eq__(__o) # TODO: Check this fix def __repr__(self) -> str: return f"Event[{self._name}]" @property - def name(self): + def name(self) -> str: return self._name @@ -313,7 +306,7 @@ def __init__( event: Event, source: State, destination: State, - callback: Callable[[Any], bool] = None, + callback: Optional[Callable[[Any], bool]] = None, ) -> None: self._event: Event = event self._source_state: State = source @@ -326,9 +319,7 @@ def __call__(self, data: Any) -> Any: raise NotImplementedError def __repr__(self) -> str: - return ( - f"Transition[{self._source_state.name} -> {self._destination_state.name}]" - ) + return f"Transition[{self._source_state.name} -> {self._destination_state.name}]" def add_criteria(self, callback: Callable[[Any], bool]) -> None: self._criteria = callback @@ -355,21 +346,16 @@ def __init__( event: Event, source: State, destination: State, - callback: Callable[[Any], bool] = None, + callback: Optional[Callable[[Any], bool]] = None, ) -> None: - super().__init__( - event=event, source=source, destination=destination, callback=callback - ) + super().__init__(event=event, source=source, destination=destination, callback=callback) self._from: State = source self._to: State = destination def __call__(self, data: Any, spoof: bool = False) -> State: if spoof: - if ( - self._from.current_time_in_state - > self._from.minimum_time_spent_in_state - ): + if self._from.current_time_in_state > self._from.minimum_time_spent_in_state: if self._action: self._action(data) @@ -418,7 +404,7 @@ class StateMachine: Whether or not the state machine is spoofing the state transitions. """ - def __init__(self, osl=None, spoof: bool = False) -> None: + def __init__(self, osl: object = None, spoof: bool = False) -> None: # State Machine Variables self._states: list[State] = [] self._events: list[Event] = [] @@ -435,7 +421,7 @@ def __init__(self, osl=None, spoof: bool = False) -> None: self._spoof: bool = spoof def __repr__(self) -> str: - return f"StateMachine" + return "StateMachine" def add_state(self, state: State, initial_state: bool = False) -> None: """ @@ -464,7 +450,7 @@ def add_transition( source: State, destination: State, event: Event, - callback: Callable[[Any], bool] = None, + callback: Optional[Callable[[Any], bool]] = None, ) -> Optional[Transition]: """ Add a transition to the state machine. @@ -478,18 +464,13 @@ def add_transition( event : Event The event that triggers the transition. callback : Callable[[Any], bool], optional - A callback function that returns a boolean value, which determines whether the transition is valid, by default None + A callback function that returns a boolean value, + which determines whether the transition is valid, by default None """ transition = None - if ( - source in self._states - and destination in self._states - and event in self._events - ): - transition = FromToTransition( - event=event, source=source, destination=destination, callback=callback - ) + if source in self._states and destination in self._states and event in self._events: + transition = FromToTransition(event=event, source=source, destination=destination, callback=callback) self._transitions.append(transition) return transition @@ -514,8 +495,10 @@ def update(self, data: Any = None) -> None: break if not validity: - assert self._osl is not None - self._osl.log.debug(f"Event isn't valid at {self._current_state.name}") + if self._osl is None: + raise ValueError("OSL object not set.") + + LOGGER.debug(f"Event isn't valid at {self._current_state.name}") def start(self, data: Any = None) -> None: if not self._initial_state: @@ -534,27 +517,24 @@ def stop(self, data: Any = None) -> None: self._exited = True def is_on(self) -> bool: - if self._current_state and self._current_state != self._exit_state: - return True - else: - return False + return bool(self._current_state and self._current_state != self._exit_state) def spoof(self, spoof: bool) -> None: self._spoof = spoof @property - def current_state(self): + def current_state(self) -> State: if self._current_state is None: return self._initial_state else: return self._current_state @property - def states(self): + def states(self) -> list[str]: return [state.name for state in self._states] @property - def is_spoofing(self): + def is_spoofing(self) -> bool: return self._spoof diff --git a/opensourceleg/logging/__init__.py b/opensourceleg/logging/__init__.py index 82a7cbc9..46406def 100644 --- a/opensourceleg/logging/__init__.py +++ b/opensourceleg/logging/__init__.py @@ -1 +1 @@ -from opensourceleg.logging.logger import * +from .logger import * # noqa: F403 diff --git a/opensourceleg/logging/decorators.py b/opensourceleg/logging/decorators.py index 23dbb06f..ef9c2a06 100644 --- a/opensourceleg/logging/decorators.py +++ b/opensourceleg/logging/decorators.py @@ -1,29 +1,30 @@ from functools import wraps +from typing import Any, Callable from opensourceleg.logging import LOGGER -def deprecated(func): +def deprecated(func: Callable) -> Callable: """ Decorator to mark a function as deprecated. """ @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Any: LOGGER.warning(f"Function `{func.__name__}` is deprecated.") return func(*args, **kwargs) return wrapper -def deprecated_with_suggestion(alternative_func): +def deprecated_with_suggestion(alternative_func: Callable) -> Callable: """ Decorator to provide an alternative function for a deprecated function. """ - def decorator(func): + def decorator(func: Callable) -> Callable: @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Any: LOGGER.warning( f"Function `{func.__name__}` is deprecated. Please use `{alternative_func.__name__}` instead." ) @@ -34,17 +35,18 @@ def wrapper(*args, **kwargs): return decorator -def deprecated_with_routing(alternative_func): +def deprecated_with_routing(alternative_func: Callable) -> Callable: """ Decorator to provide an alternative function for a deprecated function. The alternative function will be called instead of the deprecated function. """ - def decorator(func): + def decorator(func: Callable) -> Callable: @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Any: LOGGER.warning( - f"Function `{func.__name__}` is deprecated. Please use `{alternative_func.__name__}` instead, which will be called automatically now." + f"Function `{func.__name__}` is deprecated. Please use `{alternative_func.__name__}` instead, " + "which will be called automatically now." ) return alternative_func(*args, **kwargs) @@ -55,15 +57,15 @@ def wrapper(*args, **kwargs): if __name__ == "__main__": - def add(a, b): + def add(a: int, b: int) -> int: return a + b @deprecated_with_suggestion(add) - def add_old(a, b): + def add_old(a: int, b: int) -> int: return b @deprecated_with_routing(add) - def add_renamed(a, b): + def add_renamed(a: int, b: int) -> int: return a print(add_renamed(1, 2)) diff --git a/opensourceleg/logging/exceptions.py b/opensourceleg/logging/exceptions.py index 5a8f11d7..f196b041 100644 --- a/opensourceleg/logging/exceptions.py +++ b/opensourceleg/logging/exceptions.py @@ -8,9 +8,7 @@ class ActuatorStreamException(Exception): """ def __init__(self, tag: str) -> None: - super().__init__( - f"{tag} is not streaming, please call start() method before sending commands" - ) + super().__init__(f"{tag} is not streaming, please call start() method before sending commands") class ActuatorConnectionException(Exception): @@ -37,7 +35,8 @@ class ActuatorIsNoneException(Exception): def __init__(self, mode: str) -> None: super().__init__( - f"Actuator is None in {mode} mode, please pass the actuator instance to the mode during initialization or set the actuator instance using set_actuator method." + f"Actuator is None in {mode} mode, please pass the actuator instance to the mode during " + "initialization or set the actuator instance using set_actuator method." ) @@ -90,5 +89,6 @@ class ActuatorKeyException(Exception): def __init__(self, tag: str, key: str) -> None: super().__init__( - f"{tag} does not have {key} key in the actuators dictionary. Please check the actuators dictionary for the `{key}` key." + f"{tag} does not have {key} key in the actuators dictionary. " + f"Please check the actuators dictionary for the `{key}` key." ) diff --git a/opensourceleg/logging/logger.py b/opensourceleg/logging/logger.py index c2f88c22..0515f2a3 100644 --- a/opensourceleg/logging/logger.py +++ b/opensourceleg/logging/logger.py @@ -24,8 +24,6 @@ """ -from typing import Any, Callable, Dict, List, Optional, Union - import csv import logging import os @@ -34,9 +32,10 @@ from datetime import datetime from enum import Enum from logging.handlers import RotatingFileHandler +from typing import Any, Callable, Optional, Union -class LogLevel(Enum): +class LogLevel(int, Enum): DEBUG = logging.DEBUG INFO = logging.INFO WARNING = logging.WARNING @@ -47,7 +46,7 @@ class LogLevel(Enum): class Logger(logging.Logger): _instance = None - def __new__(cls, *args, **kwargs): + def __new__(cls, *args: Any, **kwargs: Any) -> "Logger": if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance @@ -73,10 +72,10 @@ def __init__( self._file_backup_count = file_backup_count self._user_file_name = file_name - self._file_path: str = "" - self._csv_path: str = "" + self._file_path: Optional[str] = None + self._csv_path: Optional[str] = None self._file: Optional[Any] = None - self._writer = None + self._writer: Any = None self._is_logging = False self._header_written = False @@ -88,6 +87,7 @@ def __init__( self._setup_logging() self._initialized: bool = True else: + self._log_path = log_path self.set_file_name(file_name) self.set_file_level(file_level) self.set_stream_level(stream_level) @@ -96,41 +96,39 @@ def __init__( self._file_backup_count = file_backup_count self.set_buffer_size(buffer_size) - self._log_path = log_path - def _setup_logging(self) -> None: - self.setLevel(level=self._file_level.value) + self.setLevel(level=self._file_level) self._std_formatter = logging.Formatter(self._log_format) self._stream_handler = logging.StreamHandler() - self._stream_handler.setLevel(level=self._stream_level.value) + self._stream_handler.setLevel(level=self._stream_level) self._stream_handler.setFormatter(fmt=self._std_formatter) self.addHandler(hdlr=self._stream_handler) def _setup_file_handler(self) -> None: - if self._file_path == "": + if not self._file_path: self._generate_file_paths() self._file_handler = RotatingFileHandler( - filename=self._file_path, - mode="a", + filename=self._file_path if self._file_path else "", + mode="w", maxBytes=self._file_max_bytes, backupCount=self._file_backup_count, ) - self._file_handler.setLevel(level=self._file_level.value) + self._file_handler.setLevel(level=self._file_level) self._file_handler.setFormatter(fmt=self._std_formatter) self.addHandler(hdlr=self._file_handler) - def _ensure_file_handler(self): + def _ensure_file_handler(self) -> None: if not hasattr(self, "_file_handler"): self._setup_file_handler() - def track_variable(self, var_func: Callable[[], Any], name: str): + def track_variable(self, var_func: Callable[[], Any], name: str) -> None: var_id = id(var_func) self._tracked_vars[var_id] = var_func self._var_names[var_id] = name - def untrack_variable(self, var_func: Callable[[], Any]): + def untrack_variable(self, var_func: Callable[[], Any]) -> None: var_id = id(var_func) self._tracked_vars.pop(var_id, None) self._var_names.pop(var_id, None) @@ -138,19 +136,22 @@ def untrack_variable(self, var_func: Callable[[], Any]): def __repr__(self) -> str: return f"Logger(file_path={self._file_path})" + def set_log_path(self, log_path: str) -> None: + self._log_path = log_path + self._generate_file_paths() + def set_file_name(self, file_name: Union[str, None]) -> None: self._user_file_name = file_name - self._file_path = "" - self._csv_path = "" + self._generate_file_paths() def set_file_level(self, level: LogLevel) -> None: self._file_level = level if hasattr(self, "_file_handler"): - self._file_handler.setLevel(level=level.value) + self._file_handler.setLevel(level=level) def set_stream_level(self, level: LogLevel) -> None: self._stream_level = level - self._stream_handler.setLevel(level=level.value) + self._stream_handler.setLevel(level=level) def set_format(self, log_format: str) -> None: self._log_format = log_format @@ -168,7 +169,7 @@ def update(self) -> None: return data = [] - for var_id, get_value in self._tracked_vars.items(): + for _var_id, get_value in self._tracked_vars.items(): value = get_value() data.append(str(value)) @@ -177,14 +178,18 @@ def update(self) -> None: if len(self._buffer) >= self._buffer_size: self.flush_buffer() - def flush_buffer(self): + def flush_buffer(self) -> None: if not self._buffer: return self._ensure_file_handler() if self._file is None: - self._file = open(self._csv_path, "a", newline="") + self._file = open( + self._csv_path if self._csv_path else "", + mode="w", + newline="", + ) self._writer = csv.writer(self._file) if not self._header_written: @@ -197,7 +202,7 @@ def flush_buffer(self): def _write_header(self) -> None: header = list(self._var_names.values()) - self._writer.writerow(header) # type: ignore + self._writer.writerow(header) self._header_written = True def _generate_file_paths(self) -> None: @@ -205,67 +210,80 @@ def _generate_file_paths(self) -> None: timestamp = now.strftime("%Y%m%d_%H%M%S") script_name = os.path.basename(__file__).split(".")[0] - if self._user_file_name: - base_name = self._user_file_name - else: - base_name = f"{script_name}_{timestamp}" + base_name = self._user_file_name if self._user_file_name else f"{script_name}_{timestamp}" + + if not os.path.exists(self._log_path): + os.makedirs(self._log_path) file_path = os.path.join(self._log_path, base_name) + self._file_path = file_path + ".log" self._csv_path = file_path + ".csv" def __enter__(self) -> "Logger": return self - def __exit__(self, exc_type, exc_val, exc_tb) -> None: - self.flush_buffer() + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self.close() - def reset(self): - self._buffer.clear() + def reset(self) -> None: + self.close() + self._setup_logging() + self._tracked_vars.clear() self._var_names.clear() self._header_written = False + if hasattr(self, "_file_handler"): self._file_handler.close() del self._file_handler + # re-initialize the logger + def close(self) -> None: + self.flush_buffer() + if self._file: self._file.close() self._file = None self._writer = None - def debug(self, msg, *args, **kwargs): + def debug(self, msg: object, *args: object, **kwargs: Any) -> None: self._ensure_file_handler() super().debug(msg, *args, **kwargs) - def info(self, msg, *args, **kwargs): + def info(self, msg: object, *args: object, **kwargs: Any) -> None: self._ensure_file_handler() super().info(msg, *args, **kwargs) - def warning(self, msg, *args, **kwargs): + def warning(self, msg: object, *args: object, **kwargs: Any) -> None: self._ensure_file_handler() super().warning(msg, *args, **kwargs) - def error(self, msg, *args, **kwargs): + def error(self, msg: object, *args: object, **kwargs: Any) -> None: self._ensure_file_handler() super().error(msg, *args, **kwargs) - def critical(self, msg, *args, **kwargs): + def critical(self, msg: object, *args: object, **kwargs: Any) -> None: self._ensure_file_handler() super().critical(msg, *args, **kwargs) - def log(self, level, msg, *args, **kwargs): + def log(self, level: int, msg: object, *args: object, **kwargs: Any) -> None: self._ensure_file_handler() super().log(level, msg, *args, **kwargs) @property - def file_path(self) -> str: - if self._file_path == "": - self._generate_file_paths() + def file_path(self) -> Optional[str]: return self._file_path + @property + def csv_path(self) -> Optional[str]: + return self._csv_path + + @property + def log_path(self) -> str: + return self._log_path + @property def buffer_size(self) -> int: return self._buffer_size @@ -288,18 +306,19 @@ def file_backup_count(self) -> int: # Initialize a global logger instance to be used throughout the library +SCRIPT_DIR = os.path.dirname(__file__) LOGGER = Logger() if __name__ == "__main__": class Test: - def __init__(self): - self.a = 0 + def __init__(self) -> None: + self.a: float = 0.0 - def update(self): + def update(self) -> None: self.a += 0.2 - my_logger = Logger(buffer_size=5000, file_name="my_log") + my_logger = Logger(buffer_size=1, file_name="test_logger", log_path="./logs") x = 0.0 y = 0.0 @@ -307,9 +326,10 @@ def update(self): my_logger.track_variable(lambda: x, "x") my_logger.track_variable(lambda: y, "y") - LOGGER.track_variable(lambda: test.a, "A") + my_logger.track_variable(lambda: test.a, "A") + my_logger.info("Starting logging...") - for i in range(1000): + for _i in range(1000): x += 0.1 y = x**2 diff --git a/opensourceleg/math/__init__.py b/opensourceleg/math/__init__.py index 519a4c13..9ee9d10f 100644 --- a/opensourceleg/math/__init__.py +++ b/opensourceleg/math/__init__.py @@ -1,16 +1,16 @@ -from opensourceleg.math.math import * +from .math import * # noqa: F403 """ Math module for opensourceleg library. Module Overview: -This module provides mathematical functionalities for the opensourceleg library. +This module provides mathematical functionalities for the opensourceleg library. Key Classes: - `ThermalModel` class is used to model the thermal behavior of an actuator. -- `SaturatingRamp` class is used to generate a saturating ramp signal that can be used to ramp up or down a signal. +- `SaturatingRamp` class is used to generate a saturating ramp signal that can be used to ramp up or down a signal. - `EdgeDetector` class is used to detect edges in a signal. - `clamp_within_vector_range` function is used to ensure that a value remains within a specified range. """ diff --git a/opensourceleg/math/math.py b/opensourceleg/math/math.py index fc749dbe..ad684b0d 100644 --- a/opensourceleg/math/math.py +++ b/opensourceleg/math/math.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, List, Optional +from typing import Any, Optional, Union import numpy as np @@ -47,14 +47,15 @@ class ThermalModel: def __init__( self, ambient: float = 21, - params: dict[Any, Any] = {}, + params: Optional[dict[Any, Any]] = None, temp_limit_windings: float = 115, soft_border_C_windings: float = 15, temp_limit_case: float = 80, soft_border_C_case: float = 5, ) -> None: - # The following parameters result from Jack Schuchmann's test with no fans + if params is None: + params = {} self.C_w: float = 0.20 * 81.46202695970649 self.R_WC = 1.0702867186480716 self.C_c = 512.249065845453 @@ -67,9 +68,7 @@ def __init__( self.T_w: float = ambient self.T_c: float = ambient self.T_a: float = ambient - self.soft_max_temp_windings: float = ( - temp_limit_windings - soft_border_C_windings - ) + self.soft_max_temp_windings: float = temp_limit_windings - soft_border_C_windings self.abs_max_temp_windings: float = temp_limit_windings self.soft_border_windings: float = soft_border_C_windings @@ -78,7 +77,7 @@ def __init__( self.soft_border_case: float = soft_border_C_case def __repr__(self) -> str: - return f"ThermalModel" + return "ThermalModel" def update(self, dt: float = 1 / 200, motor_current: float = 0) -> None: """ @@ -100,15 +99,14 @@ def update(self, dt: float = 1 / 200, motor_current: float = 0) -> None: ) # accounts for resistance change due to temp. dTw_dt = (I2R + (self.T_c - self.T_w) / self.R_WC) / self.C_w - dTc_dt: float = ( - (self.T_w - self.T_c) / self.R_WC + (self.T_a - self.T_c) / self.R_CA - ) / self.C_c + dTc_dt: float = ((self.T_w - self.T_c) / self.R_WC + (self.T_a - self.T_c) / self.R_CA) / self.C_c self.T_w += dt * dTw_dt self.T_c += dt * dTc_dt - def update_and_get_scale(self, dt, motor_current: float = 0, FOS: float = 1.0): + def update_and_get_scale(self, dt: float, motor_current: float = 0, FOS: float = 1.0) -> float: """ - Updates the temperature of the winding and the case based on the current and the ambient temperature and returns the scale factor for the torque. + Updates the temperature of the winding and the case based on the current and + the ambient temperature and returns the scale factor for the torque. Args: dt (float): Time step in seconds. @@ -139,16 +137,12 @@ def update_and_get_scale(self, dt, motor_current: float = 0, FOS: float = 1.0): if self.T_c > self.abs_max_temp_case: scale = 0.0 elif self.T_c > self.soft_max_temp_case: - scale *= (self.abs_max_temp_case - self.T_w) / ( - self.abs_max_temp_case - self.soft_max_temp_case - ) + scale *= (self.abs_max_temp_case - self.T_w) / (self.abs_max_temp_case - self.soft_max_temp_case) I2R = I2R_des * scale dTw_dt = (I2R + (self.T_c - self.T_w) / self.R_WC) / self.C_w - dTc_dt: float = ( - (self.T_w - self.T_c) / self.R_WC + (self.T_a - self.T_c) / self.R_CA - ) / self.C_c + dTc_dt: float = ((self.T_w - self.T_c) / self.R_WC + (self.T_a - self.T_c) / self.R_CA) / self.C_c self.T_w += dt * dTw_dt self.T_c += dt * dTc_dt @@ -157,7 +151,7 @@ def update_and_get_scale(self, dt, motor_current: float = 0, FOS: float = 1.0): if scale >= 1.0: return 1.0 - return np.sqrt(scale) # this is how much the torque should be scaled + return float(np.sqrt(scale)) # this is how much the torque should be scaled class EdgeDetector: @@ -170,15 +164,15 @@ class EdgeDetector: https://github.com/tkevinbest """ - def __init__(self, bool_in): - self.cur_state = bool_in - self.rising_edge = False - self.falling_edge = False + def __init__(self, bool_in: bool) -> None: + self.cur_state: bool = bool_in + self.rising_edge: bool = False + self.falling_edge: bool = False def __repr__(self) -> str: - return f"EdgeDetector" + return "EdgeDetector" - def update(self, bool_in): + def update(self, bool_in: bool) -> None: self.rising_edge = bool_in and not self.cur_state self.falling_edge = not bool_in and self.cur_state self.cur_state = bool_in @@ -202,7 +196,7 @@ class SaturatingRamp: https://github.com/tkevinbest """ - def __init__(self, loop_frequency=100, ramp_time=1.0) -> None: + def __init__(self, loop_frequency: float = 100, ramp_time: float = 1.0) -> None: """ Args: loop_frequency (int, optional): Rate in Hz (default 100 Hz). Defaults to 100. @@ -212,9 +206,9 @@ def __init__(self, loop_frequency=100, ramp_time=1.0) -> None: self.value = 0.0 def __repr__(self) -> str: - return f"SaturatingRamp" + return "SaturatingRamp" - def update(self, enable_ramp=False): + def update(self, enable_ramp: bool = False) -> float: """ Updates the ramp value and returns it as a float. If enable_ramp is true, ramp value increases @@ -229,17 +223,16 @@ def update(self, enable_ramp=False): Returns: value (float): Scalar between 0 and 1. """ - if enable_ramp: - delta = self.delta_per_update - else: - delta = -1 * self.delta_per_update + delta = self.delta_per_update if enable_ramp else -1 * self.delta_per_update self.value += delta self.value = min(max(self.value, 0), 1) return self.value -def clamp_within_vector_range(input_value, input_vector): +def clamp_within_vector_range( + input_value: Union[float, int], input_vector: list[Union[float, int]] +) -> Union[float, int]: """ This function ensures that input_value remains within the range spanned by the input_vector. If the input_value falls outside the vector's bounds, it'll return the appropriate max or min value from the vector. diff --git a/opensourceleg/robots/base.py b/opensourceleg/robots/base.py index c0bbb978..ff68ebc1 100644 --- a/opensourceleg/robots/base.py +++ b/opensourceleg/robots/base.py @@ -1,6 +1,5 @@ -from typing import Any, Dict, Generic, TypeVar - from abc import ABC, abstractmethod +from typing import Any, Generic, TypeVar from opensourceleg.actuators.base import ActuatorBase from opensourceleg.logging import LOGGER @@ -16,20 +15,20 @@ def __init__( tag: str, actuators: dict[str, TActuator], sensors: dict[str, TSensor], - ): + ) -> None: self._tag = tag self.actuators: dict[str, TActuator] = actuators self.sensors: dict[str, TSensor] = sensors - def __enter__(self): + def __enter__(self) -> "RobotBase": self.start() return self - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self.stop() @abstractmethod - def start(self): + def start(self) -> None: for actuator in self.actuators.values(): LOGGER.debug(f"Calling start method of {actuator.tag}") actuator.start() @@ -39,7 +38,7 @@ def start(self): sensor.start() @abstractmethod - def stop(self): + def stop(self) -> None: for actuator in self.actuators.values(): LOGGER.debug(f"Calling stop method of {actuator.tag}") actuator.stop() @@ -49,7 +48,7 @@ def stop(self): sensor.stop() @abstractmethod - def update(self): + def update(self) -> None: for actuator in self.actuators.values(): actuator.update() @@ -57,7 +56,7 @@ def update(self): sensor.update() @property - def tag(self): + def tag(self) -> str: return self._tag # You can pipe values from the actuators to custom @property methods diff --git a/opensourceleg/robots/osl.py b/opensourceleg/robots/osl.py index fec013b8..3a257948 100644 --- a/opensourceleg/robots/osl.py +++ b/opensourceleg/robots/osl.py @@ -1,33 +1,31 @@ -from typing import Union - import time +from typing import Union import numpy as np from opensourceleg.actuators.base import CONTROL_MODES, ActuatorBase -from opensourceleg.actuators.dephy import DephyLegacyActuator +from opensourceleg.actuators.dephy import DEFAULT_POSITION_GAINS, DephyLegacyActuator from opensourceleg.logging import LOGGER from opensourceleg.robots.base import RobotBase, TActuator, TSensor -from opensourceleg.sensors.base import IMUBase, LoadcellBase, SensorBase -from opensourceleg.sensors.imu import LordMicrostrainIMU +from opensourceleg.sensors.base import LoadcellBase, SensorBase from opensourceleg.sensors.loadcell import SRILoadcell class OpenSourceLeg(RobotBase[TActuator, TSensor]): - def start(self): + def start(self) -> None: super().start() - def stop(self): + def stop(self) -> None: super().stop() - def update(self): + def update(self) -> None: super().update() - def home(self): + def home(self) -> None: for actuator in self.actuators.values(): actuator.home() - def make_encoder_maps(self): + def make_encoder_maps(self) -> None: pass @property @@ -35,9 +33,7 @@ def knee(self) -> Union[TActuator, ActuatorBase]: try: return self.actuators["knee"] except KeyError: - LOGGER.error( - "Knee actuator not found. Please check for `knee` key in the actuators dictionary." - ) + LOGGER.error("Knee actuator not found. Please check for `knee` key in the actuators dictionary.") exit(1) @property @@ -45,9 +41,7 @@ def ankle(self) -> Union[TActuator, ActuatorBase]: try: return self.actuators["ankle"] except KeyError: - LOGGER.error( - "Ankle actuator not found. Please check for `ankle` key in the actuators dictionary." - ) + LOGGER.error("Ankle actuator not found. Please check for `ankle` key in the actuators dictionary.") exit(1) @property @@ -55,9 +49,7 @@ def loadcell(self) -> Union[TSensor, LoadcellBase]: try: return self.sensors["loadcell"] except KeyError: - LOGGER.error( - "Loadcell sensor not found. Please check for `loadcell` key in the sensors dictionary." - ) + LOGGER.error("Loadcell sensor not found. Please check for `loadcell` key in the sensors dictionary.") exit(1) @property @@ -66,7 +58,8 @@ def joint_encoder_knee(self) -> Union[TSensor, SensorBase]: return self.sensors["joint_encoder_knee"] except KeyError: LOGGER.error( - "Knee joint encoder sensor not found. Please check for `joint_encoder_knee` key in the sensors dictionary." + "Knee joint encoder sensor not found." + "Please check for `joint_encoder_knee` key in the sensors dictionary." ) exit(1) @@ -76,7 +69,8 @@ def joint_encoder_ankle(self) -> Union[TSensor, SensorBase]: return self.sensors["joint_encoder_ankle"] except KeyError: LOGGER.error( - "Ankle joint encoder sensor not found. Please check for `joint_encoder_ankle` key in the sensors dictionary." + "Ankle joint encoder sensor not found." + "Please check for `joint_encoder_ankle` key in the sensors dictionary." ) exit(1) @@ -84,26 +78,21 @@ def joint_encoder_ankle(self) -> Union[TSensor, SensorBase]: if __name__ == "__main__": frequency = 200 - LOADCELL_MATRIX = np.array( - [ - (-5.45598, -1317.68604, 27.14505, 22.8468, -11.1176, 1283.02856), - (-20.23942, 773.01343, -9.44841, -1546.70923, 21.78232, 744.52325), - (-811.76398, -16.82792, -825.67261, 2.93904, -829.06409, 7.45233), - (16.38306, 0.22658, -0.50331, -0.23233, -17.0822, -0.03632), - (-9.81471, -0.03671, 19.47362, -0.161, -9.76819, 0.25571), - (-0.51744, -20.6571, 0.18245, -20.42393, 0.01944, -20.38067), - ] - ) + LOADCELL_MATRIX = np.array([ + (-5.45598, -1317.68604, 27.14505, 22.8468, -11.1176, 1283.02856), + (-20.23942, 773.01343, -9.44841, -1546.70923, 21.78232, 744.52325), + (-811.76398, -16.82792, -825.67261, 2.93904, -829.06409, 7.45233), + (16.38306, 0.22658, -0.50331, -0.23233, -17.0822, -0.03632), + (-9.81471, -0.03671, 19.47362, -0.161, -9.76819, 0.25571), + (-0.51744, -20.6571, 0.18245, -20.42393, 0.01944, -20.38067), + ]) osl = OpenSourceLeg[DephyLegacyActuator, SensorBase]( tag="opensourceleg", actuators={ - "knee": DephyLegacyActuator( - "knee", offline=False, frequency=frequency, gear_ratio=9 * (83 / 18) - ), + "knee": DephyLegacyActuator("knee", offline=False, frequency=frequency, gear_ratio=9 * (83 / 18)), }, sensors={ - "imu": LordMicrostrainIMU(frequency=frequency, port="/dev/ttyS0"), "loadcell": SRILoadcell(calibration_matrix=LOADCELL_MATRIX), }, ) @@ -112,7 +101,12 @@ def joint_encoder_ankle(self) -> Union[TSensor, SensorBase]: osl.update() osl.knee.set_control_mode(CONTROL_MODES.POSITION) - osl.knee.set_position_gains() + osl.knee.set_position_gains( + kp=DEFAULT_POSITION_GAINS.kp, + ki=DEFAULT_POSITION_GAINS.ki, + kd=DEFAULT_POSITION_GAINS.kd, + ff=DEFAULT_POSITION_GAINS.ff, + ) osl.knee.set_output_position(osl.knee.output_position + np.deg2rad(10)) # osl.loadcell.calibrate() diff --git a/opensourceleg/safety/__init__.py b/opensourceleg/safety/__init__.py index 36fd8c45..3e698f77 100644 --- a/opensourceleg/safety/__init__.py +++ b/opensourceleg/safety/__init__.py @@ -1,4 +1,4 @@ -from opensourceleg.safety.safety import * +from .safety import * # noqa: F403 """ Safety module for opensourceleg library. diff --git a/opensourceleg/safety/safety.py b/opensourceleg/safety/safety.py index 646c363f..11d3b9f3 100644 --- a/opensourceleg/safety/safety.py +++ b/opensourceleg/safety/safety.py @@ -1,13 +1,12 @@ -from typing import Callable, List - from collections import deque from dataclasses import dataclass +from typing import Any, Callable, Optional import numpy as np class ThermalLimitException(Exception): - def __init__(self, message="Software thermal limit exceeded. Exiting."): + def __init__(self, message: str = "Software thermal limit exceeded. Exiting.") -> None: self.message = message super().__init__(self.message) @@ -16,32 +15,32 @@ def is_changing( attribute_name: str, max_points: int = 10, threshold: float = 1e-6, - proxy_attribute_name: str = None, -): + proxy_attribute_name: Optional[str] = None, +) -> Callable: """ - Creates a decorator to check if a property's value is changing. If the standard deviation of the last 'max_points' values is less than 'threshold', the decorator will raise an error or return a proxy attribute. + Creates a decorator to check if a property's value is changing. + If the standard deviation of the last 'max_points' values is less than 'threshold', + the decorator will raise an error or return a proxy attribute. Args: - attribute_name (str): Name of the attribute. - max_points (int): Number of points to consider. Defaults to 10. - threshold (float): Threshold for the standard deviation. Defaults to 1e-6. - proxy_attribute_name (str): Name of the proxy attribute to return if the property is not changing. Defaults to None. + attribute_name: Name of the attribute. + max_points: Number of points to consider. Defaults to 10. + threshold: Threshold for the standard deviation. Defaults to 1e-6. + proxy_attribute_name: Name of the proxy attribute to return if the property is not changing. Defaults to None. Returns: Callable: Decorator function. """ + history_key = f"_{attribute_name}_history" proxy_key = f"_{attribute_name}_proxy" - def decorator(func): - def wrapper(instance, *args, **kwargs): + def decorator(func: Callable) -> Callable: + def wrapper(instance: object, *args: Any, **kwargs: Any) -> Any: instance.__dict__.setdefault(history_key, deque(maxlen=max_points)) - try: - if instance.__dict__[proxy_key] is True: - return getattr(instance, proxy_attribute_name) - except KeyError: - pass + if proxy_attribute_name is not None and getattr(instance, proxy_key): + return getattr(instance, proxy_attribute_name) value = func(instance, *args, **kwargs) history = getattr(instance, history_key) @@ -50,9 +49,7 @@ def wrapper(instance, *args, **kwargs): current_std = np.std(list(history)) if current_std < threshold: if proxy_attribute_name is not None: - print( - f"{attribute_name} isn't stable, returning {proxy_attribute_name}" - ) + print(f"{attribute_name} isn't stable, returning {proxy_attribute_name}") if not hasattr(instance, proxy_key): setattr(instance, proxy_key, True) return getattr(instance, proxy_attribute_name) @@ -65,7 +62,7 @@ def wrapper(instance, *args, **kwargs): return decorator -def is_negative(clamp: bool = False): +def is_negative(clamp: bool = False) -> Callable: """ Creates a decorator to check if a property's value is negative. @@ -76,8 +73,8 @@ def is_negative(clamp: bool = False): Callable: Decorator function. """ - def decorator(func): - def wrapper(instance, *args, **kwargs): + def decorator(func: Callable) -> Callable: + def wrapper(instance: object, *args: Any, **kwargs: Any) -> Any: value = func(instance, *args, **kwargs) if value >= 0: if clamp: @@ -90,7 +87,7 @@ def wrapper(instance, *args, **kwargs): return decorator -def is_positive(clamp: bool = False): +def is_positive(clamp: bool = False) -> Callable: """ Creates a decorator to check if a property's value is positive. @@ -101,8 +98,8 @@ def is_positive(clamp: bool = False): Callable: Decorator function. """ - def decorator(func): - def wrapper(instance, *args, **kwargs): + def decorator(func: Callable) -> Callable: + def wrapper(instance: object, *args: Any, **kwargs: Any) -> Any: value = func(instance, *args, **kwargs) if value <= 0: if clamp: @@ -115,7 +112,7 @@ def wrapper(instance, *args, **kwargs): return decorator -def is_zero(clamp: bool = False): +def is_zero(clamp: bool = False) -> Callable: """ Creates a decorator to check if a property's value is zero. @@ -126,8 +123,8 @@ def is_zero(clamp: bool = False): Callable: Decorator function. """ - def decorator(func): - def wrapper(instance, *args, **kwargs): + def decorator(func: Callable) -> Callable: + def wrapper(instance: object, *args: Any, **kwargs: Any) -> Any: value = func(instance, *args, **kwargs) if value != 0: if clamp: @@ -140,14 +137,14 @@ def wrapper(instance, *args, **kwargs): return decorator -def is_within_range(min_value: float, max_value: float, clamp: bool = False): +def is_within_range(min_value: float, max_value: float, clamp: bool = False) -> Callable: """ Creates a decorator to check if a property's value is within a given range. Args: - min_value (float): Minimum value of the range. - max_value (float): Maximum value of the range. - clamp (bool): If True, the decorator will return the clamped value instead of raising an error. Defaults to False. + min_value: Minimum value of the range. + max_value: Maximum value of the range. + clamp: If True, the decorator will return the clamped value instead of raising an error. Defaults to False. Raises: ValueError: If the maximum value is less than or equal to the minimum value. @@ -159,8 +156,8 @@ def is_within_range(min_value: float, max_value: float, clamp: bool = False): if max_value <= min_value: raise ValueError("Maximum value must be greater than minimum value of range") - def decorator(func): - def wrapper(instance, *args, **kwargs): + def decorator(func: Callable) -> Callable: + def wrapper(instance: object, *args: Any, **kwargs: Any) -> Any: value = func(instance, *args, **kwargs) if value < min_value or value > max_value: if clamp: @@ -173,30 +170,28 @@ def wrapper(instance, *args, **kwargs): return decorator -def is_greater_than(min_value: float, clamp: bool = False, equality: bool = False): +def is_greater_than(min_value: float, clamp: bool = False, equality: bool = False) -> Callable: """ Creates a decorator to check if a property's value is greater than a given value. Gives user choice to implement is_greater_than_or_equal_to with equality bool Args: - min_value (float): Minimum value to check against. - clamp (bool): If True, the decorator will return the clamped value instead of raising an error. Defaults to False. - equality (bool): If True, the decorator will check for is greater than or equal to, instead of is greater than. + min_value: Minimum value to check against. + clamp: If True, the decorator will return the clamped value instead of raising an error. Defaults to False. + equality: If True, the decorator will check for is greater than or equal to, instead of is greater than. Returns: Callable: Decorator function. """ - def decorator(func): - def wrapper(instance, *args, **kwargs): + def decorator(func: Callable) -> Callable: + def wrapper(instance: object, *args: Any, **kwargs: Any) -> Any: value = func(instance, *args, **kwargs) if equality: if value < min_value: if clamp: return min_value - raise ValueError( - f"Value must be greater than or equal to {min_value}" - ) + raise ValueError(f"Value must be greater than or equal to {min_value}") else: if value <= min_value: if clamp: @@ -209,22 +204,22 @@ def wrapper(instance, *args, **kwargs): return decorator -def is_less_than(max_value: float, clamp: bool = False, equality: bool = False): +def is_less_than(max_value: float, clamp: bool = False, equality: bool = False) -> Callable: """ Creates a decorator to check if a property's value is less than a given value. Gives user choice to implement is_less_than_or_equal_to with equality bool Args: - max_value (float): Maximum value to check against. - clamp (bool): If True, the decorator will return the clamped value instead of raising an error. Defaults to False. - equality (bool): If True, the decorator will check for is less than or equal to, instead of is less than. + max_value: Maximum value to check against. + clamp: If True, the decorator will return the clamped value instead of raising an error. Defaults to False. + equality: If True, the decorator will check for is less than or equal to, instead of is less than. Returns: Callable: Decorator function. """ - def decorator(func): - def wrapper(instance, *args, **kwargs): + def decorator(func: Callable) -> Callable: + def wrapper(instance: object, *args: Any, **kwargs: Any) -> Any: value = func(instance, *args, **kwargs) if equality: if value > max_value: @@ -243,9 +238,10 @@ def wrapper(instance, *args, **kwargs): return decorator -def custom_criteria(criteria: Callable): # type: ignore +def custom_criteria(criteria: Callable) -> Callable: """ - Creates a decorator to check if a property's value meets a custom criteria. The criteria is a function that takes the property's value as an argument and returns a boolean. + Creates a decorator to check if a property's value meets a custom criteria. The criteria is a + function that takes the property's value as an argument and returns a boolean. Args: criteria (Callable): Custom criteria function. @@ -254,11 +250,11 @@ def custom_criteria(criteria: Callable): # type: ignore Callable: Decorator function. """ - def decorator(func): - def wrapper(instance, *args, **kwargs): + def decorator(func: Callable) -> Callable: + def wrapper(instance: object, *args: Any, **kwargs: Any) -> Any: value = func(instance, *args, **kwargs) if not criteria(value): - raise ValueError(f"Value does not meet custom criteria") + raise ValueError("Value does not meet custom criteria") return value return wrapper @@ -284,13 +280,18 @@ class SafetyDecorators: class SafetyManager: """ - The SafetyManager class enables the addition of safety decorators to an object's properties, specifically to their getters. When the 'start' method is invoked, these decorators are applied to the properties of the objects stored in the 'safe_objects' dictionary. The original objects are then replaced with subclasses that incorporate the decorated properties. Invoking the 'update' method accesses the properties of the objects in the 'safe_objects' dictionary, thereby triggering the decorators. + The SafetyManager class enables the addition of safety decorators to an object's properties, + specifically to their getters. When the 'start' method is invoked, these decorators are applied + to the properties of the objects stored in the 'safe_objects' dictionary. The original objects + are then replaced with subclasses that incorporate the decorated properties. + Invoking the 'update' method accesses the properties of the objects in the 'safe_objects' dictionary, + thereby triggering the decorators. """ - def __init__(self): + def __init__(self) -> None: self._safe_objects: dict[object, dict[str, list[Callable]]] = {} - def add_safety(self, instance: object, attribute: str, decorator: Callable): # type: ignore + def add_safety(self, instance: object, attribute: str, decorator: Callable) -> None: """ Adds a safety decorator to the given object's attribute. The decorator will be applied to the property's getter. @@ -301,9 +302,7 @@ def add_safety(self, instance: object, attribute: str, decorator: Callable): # """ if not hasattr(instance, attribute): - print( - f"Error: The attribute '{attribute}' does not exist in the given object." - ) + print(f"Error: The attribute '{attribute}' does not exist in the given object.") return original_attribute = getattr(instance.__class__, attribute, None) @@ -313,24 +312,21 @@ def add_safety(self, instance: object, attribute: str, decorator: Callable): # ) return - if instance in self._safe_objects.keys(): - if attribute in self._safe_objects[instance].keys(): + if instance in self._safe_objects: + if attribute in self._safe_objects[instance]: self._safe_objects[instance][attribute].append(decorator) else: self._safe_objects[instance][attribute] = [decorator] else: self._safe_objects[instance] = {attribute: [decorator]} - def start(self): + def start(self) -> None: """ Applies all decorators to the properties of the objects in the safe_objects dictionary. """ for container, safe_attributes in self.safe_objects.items(): - container_subclass = type( - f"{container.__class__.__name__}:SAFE", (container.__class__,), {} - ) + container_subclass = type(f"{container.__class__.__name__}:SAFE", (container.__class__,), {}) for attribute_name, attribute_decorators in safe_attributes.items(): - original_property = getattr(container.__class__, attribute_name) decorated_getter = original_property.fget for attribute_decorator in reversed(attribute_decorators): @@ -339,14 +335,12 @@ def start(self): setattr( container_subclass, attribute_name, - property( - decorated_getter, original_property.fset, original_property.fdel - ), + property(decorated_getter, original_property.fset, original_property.fdel), ) container.__class__ = container_subclass - def update(self): + def update(self) -> None: """ Accesses the properties of the objects in the safe_objects dictionary, thereby triggering the decorators. """ @@ -355,31 +349,31 @@ def update(self): getattr(container, attribute_name) @property - def safe_objects(self): + def safe_objects(self) -> dict[object, dict[str, list[Callable]]]: return self._safe_objects if __name__ == "__main__": class Sensor: - def __init__(self, value): - self._value = value - self._a = 10 + def __init__(self, value: float) -> None: + self._value: float = value + self._a: float = 10 @property - def value(self): + def value(self) -> float: return self._value @value.setter - def value(self, value): + def value(self, value: float) -> None: self._value = value @property - def a(self): + def a(self) -> float: return self._a @a.setter - def a(self, value): + def a(self, value: float) -> None: self._a = value sensor = Sensor(100) diff --git a/opensourceleg/sensors/adc.py b/opensourceleg/sensors/adc.py index 78d6179a..37172ca1 100644 --- a/opensourceleg/sensors/adc.py +++ b/opensourceleg/sensors/adc.py @@ -1,7 +1,6 @@ -from typing import List - import math from time import sleep +from typing import Any, ClassVar, Optional import spidev @@ -20,11 +19,11 @@ class ADS131M0x(ADCBase): _RESOLUTION = 24 _SPI_MODE = 1 - _BLANK_WORD = [0x00, 0x00, 0x00] + _BLANK_WORD: ClassVar = [0x00, 0x00, 0x00] # SPI Commands - _RESET_WORD = [0x00, 0x11, 0x00] - _STANDBY_WORD = [0x00, 0x22, 0x00] - _WAKEUP_WORD = [0x00, 0x33, 0x00] + _RESET_WORD: ClassVar = [0x00, 0x11, 0x00] + _STANDBY_WORD: ClassVar = [0x00, 0x22, 0x00] + _WAKEUP_WORD: ClassVar = [0x00, 0x33, 0x00] _RREG_PREFIX = 0b101 _WREG_PREFIX = 0b011 @@ -43,11 +42,11 @@ class ADS131M0x(ADCBase): _MODE_CFG = 0x0110 # Channel specific setting register addresses - _OCAL_MSB_ADDRS = [0x0A, 0x0F, 0x14, 0x19, 0x1E, 0x23, 0x28, 0x2D] - _OCAL_LSB_ADDRS = [0x0B, 0x10, 0x15, 0x1A, 0x1F, 0x24, 0x29, 0x2E] - _GCAL_MSB_ADDRS = [0x0C, 0x11, 0x16, 0x1B, 0x20, 0x25, 0x2A, 0x2F] - _GCAL_LSB_ADDRS = [0x0D, 0x12, 0x17, 0x1C, 0x21, 0x26, 0x2B, 0x30] - _CHANNEL_CFG_ADDRS = [0x09, 0x0E, 0x13, 0x18, 0x1D, 0x22, 0x27, 0x2C] + _OCAL_MSB_ADDRS: ClassVar = [0x0A, 0x0F, 0x14, 0x19, 0x1E, 0x23, 0x28, 0x2D] + _OCAL_LSB_ADDRS: ClassVar = [0x0B, 0x10, 0x15, 0x1A, 0x1F, 0x24, 0x29, 0x2E] + _GCAL_MSB_ADDRS: ClassVar = [0x0C, 0x11, 0x16, 0x1B, 0x20, 0x25, 0x2A, 0x2F] + _GCAL_LSB_ADDRS: ClassVar = [0x0D, 0x12, 0x17, 0x1C, 0x21, 0x26, 0x2B, 0x30] + _CHANNEL_CFG_ADDRS: ClassVar = [0x09, 0x0E, 0x13, 0x18, 0x1D, 0x22, 0x27, 0x2C] _GCAL_STEP_SIZE = 1.19e-7 @@ -59,7 +58,7 @@ def __init__( max_speed_hz: int = 8192000, channel_gains: list[int] = [32, 128] * 3, voltage_reference: float = 1.2, - gain_error: list[int] = [], + gain_error: Optional[list[int]] = None, ): """Initializes ADS131M0x class. @@ -70,21 +69,21 @@ def __init__( - max_speed_hz(int): Maximum clock frequency of the SPI communication. Default: 8192000 - channel_gains(List[int]): Gains of the programmable gain amplifier for all channels. Default: [32,128] * 3 - voltage_reference(float): Reference voltage used by the ADC. Default: 1.2 - - gain_error(List[int]): User-calculated integers used for correcting the gain of each channel for additional precision. Default: [] + - gain_error(List[int]): User-calculated integers used for correcting the gain of each channel for + additional precision. Default: [] Raises: - ValueError: If length of channel_gains is not equal to number of channels, or if gain is not a power of 2 between 1 and 128. + ValueError: If length of channel_gains is not equal to number of channels, or if gain is not a power of 2 + between 1 and 128. """ + if gain_error is None: + gain_error = [] if len(channel_gains) != num_channels: - raise ValueError( - "Length of channel_gains does not equal number of channels" - ) + raise ValueError("Length of channel_gains does not equal number of channels") if gain_error != [] and len(gain_error) != num_channels: - raise ValueError( - "Length of channel_gains does not equal number of channels" - ) + raise ValueError("Length of channel_gains does not equal number of channels") self._spi_bus = spi_bus self._spi_chip = spi_chip @@ -110,7 +109,7 @@ def __init__( self._data = [0.0] * num_channels def __repr__(self) -> str: - return f"ADS131M0x" + return "ADS131M0x" def start(self) -> None: """Opens SPI port, calibrates ADC, and begins streaming ADC data.""" @@ -131,9 +130,7 @@ def stop(self) -> None: def reset(self) -> None: """Resets all register values.""" - self._spi.xfer2( - self._RESET_WORD + self._BLANK_WORD * (self._words_per_frame - 1) - ) + self._spi.xfer2(self._RESET_WORD + self._BLANK_WORD * (self._words_per_frame - 1)) def update(self) -> None: """Reads ADC data.""" @@ -181,21 +178,19 @@ def gains(self) -> list[int]: return self._gains @property - def data(self): + def data(self) -> Any: return self._data - def _spi_message(self, bytes: list[int]) -> list[int]: + def _spi_message(self, message: list[int]) -> list[int]: """Send SPI message to ADS131M0x. Args: - - bytes(List[int]): message to be sent to the ADS131M0x separated into bytes. + - message(List[int]): message to be sent to the ADS131M0x separated into message. Returns: The response to the message sent, including the entire frame following the response. """ - self._spi.xfer2(bytes) - return (list[int])( - self._spi.readbytes(self._BYTES_PER_WORD * self._words_per_frame) - ) + self._spi.xfer2(message) + return (list[int])(self._spi.readbytes(self._BYTES_PER_WORD * self._words_per_frame)) def _channel_enable(self, state: bool) -> None: """Enables or disables streaming on all channels. @@ -203,9 +198,9 @@ def _channel_enable(self, state: bool) -> None: Arg: - state(bool): sets whether or not the ADC is streaming """ - if state == True: + if state is True: self.write_register(self._CLOCK_REG, self._ENABLE_CHANNELS_CLOCK) - elif state == False: + elif state is False: self.write_register(self._CLOCK_REG, self._DISABLE_CHANNELS_CLOCK) def _set_device_state(self, state: int) -> None: @@ -217,14 +212,10 @@ def _set_device_state(self, state: int) -> None: 1 -- Continuous Conversion Mode """ if state == 0: - self._spi.xfer2( - self._STANDBY_WORD + self._BLANK_WORD * (self._words_per_frame - 1) - ) + self._spi.xfer2(self._STANDBY_WORD + self._BLANK_WORD * (self._words_per_frame - 1)) self._streaming = False elif state == 1: - self._spi.xfer2( - self._WAKEUP_WORD + self._BLANK_WORD * (self._words_per_frame - 1) - ) + self._spi.xfer2(self._WAKEUP_WORD + self._BLANK_WORD * (self._words_per_frame - 1)) self._streaming = True def _set_voltage_source(self, source: int) -> None: @@ -290,8 +281,7 @@ def _ready_to_read(self) -> bool: def _read_data_millivolts(self) -> list[float]: """Returns a List representing the voltage in millivolts for all channels of the ADC""" mV = [ - 1000 * ((dat) / (2 ** (self._RESOLUTION - 1)) * self._voltage_reference) - for dat in self._read_data_counts() + 1000 * ((dat) / (2 ** (self._RESOLUTION - 1)) * self._voltage_reference) for dat in self._read_data_counts() ] return mV diff --git a/opensourceleg/sensors/base.py b/opensourceleg/sensors/base.py index 5ca8b6ee..57049c75 100644 --- a/opensourceleg/sensors/base.py +++ b/opensourceleg/sensors/base.py @@ -1,19 +1,20 @@ -from abc import ABC, abstractmethod +from abc import ABC, abstractmethod from functools import wraps - -import numpy as np +from typing import Any, Callable class SensorNotStreamingException(Exception): def __init__(self, sensor_name: str = "Sensor") -> None: super().__init__( - f"{sensor_name} is not streaming, please ensure that the connections are intact, power is on, and the start method is called." + f"{sensor_name} is not streaming, please ensure that the connections are intact, " + f"power is on, and the start method is called." ) -def check_sensor_stream(func): +def check_sensor_stream(func: Callable) -> Callable: @wraps(func) - def wrapper(self, *args, **kwargs): + def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: + # TODO: This could be a generic type that points to actuator, sensor, etc. if not self.is_streaming: raise SensorNotStreamingException(sensor_name=self.__repr__()) return func(self, *args, **kwargs) @@ -22,15 +23,12 @@ def wrapper(self, *args, **kwargs): class SensorBase(ABC): - def __init__(self) -> None: - pass - def __repr__(self) -> str: - return f"SensorBase" + return "SensorBase" @property @abstractmethod - def data(self): + def data(self) -> Any: pass @abstractmethod @@ -45,11 +43,11 @@ def stop(self) -> None: def update(self) -> None: pass - def __enter__(self): + def __enter__(self) -> "SensorBase": self.start() return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: self.stop() @property @@ -63,7 +61,7 @@ def __init__(self) -> None: super().__init__() def __repr__(self) -> str: - return f"ADCBase" + return "ADCBase" def reset(self) -> None: pass @@ -77,7 +75,7 @@ def __init__(self) -> None: super().__init__() def __repr__(self) -> str: - return f"EncoderBase" + return "EncoderBase" @property @abstractmethod @@ -95,7 +93,7 @@ def __init__(self) -> None: pass def __repr__(self) -> str: - return f"LoadcellBase" + return "LoadcellBase" @abstractmethod def calibrate(self) -> None: @@ -146,7 +144,7 @@ def __init__(self) -> None: pass def __repr__(self) -> str: - return f"IMU" + return "IMU" @property @abstractmethod diff --git a/opensourceleg/sensors/imu.py b/opensourceleg/sensors/imu.py index 2d812cb3..989c6a01 100644 --- a/opensourceleg/sensors/imu.py +++ b/opensourceleg/sensors/imu.py @@ -1,8 +1,4 @@ -from typing import List, Union - -import os -import time -from dataclasses import dataclass +from typing import Any, Union from opensourceleg.logging import LOGGER from opensourceleg.sensors.base import IMUBase, check_sensor_stream @@ -13,24 +9,26 @@ sys.path.append("/usr/share/python3-mscl") import mscl except ImportError: - LOGGER.error( - "Failed to import mscl. Please install the MSCL library from Lord Microstrain and append the path to the PYTHONPATH or sys.path. Checkout https://github.com/LORD-MicroStrain/MSCL/tree/master and https://lord-microstrain.github.io/MSCL/Documentation/MSCL%20API%20Documentation/index.html" + print( + "Failed to import mscl. Please install the MSCL library from Lord Microstrain and append the path" + "to the PYTHONPATH or sys.path. Checkout https://github.com/LORD-MicroStrain/MSCL/tree/master" + "and https://lord-microstrain.github.io/MSCL/Documentation/MSCL%20API%20Documentation/index.html" ) try: import adafruit_bno055 except ImportError: - LOGGER.error("Failed to import adafruit_bno055") + print("Failed to import adafruit_bno055") try: import board except ImportError: - LOGGER.error("Failed to import board") + print("Failed to import board") try: import busio except ImportError: - LOGGER.error("Failed to import busio") + print("Failed to import busio") class LordMicrostrainIMU(IMUBase): @@ -51,8 +49,7 @@ def __init__( port: str = "/dev/ttyUSB0", baud_rate: int = 921600, frequency: int = 200, - ): - + ) -> None: self._port = port self._baud_rate = baud_rate self._frequency = frequency @@ -60,7 +57,7 @@ def __init__( self._connection = None self._data: dict[str, float] = {} - def _configure_mip_channels(self): + def _configure_mip_channels(self) -> Any: channels = mscl.MipChannels() channels.append( mscl.MipChannel( @@ -89,23 +86,20 @@ def _configure_mip_channels(self): return channels - def start(self): - + def start(self) -> None: self._connection = mscl.Connection.Serial(self.port, self.baud_rate) self._node = mscl.InertialNode(self._connection) - self._node.setActiveChannelFields( - mscl.MipTypes.CLASS_ESTFILTER, self._configure_mip_channels() - ) + self._node.setActiveChannelFields(mscl.MipTypes.CLASS_ESTFILTER, self._configure_mip_channels()) self._node.enableDataStream(mscl.MipTypes.CLASS_ESTFILTER) self._is_streaming = True @check_sensor_stream - def stop(self): + def stop(self) -> None: self._node.setToIdle() self._is_streaming = False @check_sensor_stream - def ping(self): + def ping(self) -> None: response = self._node.ping() if response.success(): @@ -113,23 +107,21 @@ def ping(self): else: LOGGER.error(f"Failed to ping the IMU at {self.port}") - def update( - self, timeout: int = 500, max_packets: int = 1, return_packets: bool = False - ): + def update(self, timeout: int = 500, max_packets: int = 1, return_packets: bool = False) -> Union[None, Any]: """ Get IMU data from the Lord Microstrain IMU """ - data_packets = self._node.getDataPackets( - timeout=timeout, maxPackets=max_packets - ) + data_packets = self._node.getDataPackets(timeout=timeout, maxPackets=max_packets) data_points = data_packets[-1].data() self._data = {data.channelName(): data.as_float() for data in data_points} if return_packets: return data_packets + else: + return None def __repr__(self) -> str: - return f"IMULordMicrostrain" + return "IMULordMicrostrain" @property def port(self) -> str: @@ -239,34 +231,33 @@ class BNO055(IMUBase): def __init__( self, addr: int = 40, - ): + ) -> None: self._address: int = addr self._gyro_data: list[float] = [0.0, 0.0, 0.0] self._acc_data: list[float] = [0.0, 0.0, 0.0] self._is_streaming = False def __repr__(self) -> str: - return f"BNO055_IMU" + return "BNO055_IMU" - def start(self): + def start(self) -> None: i2c = busio.I2C(board.SCL, board.SDA) try: self._adafruit_imu = adafruit_bno055.BNO055_I2C(i2c, address=self._address) - except ValueError as ve: + except ValueError: print("BNO055 IMU Not Found on i2c bus! Check wiring!") self.configure_IMU_settings() self._is_streaming = True - def stop(self): + def stop(self) -> None: self._is_streaming = False - pass - def update(self): + def update(self) -> None: self._acc_data = self._adafruit_imu.acceleration self._gyro_data = self._adafruit_imu.gyro - def configure_IMU_settings(self): + def configure_IMU_settings(self) -> None: """ Configure the IMU mode and different range/bandwidth settings. Hard coding settings for now. diff --git a/opensourceleg/sensors/loadcell.py b/opensourceleg/sensors/loadcell.py index 23363cb2..2844484f 100644 --- a/opensourceleg/sensors/loadcell.py +++ b/opensourceleg/sensors/loadcell.py @@ -1,8 +1,6 @@ -from typing import Any, Callable, Union - import time -from dataclasses import dataclass from enum import Enum +from typing import Any, Callable, Optional import numpy as np import numpy.typing as npt @@ -49,19 +47,13 @@ def __init__( """ # Check that parameters are set correctly: if calibration_matrix.shape != (6, 6): - LOGGER.info( - f"[{self.__repr__()}] calibration_matrix must be a 6x6 array of np.double." - ) + LOGGER.info(f"[{self.__repr__()}] calibration_matrix must be a 6x6 array of np.double.") raise TypeError("calibration_matrix must be a 6x6 array of np.double.") if amp_gain <= 0: - LOGGER.info( - f"[{self.__repr__()}] amp_gain must be a floating point value greater than 0." - ) + LOGGER.info(f"[{self.__repr__()}] amp_gain must be a floating point value greater than 0.") raise ValueError("amp_gain must be a floating point value greater than 0.") if exc <= 0: - LOGGER.info( - f"[{self.__repr__()}] exc must be a floating point value greater than 0." - ) + LOGGER.info(f"[{self.__repr__()}] exc must be a floating point value greater than 0.") raise ValueError("exc must be a floating point value greater than 0.") self._amp_gain: float = amp_gain @@ -76,9 +68,7 @@ def __init__( self._prev_data: npt.NDArray[np.double] = self._data self._failed_reads = 0 - self._calibration_offset: npt.NDArray[np.double] = np.zeros( - shape=(1, 6), dtype=np.double - ) + self._calibration_offset: npt.NDArray[np.double] = np.zeros(shape=(1, 6), dtype=np.double) self._zero_calibration_offset: npt.NDArray[np.double] = self._calibration_offset self._is_calibrated: bool = False self._is_streaming: bool = False @@ -88,14 +78,14 @@ def start(self) -> None: time.sleep(1) self._is_streaming = True - def reset(self): + def reset(self) -> None: self._calibration_offset = self._zero_calibration_offset self._is_calibrated = False def update( self, - calibration_offset: npt.NDArray[np.double] = None, - data_callback: Callable[..., npt.NDArray[np.uint8]] = None, + calibration_offset: Optional[npt.NDArray[np.double]] = None, + data_callback: Optional[Callable[..., npt.NDArray[np.uint8]]] = None, ) -> None: """ Queries the loadcell for the latest data. @@ -109,16 +99,13 @@ def update( signed_data = ((data - self.OFFSET) / self.ADC_RANGE) * self._exc coupled_data = signed_data * 1000 / (self._exc * self._amp_gain) - self._data = ( - np.transpose(a=self._calibration_matrix.dot(b=np.transpose(a=coupled_data))) - - calibration_offset - ) + self._data = np.transpose(a=self._calibration_matrix.dot(b=np.transpose(a=coupled_data))) - calibration_offset def calibrate( self, number_of_iterations: int = 2000, reset: bool = False, - data_callback: Callable[[], npt.NDArray[np.uint8]] = None, + data_callback: Optional[Callable[[], npt.NDArray[np.uint8]]] = None, ) -> None: """ Obtains the initial loadcell reading (aka) loadcell_zero. @@ -140,9 +127,7 @@ def calibrate( data_callback=data_callback, ) iterative_calibration_offset = self._data - self._calibration_offset = ( - iterative_calibration_offset + self._calibration_offset - ) / 2.0 + self._calibration_offset = (iterative_calibration_offset + self._calibration_offset) / 2.0 self._is_calibrated = True LOGGER.info(f"[{self.__repr__()}] Calibration routine complete.") @@ -153,7 +138,8 @@ def calibrate( else: LOGGER.info( - f"[{self.__repr__()}] Loadcell has already been zeroed. To recalibrate, set reset=True in the calibrate method or call reset() first." + f"[{self.__repr__()}] Loadcell has already been zeroed. " + "To recalibrate, set reset=True in the calibrate method or call reset() first." ) def stop(self) -> None: @@ -161,23 +147,21 @@ def stop(self) -> None: if hasattr(self, "_smbus"): self._smbus.close() - def _read_compressed_strain(self): + def _read_compressed_strain(self) -> Any: """Used for more recent versions of strain amp firmware""" try: - data = self._smbus.read_i2c_block_data( - self._i2c_address, MEMORY_CHANNELS.CH1_H, 10 - ) + data = self._smbus.read_i2c_block_data(self._i2c_address, MEMORY_CHANNELS.CH1_H, 10) self.failed_reads = 0 - except OSError as e: + except OSError: self.failed_reads += 1 if self.failed_reads >= 5: - raise Exception("Load cell unresponsive.") + raise LoadcellNotRespondingException("Load cell unresponsive.") from None - return self._unpack_compressed_strain(data) + return self._unpack_compressed_strain(np.array(object=data, dtype=np.uint8)) @staticmethod - def _unpack_uncompressed_strain(data): + def _unpack_uncompressed_strain(data: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint16]: """Used for an older version of the strain amp firmware (at least pre-2017)""" ch1 = (data[0] << 8) | data[1] ch2 = (data[2] << 8) | data[3] @@ -188,7 +172,7 @@ def _unpack_uncompressed_strain(data): return np.array(object=[ch1, ch2, ch3, ch4, ch5, ch6]) @staticmethod - def _unpack_compressed_strain(data): + def _unpack_compressed_strain(data: npt.NDArray[np.uint8]) -> npt.NDArray[np.uint16]: """Used for more recent versions of strainamp firmware""" return np.array( object=[ @@ -219,7 +203,7 @@ def fx(self) -> float: return float(self.data[0]) @property - def fy(self): + def fy(self) -> float: """ Latest force in the y (anterior/posterior) direction in Newtons. If using the standard OSL setup, this is positive in the posterior direction. @@ -227,7 +211,7 @@ def fy(self): return float(self.data[1]) @property - def fz(self): + def fz(self) -> float: """ Latest force in the z (vertical) direction in Newtons. If using the standard OSL setup, this should be positive downwards. @@ -236,7 +220,7 @@ def fz(self): return float(self.data[2]) @property - def mx(self): + def mx(self) -> float: """ Latest moment about the x (medial/lateral) axis in Nm. If using the standard OSL setup, this axis is positive towards the user's right. @@ -244,7 +228,7 @@ def mx(self): return float(self.data[3]) @property - def my(self): + def my(self) -> float: """ Latest moment about the y (anterior/posterior) axis in Nm. If using the standard OSL setup, this axis is positive in the posterior direction. @@ -252,7 +236,7 @@ def my(self): return float(self.data[4]) @property - def mz(self): + def mz(self) -> float: """ Latest moment about the z (vertical) axis in Nm. If using the standard OSL setup, this axis is positive towards the ground. @@ -260,7 +244,7 @@ def mz(self): return float(self.data[5]) @property - def data(self): + def data(self) -> Any: """ Returns a vector of the latest loadcell data. [Fx, Fy, Fz, Mx, My, Mz] diff --git a/opensourceleg/time/__init__.py b/opensourceleg/time/__init__.py index 6c5a5d6e..fa11fcb5 100644 --- a/opensourceleg/time/__init__.py +++ b/opensourceleg/time/__init__.py @@ -9,7 +9,8 @@ Key Classes: -- `SoftRealtimeLoop` class is used to create a soft real-time loop that runs at a specified frequency. It also handles signal interruptions gracefully. +- `SoftRealtimeLoop` class is used to create a soft real-time loop that runs at a specified frequency. + It also handles signal interruptions gracefully. """ __all__ = ["SoftRealtimeLoop"] diff --git a/opensourceleg/time/time.py b/opensourceleg/time/time.py index 47e0851d..6db4a7c7 100644 --- a/opensourceleg/time/time.py +++ b/opensourceleg/time/time.py @@ -1,6 +1,7 @@ import signal import time from math import sqrt +from typing import Any, Callable, Optional PRECISION_OF_SLEEP = 0.0001 @@ -15,8 +16,8 @@ class LoopKiller: Typically, it detects the CTRL-C from your keyboard, which sends a SIGTERM signal. the function_in_loop argument to the Soft Realtime Loop's blocking_loop method is the function to be run every loop. - A typical usage would set function_in_loop to be a method of an object, so that the object could store program state. - See the 'ifmain' for two examples. + A typical usage would set function_in_loop to be a method of an object, so that the object + could store program state. See the 'ifmain' for two examples. # This library will soon be hosted as a PIP module and added as a python dependency. # https://github.com/UM-LoCoLab/NeuroLocoMiddleware/blob/main/SoftRealtimeLoop.py @@ -26,20 +27,20 @@ class LoopKiller: """ - def __init__(self, fade_time=0.0): + def __init__(self, fade_time: float = 0.0): signal.signal(signal.SIGTERM, self.handle_signal) signal.signal(signal.SIGINT, self.handle_signal) signal.signal(signal.SIGHUP, self.handle_signal) - self._fade_time = fade_time - self._soft_kill_time = None + self._fade_time: float = fade_time + self._soft_kill_time: float = 0.0 def __repr__(self) -> str: - return f"LoopKiller" + return "LoopKiller" - def handle_signal(self, signum, frame): + def handle_signal(self, signum: Any, frame: Any) -> None: self.kill_now = True - def get_fade(self): + def get_fade(self) -> float: # interpolates from 1 to zero with soft fade out if self._kill_soon: t = time.monotonic() - self._soft_kill_time @@ -52,7 +53,7 @@ def get_fade(self): _kill_soon = False @property - def kill_now(self): + def kill_now(self) -> bool: if self._kill_now: return True if self._kill_soon: @@ -62,7 +63,7 @@ def kill_now(self): return self._kill_now @kill_now.setter - def kill_now(self, val): + def kill_now(self, val: bool) -> None: if val: if self._kill_soon: # if you kill twice, then it becomes immediate self._kill_now = True @@ -75,7 +76,7 @@ def kill_now(self, val): else: self._kill_now = False self._kill_soon = False - self._soft_kill_time = None + self._soft_kill_time = 0.0 class SoftRealtimeLoop: @@ -88,8 +89,8 @@ class SoftRealtimeLoop: Typically, it detects the CTRL-C from your keyboard, which sends a SIGTERM signal. the function_in_loop argument to the Soft Realtime Loop's blocking_loop method is the function to be run every loop. - A typical usage would set function_in_loop to be a method of an object, so that the object could store program state. - See the 'ifmain' for two examples. + A typical usage would set function_in_loop to be a method of an object, so that the object could store + program state. See the 'ifmain' for two examples. This library will soon be hosted as a PIP module and added as a python dependency. https://github.com/UM-LoCoLab/NeuroLocoMiddleware/blob/main/SoftRealtimeLoop.py @@ -99,21 +100,22 @@ class SoftRealtimeLoop: """ - def __init__(self, dt=0.001, report=False, fade=0.0): - self.t0 = self.t1 = time.monotonic() - self.killer = LoopKiller(fade_time=fade) - self.dt = dt - self.ttarg = None - self.sum_err = 0.0 - self.sum_var = 0.0 - self.sleep_t_agg = 0.0 - self.n = 0 - self.report = report + def __init__(self, dt: float = 0.001, report: bool = False, fade: float = 0.0): + self.t0: float = time.monotonic() + self.t1: float = self.t0 + self.killer: LoopKiller = LoopKiller(fade_time=fade) + self.dt: float = dt + self.ttarg: Any = None + self.sum_err: float = 0.0 + self.sum_var: float = 0.0 + self.sleep_t_agg: float = 0.0 + self.n: int = 0 + self.report: bool = report def __repr__(self) -> str: - return f"SoftRealtimeLoop" + return "SoftRealtimeLoop" - def __del__(self): + def __del__(self) -> None: if self.report: print("In %d cycles at %.2f Hz:" % (self.n, 1.0 / self.dt)) print("\tavg error: %.3f milliseconds" % (1e3 * self.sum_err / self.n)) @@ -121,16 +123,13 @@ def __del__(self): "\tstddev error: %.3f milliseconds" % (1e3 * sqrt((self.sum_var - self.sum_err**2 / self.n) / (self.n - 1))) ) - print( - "\tpercent of time sleeping: %.1f %%" - % (self.sleep_t_agg / self.time() * 100.0) - ) + print("\tpercent of time sleeping: %.1f %%" % (self.sleep_t_agg / self.time() * 100.0)) @property - def fade(self): + def fade(self) -> float: return self.killer.get_fade() - def run(self, function_in_loop, dt=None): + def run(self, function_in_loop: Callable, dt: Optional[float] = None) -> None: if dt is None: dt = self.dt self.t0 = self.t1 = time.monotonic() + dt @@ -139,44 +138,35 @@ def run(self, function_in_loop, dt=None): if ret == 0: self.stop() while time.monotonic() < self.t1 and not self.killer.kill_now: - if signal.sigtimedwait( - [signal.SIGTERM, signal.SIGINT, signal.SIGHUP], 0 - ): + if signal.sigtimedwait([signal.SIGTERM, signal.SIGINT, signal.SIGHUP], 0): self.stop() self.t1 += dt - def stop(self): + def stop(self) -> None: self.killer.kill_now = True - def time(self): + def time(self) -> float: return time.monotonic() - self.t0 - def time_since(self): + def time_since(self) -> float: return time.monotonic() - self.t1 - def __iter__(self): + def __iter__(self) -> "SoftRealtimeLoop": self.t0 = self.t1 = time.monotonic() + self.dt return self - def __next__(self): + def __next__(self) -> float: if self.killer.kill_now: raise StopIteration - while ( - time.monotonic() < self.t1 - 2 * PRECISION_OF_SLEEP - and not self.killer.kill_now - ): + while time.monotonic() < self.t1 - 2 * PRECISION_OF_SLEEP and not self.killer.kill_now: t_pre_sleep = time.monotonic() - time.sleep( - max(PRECISION_OF_SLEEP, self.t1 - time.monotonic() - PRECISION_OF_SLEEP) - ) + time.sleep(max(PRECISION_OF_SLEEP, self.t1 - time.monotonic() - PRECISION_OF_SLEEP)) self.sleep_t_agg += time.monotonic() - t_pre_sleep while time.monotonic() < self.t1 and not self.killer.kill_now: try: - if signal.sigtimedwait( - [signal.SIGTERM, signal.SIGINT, signal.SIGHUP], 0 - ): + if signal.sigtimedwait([signal.SIGTERM, signal.SIGINT, signal.SIGHUP], 0): self.stop() except AttributeError: pass diff --git a/opensourceleg/units/__init__.py b/opensourceleg/units/__init__.py index 46758c47..b624d670 100644 --- a/opensourceleg/units/__init__.py +++ b/opensourceleg/units/__init__.py @@ -1,4 +1,4 @@ -from opensourceleg.units.units import * +from .units import * # noqa: F403 """ Units module for opensourceleg library. diff --git a/opensourceleg/units/units.py b/opensourceleg/units/units.py index 54c8837a..30768787 100644 --- a/opensourceleg/units/units.py +++ b/opensourceleg/units/units.py @@ -1,6 +1,5 @@ # Global Units Dictionary -import enum -from dataclasses import dataclass +from enum import Enum """ Global Units Dictionary @@ -11,123 +10,87 @@ Usage: 1. Utilize the `force`, `torque`, etc., classes to represent quantities and configure their unit conversion factors. -2. Use the `convert_to_default` function to convert a value from a user unit to the default unit for the corresponding quantity. +2. Use the `convert_to_default` function to convert a value from a user unit to the default unit + for the corresponding quantity. 3. Use the `convert_from_default` function to convert a value from the default unit to a user-specified unit. """ - -@dataclass -class force: +__all__ = [ + "Force", + "Torque", + "Stiffness", + "Damping", + "Length", + "Position", + "Mass", + "Velocity", + "Acceleration", + "Current", + "Voltage", + "convert_to_default", + "convert_from_default", +] + + +class Force(float, Enum): N = 1.0 lbf = 4.4482216152605 kgf = 9.80665 - def __repr__(self) -> str: - return f"force" - -@dataclass -class torque: +class Torque(float, Enum): N_m = 1.0 lbf_inch = 0.1129848290276167 kgf_cm = 0.0980665 - def __repr__(self) -> str: - return f"torque" - -@dataclass -class stiffness: +class Stiffness(float, Enum): N_m_per_rad = 1.0 N_m_per_deg = 0.017453292519943295 - def __repr__(self) -> str: - return f"stiffness" - -@dataclass -class damping: +class Damping(float, Enum): N_m_per_rad_per_s = 1.0 N_m_per_deg_per_s = 0.017453292519943295 - def __repr__(self) -> str: - return f"damping" - -@dataclass -class length: +class Length(float, Enum): m = 1.0 cm = 0.01 inch = 0.0254 - def __repr__(self) -> str: - return f"length" - -@dataclass -class position: +class Position(float, Enum): rad = 1.0 deg = 0.017453292519943295 - def __repr__(self) -> str: - return f"position" - -@dataclass -class mass: +class Mass(float, Enum): kg = 1.0 g = 0.001 lb = 0.45359237 - def __repr__(self) -> str: - return f"mass" - -@dataclass -class velocity: +class Velocity(float, Enum): rad_per_s = 1.0 deg_per_s = 0.017453292519943295 rpm = 0.10471975511965977 - def __repr__(self) -> str: - return f"velocity" - -@dataclass -class acceleration: +class Acceleration(float, Enum): rad_per_s2 = 1.0 deg_per_s2 = 0.017453292519943295 - def __repr__(self) -> str: - return f"acceleration" - - -@dataclass -class time: - s = 1.0 - ms = 0.001 - - def __repr__(self) -> str: - return f"time" - -@dataclass -class current: +class Current(float, Enum): mA = 1 A = 1000 - def __repr__(self) -> str: - return f"current" - -@dataclass -class voltage: +class Voltage(float, Enum): mV = 1 V = 1000 - def __repr__(self) -> str: - return f"voltage" - def convert_to_default(value: float, from_unit: float) -> float: """ @@ -141,10 +104,10 @@ def convert_to_default(value: float, from_unit: float) -> float: float: Converted value in default units. Example: - >>> convert_to_default(2, units.current.A) + >>> convert_to_default(2, units.Current.A) 2000 # returns value in mA, which is the default unit for current - >>> convert_to_default(10, units.voltage.V) + >>> convert_to_default(10, units.Voltage.V) 10000 # returns value in mV, which is the default unit for voltage >>> convert_to_default(45, units.position.deg) @@ -166,10 +129,10 @@ def convert_from_default(value: float, to_unit: float) -> float: Example: - >>> convert_from_default(2000, units.current.A) + >>> convert_from_default(2000, units.Current.A) 2.0 # A - >>> convert_from_default(10000, units.voltage.V) + >>> convert_from_default(10000, units.Voltage.V) 10.0 # V >>> convert_from_default(0.7853981633974483, units.position.deg) @@ -180,9 +143,9 @@ def convert_from_default(value: float, to_unit: float) -> float: if __name__ == "__main__": - print(convert_to_default(2, current.A)) - print(convert_to_default(10, voltage.V)) - print(convert_to_default(45, position.deg)) - print(convert_from_default(2000, current.A)) - print(convert_from_default(10000, voltage.V)) - print(convert_from_default(0.7853981633974483, position.deg)) + print(convert_to_default(2, Current.A)) + print(convert_to_default(10, Voltage.V)) + print(convert_to_default(45, Position.deg)) + print(convert_from_default(2000, Current.A)) + print(convert_from_default(10000, Voltage.V)) + print(convert_from_default(0.7853981633974483, Position.deg)) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..8ff055f3 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,3013 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "adafruit-blinka" +version = "8.50.0" +description = "CircuitPython APIs for non-CircuitPython versions of Python such as CPython on Linux and MicroPython." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "Adafruit_Blinka-8.50.0-py3-none-any.whl", hash = "sha256:a0832316ac4155df956a1e35561c75e37e6f8d8e2f28b58859c80207e7819351"}, + {file = "adafruit_blinka-8.50.0.tar.gz", hash = "sha256:64feecaa7f806fdd02b39521b5b400ebfb629a06a27a3360644375af0bbe67c2"}, +] + +[package.dependencies] +adafruit-circuitpython-typing = "*" +Adafruit-PlatformDetect = ">=3.70.1" +Adafruit-PureIO = ">=1.1.7" +binho-host-adapter = ">=0.1.6" +pyftdi = ">=0.40.0" +sysv-ipc = {version = ">=1.1.0", markers = "sys_platform == \"linux\" and platform_machine != \"mips\""} + +[[package]] +name = "adafruit-circuitpython-bno055" +version = "5.4.15" +description = "CircuitPython library for BNO055 9-DOF absolute orientation sensor." +optional = false +python-versions = "*" +files = [ + {file = "adafruit_circuitpython_bno055-5.4.15-py3-none-any.whl", hash = "sha256:8df0b295684c25e032791b415546c73f43ad3fbb2a8a5a69be8bc5820bb2c78a"}, + {file = "adafruit_circuitpython_bno055-5.4.15.tar.gz", hash = "sha256:3cba6cbe0e6e0b9854de8e244d7c14115bfd8c708d2a1442573677e7fb3c154d"}, +] + +[package.dependencies] +Adafruit-Blinka = "*" +adafruit-circuitpython-busdevice = "*" +adafruit-circuitpython-register = "*" + +[[package]] +name = "adafruit-circuitpython-busdevice" +version = "5.2.10" +description = "CircuitPython bus device classes to manage bus sharing." +optional = false +python-versions = "*" +files = [ + {file = "adafruit_circuitpython_busdevice-5.2.10-py3-none-any.whl", hash = "sha256:9e37e303b9cced482aec004eb37a6480c5ccac60d3cfe6ea4d9c4c0a818f1fc0"}, + {file = "adafruit_circuitpython_busdevice-5.2.10.tar.gz", hash = "sha256:b3797242cb2aeb27ed6382f7946d62ab4d6dbb16631e71e50b12ba07afec0e87"}, +] + +[package.dependencies] +Adafruit-Blinka = ">=7.0.0" +adafruit-circuitpython-typing = "*" + +[[package]] +name = "adafruit-circuitpython-connectionmanager" +version = "3.1.2" +description = "A urllib3.poolmanager/urllib3.connectionpool-like library for managing sockets and connections" +optional = false +python-versions = "*" +files = [ + {file = "adafruit_circuitpython_connectionmanager-3.1.2-py3-none-any.whl", hash = "sha256:1d3af302e587d4ab0827f4382d2eeb400a152688a2eb14272189f971d7ff477b"}, + {file = "adafruit_circuitpython_connectionmanager-3.1.2.tar.gz", hash = "sha256:330485ac00da6c5f1ca567e77493cddb55ab9674afed341eddf5f9b85bd22fdb"}, +] + +[package.dependencies] +Adafruit-Blinka = "*" + +[[package]] +name = "adafruit-circuitpython-lis3dh" +version = "5.2.3" +description = "CircuitPython library for LIS3DH accelerometer." +optional = false +python-versions = "*" +files = [ + {file = "adafruit_circuitpython_lis3dh-5.2.3-py3-none-any.whl", hash = "sha256:83743264020fab01aa7ced227b24bc71b437422f8915dbe7527b132754f80e2a"}, + {file = "adafruit_circuitpython_lis3dh-5.2.3.tar.gz", hash = "sha256:f4d83215141b1965562f024fca4036703f41a930106354cb3af7ac92f6859701"}, +] + +[package.dependencies] +Adafruit-Blinka = "*" +adafruit-circuitpython-busdevice = "*" + +[[package]] +name = "adafruit-circuitpython-register" +version = "1.10.1" +description = "CircuitPython data descriptor classes to represent hardware registers on I2C and SPI devices." +optional = false +python-versions = "*" +files = [ + {file = "adafruit_circuitpython_register-1.10.1-py3-none-any.whl", hash = "sha256:872b0da50a4fe18464d1125002194fddf2fa8627f11650e90547e519c7f846d8"}, + {file = "adafruit_circuitpython_register-1.10.1.tar.gz", hash = "sha256:452aeedd515fb26405b100291bfc69df639d82dfac02087367508566123490a3"}, +] + +[package.dependencies] +Adafruit-Blinka = "*" +adafruit-circuitpython-busdevice = "*" +adafruit-circuitpython-typing = ">=1.3.1,<2.dev0" +typing-extensions = ">=4.0,<5.0" + +[[package]] +name = "adafruit-circuitpython-requests" +version = "4.1.8" +description = "A requests-like library for web interfacing" +optional = false +python-versions = "*" +files = [ + {file = "adafruit_circuitpython_requests-4.1.8-py3-none-any.whl", hash = "sha256:6ab3744175f1ddaf2ecfb2510c9ea2cf717c21848695849fcdf28de6e856fd04"}, + {file = "adafruit_circuitpython_requests-4.1.8.tar.gz", hash = "sha256:50d787bc05d953318f4ee654230195e631eb379e33179c465a68f763d4638914"}, +] + +[package.dependencies] +Adafruit-Blinka = "*" +Adafruit-Circuitpython-ConnectionManager = "*" + +[package.extras] +optional = ["requests"] + +[[package]] +name = "adafruit-circuitpython-typing" +version = "1.11.1" +description = "Types needed for type annotation that are not in `typing`" +optional = false +python-versions = ">=3.8" +files = [ + {file = "adafruit_circuitpython_typing-1.11.1-py3-none-any.whl", hash = "sha256:c223b56538c9535e6ecc45fb39b23acab6840fecf068a85c6a340cb42368d413"}, + {file = "adafruit_circuitpython_typing-1.11.1.tar.gz", hash = "sha256:efcadbf39b70a6ea1b7561a10551b665ad5306b31340c55ece4a42328da3c998"}, +] + +[package.dependencies] +Adafruit-Blinka = "*" +adafruit-circuitpython-busdevice = "*" +adafruit-circuitpython-requests = "*" +typing-extensions = ">=4.0,<5.0" + +[[package]] +name = "adafruit-platformdetect" +version = "3.75.0" +description = "Platform detection for use by libraries like Adafruit-Blinka." +optional = false +python-versions = "*" +files = [ + {file = "Adafruit_PlatformDetect-3.75.0-py3-none-any.whl", hash = "sha256:f523a118bfe270b17c3e1eb1f6115a75dfc8f8f029a67ae51f4934571229c5b8"}, + {file = "adafruit_platformdetect-3.75.0.tar.gz", hash = "sha256:e40c2f9f3c34b6066145756c8533aecdeadd39127ee50087dcf0c1ee96e3081d"}, +] + +[[package]] +name = "adafruit-pureio" +version = "1.1.11" +description = "Pure python (i.e. no native extensions) access to Linux IO including I2C and SPI. Drop in replacement for smbus and spidev modules." +optional = false +python-versions = ">=3.5.0" +files = [ + {file = "Adafruit_PureIO-1.1.11-py3-none-any.whl", hash = "sha256:281ab2099372cc0decc26326918996cbf21b8eed694ec4764d51eefa029d324e"}, + {file = "Adafruit_PureIO-1.1.11.tar.gz", hash = "sha256:c4cfbb365731942d1f1092a116f47dfdae0aef18c5b27f1072b5824ad5ea8c7c"}, +] + +[[package]] +name = "babel" +version = "2.16.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "binho-host-adapter" +version = "0.1.6" +description = "Python Libraries for Binho Multi-Protocol USB Host Adapters" +optional = false +python-versions = "*" +files = [ + {file = "binho-host-adapter-0.1.6.tar.gz", hash = "sha256:1e6da7a84e208c13b5f489066f05774bff1d593d0f5bf1ca149c2b8e83eae856"}, + {file = "binho_host_adapter-0.1.6-py3-none-any.whl", hash = "sha256:f71ca176c1e2fc1a5dce128beb286da217555c6c7c805f2ed282a6f3507ec277"}, +] + +[package.dependencies] +pyserial = "*" + +[[package]] +name = "board" +version = "1.0" +description = "Standard Board mechanism for Dojo tasks" +optional = false +python-versions = "*" +files = [ + {file = "board-1.0-py2.py3-none-any.whl", hash = "sha256:b97c986d0190a2a5181a7fe28959cd8515520b87f672796e21e1b0d32cc63e2f"}, + {file = "board-1.0.tar.gz", hash = "sha256:cd718c3322a126d86455e24ae0cebb59a567cbdbaf03696391dccf88c8f456d6"}, +] + +[[package]] +name = "boto3" +version = "1.35.59" +description = "The AWS SDK for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "boto3-1.35.59-py3-none-any.whl", hash = "sha256:8f8ff97cb9cb2e1ec7374209d0c09c1926b75604d6464c34bafaffd6d6cf0529"}, + {file = "boto3-1.35.59.tar.gz", hash = "sha256:81f4d8d6eff3e26b82cabd42eda816cfac9482821fdef353f18d2ba2f6e75f2d"}, +] + +[package.dependencies] +botocore = ">=1.35.59,<1.36.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.10.0,<0.11.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.35.59" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.8" +files = [ + {file = "botocore-1.35.59-py3-none-any.whl", hash = "sha256:bcd66d7f55c8d1b6020eb86f2d87893fe591fb4be6a7d2a689c18be586452334"}, + {file = "botocore-1.35.59.tar.gz", hash = "sha256:de0ce655fedfc02c87869dfaa3b622488a17ff37da316ef8106cbe1573b83c98"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.22.0)"] + +[[package]] +name = "cachetools" +version = "5.5.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "contourpy" +version = "1.3.0" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.9" +files = [ + {file = "contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7"}, + {file = "contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41"}, + {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d"}, + {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223"}, + {file = "contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f"}, + {file = "contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b"}, + {file = "contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad"}, + {file = "contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d"}, + {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c"}, + {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb"}, + {file = "contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c"}, + {file = "contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67"}, + {file = "contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f"}, + {file = "contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09"}, + {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd"}, + {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35"}, + {file = "contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb"}, + {file = "contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b"}, + {file = "contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3"}, + {file = "contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da"}, + {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14"}, + {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8"}, + {file = "contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294"}, + {file = "contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087"}, + {file = "contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8"}, + {file = "contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6"}, + {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2"}, + {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927"}, + {file = "contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8"}, + {file = "contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2"}, + {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e"}, + {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800"}, + {file = "contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5"}, + {file = "contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843"}, + {file = "contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c"}, + {file = "contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779"}, + {file = "contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4"}, + {file = "contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0"}, + {file = "contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102"}, + {file = "contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb"}, + {file = "contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4"}, +] + +[package.dependencies] +numpy = ">=1.23" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "deptry" +version = "0.16.2" +description = "A command line utility to check for unused, missing and transitive dependencies in a Python project." +optional = false +python-versions = ">=3.8" +files = [ + {file = "deptry-0.16.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:24bfbae07bd6533c852c795e8d88d05a8ad0801bec0d3662e1a37db763c52540"}, + {file = "deptry-0.16.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc881688a2eaeafe51c0617d32a6535057bccdb74559cc667109f48f81cd976e"}, + {file = "deptry-0.16.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fed4b692f556e4c80acb42cec93e3b5fdc7fc2323049c2a0cfd9dfc4a9c7033e"}, + {file = "deptry-0.16.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ec508a932d8f06c3bd1aa7a4548d5dbec92c3060d42eedcda3be9729bd7c3b"}, + {file = "deptry-0.16.2-cp38-abi3-win_amd64.whl", hash = "sha256:eb92e9aacde66cfe001d6318eb0851ae0ca26fea441defed4765a47644daf8bb"}, + {file = "deptry-0.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dfdceca2fbc87f4bce04df4207914a5eb37e67fb2107579ad2e88107c22d2456"}, + {file = "deptry-0.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:96ab62dd5f4658735aac72d0e49f6d896eabf50a0e4e2cdecb436a1362aa696b"}, + {file = "deptry-0.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e4408fa5a8d146b55bc40f0829fb875efef33174a2679bd9954ce988b9bc0d7"}, + {file = "deptry-0.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af976afc2a0583f48dc25f616d2566fecd7af5080675c8eccb161def88d93503"}, + {file = "deptry-0.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd86c9d34aa75b91fb72b34110f0660b2277bf9a95fe9cae3ead36d465bc44ac"}, + {file = "deptry-0.16.2.tar.gz", hash = "sha256:f0f752cf6f5e9f7445a79fcf195b772cd2d4b889cd260e23867dd8013caa74c1"}, +] + +[package.dependencies] +click = ">=8.0.0,<9" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.16.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "flexsea" +version = "8.0.1" +description = "Dephy's Actuator Package API library" +optional = false +python-versions = ">= 3.7.*" +files = [ + {file = "flexsea-8.0.1-py3-none-any.whl", hash = "sha256:6b728a7cd8c74b1184cea4dad13138f8e843a2f58d4f2291ff24f14ca0d0e7aa"}, +] + +[package.dependencies] +matplotlib = "==3.*" +numpy = "==1.*" +PyYAML = "==6.*" +tornado = "==6.*" + +[[package]] +name = "flexsea" +version = "12.0.4" +description = "" +optional = false +python-versions = "<4.0,>=3.11" +files = [ + {file = "flexsea-12.0.4-py3-none-any.whl", hash = "sha256:b8dad7a8bd8e930b82aef1e266add3d17bc2871617c70b93693cc887abb60fa4"}, + {file = "flexsea-12.0.4.tar.gz", hash = "sha256:d45ecbbb7318a6c0a7fa22026211db8f9d7f2f7d5fef3d3036298f56fba5d2f3"}, +] + +[package.dependencies] +boto3 = ">=1.26.110,<2.0.0" +pendulum = [ + {version = ">=2.1.2,<3.0.0", markers = "python_version < \"3.12\""}, + {version = ">=3.0.0,<4.0.0", markers = "python_version >= \"3.12\""}, +] +pyudev = {version = ">=0.24.1,<0.25.0", markers = "sys_platform == \"linux\""} +pyyaml = ">=6.0,<7.0" +semantic-version = ">=2.10.0,<3.0.0" + +[[package]] +name = "fonttools" +version = "4.54.1" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ed7ee041ff7b34cc62f07545e55e1468808691dddfd315d51dd82a6b37ddef2"}, + {file = "fonttools-4.54.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41bb0b250c8132b2fcac148e2e9198e62ff06f3cc472065dff839327945c5882"}, + {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7965af9b67dd546e52afcf2e38641b5be956d68c425bef2158e95af11d229f10"}, + {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278913a168f90d53378c20c23b80f4e599dca62fbffae4cc620c8eed476b723e"}, + {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0e88e3018ac809b9662615072dcd6b84dca4c2d991c6d66e1970a112503bba7e"}, + {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4817f0031206e637d1e685251ac61be64d1adef111060df84fdcbc6ab6c44"}, + {file = "fonttools-4.54.1-cp310-cp310-win32.whl", hash = "sha256:7e3b7d44e18c085fd8c16dcc6f1ad6c61b71ff463636fcb13df7b1b818bd0c02"}, + {file = "fonttools-4.54.1-cp310-cp310-win_amd64.whl", hash = "sha256:dd9cc95b8d6e27d01e1e1f1fae8559ef3c02c76317da650a19047f249acd519d"}, + {file = "fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5419771b64248484299fa77689d4f3aeed643ea6630b2ea750eeab219588ba20"}, + {file = "fonttools-4.54.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:301540e89cf4ce89d462eb23a89464fef50915255ece765d10eee8b2bf9d75b2"}, + {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ae5091547e74e7efecc3cbf8e75200bc92daaeb88e5433c5e3e95ea8ce5aa7"}, + {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07"}, + {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d26732ae002cc3d2ecab04897bb02ae3f11f06dd7575d1df46acd2f7c012a8d8"}, + {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58974b4987b2a71ee08ade1e7f47f410c367cdfc5a94fabd599c88165f56213a"}, + {file = "fonttools-4.54.1-cp311-cp311-win32.whl", hash = "sha256:ab774fa225238986218a463f3fe151e04d8c25d7de09df7f0f5fce27b1243dbc"}, + {file = "fonttools-4.54.1-cp311-cp311-win_amd64.whl", hash = "sha256:07e005dc454eee1cc60105d6a29593459a06321c21897f769a281ff2d08939f6"}, + {file = "fonttools-4.54.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:54471032f7cb5fca694b5f1a0aaeba4af6e10ae989df408e0216f7fd6cdc405d"}, + {file = "fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fa92cb248e573daab8d032919623cc309c005086d743afb014c836636166f08"}, + {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a911591200114969befa7f2cb74ac148bce5a91df5645443371aba6d222e263"}, + {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93d458c8a6a354dc8b48fc78d66d2a8a90b941f7fec30e94c7ad9982b1fa6bab"}, + {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5eb2474a7c5be8a5331146758debb2669bf5635c021aee00fd7c353558fc659d"}, + {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9c563351ddc230725c4bdf7d9e1e92cbe6ae8553942bd1fb2b2ff0884e8b714"}, + {file = "fonttools-4.54.1-cp312-cp312-win32.whl", hash = "sha256:fdb062893fd6d47b527d39346e0c5578b7957dcea6d6a3b6794569370013d9ac"}, + {file = "fonttools-4.54.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4564cf40cebcb53f3dc825e85910bf54835e8a8b6880d59e5159f0f325e637e"}, + {file = "fonttools-4.54.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6e37561751b017cf5c40fce0d90fd9e8274716de327ec4ffb0df957160be3bff"}, + {file = "fonttools-4.54.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:357cacb988a18aace66e5e55fe1247f2ee706e01debc4b1a20d77400354cddeb"}, + {file = "fonttools-4.54.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e953cc0bddc2beaf3a3c3b5dd9ab7554677da72dfaf46951e193c9653e515a"}, + {file = "fonttools-4.54.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58d29b9a294573d8319f16f2f79e42428ba9b6480442fa1836e4eb89c4d9d61c"}, + {file = "fonttools-4.54.1-cp313-cp313-win32.whl", hash = "sha256:9ef1b167e22709b46bf8168368b7b5d3efeaaa746c6d39661c1b4405b6352e58"}, + {file = "fonttools-4.54.1-cp313-cp313-win_amd64.whl", hash = "sha256:262705b1663f18c04250bd1242b0515d3bbae177bee7752be67c979b7d47f43d"}, + {file = "fonttools-4.54.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ed2f80ca07025551636c555dec2b755dd005e2ea8fbeb99fc5cdff319b70b23b"}, + {file = "fonttools-4.54.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dc080e5a1c3b2656caff2ac2633d009b3a9ff7b5e93d0452f40cd76d3da3b3c"}, + {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d152d1be65652fc65e695e5619e0aa0982295a95a9b29b52b85775243c06556"}, + {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8583e563df41fdecef31b793b4dd3af8a9caa03397be648945ad32717a92885b"}, + {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0d1d353ef198c422515a3e974a1e8d5b304cd54a4c2eebcae708e37cd9eeffb1"}, + {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fda582236fee135d4daeca056c8c88ec5f6f6d88a004a79b84a02547c8f57386"}, + {file = "fonttools-4.54.1-cp38-cp38-win32.whl", hash = "sha256:e7d82b9e56716ed32574ee106cabca80992e6bbdcf25a88d97d21f73a0aae664"}, + {file = "fonttools-4.54.1-cp38-cp38-win_amd64.whl", hash = "sha256:ada215fd079e23e060157aab12eba0d66704316547f334eee9ff26f8c0d7b8ab"}, + {file = "fonttools-4.54.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5b8a096e649768c2f4233f947cf9737f8dbf8728b90e2771e2497c6e3d21d13"}, + {file = "fonttools-4.54.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e10d2e0a12e18f4e2dd031e1bf7c3d7017be5c8dbe524d07706179f355c5dac"}, + {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31c32d7d4b0958600eac75eaf524b7b7cb68d3a8c196635252b7a2c30d80e986"}, + {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c39287f5c8f4a0c5a55daf9eaf9ccd223ea59eed3f6d467133cc727d7b943a55"}, + {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a7a310c6e0471602fe3bf8efaf193d396ea561486aeaa7adc1f132e02d30c4b9"}, + {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d3b659d1029946f4ff9b6183984578041b520ce0f8fb7078bb37ec7445806b33"}, + {file = "fonttools-4.54.1-cp39-cp39-win32.whl", hash = "sha256:e96bc94c8cda58f577277d4a71f51c8e2129b8b36fd05adece6320dd3d57de8a"}, + {file = "fonttools-4.54.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8a4b261c1ef91e7188a30571be6ad98d1c6d9fa2427244c545e2fa0a2494dd7"}, + {file = "fonttools-4.54.1-py3-none-any.whl", hash = "sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd"}, + {file = "fonttools-4.54.1.tar.gz", hash = "sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285"}, +] + +[package.extras] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres", "pycairo", "scipy"] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=15.1.0)"] +woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "griffe" +version = "1.4.0" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.8" +files = [ + {file = "griffe-1.4.0-py3-none-any.whl", hash = "sha256:e589de8b8c137e99a46ec45f9598fc0ac5b6868ce824b24db09c02d117b89bc5"}, + {file = "griffe-1.4.0.tar.gz", hash = "sha256:8fccc585896d13f1221035d32c50dec65830c87d23f9adb9b1e6f3d63574f7f5"}, +] + +[package.dependencies] +colorama = ">=0.4" + +[[package]] +name = "grpcio" +version = "1.68.0" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.8" +files = [ + {file = "grpcio-1.68.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:619b5d0f29f4f5351440e9343224c3e19912c21aeda44e0c49d0d147a8d01544"}, + {file = "grpcio-1.68.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:a59f5822f9459bed098ffbceb2713abbf7c6fd13f2b9243461da5c338d0cd6c3"}, + {file = "grpcio-1.68.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:c03d89df516128febc5a7e760d675b478ba25802447624edf7aa13b1e7b11e2a"}, + {file = "grpcio-1.68.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44bcbebb24363d587472089b89e2ea0ab2e2b4df0e4856ba4c0b087c82412121"}, + {file = "grpcio-1.68.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79f81b7fbfb136247b70465bd836fa1733043fdee539cd6031cb499e9608a110"}, + {file = "grpcio-1.68.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:88fb2925789cfe6daa20900260ef0a1d0a61283dfb2d2fffe6194396a354c618"}, + {file = "grpcio-1.68.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:99f06232b5c9138593ae6f2e355054318717d32a9c09cdc5a2885540835067a1"}, + {file = "grpcio-1.68.0-cp310-cp310-win32.whl", hash = "sha256:a6213d2f7a22c3c30a479fb5e249b6b7e648e17f364598ff64d08a5136fe488b"}, + {file = "grpcio-1.68.0-cp310-cp310-win_amd64.whl", hash = "sha256:15327ab81131ef9b94cb9f45b5bd98803a179c7c61205c8c0ac9aff9d6c4e82a"}, + {file = "grpcio-1.68.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:3b2b559beb2d433129441783e5f42e3be40a9e1a89ec906efabf26591c5cd415"}, + {file = "grpcio-1.68.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e46541de8425a4d6829ac6c5d9b16c03c292105fe9ebf78cb1c31e8d242f9155"}, + {file = "grpcio-1.68.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:c1245651f3c9ea92a2db4f95d37b7597db6b246d5892bca6ee8c0e90d76fb73c"}, + {file = "grpcio-1.68.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1931c7aa85be0fa6cea6af388e576f3bf6baee9e5d481c586980c774debcb4"}, + {file = "grpcio-1.68.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ff09c81e3aded7a183bc6473639b46b6caa9c1901d6f5e2cba24b95e59e30"}, + {file = "grpcio-1.68.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8c73f9fbbaee1a132487e31585aa83987ddf626426d703ebcb9a528cf231c9b1"}, + {file = "grpcio-1.68.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6b2f98165ea2790ea159393a2246b56f580d24d7da0d0342c18a085299c40a75"}, + {file = "grpcio-1.68.0-cp311-cp311-win32.whl", hash = "sha256:e1e7ed311afb351ff0d0e583a66fcb39675be112d61e7cfd6c8269884a98afbc"}, + {file = "grpcio-1.68.0-cp311-cp311-win_amd64.whl", hash = "sha256:e0d2f68eaa0a755edd9a47d40e50dba6df2bceda66960dee1218da81a2834d27"}, + {file = "grpcio-1.68.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8af6137cc4ae8e421690d276e7627cfc726d4293f6607acf9ea7260bd8fc3d7d"}, + {file = "grpcio-1.68.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4028b8e9a3bff6f377698587d642e24bd221810c06579a18420a17688e421af7"}, + {file = "grpcio-1.68.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:f60fa2adf281fd73ae3a50677572521edca34ba373a45b457b5ebe87c2d01e1d"}, + {file = "grpcio-1.68.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e18589e747c1e70b60fab6767ff99b2d0c359ea1db8a2cb524477f93cdbedf5b"}, + {file = "grpcio-1.68.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d30f3fee9372796f54d3100b31ee70972eaadcc87314be369360248a3dcffe"}, + {file = "grpcio-1.68.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7e0a3e72c0e9a1acab77bef14a73a416630b7fd2cbd893c0a873edc47c42c8cd"}, + {file = "grpcio-1.68.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a831dcc343440969aaa812004685ed322cdb526cd197112d0db303b0da1e8659"}, + {file = "grpcio-1.68.0-cp312-cp312-win32.whl", hash = "sha256:5a180328e92b9a0050958ced34dddcb86fec5a8b332f5a229e353dafc16cd332"}, + {file = "grpcio-1.68.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bddd04a790b69f7a7385f6a112f46ea0b34c4746f361ebafe9ca0be567c78e9"}, + {file = "grpcio-1.68.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:fc05759ffbd7875e0ff2bd877be1438dfe97c9312bbc558c8284a9afa1d0f40e"}, + {file = "grpcio-1.68.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:15fa1fe25d365a13bc6d52fcac0e3ee1f9baebdde2c9b3b2425f8a4979fccea1"}, + {file = "grpcio-1.68.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:32a9cb4686eb2e89d97022ecb9e1606d132f85c444354c17a7dbde4a455e4a3b"}, + {file = "grpcio-1.68.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dba037ff8d284c8e7ea9a510c8ae0f5b016004f13c3648f72411c464b67ff2fb"}, + {file = "grpcio-1.68.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0efbbd849867e0e569af09e165363ade75cf84f5229b2698d53cf22c7a4f9e21"}, + {file = "grpcio-1.68.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:4e300e6978df0b65cc2d100c54e097c10dfc7018b9bd890bbbf08022d47f766d"}, + {file = "grpcio-1.68.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:6f9c7ad1a23e1047f827385f4713b5b8c6c7d325705be1dd3e31fb00dcb2f665"}, + {file = "grpcio-1.68.0-cp313-cp313-win32.whl", hash = "sha256:3ac7f10850fd0487fcce169c3c55509101c3bde2a3b454869639df2176b60a03"}, + {file = "grpcio-1.68.0-cp313-cp313-win_amd64.whl", hash = "sha256:afbf45a62ba85a720491bfe9b2642f8761ff348006f5ef67e4622621f116b04a"}, + {file = "grpcio-1.68.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:f8f695d9576ce836eab27ba7401c60acaf9ef6cf2f70dfe5462055ba3df02cc3"}, + {file = "grpcio-1.68.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9fe1b141cda52f2ca73e17d2d3c6a9f3f3a0c255c216b50ce616e9dca7e3441d"}, + {file = "grpcio-1.68.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:4df81d78fd1646bf94ced4fb4cd0a7fe2e91608089c522ef17bc7db26e64effd"}, + {file = "grpcio-1.68.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46a2d74d4dd8993151c6cd585594c082abe74112c8e4175ddda4106f2ceb022f"}, + {file = "grpcio-1.68.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17278d977746472698460c63abf333e1d806bd41f2224f90dbe9460101c9796"}, + {file = "grpcio-1.68.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:15377bce516b1c861c35e18eaa1c280692bf563264836cece693c0f169b48829"}, + {file = "grpcio-1.68.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cc5f0a4f5904b8c25729a0498886b797feb817d1fd3812554ffa39551112c161"}, + {file = "grpcio-1.68.0-cp38-cp38-win32.whl", hash = "sha256:def1a60a111d24376e4b753db39705adbe9483ef4ca4761f825639d884d5da78"}, + {file = "grpcio-1.68.0-cp38-cp38-win_amd64.whl", hash = "sha256:55d3b52fd41ec5772a953612db4e70ae741a6d6ed640c4c89a64f017a1ac02b5"}, + {file = "grpcio-1.68.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:0d230852ba97654453d290e98d6aa61cb48fa5fafb474fb4c4298d8721809354"}, + {file = "grpcio-1.68.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:50992f214264e207e07222703c17d9cfdcc2c46ed5a1ea86843d440148ebbe10"}, + {file = "grpcio-1.68.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:14331e5c27ed3545360464a139ed279aa09db088f6e9502e95ad4bfa852bb116"}, + {file = "grpcio-1.68.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f84890b205692ea813653ece4ac9afa2139eae136e419231b0eec7c39fdbe4c2"}, + {file = "grpcio-1.68.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0cf343c6f4f6aa44863e13ec9ddfe299e0be68f87d68e777328bff785897b05"}, + {file = "grpcio-1.68.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:fd2c2d47969daa0e27eadaf15c13b5e92605c5e5953d23c06d0b5239a2f176d3"}, + {file = "grpcio-1.68.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:18668e36e7f4045820f069997834e94e8275910b1f03e078a6020bd464cb2363"}, + {file = "grpcio-1.68.0-cp39-cp39-win32.whl", hash = "sha256:2af76ab7c427aaa26aa9187c3e3c42f38d3771f91a20f99657d992afada2294a"}, + {file = "grpcio-1.68.0-cp39-cp39-win_amd64.whl", hash = "sha256:e694b5928b7b33ca2d3b4d5f9bf8b5888906f181daff6b406f4938f3a997a490"}, + {file = "grpcio-1.68.0.tar.gz", hash = "sha256:7e7483d39b4a4fddb9906671e9ea21aaad4f031cdfc349fec76bdfa1e404543a"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.68.0)"] + +[[package]] +name = "grpcio-tools" +version = "1.68.0" +description = "Protobuf code generator for gRPC" +optional = false +python-versions = ">=3.8" +files = [ + {file = "grpcio_tools-1.68.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:9509a5c3ed3d54fa7ac20748d501cb86668f764605a0a68f275339ee0f1dc1a6"}, + {file = "grpcio_tools-1.68.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:59a885091bf29700ba0e14a954d156a18714caaa2006a7f328b18e1ac4b1e721"}, + {file = "grpcio_tools-1.68.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:d3e678162e1d7a8720dc05fdd537fc8df082a50831791f7bb1c6f90095f8368b"}, + {file = "grpcio_tools-1.68.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10d03e3ad4af6284fd27cb14f5a3d52045913c1253e3e24a384ed91bc8adbfcd"}, + {file = "grpcio_tools-1.68.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1769d7f529de1cc102f7fb900611e3c0b69bdb244fca1075b24d6e5b49024586"}, + {file = "grpcio_tools-1.68.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:88640d95ee41921ac7352fa5fadca52a06d7e21fbe53e6a706a9a494f756be7d"}, + {file = "grpcio_tools-1.68.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e903d07bc65232aa9e7704c829aec263e1e139442608e473d7912417a9908e29"}, + {file = "grpcio_tools-1.68.0-cp310-cp310-win32.whl", hash = "sha256:66b70b37184d40806844f51c2757c6b852511d4ea46a3bf2c7e931a47b455bc6"}, + {file = "grpcio_tools-1.68.0-cp310-cp310-win_amd64.whl", hash = "sha256:b47ae076ffb29a68e517bc03552bef0d9c973f8e18adadff180b123e973a26ea"}, + {file = "grpcio_tools-1.68.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:f65942fab440e99113ce14436deace7554d5aa554ea18358e3a5f3fc47efe322"}, + {file = "grpcio_tools-1.68.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8fefc6d000e169a97336feded23ce614df3fb9926fc48c7a9ff8ea459d93b5b0"}, + {file = "grpcio_tools-1.68.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:6dd69c9f3ff85eee8d1f71adf7023c638ca8d465633244ac1b7f19bc3668612d"}, + {file = "grpcio_tools-1.68.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc5195dc02057668cc22da1ff1aea1811f6fa0deb801b3194dec1fe0bab1cf0"}, + {file = "grpcio_tools-1.68.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:849b12bec2320e49e988df104c92217d533e01febac172a4495caab36d9f0edc"}, + {file = "grpcio_tools-1.68.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:766c2cd2e365e0fc0e559af56f2c2d144d95fd7cb8668a34d533e66d6435eb34"}, + {file = "grpcio_tools-1.68.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2ec3a2e0afa4866ccc5ba33c071aebaa619245dfdd840cbb74f2b0591868d085"}, + {file = "grpcio_tools-1.68.0-cp311-cp311-win32.whl", hash = "sha256:80b733014eb40d920d836d782e5cdea0dcc90d251a2ffb35ab378ef4f8a42c14"}, + {file = "grpcio_tools-1.68.0-cp311-cp311-win_amd64.whl", hash = "sha256:f95103e3e4e7fee7c6123bc9e4e925e07ad24d8d09d7c1c916fb6c8d1cb9e726"}, + {file = "grpcio_tools-1.68.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:dd9a654af8536b3de8525bff72a245fef62d572eabf96ac946fe850e707cb27d"}, + {file = "grpcio_tools-1.68.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0f77957e3a0916a0dd18d57ce6b49d95fc9a5cfed92310f226339c0fda5394f6"}, + {file = "grpcio_tools-1.68.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:92a09afe64fe26696595de2036e10967876d26b12c894cc9160f00152cacebe7"}, + {file = "grpcio_tools-1.68.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28ebdbad2ef16699d07400b65260240851049a75502eff69a59b127d3ab960f1"}, + {file = "grpcio_tools-1.68.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d3150d784d8050b10dcf5eb06e04fb90747a1547fed3a062a608d940fe57066"}, + {file = "grpcio_tools-1.68.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:261d98fd635595de42aadee848f9af46da6654d63791c888891e94f66c5d0682"}, + {file = "grpcio_tools-1.68.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:061345c0079b9471f32230186ab01acb908ea0e577bc1699a8cf47acef8be4af"}, + {file = "grpcio_tools-1.68.0-cp312-cp312-win32.whl", hash = "sha256:533ce6791a5ba21e35d74c6c25caf4776f5692785a170c01ea1153783ad5af31"}, + {file = "grpcio_tools-1.68.0-cp312-cp312-win_amd64.whl", hash = "sha256:56842a0ce74b4b92eb62cd5ee00181b2d3acc58ba0c4fd20d15a5db51f891ba6"}, + {file = "grpcio_tools-1.68.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:1117a81592542f0c36575082daa6413c57ca39188b18a4c50ec7332616f4b97e"}, + {file = "grpcio_tools-1.68.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:51e5a090849b30c99a2396d42140b8a3e558eff6cdfa12603f9582e2cd07724e"}, + {file = "grpcio_tools-1.68.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:4fe611d89a1836df8936f066d39c7eb03d4241806449ec45d4b8e1c843ae8011"}, + {file = "grpcio_tools-1.68.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c10f3faa0cc4d89eb546f53b623837af23e86dc495d3b89510bcc0e0a6c0b8b2"}, + {file = "grpcio_tools-1.68.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46b537480b8fd2195d988120a28467601a2a3de2e504043b89fb90318e1eb754"}, + {file = "grpcio_tools-1.68.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:17d0c9004ea82b4213955a585401e80c30d4b37a1d4ace32ccdea8db4d3b7d43"}, + {file = "grpcio_tools-1.68.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:2919faae04fe47bad57fc9b578aeaab527da260e851f321a253b6b11862254a8"}, + {file = "grpcio_tools-1.68.0-cp313-cp313-win32.whl", hash = "sha256:ee86157ef899f58ba2fe1055cce0d33bd703e99aa6d5a0895581ac3969f06bfa"}, + {file = "grpcio_tools-1.68.0-cp313-cp313-win_amd64.whl", hash = "sha256:d0470ffc6a93c86cdda48edd428d22e2fef17d854788d60d0d5f291038873157"}, + {file = "grpcio_tools-1.68.0-cp38-cp38-linux_armv7l.whl", hash = "sha256:795f2cd76f68a12b0b5541b98187ba367dd69b49d359cf98b781ead742961370"}, + {file = "grpcio_tools-1.68.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:57e29e78c33fb1b1d557fbe7650d722d1f2b0a9f53ea73beb8ea47e627b6000b"}, + {file = "grpcio_tools-1.68.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:700f171cd3293ee8d50cd43171562ff07b14fa8e49ee471cd91c6924c7da8644"}, + {file = "grpcio_tools-1.68.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:196cd8a3a5963a4c9e424314df9eb573b305e6f958fe6508d26580ce01e7aa56"}, + {file = "grpcio_tools-1.68.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cad40c3164ee9cef62524dea509449ea581b17ea493178beef051bf79b5103ca"}, + {file = "grpcio_tools-1.68.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab93fab49fa1e699e577ff5fbb99aba660164d710d4c33cfe0aa9d06f585539f"}, + {file = "grpcio_tools-1.68.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:511224a99726eb84db9ddb84dc8a75377c3eae797d835f99e80128ec618376d5"}, + {file = "grpcio_tools-1.68.0-cp38-cp38-win32.whl", hash = "sha256:b4ca81770cd729a9ea536d871aacedbde2b732bb9bb83c9d993d63f58502153d"}, + {file = "grpcio_tools-1.68.0-cp38-cp38-win_amd64.whl", hash = "sha256:6950725bf7a496f81d3ec3324334ffc9dbec743b510dd0e897f51f8627eeb6ac"}, + {file = "grpcio_tools-1.68.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:01ace351a51d7ee120963a4612b1f00e964462ec548db20d17f8902e238592c8"}, + {file = "grpcio_tools-1.68.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5afd2f3f7257b52228a7808a2b4a765893d4d802d7a2377d9284853e67d045c6"}, + {file = "grpcio_tools-1.68.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:453ee3193d59c974c678d91f08786f43c25ef753651b0825dc3d008c31baf68d"}, + {file = "grpcio_tools-1.68.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094b22919b786ad73c20372ef5e546330e7cd2c6dc12293b7ed586975f35d38"}, + {file = "grpcio_tools-1.68.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26335eea976dfc1ff5d90b19c309a9425bd53868112a0507ad20f297f2c21d3e"}, + {file = "grpcio_tools-1.68.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c77ecc5164bb413a613bdac9091dcc29d26834a2ac42fcd1afdfcda9e3003e68"}, + {file = "grpcio_tools-1.68.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e31be6dc61496a59c1079b0a669f93dfcc2cdc4b1dbdc4374247cd09cee1329b"}, + {file = "grpcio_tools-1.68.0-cp39-cp39-win32.whl", hash = "sha256:3aa40958355920ae2846c6fb5cadac4f2c8e33234a2982fef8101da0990e3968"}, + {file = "grpcio_tools-1.68.0-cp39-cp39-win_amd64.whl", hash = "sha256:19bafb80948eda979b1b3a63c1567162d06249f43068a0e46a028a448e6f72d4"}, + {file = "grpcio_tools-1.68.0.tar.gz", hash = "sha256:737804ec2225dd4cc27e633b4ca0e963b0795161bf678285fab6586e917fd867"}, +] + +[package.dependencies] +grpcio = ">=1.68.0" +protobuf = ">=5.26.1,<6.0dev" +setuptools = "*" + +[[package]] +name = "identify" +version = "2.6.1" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, + {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "importlib-resources" +version = "6.4.5" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"}, + {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] +type = ["pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "kiwisolver" +version = "1.4.7" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.8" +files = [ + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"}, + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"}, + {file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"}, + {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"}, + {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"}, + {file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"}, + {file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"}, + {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"}, + {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"}, + {file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"}, + {file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"}, + {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"}, + {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"}, + {file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"}, + {file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"}, + {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"}, + {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"}, + {file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"}, + {file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"}, + {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"}, + {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"}, + {file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"}, + {file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"}, + {file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"}, + {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"}, + {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"}, + {file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"}, + {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"}, + {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"}, + {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"}, + {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, +] + +[[package]] +name = "markdown" +version = "3.7" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "matplotlib" +version = "3.9.2" +description = "Python plotting package" +optional = false +python-versions = ">=3.9" +files = [ + {file = "matplotlib-3.9.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb"}, + {file = "matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4"}, + {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64"}, + {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66"}, + {file = "matplotlib-3.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a"}, + {file = "matplotlib-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae"}, + {file = "matplotlib-3.9.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772"}, + {file = "matplotlib-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41"}, + {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f"}, + {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447"}, + {file = "matplotlib-3.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e"}, + {file = "matplotlib-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7"}, + {file = "matplotlib-3.9.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9"}, + {file = "matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d"}, + {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7"}, + {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c"}, + {file = "matplotlib-3.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e"}, + {file = "matplotlib-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3"}, + {file = "matplotlib-3.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9"}, + {file = "matplotlib-3.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa"}, + {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b"}, + {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413"}, + {file = "matplotlib-3.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b"}, + {file = "matplotlib-3.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49"}, + {file = "matplotlib-3.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03"}, + {file = "matplotlib-3.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30"}, + {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51"}, + {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c"}, + {file = "matplotlib-3.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e"}, + {file = "matplotlib-3.9.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2"}, + {file = "matplotlib-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a"}, + {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5"}, + {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca"}, + {file = "matplotlib-3.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea"}, + {file = "matplotlib-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697"}, + {file = "matplotlib-3.9.2.tar.gz", hash = "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""} +kiwisolver = ">=1.3.1" +numpy = ">=1.23" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=2.3.1" +python-dateutil = ">=2.7" + +[package.extras] +dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "1.2.0" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f"}, + {file = "mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f"}, +] + +[package.dependencies] +Markdown = ">=3.3" +markupsafe = ">=2.0.1" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-material" +version = "9.5.44" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca"}, + {file = "mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "mkdocstrings" +version = "0.26.1" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings-0.26.1-py3-none-any.whl", hash = "sha256:29738bfb72b4608e8e55cc50fb8a54f325dc7ebd2014e4e3881a49892d5983cf"}, + {file = "mkdocstrings-0.26.1.tar.gz", hash = "sha256:bb8b8854d6713d5348ad05b069a09f3b79edbc6a0f33a34c6821141adb03fe33"}, +] + +[package.dependencies] +click = ">=7.0" +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +Jinja2 = ">=2.11.1" +Markdown = ">=3.6" +MarkupSafe = ">=1.1" +mkdocs = ">=1.4" +mkdocs-autorefs = ">=1.2" +mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} +platformdirs = ">=2.2" +pymdown-extensions = ">=6.3" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.11.1" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings_python-1.11.1-py3-none-any.whl", hash = "sha256:a21a1c05acef129a618517bb5aae3e33114f569b11588b1e7af3e9d4061a71af"}, + {file = "mkdocstrings_python-1.11.1.tar.gz", hash = "sha256:8824b115c5359304ab0b5378a91f6202324a849e1da907a3485b59208b797322"}, +] + +[package.dependencies] +griffe = ">=0.49" +mkdocs-autorefs = ">=1.2" +mkdocstrings = ">=0.26" + +[[package]] +name = "moteus" +version = "0.3.74" +description = "moteus brushless controller library and tools" +optional = false +python-versions = "<4,>=3.7" +files = [ + {file = "moteus-0.3.74-py3-none-any.whl", hash = "sha256:18542d598081b70ba39b97bb82e0de6c19bdad9c6f828796b5575f32b747e13a"}, +] + +[package.dependencies] +importlib-metadata = ">=3.6" +numpy = "<2" +pyelftools = ">=0.26" +pyserial = ">=3.5" +python-can = ">=3.3" +pywin32 = {version = "*", markers = "platform_system == \"Windows\""} +scipy = ">=1.8.0" + +[[package]] +name = "moteus-pi3hat" +version = "0.3.30" +description = "moteus brushless controller library and tools" +optional = false +python-versions = ">=3.7" +files = [ + {file = "moteus_pi3hat-0.3.30-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:cf318ec718b45b6af4f2141ce28d32a78cc5774417beb3c55152299f8b05b659"}, + {file = "moteus_pi3hat-0.3.30-cp310-cp310-manylinux_2_17_armv7l.whl", hash = "sha256:293899d804a21050c0f99455ee1b12d396e17b167b9c3f1c0c688a906ae3a0c7"}, + {file = "moteus_pi3hat-0.3.30-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:b5ffc7f0c78995072b2a3e33e3d67f427fc3f870683baa256d9c3bf02712685f"}, + {file = "moteus_pi3hat-0.3.30-cp311-cp311-manylinux_2_17_armv7l.whl", hash = "sha256:3e83e017559ca81fc76559a551cb68c07c365a5d30005d55bd797a186fe941f7"}, + {file = "moteus_pi3hat-0.3.30-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:c75bbf96f88439aef062414bcc9cde78227d1eb10b47d5a2c7df7e3cae299c0e"}, + {file = "moteus_pi3hat-0.3.30-cp312-cp312-manylinux_2_17_armv7l.whl", hash = "sha256:0c18f4c9a97507e0e3907af5eb2b31a8aced9ab51986dc185ef7b7e6f783e35d"}, + {file = "moteus_pi3hat-0.3.30-cp37-abi3-linux_armv7l.whl", hash = "sha256:180d07cdc1e4ad168602725239c219de0efa0c4ef7909e971b9da1da9cba2fa1"}, + {file = "moteus_pi3hat-0.3.30-cp37-abi3-manylinux_2_17_aarch64.whl", hash = "sha256:a1c88daa3b7d019ba49adfa88ac3f10cb1357a063928b3e90dcb7dc1314b105b"}, + {file = "moteus_pi3hat-0.3.30-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:13d3925e86070226c1e01538375b4a8a7ae46ae7ec021e18c0b8e25c798f9d99"}, + {file = "moteus_pi3hat-0.3.30-cp39-cp39-manylinux_2_17_armv7l.whl", hash = "sha256:d73b698b86c008f0bf9d841e26ad0e964aed24401f34d4fd8104db88e8c3a876"}, +] + +[package.dependencies] +moteus = ">=0.3.22" + +[[package]] +name = "msgpack" +version = "1.0.8" +description = "MessagePack serializer" +optional = false +python-versions = ">=3.8" +files = [ + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, + {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, + {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, + {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, + {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, + {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, + {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, + {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, + {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, + {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, + {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, + {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, + {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, + {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, + {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, + {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, + {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, + {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, + {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, + {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, + {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, + {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, + {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, + {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, + {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, + {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, + {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, +] + +[[package]] +name = "mypy" +version = "1.13.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, + {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, + {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, + {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, + {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, + {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, + {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, + {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, + {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, + {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, + {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, + {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, + {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, + {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, + {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, + {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, + {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, + {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, + {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, + {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, + {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, + {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, + {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, + {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, + {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, + {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, + {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "paginate" +version = "0.5.7" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, +] + +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + +[[package]] +name = "pandas" +version = "2.2.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pendulum" +version = "2.1.2" +description = "Python datetimes made easy" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pendulum-2.1.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:b6c352f4bd32dff1ea7066bd31ad0f71f8d8100b9ff709fb343f3b86cee43efe"}, + {file = "pendulum-2.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:318f72f62e8e23cd6660dbafe1e346950281a9aed144b5c596b2ddabc1d19739"}, + {file = "pendulum-2.1.2-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:0731f0c661a3cb779d398803655494893c9f581f6488048b3fb629c2342b5394"}, + {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3481fad1dc3f6f6738bd575a951d3c15d4b4ce7c82dce37cf8ac1483fde6e8b0"}, + {file = "pendulum-2.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9702069c694306297ed362ce7e3c1ef8404ac8ede39f9b28b7c1a7ad8c3959e3"}, + {file = "pendulum-2.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:fb53ffa0085002ddd43b6ca61a7b34f2d4d7c3ed66f931fe599e1a531b42af9b"}, + {file = "pendulum-2.1.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:c501749fdd3d6f9e726086bf0cd4437281ed47e7bca132ddb522f86a1645d360"}, + {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c807a578a532eeb226150d5006f156632df2cc8c5693d778324b43ff8c515dd0"}, + {file = "pendulum-2.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2d1619a721df661e506eff8db8614016f0720ac171fe80dda1333ee44e684087"}, + {file = "pendulum-2.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f888f2d2909a414680a29ae74d0592758f2b9fcdee3549887779cd4055e975db"}, + {file = "pendulum-2.1.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e95d329384717c7bf627bf27e204bc3b15c8238fa8d9d9781d93712776c14002"}, + {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4c9c689747f39d0d02a9f94fcee737b34a5773803a64a5fdb046ee9cac7442c5"}, + {file = "pendulum-2.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1245cd0075a3c6d889f581f6325dd8404aca5884dea7223a5566c38aab94642b"}, + {file = "pendulum-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:db0a40d8bcd27b4fb46676e8eb3c732c67a5a5e6bfab8927028224fbced0b40b"}, + {file = "pendulum-2.1.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f5e236e7730cab1644e1b87aca3d2ff3e375a608542e90fe25685dae46310116"}, + {file = "pendulum-2.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:de42ea3e2943171a9e95141f2eecf972480636e8e484ccffaf1e833929e9e052"}, + {file = "pendulum-2.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7c5ec650cb4bec4c63a89a0242cc8c3cebcec92fcfe937c417ba18277d8560be"}, + {file = "pendulum-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:33fb61601083f3eb1d15edeb45274f73c63b3c44a8524703dc143f4212bf3269"}, + {file = "pendulum-2.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:29c40a6f2942376185728c9a0347d7c0f07905638c83007e1d262781f1e6953a"}, + {file = "pendulum-2.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:94b1fc947bfe38579b28e1cccb36f7e28a15e841f30384b5ad6c5e31055c85d7"}, + {file = "pendulum-2.1.2.tar.gz", hash = "sha256:b06a0ca1bfe41c990bbf0c029f0b6501a7f2ec4e38bfec730712015e8860f207"}, +] + +[package.dependencies] +python-dateutil = ">=2.6,<3.0" +pytzdata = ">=2020.1" + +[[package]] +name = "pendulum" +version = "3.0.0" +description = "Python datetimes made easy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pendulum-3.0.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2cf9e53ef11668e07f73190c805dbdf07a1939c3298b78d5a9203a86775d1bfd"}, + {file = "pendulum-3.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fb551b9b5e6059377889d2d878d940fd0bbb80ae4810543db18e6f77b02c5ef6"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c58227ac260d5b01fc1025176d7b31858c9f62595737f350d22124a9a3ad82d"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60fb6f415fea93a11c52578eaa10594568a6716602be8430b167eb0d730f3332"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b69f6b4dbcb86f2c2fe696ba991e67347bcf87fe601362a1aba6431454b46bde"}, + {file = "pendulum-3.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:138afa9c373ee450ede206db5a5e9004fd3011b3c6bbe1e57015395cd076a09f"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:83d9031f39c6da9677164241fd0d37fbfc9dc8ade7043b5d6d62f56e81af8ad2"}, + {file = "pendulum-3.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c2308af4033fa534f089595bcd40a95a39988ce4059ccd3dc6acb9ef14ca44a"}, + {file = "pendulum-3.0.0-cp310-none-win_amd64.whl", hash = "sha256:9a59637cdb8462bdf2dbcb9d389518c0263799189d773ad5c11db6b13064fa79"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3725245c0352c95d6ca297193192020d1b0c0f83d5ee6bb09964edc2b5a2d508"}, + {file = "pendulum-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c035f03a3e565ed132927e2c1b691de0dbf4eb53b02a5a3c5a97e1a64e17bec"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597e66e63cbd68dd6d58ac46cb7a92363d2088d37ccde2dae4332ef23e95cd00"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99a0f8172e19f3f0c0e4ace0ad1595134d5243cf75985dc2233e8f9e8de263ca"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77d8839e20f54706aed425bec82a83b4aec74db07f26acd039905d1237a5e1d4"}, + {file = "pendulum-3.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afde30e8146292b059020fbc8b6f8fd4a60ae7c5e6f0afef937bbb24880bdf01"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:660434a6fcf6303c4efd36713ca9212c753140107ee169a3fc6c49c4711c2a05"}, + {file = "pendulum-3.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dee9e5a48c6999dc1106eb7eea3e3a50e98a50651b72c08a87ee2154e544b33e"}, + {file = "pendulum-3.0.0-cp311-none-win_amd64.whl", hash = "sha256:d4cdecde90aec2d67cebe4042fd2a87a4441cc02152ed7ed8fb3ebb110b94ec4"}, + {file = "pendulum-3.0.0-cp311-none-win_arm64.whl", hash = "sha256:773c3bc4ddda2dda9f1b9d51fe06762f9200f3293d75c4660c19b2614b991d83"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:409e64e41418c49f973d43a28afe5df1df4f1dd87c41c7c90f1a63f61ae0f1f7"}, + {file = "pendulum-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a38ad2121c5ec7c4c190c7334e789c3b4624798859156b138fcc4d92295835dc"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde4d0b2024b9785f66b7f30ed59281bd60d63d9213cda0eb0910ead777f6d37"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2c5675769fb6d4c11238132962939b960fcb365436b6d623c5864287faa319"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8af95e03e066826f0f4c65811cbee1b3123d4a45a1c3a2b4fc23c4b0dff893b5"}, + {file = "pendulum-3.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2165a8f33cb15e06c67070b8afc87a62b85c5a273e3aaa6bc9d15c93a4920d6f"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ad5e65b874b5e56bd942546ea7ba9dd1d6a25121db1c517700f1c9de91b28518"}, + {file = "pendulum-3.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17fe4b2c844bbf5f0ece69cfd959fa02957c61317b2161763950d88fed8e13b9"}, + {file = "pendulum-3.0.0-cp312-none-win_amd64.whl", hash = "sha256:78f8f4e7efe5066aca24a7a57511b9c2119f5c2b5eb81c46ff9222ce11e0a7a5"}, + {file = "pendulum-3.0.0-cp312-none-win_arm64.whl", hash = "sha256:28f49d8d1e32aae9c284a90b6bb3873eee15ec6e1d9042edd611b22a94ac462f"}, + {file = "pendulum-3.0.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d4e2512f4e1a4670284a153b214db9719eb5d14ac55ada5b76cbdb8c5c00399d"}, + {file = "pendulum-3.0.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:3d897eb50883cc58d9b92f6405245f84b9286cd2de6e8694cb9ea5cb15195a32"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e169cc2ca419517f397811bbe4589cf3cd13fca6dc38bb352ba15ea90739ebb"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17c3084a4524ebefd9255513692f7e7360e23c8853dc6f10c64cc184e1217ab"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:826d6e258052715f64d05ae0fc9040c0151e6a87aae7c109ba9a0ed930ce4000"}, + {file = "pendulum-3.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2aae97087872ef152a0c40e06100b3665d8cb86b59bc8471ca7c26132fccd0f"}, + {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ac65eeec2250d03106b5e81284ad47f0d417ca299a45e89ccc69e36130ca8bc7"}, + {file = "pendulum-3.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a5346d08f3f4a6e9e672187faa179c7bf9227897081d7121866358af369f44f9"}, + {file = "pendulum-3.0.0-cp37-none-win_amd64.whl", hash = "sha256:235d64e87946d8f95c796af34818c76e0f88c94d624c268693c85b723b698aa9"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a881d9c2a7f85bc9adafcfe671df5207f51f5715ae61f5d838b77a1356e8b7b"}, + {file = "pendulum-3.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d7762d2076b9b1cb718a6631ad6c16c23fc3fac76cbb8c454e81e80be98daa34"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e8e36a8130819d97a479a0e7bf379b66b3b1b520e5dc46bd7eb14634338df8c"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7dc843253ac373358ffc0711960e2dd5b94ab67530a3e204d85c6e8cb2c5fa10"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a78ad3635d609ceb1e97d6aedef6a6a6f93433ddb2312888e668365908c7120"}, + {file = "pendulum-3.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a137e9e0d1f751e60e67d11fc67781a572db76b2296f7b4d44554761049d6"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c95984037987f4a457bb760455d9ca80467be792236b69d0084f228a8ada0162"}, + {file = "pendulum-3.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d29c6e578fe0f893766c0d286adbf0b3c726a4e2341eba0917ec79c50274ec16"}, + {file = "pendulum-3.0.0-cp38-none-win_amd64.whl", hash = "sha256:deaba8e16dbfcb3d7a6b5fabdd5a38b7c982809567479987b9c89572df62e027"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b11aceea5b20b4b5382962b321dbc354af0defe35daa84e9ff3aae3c230df694"}, + {file = "pendulum-3.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a90d4d504e82ad236afac9adca4d6a19e4865f717034fc69bafb112c320dcc8f"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:825799c6b66e3734227756fa746cc34b3549c48693325b8b9f823cb7d21b19ac"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad769e98dc07972e24afe0cff8d365cb6f0ebc7e65620aa1976fcfbcadc4c6f3"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6fc26907eb5fb8cc6188cc620bc2075a6c534d981a2f045daa5f79dfe50d512"}, + {file = "pendulum-3.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c717eab1b6d898c00a3e0fa7781d615b5c5136bbd40abe82be100bb06df7a56"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3ddd1d66d1a714ce43acfe337190be055cdc221d911fc886d5a3aae28e14b76d"}, + {file = "pendulum-3.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:822172853d7a9cf6da95d7b66a16c7160cb99ae6df55d44373888181d7a06edc"}, + {file = "pendulum-3.0.0-cp39-none-win_amd64.whl", hash = "sha256:840de1b49cf1ec54c225a2a6f4f0784d50bd47f68e41dc005b7f67c7d5b5f3ae"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b1f74d1e6ffe5d01d6023870e2ce5c2191486928823196f8575dcc786e107b1"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:729e9f93756a2cdfa77d0fc82068346e9731c7e884097160603872686e570f07"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e586acc0b450cd21cbf0db6bae386237011b75260a3adceddc4be15334689a9a"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22e7944ffc1f0099a79ff468ee9630c73f8c7835cd76fdb57ef7320e6a409df4"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fa30af36bd8e50686846bdace37cf6707bdd044e5cb6e1109acbad3277232e04"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:440215347b11914ae707981b9a57ab9c7b6983ab0babde07063c6ee75c0dc6e7"}, + {file = "pendulum-3.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:314c4038dc5e6a52991570f50edb2f08c339debdf8cea68ac355b32c4174e820"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5acb1d386337415f74f4d1955c4ce8d0201978c162927d07df8eb0692b2d8533"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a789e12fbdefaffb7b8ac67f9d8f22ba17a3050ceaaa635cd1cc4645773a4b1e"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:860aa9b8a888e5913bd70d819306749e5eb488e6b99cd6c47beb701b22bdecf5"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5ebc65ea033ef0281368217fbf59f5cb05b338ac4dd23d60959c7afcd79a60a0"}, + {file = "pendulum-3.0.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d9fef18ab0386ef6a9ac7bad7e43ded42c83ff7ad412f950633854f90d59afa8"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c134ba2f0571d0b68b83f6972e2307a55a5a849e7dac8505c715c531d2a8795"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:385680812e7e18af200bb9b4a49777418c32422d05ad5a8eb85144c4a285907b"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eec91cd87c59fb32ec49eb722f375bd58f4be790cae11c1b70fac3ee4f00da0"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4386bffeca23c4b69ad50a36211f75b35a4deb6210bdca112ac3043deb7e494a"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dfbcf1661d7146d7698da4b86e7f04814221081e9fe154183e34f4c5f5fa3bf8"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:04a1094a5aa1daa34a6b57c865b25f691848c61583fb22722a4df5699f6bf74c"}, + {file = "pendulum-3.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5b0ec85b9045bd49dd3a3493a5e7ddfd31c36a2a60da387c419fa04abcaecb23"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0a15b90129765b705eb2039062a6daf4d22c4e28d1a54fa260892e8c3ae6e157"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:bb8f6d7acd67a67d6fedd361ad2958ff0539445ef51cbe8cd288db4306503cd0"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd69b15374bef7e4b4440612915315cc42e8575fcda2a3d7586a0d88192d0c88"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc00f8110db6898360c53c812872662e077eaf9c75515d53ecc65d886eec209a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:83a44e8b40655d0ba565a5c3d1365d27e3e6778ae2a05b69124db9e471255c4a"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1a3604e9fbc06b788041b2a8b78f75c243021e0f512447806a6d37ee5214905d"}, + {file = "pendulum-3.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:92c307ae7accebd06cbae4729f0ba9fa724df5f7d91a0964b1b972a22baa482b"}, + {file = "pendulum-3.0.0.tar.gz", hash = "sha256:5d034998dea404ec31fae27af6b22cff1708f830a1ed7353be4d1019bb9f584e"}, +] + +[package.dependencies] +python-dateutil = ">=2.6" +tzdata = ">=2020.1" + +[package.extras] +test = ["time-machine (>=2.6.0)"] + +[[package]] +name = "pillow" +version = "11.0.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, + {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, + {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, + {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, + {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, + {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, + {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, + {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, + {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, + {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, + {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, + {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, + {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, + {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, + {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, + {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, + {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, + {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, + {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, + {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, + {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, + {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, + {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, + {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, + {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, + {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, + {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, + {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, + {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, + {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, + {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, + {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, + {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, + {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, + {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, + {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, + {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, + {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, + {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, + {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.5.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, + {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "protobuf" +version = "5.28.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-5.28.3-cp310-abi3-win32.whl", hash = "sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24"}, + {file = "protobuf-5.28.3-cp310-abi3-win_amd64.whl", hash = "sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868"}, + {file = "protobuf-5.28.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687"}, + {file = "protobuf-5.28.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584"}, + {file = "protobuf-5.28.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135"}, + {file = "protobuf-5.28.3-cp38-cp38-win32.whl", hash = "sha256:3e6101d095dfd119513cde7259aa703d16c6bbdfae2554dfe5cfdbe94e32d548"}, + {file = "protobuf-5.28.3-cp38-cp38-win_amd64.whl", hash = "sha256:27b246b3723692bf1068d5734ddaf2fccc2cdd6e0c9b47fe099244d80200593b"}, + {file = "protobuf-5.28.3-cp39-cp39-win32.whl", hash = "sha256:135658402f71bbd49500322c0f736145731b16fc79dc8f367ab544a17eab4535"}, + {file = "protobuf-5.28.3-cp39-cp39-win_amd64.whl", hash = "sha256:70585a70fc2dd4818c51287ceef5bdba6387f88a578c86d47bb34669b5552c36"}, + {file = "protobuf-5.28.3-py3-none-any.whl", hash = "sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed"}, + {file = "protobuf-5.28.3.tar.gz", hash = "sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b"}, +] + +[[package]] +name = "pyelftools" +version = "0.31" +description = "Library for analyzing ELF files and DWARF debugging information" +optional = false +python-versions = "*" +files = [ + {file = "pyelftools-0.31-py3-none-any.whl", hash = "sha256:f52de7b3c7e8c64c8abc04a79a1cf37ac5fb0b8a49809827130b858944840607"}, + {file = "pyelftools-0.31.tar.gz", hash = "sha256:c774416b10310156879443b81187d182d8d9ee499660380e645918b50bc88f99"}, +] + +[[package]] +name = "pyftdi" +version = "0.55.4" +description = "FTDI device driver (pure Python)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyftdi-0.55.4-py3-none-any.whl", hash = "sha256:64a804ea1c652e6935897df7867dbba18910848cad0a4927ccdb06813c072c85"}, + {file = "pyftdi-0.55.4.tar.gz", hash = "sha256:cb2770b606507024a65e0a520bf892f239a36cec25cab45338ce3e3027de53c8"}, +] + +[package.dependencies] +pyserial = ">=3.0" +pyusb = ">=1.0.0,<1.2.0 || >1.2.0" + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.12" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77"}, + {file = "pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pyparsing" +version = "3.2.0" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"}, + {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pyproject-api" +version = "1.8.0" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"}, + {file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"}, +] + +[package.dependencies] +packaging = ">=24.1" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.3.3)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] + +[[package]] +name = "pyserial" +version = "3.5" +description = "Python Serial Port Extension" +optional = false +python-versions = "*" +files = [ + {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, + {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, +] + +[package.extras] +cp2110 = ["hidapi"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "python-can" +version = "4.4.2" +description = "Controller Area Network interface module for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_can-4.4.2-py3-none-any.whl", hash = "sha256:e956d781b45563c244c1f3c8fe001e292d1857519cff663d7c184673a68879f9"}, + {file = "python_can-4.4.2.tar.gz", hash = "sha256:1c46c0935f39f7a9c3e76b03249af0580689ebf7a1844195e92f87257f009df5"}, +] + +[package.dependencies] +msgpack = {version = ">=1.0.0,<1.1.0", markers = "platform_system != \"Windows\""} +packaging = ">=23.1" +pywin32 = {version = ">=305", markers = "platform_system == \"Windows\" and platform_python_implementation == \"CPython\""} +typing-extensions = ">=3.10.0.0" +wrapt = ">=1.10,<2.0" + +[package.extras] +canalystii = ["canalystii (>=0.1.0)"] +canine = ["python-can-canine (>=0.2.2)"] +cantact = ["cantact (>=0.0.7)"] +cvector = ["python-can-cvector"] +gs-usb = ["gs-usb (>=0.2.1)"] +lint = ["black (==24.4.*)", "mypy (==1.10.*)", "pylint (==3.2.*)", "ruff (==0.4.8)"] +mf4 = ["asammdf (>=6.0.0)"] +neovi = ["filelock", "python-ics (>=2.12)"] +nixnet = ["nixnet (>=0.3.2)"] +pcan = ["uptime (>=3.0.1,<3.1.0)"] +remote = ["python-can-remote"] +seeedstudio = ["pyserial (>=3.0)"] +serial = ["pyserial (>=3.0,<4.0)"] +sontheim = ["python-can-sontheim (>=0.1.2)"] +viewer = ["windows-curses"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2024.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, +] + +[[package]] +name = "pytzdata" +version = "2020.1" +description = "The Olson timezone database for Python." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"}, + {file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"}, +] + +[[package]] +name = "pyudev" +version = "0.24.3" +description = "A libudev binding" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyudev-0.24.3-py3-none-any.whl", hash = "sha256:e8246f0a014fe370119ba2bc781bfbe62c0298d0d6b39c94e83102a8a3f56960"}, + {file = "pyudev-0.24.3.tar.gz", hash = "sha256:2e945427a21674893bb97632401db62139d91cea1ee96137cc7b07ad22198fc7"}, +] + +[[package]] +name = "pyusb" +version = "1.2.1" +description = "Python USB access module" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "pyusb-1.2.1-py3-none-any.whl", hash = "sha256:2b4c7cb86dbadf044dfb9d3a4ff69fd217013dbe78a792177a3feb172449ea36"}, + {file = "pyusb-1.2.1.tar.gz", hash = "sha256:a4cc7404a203144754164b8b40994e2849fde1cfff06b08492f12fff9d9de7b9"}, +] + +[[package]] +name = "pywin32" +version = "308" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +files = [ + {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, + {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, + {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, + {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, + {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, + {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, + {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, + {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, + {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, + {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, + {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, + {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, + {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, + {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, + {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, + {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, + {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, + {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "regex" +version = "2024.11.6" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +files = [ + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "s3transfer" +version = "0.10.3" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">=3.8" +files = [ + {file = "s3transfer-0.10.3-py3-none-any.whl", hash = "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d"}, + {file = "s3transfer-0.10.3.tar.gz", hash = "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c"}, +] + +[package.dependencies] +botocore = ">=1.33.2,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] + +[[package]] +name = "scipy" +version = "1.13.1" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"}, + {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"}, + {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"}, + {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"}, + {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"}, + {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"}, + {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"}, + {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"}, + {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"}, + {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"}, + {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"}, + {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"}, + {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"}, + {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"}, + {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"}, + {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"}, + {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"}, + {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"}, + {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"}, + {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"}, + {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"}, + {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"}, + {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"}, + {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"}, + {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"}, +] + +[package.dependencies] +numpy = ">=1.22.4,<2.3" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] +doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"] +test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "semantic-version" +version = "2.10.0" +description = "A library implementing the 'SemVer' scheme." +optional = false +python-versions = ">=2.7" +files = [ + {file = "semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177"}, + {file = "semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c"}, +] + +[package.extras] +dev = ["Django (>=1.11)", "check-manifest", "colorama (<=0.4.1)", "coverage", "flake8", "nose2", "readme-renderer (<25.0)", "tox", "wheel", "zest.releaser[recommended]"] +doc = ["Sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "setuptools" +version = "75.6.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +files = [ + {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, + {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "smbus2" +version = "0.5.0" +description = "smbus2 is a drop-in replacement for smbus-cffi/smbus-python in pure Python" +optional = false +python-versions = "*" +files = [ + {file = "smbus2-0.5.0-py2.py3-none-any.whl", hash = "sha256:1a15c3b9fa69357beb038cc0b5d37939702f8bfde1ddc89ca9f17d8461dbe949"}, + {file = "smbus2-0.5.0.tar.gz", hash = "sha256:4a5946fd82277870c2878befdb1a29bb28d15cda14ea4d8d2d54cf3d4bdcb035"}, +] + +[package.extras] +docs = ["sphinx (>=1.5.3)"] +qa = ["flake8"] + +[[package]] +name = "sysv-ipc" +version = "1.1.0" +description = "System V IPC primitives (semaphores, shared memory and message queues) for Python" +optional = false +python-versions = "*" +files = [ + {file = "sysv_ipc-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ff6e3c98d8aa3e1ae5378d86869c4699c76f94318705d08085c97fb010770a31"}, + {file = "sysv_ipc-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:30611dccecd8fa0dc9f83d4d465e5047692df0ec9a095abef8b90ccc64afbdaf"}, + {file = "sysv_ipc-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4bbbe4e5ad5ab859e6d21fc8ff3e3b01082b959fa5c2cd76289dbcd56350c5f9"}, + {file = "sysv_ipc-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:61190b934b0277f7a189092dcabceee8d9736fd73de90fd9b50ea69f0bac4723"}, + {file = "sysv_ipc-1.1.0.tar.gz", hash = "sha256:0f063cbd36ec232032e425769ebc871f195a7d183b9af32f9901589ea7129ac3"}, +] + +[[package]] +name = "tomli" +version = "2.1.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, +] + +[[package]] +name = "tornado" +version = "6.4.1" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.8" +files = [ + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, +] + +[[package]] +name = "tox" +version = "4.23.2" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tox-4.23.2-py3-none-any.whl", hash = "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38"}, + {file = "tox-4.23.2.tar.gz", hash = "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c"}, +] + +[package.dependencies] +cachetools = ">=5.5" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.16.1" +packaging = ">=24.1" +platformdirs = ">=4.3.6" +pluggy = ">=1.5" +pyproject-api = ">=1.8" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.12.2", markers = "python_version < \"3.11\""} +virtualenv = ">=20.26.6" + +[package.extras] +test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"] + +[[package]] +name = "types-protobuf" +version = "4.25.0.20240417" +description = "Typing stubs for protobuf" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-protobuf-4.25.0.20240417.tar.gz", hash = "sha256:c34eff17b9b3a0adb6830622f0f302484e4c089f533a46e3f147568313544352"}, + {file = "types_protobuf-4.25.0.20240417-py3-none-any.whl", hash = "sha256:e9b613227c2127e3d4881d75d93c93b4d6fd97b5f6a099a0b654a05351c8685d"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.27.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +files = [ + {file = "virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4"}, + {file = "virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "watchdog" +version = "4.0.2" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22"}, + {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1"}, + {file = "watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503"}, + {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930"}, + {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b"}, + {file = "watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef"}, + {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a"}, + {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29"}, + {file = "watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a"}, + {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b"}, + {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d"}, + {file = "watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7"}, + {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040"}, + {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7"}, + {file = "watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4"}, + {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9"}, + {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578"}, + {file = "watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b"}, + {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa"}, + {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3"}, + {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508"}, + {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee"}, + {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1"}, + {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757"}, + {file = "watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8"}, + {file = "watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19"}, + {file = "watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b"}, + {file = "watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c"}, + {file = "watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + +[[package]] +name = "zipp" +version = "3.20.2" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[extras] +communication = ["smbus2"] +dephy = ["flexsea", "flexsea"] +messaging = ["grpcio", "grpcio-tools", "types-protobuf"] +moteus = ["moteus", "moteus-pi3hat"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<4.0" +content-hash = "2a1cefd81c534f7d7e3c6a9d4a70950b4d1b20898c1af36fc108aac3aff1e026" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 00000000..ab1033bd --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml index e8b63000..0d70747a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,42 +1,21 @@ -# Poetry pyproject.toml: https://python-poetry.org/docs/pyproject/ -[build-system] -requires = ["poetry_core>=1.0.0"] -build-backend = "poetry.core.masonry.api" - [tool.poetry] name = "opensourceleg" version = "3.0.0" description = "An open-source software library for numerical computation, data acquisition, and control of lower-limb robotic prostheses." -readme = "README.md" -authors = ["Open-source Leg "] -license = "GNU LGPL v2.1" +authors = ["Open-Source Leg "] repository = "https://github.com/neurobionics/opensourceleg" -homepage = "https://github.com/neurobionics/opensourceleg" - - -# Keywords description https://python-poetry.org/docs/pyproject/#keywords -keywords = [] #! Update me - -# Pypi classifiers: https://pypi.org/classifiers/ -classifiers = [ #! Update me - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Topic :: Software Development :: Libraries :: Python Modules", - "License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", +documentation = "https://neurobionics.github.io/opensourceleg/" +readme = "README.md" +packages = [ + {include = "opensourceleg"} ] -[virtualenvs] -in-project = true - [tool.poetry.dependencies] -python = ">=3.9,<4.0.0" +python = ">=3.9,<4.0" numpy = "^1.24.3" flexsea = [ {version = "^12.0.3", python = ">=3.11"}, - {version = "==8.0.1", python = "<3.10"} + {version = "==8.0.1", python = "<3.11"} ] smbus2 = ">=0.4.2" grpcio = "^1.65.5" @@ -48,6 +27,7 @@ board = "^1.0" adafruit-circuitpython-bno055 = {version = "^5.4.13", markers = "sys_platform == 'linux' and platform_machine == 'aarch64'"} adafruit-circuitpython-lis3dh = {version = "^5.2.2", markers = "sys_platform == 'linux' and platform_machine == 'aarch64'"} +pandas = "^2.2.3" [tool.poetry.extras] dephy = ["flexsea"] moteus = ["moteus", "moteus-pi3hat"] @@ -55,118 +35,91 @@ communication = ["smbus2"] messaging = ["grpcio", "grpcio-tools", "types-protobuf"] [tool.poetry.group.dev.dependencies] -bandit = "^1.7.1" -black = {version = ">=24.3,<25.0", allow-prereleases = true} -darglint = "^1.8.1" -isort = {extras = ["colors"], version = "^5.10.1"} -mypy = ">=0.991,<1.1" -mypy-extensions = "^0.4.3" -pre-commit = "^2.15.0" -pydocstyle = "^6.1.1" -pylint = ">=2.13.7,<4.0.0" -pytest = "^8.1.1" -pytest-mock = "^3.12.0" -pyupgrade = ">=2.29.1,<4.0.0" -safety = "^2.2.0" -coverage = "^6.1.2" -coverage-badge = "^1.1.0" -pytest-html = "^4.1.1" -pytest-cov = ">=3,<6" -click = "8.1.7" -sphinx = "^7.4.7" -wheel = "^0.41.2" -sphinx-book-theme = "^1.0.1" - -tornado = "6.4.1" -[tool.bandit] -skips = ["B101"] - -[tool.black] -# https://github.com/psf/black -target-version = ["py39"] -line-length = 88 -color = true +pytest = "^7.2.0" +pytest-cov = "^4.0.0" +deptry = "^0.16.2" +mypy = "^1.5.1" +pre-commit = "^3.4.0" +tox = "^4.11.1" + +[tool.poetry.group.docs.dependencies] +mkdocs = "^1.4.2" +mkdocs-material = "^9.2.7" +mkdocstrings = {extras = ["python"], version = "^0.26.1"} -exclude = ''' -/( - \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - | env - | venv -)/ -''' - -[tool.isort] -# https://github.com/timothycrosley/isort/ -py_version = 39 -line_length = 88 - -known_typing = ["typing", "types", "typing_extensions", "mypy", "mypy_extensions"] -sections = ["FUTURE", "TYPING", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] -include_trailing_comma = true -profile = "black" -multi_line_output = 3 -indent = 4 -color_output = true +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" [tool.mypy] -# https://mypy.readthedocs.io/en/latest/config_file.html#using-a-pyproject-toml-file -python_version = 3.9 -pretty = true -show_traceback = true -color_output = true -exclude = ["tests"] - -allow_redefinition = false -check_untyped_defs = false -disallow_any_generics = true -disallow_incomplete_defs = false -ignore_missing_imports = true -implicit_reexport = false -no_implicit_optional = false -show_column_numbers = true -show_error_codes = true -show_error_context = true -strict_equality = true -strict_optional = true -warn_no_return = true -warn_redundant_casts = true -warn_return_any = true -warn_unreachable = true -warn_unused_configs = true -warn_unused_ignores = true +files = ["opensourceleg"] +exclude = ["opensourceleg/actuators/moteus.py", "opensourceleg/actuators/tmotor.py"] +ignore_missing_imports = "True" +disallow_untyped_defs = "True" +disallow_any_unimported = "True" +no_implicit_optional = "True" +check_untyped_defs = "True" +warn_return_any = "True" +warn_unused_ignores = "True" +show_error_codes = "True" [tool.pytest.ini_options] -# https://docs.pytest.org/en/6.2.x/customize.html#pyproject-toml -# Directories that are not visited by pytest collector: -norecursedirs =["hooks", "*.egg", ".eggs", "dist", "build", "docs", ".tox", ".git", "__pycache__"] -doctest_optionflags = ["NUMBER", "NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL"] - -# Extra options: -addopts = [ - "--strict-markers", - "--tb=short", - "--doctest-modules", - "--doctest-continue-on-failure", +testpaths = ["tests"] + +[tool.ruff] +target-version = "py39" +line-length = 120 +fix = true +select = [ + # flake8-2020 + "YTT", + # flake8-bandit + "S", + # flake8-bugbear + "B", + # flake8-builtins + "A", + # flake8-comprehensions + "C4", + # flake8-debugger + "T10", + # flake8-simplify + "SIM", + # isort + "I", + # mccabe + "C90", + # pycodestyle + "E", "W", + # pyflakes + "F", + # pygrep-hooks + "PGH", + # pyupgrade + "UP", + # ruff + "RUF", + # tryceratops + "TRY", +] +ignore = [ + # DoNotAssignLambda + "E731", + "TRY003", + "SIM115", ] -[tool.coverage.run] -source = ["tests"] +[tool.ruff.format] +preview = true -[coverage.paths] -source = "opensourceleg" +[tool.coverage.report] +skip_empty = true -[coverage.run] +[tool.coverage.run] branch = true +source = ["opensourceleg"] + -[coverage.report] -fail_under = 50 -show_missing = true +[tool.ruff.per-file-ignores] +"tests/*" = ["S101"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index aaca9ab9..00000000 --- a/requirements.txt +++ /dev/null @@ -1,60 +0,0 @@ -adafruit-blinka==8.50.0 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -adafruit-circuitpython-bno055==5.4.14 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -adafruit-circuitpython-busdevice==5.2.10 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -adafruit-circuitpython-connectionmanager==3.1.2 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -adafruit-circuitpython-lis3dh==5.2.3 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -adafruit-circuitpython-register==1.10.1 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -adafruit-circuitpython-requests==4.1.8 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -adafruit-circuitpython-typing==1.11.1 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -adafruit-platformdetect==3.75.0 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -adafruit-pureio==1.1.11 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -binho-host-adapter==0.1.6 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -board==1.0 ; python_version >= "3.9" and python_full_version < "4.0.0" -boto3==1.35.54 ; python_version >= "3.11" and python_version < "4.0" -botocore==1.35.54 ; python_version >= "3.11" and python_version < "4.0" -contourpy==1.3.0 ; python_version >= "3.9" and python_version < "3.10" -cycler==0.12.1 ; python_version >= "3.9" and python_version < "3.10" -flexsea==12.0.4 ; python_version >= "3.11" and python_version < "4.0" -flexsea==8.0.1 ; python_version >= "3.9" and python_version < "3.10" -fonttools==4.54.1 ; python_version >= "3.9" and python_version < "3.10" -grpcio-tools==1.67.1 ; python_version >= "3.9" and python_full_version < "4.0.0" -grpcio==1.67.1 ; python_version >= "3.9" and python_full_version < "4.0.0" -importlib-metadata==8.5.0 ; python_version >= "3.9" and python_version < "4" -importlib-resources==6.4.5 ; python_version >= "3.9" and python_version < "3.10" -jmespath==1.0.1 ; python_version >= "3.11" and python_version < "4.0" -kiwisolver==1.4.7 ; python_version >= "3.9" and python_version < "3.10" -matplotlib==3.9.2 ; python_version >= "3.9" and python_version < "3.10" -moteus-pi3hat==0.3.30 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -moteus==0.3.73 ; python_version >= "3.9" and python_version < "4" -msgpack==1.0.8 ; python_version >= "3.9" and python_version < "4" and platform_system != "Windows" -numpy==1.26.4 ; python_version >= "3.9" and python_version < "4" -packaging==24.1 ; python_version >= "3.9" and python_version < "4" -pendulum==2.1.2 ; python_version >= "3.11" and python_version < "3.12" -pendulum==3.0.0 ; python_version >= "3.12" and python_version < "4.0" -pillow==11.0.0 ; python_version >= "3.9" and python_version < "3.10" -protobuf==5.28.3 ; python_version >= "3.9" and python_full_version < "4.0.0" -pyelftools==0.31 ; python_version >= "3.9" and python_version < "4" -pyftdi==0.55.4 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -pyparsing==3.2.0 ; python_version >= "3.9" and python_version < "3.10" -pyserial==3.5 ; python_version >= "3.9" and python_version < "4" -python-can==4.4.2 ; python_version >= "3.9" and python_version < "4" -python-dateutil==2.9.0.post0 ; python_version >= "3.11" and python_version < "4.0" or python_version >= "3.9" and python_version < "3.10" -pytzdata==2020.1 ; python_version >= "3.11" and python_version < "3.12" -pyudev==0.24.3 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "linux" -pyusb==1.2.1 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -pywin32==308 ; python_version >= "3.9" and python_version < "4" and platform_system == "Windows" -pyyaml==6.0.2 ; python_version >= "3.11" and python_version < "4.0" or python_version >= "3.9" and python_version < "3.10" -s3transfer==0.10.3 ; python_version >= "3.11" and python_version < "4.0" -scipy==1.13.1 ; python_version >= "3.9" and python_version < "4" -semantic-version==2.10.0 ; python_version >= "3.11" and python_version < "4.0" -setuptools==75.3.0 ; python_version >= "3.9" and python_full_version < "4.0.0" -six==1.16.0 ; python_version >= "3.11" and python_version < "4.0" or python_version >= "3.9" and python_version < "3.10" -smbus2==0.5.0 ; python_version >= "3.9" and python_full_version < "4.0.0" -sysv-ipc==1.1.0 ; sys_platform == "linux" and platform_machine == "aarch64" and python_version >= "3.9" and python_full_version < "4.0.0" -tornado==6.4.1 ; python_version >= "3.9" and python_version < "3.10" -types-protobuf==4.25.0.20240417 ; python_version >= "3.9" and python_full_version < "4.0.0" -typing-extensions==4.12.2 ; python_version >= "3.9" and python_version < "4" -tzdata==2024.2 ; python_version >= "3.12" and python_version < "4.0" -urllib3==2.2.3 ; python_version >= "3.11" and python_version < "4.0" -wrapt==1.16.0 ; python_version >= "3.9" and python_version < "4" -zipp==3.20.2 ; python_version >= "3.9" and python_version < "4" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 4b801204..00000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[metadata] -description-file = README.md - -[darglint] -# https://github.com/terrencepreilly/darglint -strictness = long -docstring_style = google diff --git a/tests/test_actuators/test_actuators_base.py b/tests/test_actuators/test_actuators_base.py index 7b03b705..1009f6a7 100644 --- a/tests/test_actuators/test_actuators_base.py +++ b/tests/test_actuators/test_actuators_base.py @@ -1,11 +1,21 @@ -import unittest +from typing import ClassVar from unittest.mock import Mock, patch import numpy as np import pytest -from opensourceleg.actuators.base import * -from opensourceleg.actuators.base import MOTOR_CONSTANTS +from opensourceleg.actuators.base import ( + CONTROL_MODE_CONFIGS, + CONTROL_MODE_METHODS, + CONTROL_MODES, + MOTOR_CONSTANTS, + ActuatorBase, + ControlGains, + ControlModeConfig, + MethodWithRequiredModes, + T, + requires, +) DEFAULT_VALUES = [0, 1, 1000, -1000] @@ -40,8 +50,8 @@ def create_motor_constants(values): def test_motor_constants_init(non_zero_positive_values): motor_constants = create_motor_constants(non_zero_positive_values) assert list(motor_constants.__dict__.values()) == list(non_zero_positive_values) - assert motor_constants.RAD_PER_COUNT == 2 * np.pi / non_zero_positive_values[0] - assert motor_constants.NM_PER_MILLIAMP == non_zero_positive_values[1] / 1000 + assert 2 * np.pi / non_zero_positive_values[0] == motor_constants.RAD_PER_COUNT + assert non_zero_positive_values[1] / 1000 == motor_constants.NM_PER_MILLIAMP @pytest.mark.parametrize( @@ -56,15 +66,15 @@ def test_motor_constants_init(non_zero_positive_values): ) def test_motor_constants_init_values(zero_values, non_zero_negative_values): with pytest.raises(ValueError): - motor_constants = create_motor_constants(zero_values) + create_motor_constants(zero_values) with pytest.raises(ValueError): - motor_constants = create_motor_constants(non_zero_negative_values) + create_motor_constants(non_zero_negative_values) def test_motor_constants_init_types(): with pytest.raises(TypeError): - motor_constants = MOTOR_CONSTANTS(1, 2) + MOTOR_CONSTANTS(1, 2) @pytest.mark.parametrize( @@ -74,12 +84,12 @@ def test_motor_constants_init_types(): ) def test_motor_constants_properties(non_zero_positive_values): motor_constants = create_motor_constants(non_zero_positive_values) - assert motor_constants.RAD_PER_COUNT == 2 * np.pi / non_zero_positive_values[0] - assert motor_constants.NM_PER_MILLIAMP == non_zero_positive_values[1] / 1000 + assert 2 * np.pi / non_zero_positive_values[0] == motor_constants.RAD_PER_COUNT + assert non_zero_positive_values[1] / 1000 == motor_constants.NM_PER_MILLIAMP def test_control_modes_default_four(): - {"POSITION", "CURRENT", "VOLTAGE", "IMPEDANCE"} <= {e.name for e in CONTROL_MODES} + assert {"POSITION", "CURRENT", "VOLTAGE", "IMPEDANCE"} <= {e.name for e in CONTROL_MODES} def test_control_modes_dephy_order(): @@ -139,7 +149,7 @@ def test_control_gains_init_partial(default_value): @pytest.mark.parametrize("default_value", DEFAULT_VALUES) def test_control_gains_init_invalid(default_value): with pytest.raises(TypeError): - control_gains = ControlGains(kt=default_value) + ControlGains(kt=default_value) # Python3.9 does not support @dataclass(kw_only=True) # with pytest.raises(TypeError): @@ -163,22 +173,22 @@ def test_control_mode_config_init_default(): entry_callback=lambda _: None, exit_callback=lambda _: None, ) - assert control_mode_config.has_gains == False - assert control_mode_config.max_gains == None + assert control_mode_config.has_gains is False + assert control_mode_config.max_gains is None def test_control_mode_config_init_invalid(): with pytest.raises(TypeError): - control_mode_config = ControlModeConfig() + ControlModeConfig() with pytest.raises(TypeError): - control_mode_config = ControlModeConfig( + ControlModeConfig( control_mode=CONTROL_MODES.POSITION, gains=ControlGains(), ) with pytest.raises(TypeError): - control_mode_config = ControlModeConfig( + ControlModeConfig( has_gains=True, ) @@ -207,13 +217,13 @@ def test_control_mode_config_init(): def test_control_mode_configs_init_default(): control_mode_configs = CONTROL_MODE_CONFIGS() - assert control_mode_configs.POSITION == None - assert control_mode_configs.CURRENT == None - assert control_mode_configs.VOLTAGE == None - assert control_mode_configs.IMPEDANCE == None - assert control_mode_configs.VELOCITY == None - assert control_mode_configs.TORQUE == None - assert control_mode_configs.IDLE == None + assert control_mode_configs.POSITION is None + assert control_mode_configs.CURRENT is None + assert control_mode_configs.VOLTAGE is None + assert control_mode_configs.IMPEDANCE is None + assert control_mode_configs.VELOCITY is None + assert control_mode_configs.TORQUE is None + assert control_mode_configs.IDLE is None def test_control_mode_configs_init(): @@ -232,18 +242,18 @@ def test_control_mode_configs_init(): IMPEDANCE=default_control_mode_config, ) - assert control_mode_configs.POSITION == default_control_mode_config - assert control_mode_configs.CURRENT == default_control_mode_config - assert control_mode_configs.VOLTAGE == default_control_mode_config - assert control_mode_configs.IMPEDANCE == default_control_mode_config - assert control_mode_configs.VELOCITY == None - assert control_mode_configs.TORQUE == None - assert control_mode_configs.IDLE == None + assert default_control_mode_config == control_mode_configs.POSITION + assert default_control_mode_config == control_mode_configs.CURRENT + assert default_control_mode_config == control_mode_configs.VOLTAGE + assert default_control_mode_config == control_mode_configs.IMPEDANCE + assert control_mode_configs.VELOCITY is None + assert control_mode_configs.TORQUE is None + assert control_mode_configs.IDLE is None def test_control_mode_methods(): - assert type(CONTROL_MODE_METHODS) == list - assert all([type(x) == str for x in CONTROL_MODE_METHODS]) + assert type(CONTROL_MODE_METHODS) is list + assert all(type(x) is str for x in CONTROL_MODE_METHODS) assert len(CONTROL_MODE_METHODS) >= 12 @@ -265,7 +275,7 @@ def test_typevar_usage_invalid(): def test_method_with_required_modes(): class TestClass(MethodWithRequiredModes): - _required_modes = {CONTROL_MODES.POSITION, CONTROL_MODES.CURRENT} + _required_modes: ClassVar = {CONTROL_MODES.POSITION, CONTROL_MODES.CURRENT} test_class_instance = TestClass() @@ -403,6 +413,21 @@ def set_motor_position(self, value): def set_motor_torque(self, value): pass + def set_output_torque(self, value): + pass + + def set_current(self, value): + pass + + def set_voltage(self, value): + pass + + def set_motor_impedance(self, value): + pass + + def set_output_impedance(self, value): + pass + def set_joint_torque(self, value): pass @@ -500,14 +525,10 @@ def test_set_joint_direction(mock_actuator: MockActuator): def test_output_position_and_velocity(mock_actuator: MockActuator): - with patch.object( - MockActuator, "motor_position", new_callable=Mock(return_value=10.0) - ): + with patch.object(MockActuator, "motor_position", new_callable=Mock(return_value=10.0)): assert mock_actuator.output_position == 1.0 # 10.0 / 10.0 (gear_ratio) - with patch.object( - MockActuator, "motor_velocity", new_callable=Mock(return_value=5.0) - ): + with patch.object(MockActuator, "motor_velocity", new_callable=Mock(return_value=5.0)): assert mock_actuator.output_velocity == 0.5 # 5.0 / 10.0 (gear_ratio) @@ -518,9 +539,7 @@ def test_temperature_limits(mock_actuator: MockActuator): def test_method_restriction(mock_actuator: MockActuator): mock_actuator.set_control_mode(CONTROL_MODES.IDLE) - assert ( - mock_actuator.set_motor_voltage(5.0) is None - ) # Should be restricted in IDLE mode + assert mock_actuator.set_motor_voltage(5.0) is None # Should be restricted in IDLE mode mock_actuator.set_control_mode(CONTROL_MODES.VOLTAGE) with patch.object(mock_actuator, "set_motor_voltage") as mock_method: @@ -554,15 +573,3 @@ def test_motor_constants(mock_actuator: MockActuator): assert mock_actuator.MOTOR_CONSTANTS.NM_S_PER_RAD_TO_B == 0.1 assert mock_actuator.MOTOR_CONSTANTS.MAX_CASE_TEMPERATURE == 100.0 assert mock_actuator.MOTOR_CONSTANTS.MAX_WINDING_TEMPERATURE == 150.0 - - -def test_motor_constants_properties(mock_actuator: MockActuator): - assert ( - pytest.approx(mock_actuator.MOTOR_CONSTANTS.RAD_PER_COUNT, 0.00001) - == 2 * 3.14159 / 1000 - ) - assert mock_actuator.MOTOR_CONSTANTS.NM_PER_MILLIAMP == 0.0001 - - -def test_hello_world(): - assert len("Hello World") == 11 diff --git a/tests/test_control/test_compiled_controller.py b/tests/test_control/test_compiled_controller.py index 4b06d2f2..0faab5b1 100644 --- a/tests/test_control/test_compiled_controller.py +++ b/tests/test_control/test_compiled_controller.py @@ -384,9 +384,7 @@ def test_cleanup_function_called_on_exception(monkeypatch): mock_cleanup_function = MagicMock() mock_lib.cleanup_func = mock_cleanup_function - controller = CompiledController( - "test_lib", "/path/to/lib", "main_func", cleanup_function_name="cleanup_func" - ) + controller = CompiledController("test_lib", "/path/to/lib", "main_func", cleanup_function_name="cleanup_func") # Mock main_function to raise an exception def mock_main_function(inputs, outputs): diff --git a/tests/test_control/test_state_machine.py b/tests/test_control/test_state_machine.py index 4bcf7369..1b9c1632 100644 --- a/tests/test_control/test_state_machine.py +++ b/tests/test_control/test_state_machine.py @@ -174,9 +174,7 @@ def criteria(data): def action(data): action_called.append(True) - transition = FromToTransition( - event=event, source=state1, destination=state2, callback=criteria - ) + transition = FromToTransition(event=event, source=state1, destination=state2, callback=criteria) transition.add_action(action) state1.start(data) next_state = transition(data) @@ -194,9 +192,7 @@ def test_from_to_transition_no_criteria(): def criteria(data): return False - transition = FromToTransition( - event=event, source=state1, destination=state2, callback=criteria - ) + transition = FromToTransition(event=event, source=state1, destination=state2, callback=criteria) state1.start(data) next_state = transition(data) assert next_state == state1 # Should remain in state1 diff --git a/tests/test_logging/test_logging_decorators.py b/tests/test_logging/test_logging_decorators.py index 2f788f73..579af064 100644 --- a/tests/test_logging/test_logging_decorators.py +++ b/tests/test_logging/test_logging_decorators.py @@ -1,6 +1,7 @@ from unittest.mock import Mock -from opensourceleg.logging.decorators import * +from opensourceleg.logging import LOGGER +from opensourceleg.logging.decorators import deprecated, deprecated_with_routing, deprecated_with_suggestion # test deprecated & decorator @@ -13,9 +14,7 @@ def testing(x): return x assert testing(2) == 2 - LOGGER.warning.assert_called_once_with( - f"Function `{testing.__name__}` is deprecated." - ) + LOGGER.warning.assert_called_once_with(f"Function `{testing.__name__}` is deprecated.") LOGGER.warning = LOGGER.original_warning del LOGGER.original_warning @@ -61,7 +60,9 @@ def testing(x): assert testing(2) == 6 LOGGER.warning.assert_called_once_with( - f"Function `{testing.__name__}` is deprecated. Please use `{alternate.__name__}` instead, which will be called automatically now." + f"Function `{testing.__name__}` is deprecated. " + f"Please use `{alternate.__name__}` instead, " + f"which will be called automatically now." ) LOGGER.warning = LOGGER.original_warning diff --git a/tests/test_logging/test_logging_exceptions.py b/tests/test_logging/test_logging_exceptions.py index a95518b5..87ae6cde 100644 --- a/tests/test_logging/test_logging_exceptions.py +++ b/tests/test_logging/test_logging_exceptions.py @@ -1,6 +1,13 @@ import pytest -from opensourceleg.logging.exceptions import * +from opensourceleg.logging.exceptions import ( + ActuatorConnectionException, + ActuatorIsNoneException, + ActuatorKeyException, + ActuatorStreamException, + ControlModeException, + VoltageModeMissingException, +) # Test ActuatorStreamException @@ -8,10 +15,7 @@ def test_actuator_stream_exception(): test_str = "test" with pytest.raises(ActuatorStreamException) as e: raise ActuatorStreamException(test_str) - assert ( - str(e.value) - == f"{test_str} is not streaming, please call start() method before sending commands" - ) + assert str(e.value) == f"{test_str} is not streaming, please call start() method before sending commands" # Test ActuatorConnectionException @@ -27,9 +31,9 @@ def test_actuator_is_none_exception(): test_str = "test" with pytest.raises(ActuatorIsNoneException) as e: raise ActuatorIsNoneException(test_str) - assert ( - str(e.value) - == f"Actuator is None in {test_str} mode, please pass the actuator instance to the mode during initialization or set the actuator instance using set_actuator method." + assert str(e.value) == ( + f"Actuator is None in {test_str} mode, please pass the actuator instance to the mode during initialization " + "or set the actuator instance using set_actuator method." ) @@ -61,6 +65,6 @@ def test_actuator_key_exception(): with pytest.raises(ActuatorKeyException) as e: raise ActuatorKeyException(test_str, test_key) assert ( - str(e.value) - == f"{test_str} does not have {test_key} key in the actuators dictionary. Please check the actuators dictionary for the `{test_key}` key." + str(e.value) == f"{test_str} does not have {test_key} key in the actuators dictionary. " + f"Please check the actuators dictionary for the `{test_key}` key." ) diff --git a/tests/test_logging/test_logging_logger.py b/tests/test_logging/test_logging_logger.py index fd5cc924..73168863 100644 --- a/tests/test_logging/test_logging_logger.py +++ b/tests/test_logging/test_logging_logger.py @@ -1,74 +1,75 @@ +import csv +import logging +import os +from collections import deque from unittest.mock import Mock import pytest -from opensourceleg.logging.logger import * +from opensourceleg.logging.logger import LOGGER, Logger, LogLevel + +CURR_DIR = os.path.dirname(os.path.realpath(__file__)) # Test LogLevel class def test_log_level_default(): - {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} <= {e.name for e in LogLevel} + assert {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} <= {e.name for e in LogLevel} def test_log_level_value_logging(): - assert all( - [ - LogLevel.DEBUG.value == logging.DEBUG, - LogLevel.INFO.value == logging.INFO, - LogLevel.WARNING.value == logging.WARNING, - LogLevel.ERROR.value == logging.ERROR, - LogLevel.CRITICAL.value == logging.CRITICAL, - ] - ) + assert all([ + LogLevel.DEBUG.value == logging.DEBUG, + LogLevel.INFO.value == logging.INFO, + LogLevel.WARNING.value == logging.WARNING, + LogLevel.ERROR.value == logging.ERROR, + LogLevel.CRITICAL.value == logging.CRITICAL, + ]) def test_log_level_len(): assert len(LogLevel) == 5 -# Test Logger class @pytest.fixture(scope="function") def test_logger(): - log = Logger() + log = Logger( + log_path=CURR_DIR, + file_name="test_logging", + ) log.reset() yield log +# check if the fixture is being reset after each test +def test_fixture_reset(test_logger: Logger): + assert not hasattr(test_logger, "_file_handler") + + # Test new -def test_logger_new(): - logger = Logger() - logger2 = Logger() - assert logger is logger2 - logger.reset() - logger2.reset() +def test_logger_new(test_logger: Logger): + new_logger = Logger.__new__(Logger) + assert new_logger is test_logger + new_logger.reset() # Test init -def test_logger_init_default(): - logger = Logger() - assert all( - [ - logger._log_path == "./", - isinstance(logger.file_level, LogLevel), - isinstance(logger.stream_level, LogLevel), - logger.file_max_bytes == 0, - logger._file_backup_count == 5, - logger.buffer_size == 1000, - ] - ) - logger.reset() +def test_logger_init_default(test_logger: Logger): + assert all([ + isinstance(test_logger.file_level, LogLevel), + isinstance(test_logger.stream_level, LogLevel), + test_logger.file_max_bytes == 0, + test_logger._file_backup_count == 5, + test_logger.buffer_size == 1000, + ]) -def test_logger_init_set(): - logger = Logger(buffer_size=10, file_level=LogLevel.CRITICAL) - assert all( - [ - logger._log_format == "[%(asctime)s] %(levelname)s: %(message)s", - logger._buffer_size == 10, - logger._file_level == LogLevel.CRITICAL, - ] - ) - logger.reset() +def test_logger_init_set(test_logger: Logger): + test_logger = Logger(buffer_size=10, file_level=LogLevel.CRITICAL) + assert all([ + test_logger._log_format == "[%(asctime)s] %(levelname)s: %(message)s", + test_logger._buffer_size == 10, + test_logger._file_level == LogLevel.CRITICAL, + ]) # Test setup logging @@ -79,40 +80,28 @@ def test_setup_logging(): stream_level=LogLevel.INFO, ) test_logger._setup_logging() - assert all( - [ - test_logger.level == LogLevel.DEBUG.value, - test_logger._stream_handler.level == LogLevel.INFO.value, - test_logger._stream_handler.formatter._fmt == "[%(levelname)s]", - ] - ) + assert all([ + test_logger.level == LogLevel.DEBUG.value, + test_logger._stream_handler.level == LogLevel.INFO.value, + test_logger._stream_handler.formatter._fmt == "[%(levelname)s]", + ]) test_logger.reset() # Test setup file handler -def test_setup_file_handler(): - test_logger = Logger( - file_max_bytes=0, - file_backup_count=20, - file_level=LogLevel.WARNING, - log_format="[%(levelname)s]", - ) +def test_setup_file_handler(test_logger: Logger): assert not hasattr(test_logger, "_file_handler") test_logger._setup_file_handler() - assert all( - [ - test_logger._file_handler.maxBytes == 0, - test_logger._file_handler.backupCount == 20, - test_logger._file_handler.level == LogLevel.WARNING.value, - test_logger._file_handler.mode == "a", - test_logger._file_handler.formatter._fmt == "[%(levelname)s]", - hasattr(test_logger, "_file_handler"), - ] - ) - - test_logger.reset() + assert all([ + test_logger._file_handler.maxBytes == 0, + test_logger._file_handler.backupCount == 5, + test_logger._file_handler.level == LogLevel.DEBUG.value, + test_logger._file_handler.mode == "w", + test_logger._file_handler.formatter._fmt == "[%(asctime)s] %(levelname)s: %(message)s", + hasattr(test_logger, "_file_handler"), + ]) # Test ensure file handler @@ -142,20 +131,16 @@ def test_func() -> list: assert test_func() == [1, 2, 3] test_logger.track_variable(test_func, "Testing") - assert all( - [ - test_func in list(test_logger._tracked_vars.values()), - "Testing" in list(test_logger._var_names.values()), - ] - ) + assert all([ + test_func in list(test_logger._tracked_vars.values()), + "Testing" in list(test_logger._var_names.values()), + ]) test_logger.untrack_variable(test_func) - assert all( - [ - not test_func in list(test_logger._tracked_vars.values()), - not "Testing" in list(test_logger._var_names.values()), - ] - ) + assert all([ + test_func not in list(test_logger._tracked_vars.values()), + "Testing" not in list(test_logger._var_names.values()), + ]) # Test repr @@ -167,83 +152,65 @@ def test_repr(test_logger: Logger): # Test set file name def test_set_file_name_str(test_logger: Logger): test_logger.set_file_name("test_file") - assert all( - [ - test_logger._user_file_name == "test_file", - test_logger._file_path == "", - test_logger._csv_path == "", - ] - ) + assert all([ + test_logger._user_file_name == "test_file", + test_logger._file_path == f"{CURR_DIR}/test_file.log", + test_logger._csv_path == f"{CURR_DIR}/test_file.csv", + ]) def test_set_file_name_none(test_logger: Logger): test_logger.set_file_name(None) - assert all( - [ - test_logger._user_file_name == None, - test_logger._file_path == "", - test_logger._csv_path == "", - ] - ) + assert all([ + test_logger._user_file_name is None, + ]) # Test set file level def test_set_file_level(test_logger: Logger): test_logger.set_file_level(LogLevel.CRITICAL) - assert all( - [ - test_logger._file_level == LogLevel.CRITICAL, - not hasattr(test_logger, "_file_handler"), - ] - ) + assert all([ + test_logger._file_level == LogLevel.CRITICAL, + not hasattr(test_logger, "_file_handler"), + ]) def test_set_file_level_has_attr(test_logger: Logger): test_logger._setup_file_handler() - assert all( - [hasattr(test_logger, "_file_handler"), test_logger._file_handler.mode == "a"] - ) + assert all([hasattr(test_logger, "_file_handler"), test_logger._file_handler.mode == "w"]) test_logger.set_file_level(LogLevel.DEBUG) - assert all( - [ - test_logger._file_level == LogLevel.DEBUG, - test_logger._file_handler.level == LogLevel.DEBUG.value, - ] - ) + assert all([ + test_logger._file_level == LogLevel.DEBUG, + test_logger._file_handler.level == LogLevel.DEBUG.value, + ]) # Test set stream level def test_set_stream_level(test_logger: Logger): test_logger.set_stream_level(LogLevel.ERROR) - assert all( - [ - hasattr(test_logger, "_stream_handler"), - test_logger._stream_level == LogLevel.ERROR, - test_logger._stream_handler.level == LogLevel.ERROR.value, - ] - ) + assert all([ + hasattr(test_logger, "_stream_handler"), + test_logger._stream_level == LogLevel.ERROR, + test_logger._stream_handler.level == LogLevel.ERROR.value, + ]) # Test set format def test_set_format(test_logger: Logger): test_logger.set_format("[%(levelname)s]") - assert all( - [ - test_logger._log_format == "[%(levelname)s]", - isinstance(test_logger._std_formatter, logging.Formatter), - test_logger._std_formatter._fmt == "[%(levelname)s]", - not hasattr(test_logger, "_file_handler"), - test_logger._stream_handler.formatter._fmt == "[%(levelname)s]", - ] - ) + assert all([ + test_logger._log_format == "[%(levelname)s]", + isinstance(test_logger._std_formatter, logging.Formatter), + test_logger._std_formatter._fmt == "[%(levelname)s]", + not hasattr(test_logger, "_file_handler"), + test_logger._stream_handler.formatter._fmt == "[%(levelname)s]", + ]) def test_set_format_has_attr(test_logger: Logger): test_logger._setup_file_handler() - assert all( - [hasattr(test_logger, "_file_handler"), test_logger._file_handler.mode == "a"] - ) + assert all([hasattr(test_logger, "_file_handler"), test_logger._file_handler.mode == "w"]) test_logger.set_format("[%(test)s]") assert test_logger._file_handler.formatter._fmt == "[%(test)s]" @@ -252,13 +219,11 @@ def test_set_format_has_attr(test_logger: Logger): # Test set buffer size def test_set_buffer_size(test_logger: Logger): test_logger.set_buffer_size(5) - assert all( - [ - test_logger._buffer_size == 5, - isinstance(test_logger._buffer, deque), - test_logger._buffer.maxlen == 5, - ] - ) + assert all([ + test_logger._buffer_size == 5, + isinstance(test_logger._buffer, deque), + test_logger._buffer.maxlen == 5, + ]) # Test update @@ -274,13 +239,11 @@ def test_func2() -> int: test_logger.update() test_logger.track_variable(test_func2, "second") test_logger.update() - assert all( - [ - test_logger._buffer[0] == ["18"], - test_logger._buffer[1] == ["18", "8"], - len(test_logger._buffer) == 2, - ] - ) + assert all([ + test_logger._buffer[0] == ["18"], + test_logger._buffer[1] == ["18", "8"], + len(test_logger._buffer) == 2, + ]) # Test update size exceeded @@ -309,10 +272,6 @@ def test_func() -> int: test_logger._ensure_file_handler() - # Clear the file to start since it is being used by other tests - test_logger._file = open(test_logger._csv_path, "w", newline="") - test_logger.close() - test_logger.flush_buffer() assert len(test_logger._buffer) == 0 @@ -333,7 +292,7 @@ def test_write_header(test_logger: Logger): test_logger._writer = csv.writer(test_logger._file) test_logger._write_header() test_logger.close() - assert test_logger._header_written == True + assert test_logger._header_written is True # Ensure expected output was written file = open(test_logger._csv_path) @@ -355,31 +314,25 @@ def test_generate_file_paths_no_input_filename(test_logger: Logger): def test_generate_file_paths_with_input_filename(test_logger: Logger): test_logger._user_file_name = "test_file" test_logger._generate_file_paths() - assert test_logger._csv_path == "./test_file.csv" + assert test_logger._csv_path == f"{test_logger.log_path}/test_file.csv" # Test enter def test_enter(test_logger: Logger): - assert isinstance(test_logger.__enter__(), Logger) assert test_logger.__enter__() is test_logger # Test exit def test_exit(test_logger: Logger): - test_logger.track_variable(lambda: 2, "first") - test_logger.update() - - original_flush = test_logger.flush_buffer - original_close = test_logger.close - test_logger.flush_buffer = Mock() - test_logger.close = Mock() + with test_logger: + # Perform some logging operations + test_logger.track_variable(lambda: 1, "test_var") + test_logger.update() - test_logger.__exit__(1, 1, 1) - test_logger.flush_buffer.assert_called_once() - test_logger.close.assert_called_once() - - test_logger.flush_buffer = original_flush - test_logger.close = original_close + # After exiting the context, the logger should be closed + assert len(test_logger._buffer) == 0 + assert test_logger._file is None + assert test_logger._writer is None # Test reset @@ -387,34 +340,30 @@ def test_reset(test_logger: Logger): test_logger.track_variable(lambda: 2, "test") test_logger.update() test_logger._setup_file_handler() - assert all( - [ - len(test_logger._buffer) == 1, - len(test_logger._tracked_vars) == 1, - len(test_logger._var_names) == 1, - hasattr(test_logger, "_file_handler"), - ] - ) + assert all([ + len(test_logger._buffer) == 1, + len(test_logger._tracked_vars) == 1, + len(test_logger._var_names) == 1, + hasattr(test_logger, "_file_handler"), + ]) test_logger.reset() - assert all( - [ - len(test_logger._buffer) == 0, - len(test_logger._tracked_vars) == 0, - len(test_logger._var_names) == 0, - not hasattr(test_logger, "_file_handler"), - ] - ) + assert all([ + len(test_logger._buffer) == 0, + len(test_logger._tracked_vars) == 0, + len(test_logger._var_names) == 0, + not hasattr(test_logger, "_file_handler"), + ]) def test_reset_header(test_logger: Logger): test_logger.track_variable(lambda: 2, "test") test_logger.update() test_logger.flush_buffer() - assert test_logger._header_written == True + assert test_logger._header_written is True test_logger.reset() - assert test_logger._header_written == False + assert test_logger._header_written is False # Test close @@ -519,16 +468,15 @@ def test_log(test_logger: Logger): logging.Logger.log = original_log -# Test file path -def test_file_path(test_logger: Logger): - original_generate = test_logger._generate_file_paths +# # Test file path +# def test_file_path(test_logger: Logger): +# original_generate = test_logger._generate_file_paths - test_logger._generate_file_paths = Mock() - test_logger._file_path = "" - filename = test_logger.file_path - test_logger._generate_file_paths.assert_called_once() +# test_logger._generate_file_paths = Mock() +# test_logger._file_path = "" +# test_logger._generate_file_paths.assert_called_once() - test_logger._generate_file_paths = original_generate +# test_logger._generate_file_paths = original_generate # Test buffer size @@ -545,6 +493,7 @@ def test_file_level(test_logger: Logger): # Test stream level def test_stream_level(test_logger: Logger): + assert test_logger._file is None test_logger.set_stream_level(LogLevel.INFO) assert test_logger.stream_level == LogLevel.INFO diff --git a/tests/test_math/test_math.py b/tests/test_math/test_math.py index 0293cdbc..43f350b6 100644 --- a/tests/test_math/test_math.py +++ b/tests/test_math/test_math.py @@ -4,36 +4,32 @@ def test_edge_detector_init(): - edi = EdgeDetector(bool_in=False) - assert edi.cur_state == False - assert edi.rising_edge == False - assert edi.falling_edge == False + assert edi.cur_state is False + assert edi.rising_edge is False + assert edi.falling_edge is False def test_edge_detector_update(): - edu = EdgeDetector(bool_in=False) edu.update(bool_in=True) - assert edu.rising_edge == True - assert edu.falling_edge == False - assert edu.cur_state == True + assert edu.rising_edge is True + assert edu.falling_edge is False + assert edu.cur_state is True edu2 = EdgeDetector(bool_in=True) edu2.update(bool_in=False) - assert edu2.rising_edge == False - assert edu2.falling_edge == True - assert edu2.cur_state == False + assert edu2.rising_edge is False + assert edu2.falling_edge is True + assert edu2.cur_state is False def test_saturating_ramp_init(): - sri = SaturatingRamp(loop_frequency=100, ramp_time=1.0) assert sri.delta_per_update == 1.0 / 100 assert sri.value == 0.0 def test_saturating_ramp_update(): - sru = SaturatingRamp(loop_frequency=100, ramp_time=1.0) sru.update(enable_ramp=True) assert sru.value == 0.01 @@ -60,7 +56,7 @@ def test_saturating_ramp_update(): (test_model_default, "R_WC", 1.0702867186480716), (test_model_default, "C_c", 512.249065845453), (test_model_default, "R_CA", 1.9406620046327363), - (test_model_default, "α", 0.393 * 1 / 100), + (test_model_default, "α", 0.393 * 1 / 100), # noqa: RUF001 (test_model_default, "R_T_0", 65), (test_model_default, "T_w", 21), (test_model_default, "T_c", 21), @@ -75,7 +71,7 @@ def test_saturating_ramp_update(): (test_model_specified, "R_WC", 1.0702867186480716), (test_model_specified, "C_c", 512.249065845453), (test_model_specified, "R_CA", 1.9406620046327363), - (test_model_specified, "α", 0.393 * 1 / 100), + (test_model_specified, "α", 0.393 * 1 / 100), # noqa: RUF001 (test_model_specified, "R_T_0", 65), (test_model_specified, "T_w", 10), (test_model_specified, "T_c", 10), @@ -101,7 +97,8 @@ def test_update(): """ Tests the ThermalModel update method\n Calls the update method with no arguments and asserts the class variables are equal to the expected values. - Then calls the update method with a motor_current argument and asserts the class variables are equal to the expected values. + Then calls the update method with a motor_current argument and asserts the class variables + are equal to the expected values. This is repeated once more to test the update method with a motor_current argument. """ @@ -113,14 +110,7 @@ def test_update(): # Testing the default ThermalModel update method with motor_current arg test_model_default.update(motor_current=1000) default_T_w_update1 = ( - 21 - + ( - ((1000 * 1e-3) ** 2) - * 0.376 - * (1 + (0.393 * 1 / 100) * (21 - 65)) - / (16.292405391941298) - ) - / 200 + 21 + (((1000 * 1e-3) ** 2) * 0.376 * (1 + (0.393 * 1 / 100) * (21 - 65)) / (16.292405391941298)) / 200 ) assert test_model_default.T_w == default_T_w_update1 assert test_model_default.T_c == 21 @@ -131,9 +121,7 @@ def test_update(): default_T_w_update1 + ( ( - ((1000 * 1e-3) ** 2) - * 0.376 - * (1 + (0.393 * 1 / 100) * (default_T_w_update1 - 65)) + ((1000 * 1e-3) ** 2) * 0.376 * (1 + (0.393 * 1 / 100) * (default_T_w_update1 - 65)) + (21 - default_T_w_update1) / 1.0702867186480716 ) / (16.292405391941298) @@ -142,10 +130,6 @@ def test_update(): ) assert test_model_default.T_w == default_T_w_update2 assert ( - test_model_default.T_c - == (((default_T_w_update1 - 21) / 1.0702867186480716) / 512.249065845453) - * 1 - / 200 - + 21 + test_model_default.T_c == (((default_T_w_update1 - 21) / 1.0702867186480716) / 512.249065845453) * 1 / 200 + 21 ) assert test_model_default.T_a == 21 diff --git a/tests/test_robots/test_robots_base.py b/tests/test_robots/test_robots_base.py index 9c3e3acb..9bc09742 100644 --- a/tests/test_robots/test_robots_base.py +++ b/tests/test_robots/test_robots_base.py @@ -1,12 +1,15 @@ +import os from unittest.mock import Mock import pytest -from opensourceleg.logging import LOGGER -from opensourceleg.robots.base import * +from opensourceleg.logging import Logger +from opensourceleg.robots.base import RobotBase from tests.test_actuators.test_actuators_base import MOTOR_CONSTANTS, MockActuator from tests.test_sensors.test_sensors_base import MockSensor +CURR_DIR = os.path.dirname(os.path.realpath(__file__)) + # Creating a Mock Robot Base Class class MockRobot(RobotBase): @@ -24,7 +27,7 @@ def update(self): def mock_robot(): tag_name = "test" test_tactuator = MockActuator( - "act_tag", + "MockActuator", 10, MOTOR_CONSTANTS( MOTOR_COUNT_PER_REV=1000, @@ -41,6 +44,16 @@ def mock_robot(): return MockRobot(tag=tag_name, actuators=test_actuators, sensors=test_sensors) +@pytest.fixture(scope="function") +def test_logger(): + log = Logger( + log_path=CURR_DIR, + file_name="test_robots_base", + ) + log.reset() + yield log + + # Test init def test_robot_base_init(): tag_name = "test" @@ -59,18 +72,14 @@ def test_robot_base_init(): test_actuators = {"actuator": test_tactuator} test_tsensor = MockSensor() test_sensors = {"sensor": test_tsensor} - sample_robot = MockRobot( - tag=tag_name, actuators=test_actuators, sensors=test_sensors - ) - assert all( - [ - sample_robot.tag == tag_name, - sample_robot.actuators == test_actuators, - sample_robot.sensors == test_sensors, - type(sample_robot.actuators) == dict, - type(sample_robot.sensors) == dict, - ] - ) + sample_robot = MockRobot(tag=tag_name, actuators=test_actuators, sensors=test_sensors) + assert all([ + sample_robot.tag == tag_name, + sample_robot.actuators == test_actuators, + sample_robot.sensors == test_sensors, + type(sample_robot.actuators) is dict, + type(sample_robot.sensors) is dict, + ]) # Test enter @@ -91,8 +100,9 @@ def test_robot_base_exit(mock_robot: MockRobot): # Test start -def test_robot_base_start(mock_robot: MockRobot): - # "pass" is implementation for start function in both Actuator and Sensor base classes so just testing if called for that part of function +def test_robot_base_start(mock_robot: MockRobot, test_logger: Logger): + # "pass" is implementation for start function in both Actuator and Sensor base classes + # so just testing if called for that part of function MockActuator.start = Mock() MockSensor.start = Mock() @@ -101,9 +111,9 @@ def test_robot_base_start(mock_robot: MockRobot): mock_robot.start() - file = open(LOGGER._file_path, "r") + file = open(test_logger.file_path) contents = file.read() - assert (f"DEBUG: Calling start method of act_tag") in contents + assert ("DEBUG: Calling start method of MockActuator") in contents assert "DEBUG: Calling start method of SensorBase" in contents file.close() @@ -112,8 +122,9 @@ def test_robot_base_start(mock_robot: MockRobot): # Test stop -def test_robot_base_stop(mock_robot: MockRobot): - # "pass" is implementation for stop function in both Actuator and Sensor base classes so just testing if called for that part of function +def test_robot_base_stop(mock_robot: MockRobot, test_logger: Logger): + # "pass" is implementation for stop function in both Actuator and Sensor base classes + # so just testing if called for that part of function MockActuator.stop = Mock() MockSensor.stop = Mock() @@ -122,9 +133,9 @@ def test_robot_base_stop(mock_robot: MockRobot): mock_robot.stop() - file = open(LOGGER._file_path, "r") + file = open(test_logger.file_path) contents = file.read() - assert "DEBUG: Calling stop method of act_tag" in contents + assert "DEBUG: Calling stop method of MockActuator" in contents assert "DEBUG: Calling stop method of SensorBase" in contents file.close() diff --git a/tests/test_robots/test_robots_osl.py b/tests/test_robots/test_robots_osl.py deleted file mode 100644 index 277db6eb..00000000 --- a/tests/test_robots/test_robots_osl.py +++ /dev/null @@ -1,186 +0,0 @@ -from unittest.mock import Mock - -import pytest - -from opensourceleg.logging import LOGGER -from opensourceleg.robots.osl import OpenSourceLeg -from tests.test_actuators.test_actuators_base import MOTOR_CONSTANTS, MockActuator -from tests.test_sensors.test_sensors_base import MockEncoder, MockLoadcell, MockSensor - - -@pytest.fixture -def sample_osl(): - tag_name = "test" - test_tactuator = MockActuator( - "act_tag", - 10, - MOTOR_CONSTANTS( - MOTOR_COUNT_PER_REV=1000, - NM_PER_AMP=0.1, - NM_PER_RAD_TO_K=1.0, - NM_S_PER_RAD_TO_B=0.1, - MAX_CASE_TEMPERATURE=100.0, - MAX_WINDING_TEMPERATURE=150.0, - ), - ) - test_actuators = {"actuator1": test_tactuator} - test_tsensor = MockSensor() - test_sensors = {"sensor1": test_tsensor} - - return OpenSourceLeg(tag=tag_name, actuators=test_actuators, sensors=test_sensors) - - -# Test home -def test_osl_home(sample_osl: OpenSourceLeg): - # Only one actuator in mock_robot.actuators - MockActuator.home = Mock() - MockActuator.home.assert_not_called() - sample_osl.home() - MockActuator.home.assert_called_once() - - -# Test knee -def test_knee(sample_osl: OpenSourceLeg): - # Test error case - expected_error = "ERROR: Knee actuator not found. Please check for `knee` key in the actuators dictionary.\n" - - with open(LOGGER.file_path) as f: - output = f.read() - assert expected_error not in output - - with pytest.raises(SystemExit) as e: - sample_osl.knee() - assert e.type == SystemExit - - with open(LOGGER.file_path) as f: - output = f.read() - assert expected_error in output - - # Test non-error case - test_knee_actuator = MockActuator( - "knee", - 10, - MOTOR_CONSTANTS( - MOTOR_COUNT_PER_REV=1000, - NM_PER_AMP=0.1, - NM_PER_RAD_TO_K=1.0, - NM_S_PER_RAD_TO_B=0.1, - MAX_CASE_TEMPERATURE=100.0, - MAX_WINDING_TEMPERATURE=150.0, - ), - ) - test_actuators = {"knee": test_knee_actuator} - sample_osl.actuators = test_actuators - - assert sample_osl.knee == test_knee_actuator - - -# Test ankle -def test_ankle(sample_osl: OpenSourceLeg): - # Test error case - expected_error = "ERROR: Ankle actuator not found. Please check for `ankle` key in the actuators dictionary.\n" - - with open(LOGGER.file_path) as f: - output = f.read() - assert expected_error not in output - - with pytest.raises(SystemExit) as e: - sample_osl.ankle() - assert e.type == SystemExit - - with open(LOGGER.file_path) as f: - output = f.read() - assert expected_error in output - - # Test non-error case - test_ankle_actuator = MockActuator( - "ankle", - 10, - MOTOR_CONSTANTS( - MOTOR_COUNT_PER_REV=1000, - NM_PER_AMP=0.1, - NM_PER_RAD_TO_K=1.0, - NM_S_PER_RAD_TO_B=0.1, - MAX_CASE_TEMPERATURE=100.0, - MAX_WINDING_TEMPERATURE=150.0, - ), - ) - test_actuators = {"ankle": test_ankle_actuator} - sample_osl.actuators = test_actuators - - assert sample_osl.ankle == test_ankle_actuator - - -# Test loadcell -def test_loadcell(sample_osl: OpenSourceLeg): - # Test error case - expected_error = "ERROR: Loadcell sensor not found. Please check for `loadcell` key in the sensors dictionary.\n" - - with open(LOGGER.file_path) as f: - output = f.read() - assert expected_error not in output - - with pytest.raises(SystemExit) as e: - sample_osl.loadcell() - assert e.type == SystemExit - - with open(LOGGER.file_path) as f: - output = f.read() - assert expected_error in output - - # Test non-error case - test_loadcell_sensor = MockLoadcell() - test_sensors = {"loadcell": test_loadcell_sensor} - sample_osl.sensors = test_sensors - - assert sample_osl.loadcell == test_loadcell_sensor - - -# Test joint encoder knee -def test_joint_encoder_knee(sample_osl: OpenSourceLeg): - # Test error case - expected_error = "ERROR: Knee joint encoder sensor not found. Please check for `joint_encoder_knee` key in the sensors dictionary.\n" - - with open(LOGGER.file_path) as f: - output = f.read() - assert expected_error not in output - - with pytest.raises(SystemExit) as e: - sample_osl.joint_encoder_knee() - assert e.type == SystemExit - - with open(LOGGER.file_path) as f: - output = f.read() - assert expected_error in output - - # Test non-error case - test_joint_knee = MockEncoder() - test_sensors = {"joint_encoder_knee": test_joint_knee} - sample_osl.sensors = test_sensors - - assert sample_osl.joint_encoder_knee == test_joint_knee - - -# Test joint encoder ankle -def test_joint_encoder_ankle(sample_osl: OpenSourceLeg): - # Test error case - expected_error = "ERROR: Ankle joint encoder sensor not found. Please check for `joint_encoder_ankle` key in the sensors dictionary.\n" - - with open(LOGGER.file_path) as f: - output = f.read() - assert expected_error not in output - - with pytest.raises(SystemExit) as e: - sample_osl.joint_encoder_ankle() - assert e.type == SystemExit - - with open(LOGGER.file_path) as f: - output = f.read() - assert expected_error in output - - # Test non-error case - test_joint_ankle = MockEncoder() - test_sensors = {"joint_encoder_ankle": test_joint_ankle} - sample_osl.sensors = test_sensors - - assert sample_osl.joint_encoder_ankle == test_joint_ankle diff --git a/tests/test_safety/test_safety.py b/tests/test_safety/test_safety.py index 6271aeb2..380e1eb7 100644 --- a/tests/test_safety/test_safety.py +++ b/tests/test_safety/test_safety.py @@ -1,9 +1,20 @@ -from io import StringIO -from unittest.mock import Mock, patch +from unittest.mock import patch import pytest -from opensourceleg.safety.safety import * +from opensourceleg.safety.safety import ( + SafetyDecorators, + SafetyManager, + ThermalLimitException, + custom_criteria, + is_changing, + is_greater_than, + is_less_than, + is_negative, + is_positive, + is_within_range, + is_zero, +) # Created class for testing purposes @@ -37,9 +48,7 @@ def test_Thermal_Limit_Exception_initialization(): def test_Thermal_Limit_Exception_initialization_with_default(): thermal_limit_exception = ThermalLimitException() - assert ( - thermal_limit_exception.message == "Software thermal limit exceeded. Exiting." - ) + assert thermal_limit_exception.message == "Software thermal limit exceeded. Exiting." # Test def is_changing & decorator @@ -62,13 +71,12 @@ def test_is_changing_four_points_not_less_than_threshold(): max_points = 4 instance = Sample() - for x in range(6, 10): - - @is_changing(att, max_points) - def test_changing_point(instance): - return x + def test_changing_point(instance, x): + return x - assert test_changing_point(instance) == x + for x in range(6, 10): + wrapped_func = is_changing(att, max_points)(test_changing_point) + assert wrapped_func(instance, x) == x def test_is_changing_four_points_less_than_threshold(): @@ -78,13 +86,13 @@ def test_is_changing_four_points_less_than_threshold(): max_points = 4 test_threshold = 2 instance = Sample() - for x in range(6, 9): - @is_changing(att, max_points, test_threshold) - def test_changing_point(instance): - return x + def test_changing_point(instance, x): + return x - assert test_changing_point(instance) == x + for x in range(6, 9): + wrapped_func = is_changing(att, max_points, test_threshold)(test_changing_point) + assert wrapped_func(instance, x) == x @is_changing(att, max_points, test_threshold) def test_changing_point1(instance): @@ -94,32 +102,31 @@ def test_changing_point1(instance): test_changing_point1(instance) -def test_is_changing_four_points_less_than_threshold_with_proxy_att_name(): - # Test case of max points reached, with stddev < defined threshold - # Stddev of 6,7,8,9 ~ 1.118, with defined threshold = 2 - # For this case, proxy attribute name is not None - att = "test_att" - max_points = 4 - test_threshold = 2 - proxy_att_name = "test_proxy_att_name" - instance = Sample() - for x in range(6, 9): +# def test_is_changing_four_points_less_than_threshold_with_proxy_att_name(): +# # Test case of max points reached, with stddev < defined threshold +# # Stddev of 6,7,8,9 ~ 1.118, with defined threshold = 2 +# # For this case, proxy attribute name is not None +# att = "test_att" +# max_points = 4 +# test_threshold = 2 +# proxy_att_name = "test_proxy_att_name" +# instance = Sample() - @is_changing(att, max_points, test_threshold, proxy_att_name) - def test_changing_point(instance): - return x +# @is_changing(att, max_points, test_threshold, proxy_att_name) +# def test_changing_point(instance): +# return x - assert test_changing_point(instance) == x +# for x in range(6, 9): +# wrapped_func = is_changing(att, max_points, test_threshold, proxy_att_name)(test_changing_point) +# assert wrapped_func(instance) == x - @is_changing(att, max_points, test_threshold, proxy_att_name) - def test_changing_point1(instance): - return 9 +# @is_changing(att, max_points, test_threshold, proxy_att_name) +# def test_changing_point1(instance): +# return 9 - with patch("sys.stdout", new=StringIO()) as temp_out: - test_changing_point1(instance) - assert ( - temp_out.getvalue() == f"{att} isn't stable, returning {proxy_att_name}\n" - ) +# with patch("sys.stdout", new=StringIO()) as temp_out: +# test_changing_point1(instance) +# assert temp_out.getvalue() == f"{att} isn't stable, returning {proxy_att_name}\n" def test_is_changing_parameters(): @@ -298,10 +305,10 @@ def test_for_type_error(): # Test def is_within_range & decorator def test_is_within_range_def_in(): # Test clamp default = False, with property value = 2 in range - min = 1 - max = 4 + value_min = 1 + value_max = 4 - @is_within_range(min, max) + @is_within_range(value_min, value_max) def test_within_range_with_default_clamp(instance: object): return 2 @@ -310,52 +317,52 @@ def test_within_range_with_default_clamp(instance: object): def test_is_within_range_def_out(): # Test clamp default = False, with property value = 5 out of range - min = 1 - max = 4 + value_min = 1 + value_max = 4 - @is_within_range(min, max) + @is_within_range(value_min, value_max) def test_out_of_range_with_default_clamp(instance: object): return 5 - with pytest.raises(ValueError, match=f"Value must be within {min} and {max}"): + with pytest.raises(ValueError, match=f"Value must be within {value_min} and {value_max}"): test_out_of_range_with_default_clamp({}) def test_is_within_range_false_out(): # Test clamp explicitly set to False, with property value = 0 out of range - min = 1 - max = 4 + value_min = 1 + value_max = 4 - @is_within_range(min, max, False) + @is_within_range(value_min, value_max, False) def test_out_of_range_with_false_clamp(instance: object): return 0 - with pytest.raises(ValueError, match=f"Value must be within {min} and {max}"): + with pytest.raises(ValueError, match=f"Value must be within {value_min} and {value_max}"): test_out_of_range_with_false_clamp({}) def test_is_within_range_true_smaller(): - # Test clamp explicitly set to True, with property value = -2 lower than min - min = 1 - max = 4 + # Test clamp explicitly set to True, with property value = -2 lower than value_min + value_min = 1 + value_max = 4 - @is_within_range(min, max, True) + @is_within_range(value_min, value_max, True) def test_lower_with_true_clamp(instance: object): return -2 - assert test_lower_with_true_clamp({}) == min + assert test_lower_with_true_clamp({}) == value_min def test_is_within_range_true_larger(): - # Test clamp explicitly set to True, with property value = 6 higher than max - min = 1 - max = 4 + # Test clamp explicitly set to True, with property value = 6 higher than value_max + value_min = 1 + value_max = 4 - @is_within_range(min, max, True) + @is_within_range(value_min, value_max, True) def test_higher_with_true_clamp(instance: object): return 5 - assert test_higher_with_true_clamp({}) == max + assert test_higher_with_true_clamp({}) == value_max def test_is_within_range_parameter(): @@ -372,10 +379,10 @@ def test_for_type_error(instance: object): def test_is_within_range_parameter2(): # Test without parameter instance in func def with pytest.raises(TypeError): - min = 1 - max = 4 + value_min = 1 + value_max = 4 - @is_within_range(min, max) + @is_within_range(value_min, value_max) def test_for_type_error(): return 0 @@ -384,13 +391,11 @@ def test_for_type_error(): def test_is_within_range_max_less_min(): # Test maximum value greater than minimum value for range - min = 4 - max = 1 - with pytest.raises( - ValueError, match=f"Maximum value must be greater than minimum value of range" - ): + value_min = 4 + value_max = 1 + with pytest.raises(ValueError, match="Maximum value must be greater than minimum value of range"): - @is_within_range(min, max) + @is_within_range(value_min, value_max) def test_for_min_greater_than_max(instance: object): return 0 @@ -399,10 +404,10 @@ def test_for_min_greater_than_max(instance: object): # Test def is_greater_than & decorator def test_is_greater_def_in(): - # Test clamp default = False, with value greater than min - min = 2 + # Test clamp default = False, with value greater than value_min + value_min = 2 - @is_greater_than(min) + @is_greater_than(value_min) def test_greater_with_default_clamp(instance: object): return 4 @@ -410,74 +415,72 @@ def test_greater_with_default_clamp(instance: object): def test_is_greater_def_out(): - # Test clamp default = False, with value less than min - min = 2 + # Test clamp default = False, with value less than value_min + value_min = 2 - @is_greater_than(min) + @is_greater_than(value_min) def test_less_with_default_clamp(instance: object): return 1 - with pytest.raises(ValueError, match=f"Value must be greater than {min}"): + with pytest.raises(ValueError, match=f"Value must be greater than {value_min}"): test_less_with_default_clamp({}) def test_is_greater_false_out(): - # Test clamp explicitly set to False, with value less than min - min = 2 + # Test clamp explicitly set to False, with value less than value_min + value_min = 2 - @is_greater_than(min, False) + @is_greater_than(value_min, False) def test_less_with_false_clamp(instance: object): return 1 - with pytest.raises(ValueError, match=f"Value must be greater than {min}"): + with pytest.raises(ValueError, match=f"Value must be greater than {value_min}"): test_less_with_false_clamp({}) def test_is_greater_true_out(): - # Test clamp explicitly set to True, with value less than min - min = 2 + # Test clamp explicitly set to True, with value less than value_min + value_min = 2 - @is_greater_than(min, True) + @is_greater_than(value_min, True) def test_less_with_true_clamp(instance: object): return -2 - assert test_less_with_true_clamp({}) == min + assert test_less_with_true_clamp({}) == value_min def test_is_greater_equality_false_equal(): - # Test clamp explicitly set to False, with value equal to min and equality set to True - min = 2 + # Test clamp explicitly set to False, with value equal to value_min and equality set to True + value_min = 2 - @is_greater_than(min, False, True) + @is_greater_than(value_min, False, True) def test_equal_with_false_clamp(instance: object): return 2 - assert test_equal_with_false_clamp({}) == min + assert test_equal_with_false_clamp({}) == value_min def test_is_greater_equality_false_out(): - # Test clamp explicitly set to False, with value less than min and equality set to True - min = 2 + # Test clamp explicitly set to False, with value less than value_min and equality set to True + value_min = 2 - @is_greater_than(min, False, True) + @is_greater_than(value_min, False, True) def test_less_with_false_clamp(instance: object): return 1 - with pytest.raises( - ValueError, match=f"Value must be greater than or equal to {min}" - ): + with pytest.raises(ValueError, match=f"Value must be greater than or equal to {value_min}"): test_less_with_false_clamp({}) def test_is_greater_equality_true_out(): - # Test clamp explicitly set to True, with value less than min and equality set to True - min = 2 + # Test clamp explicitly set to True, with value less than value_min and equality set to True + value_min = 2 - @is_greater_than(min, True, True) + @is_greater_than(value_min, True, True) def test_less_with_true_clamp(instance: object): return 1 - assert test_less_with_true_clamp({}) == min + assert test_less_with_true_clamp({}) == value_min def test_is_greater_parameter(): @@ -494,9 +497,9 @@ def test_for_type_error(instance: object): def test_is_greater_parameter2(): # Test without parameter instance in func def with pytest.raises(TypeError): - min = 2 + value_min = 2 - @is_greater_than(min) + @is_greater_than(value_min) def test_for_type_error(): return 0 @@ -505,10 +508,10 @@ def test_for_type_error(): # Test def is_less_than & decorator def test_is_less_def_in(): - # Test clamp default = False, with value less than max - max = 4 + # Test clamp default = False, with value less than value_max + value_max = 4 - @is_less_than(max) + @is_less_than(value_max) def test_less_with_default_clamp(instance: object): return 2 @@ -516,72 +519,72 @@ def test_less_with_default_clamp(instance: object): def test_is_less_def_out(): - # Test clamp default = False, with value greater than max - max = 2 + # Test clamp default = False, with value greater than value_max + value_max = 2 - @is_less_than(max) + @is_less_than(value_max) def test_greater_with_default_clamp(instance: object): return 4 - with pytest.raises(ValueError, match=f"Value must be less than {max}"): + with pytest.raises(ValueError, match=f"Value must be less than {value_max}"): test_greater_with_default_clamp({}) def test_is_less_false_out(): - # Test clamp explicitly set to False, with value greater than max - max = 2 + # Test clamp explicitly set to False, with value greater than value_max + value_max = 2 - @is_less_than(max, False) + @is_less_than(value_max, False) def test_greater_with_false_clamp(instance: object): return 4 - with pytest.raises(ValueError, match=f"Value must be less than {max}"): + with pytest.raises(ValueError, match=f"Value must be less than {value_max}"): test_greater_with_false_clamp({}) def test_is_less_true_out(): - # Test clamp explicitly set to True, with value greater than max - max = 2 + # Test clamp explicitly set to True, with value greater than value_max + value_max = 2 - @is_less_than(max, True) + @is_less_than(value_max, True) def test_greater_with_true_clamp(instance: object): return 5 - assert test_greater_with_true_clamp({}) == max + assert test_greater_with_true_clamp({}) == value_max def test_is_less_equality_false_equal(): - # Test clamp explicitly set to False, with value equal to max and equality set to True - max = 2 + # Test clamp explicitly set to False, with value equal to value_max and equality set to True + value_max = 2 - @is_less_than(max, False, True) + @is_less_than(value_max, False, True) def test_equal_with_false_clamp(instance: object): return 2 - assert test_equal_with_false_clamp({}) == max + assert test_equal_with_false_clamp({}) == value_max def test_is_less_equality_false_out(): - # Test clamp explicitly set to False, with value greater than max and equality set to True - max = 2 + # Test clamp explicitly set to False, with value greater than value_max and equality set to True + value_max = 2 - @is_less_than(max, False, True) + @is_less_than(value_max, False, True) def test_less_with_false_clamp(instance: object): return 3 - with pytest.raises(ValueError, match=f"Value must be less than or equal to {max}"): + with pytest.raises(ValueError, match=f"Value must be less than or equal to {value_max}"): test_less_with_false_clamp({}) def test_is_less_equality_true_out(): - # Test clamp explicitly set to True, with value greater than max and equality set to True - max = 2 + # Test clamp explicitly set to True, with value greater than value_max and equality set to True + value_max = 2 - @is_less_than(max, True, True) + @is_less_than(value_max, True, True) def test_less_with_true_clamp(instance: object): return 3 - assert test_less_with_true_clamp({}) == max + assert test_less_with_true_clamp({}) == value_max def test_is_less_parameter(): @@ -598,9 +601,9 @@ def test_for_type_error(instance: object): def test_is_less_parameter2(): # Test without parameter instance in func def with pytest.raises(TypeError): - max = 2 + value_max = 2 - @is_less_than(max) + @is_less_than(value_max) def test_for_type_error(): return 0 @@ -609,9 +612,7 @@ def test_for_type_error(): # Test def custom_criteria & decorator. Will return true if x > 2 def custom_func(x: float): - if x ^ 2 > 4: - return True - return False + return x ^ 2 > 4 def test_custom_criteria_def_in(): @@ -675,7 +676,7 @@ def test_safetys_init_default(): # Test init def test_safety_manager_init(): test_manager = SafetyManager() - assert type(test_manager._safe_objects) == dict + assert isinstance(test_manager._safe_objects, dict) assert test_manager._safe_objects == {} @@ -687,9 +688,7 @@ def test_add_safety_att_doesnt_exist(mock_print): attribute = "wrong" test_manager.add_safety(samp, attribute, SafetyDecorators.is_positive()) - mock_print.assert_called_once_with( - f"Error: The attribute '{attribute}' does not exist in the given object." - ) + mock_print.assert_called_once_with(f"Error: The attribute '{attribute}' does not exist in the given object.") @patch("builtins.print") @@ -739,7 +738,7 @@ def test_start(): test_manager.start() with pytest.raises(ValueError, match="Value must be negative"): - test = samp.test + _ = samp.test # Test update & safe objects diff --git a/tests/test_sensors/test_imu.py b/tests/test_sensors/test_imu.py index 69b3113f..08e3f811 100644 --- a/tests/test_sensors/test_imu.py +++ b/tests/test_sensors/test_imu.py @@ -1,10 +1,8 @@ -from unittest.mock import Mock - import pytest from opensourceleg.logging import LOGGER -from opensourceleg.sensors.base import * -from opensourceleg.sensors.imu import * +from opensourceleg.sensors.base import SensorNotStreamingException +from opensourceleg.sensors.imu import LordMicrostrainIMU # Patched classes from mscl module @@ -134,17 +132,15 @@ def sample_imu_init(): # Test init def test_init_default(sample_imu: MockLordMicrostrainIMU): - assert all( - [ - sample_imu._port == "/dev/ttyUSB0", - sample_imu._baud_rate == 921600, - sample_imu._frequency == 200, - sample_imu._is_streaming == False, - sample_imu._connection == None, - sample_imu._data == {}, - type(sample_imu._data) == dict, - ] - ) + assert all([ + sample_imu._port == "/dev/ttyUSB0", + sample_imu._baud_rate == 921600, + sample_imu._frequency == 200, + sample_imu._is_streaming is False, + sample_imu._connection is None, + sample_imu._data == {}, + isinstance(sample_imu._data, dict), + ]) def test_init_set(): @@ -152,13 +148,11 @@ def test_init_set(): baud = 10 freq = 20 sample_imu = MockLordMicrostrainIMU(port=port_name, baud_rate=baud, frequency=freq) - assert all( - [ - sample_imu._port == port_name, - sample_imu._baud_rate == baud, - sample_imu._frequency == freq, - ] - ) + assert all([ + sample_imu._port == port_name, + sample_imu._baud_rate == baud, + sample_imu._frequency == freq, + ]) # Test configure mip channels @@ -166,60 +160,54 @@ def test_configure_mip_channels(sample_imu: MockLordMicrostrainIMU): channels = sample_imu._configure_mip_channels() assert len(channels) == 4 for i in range(0, 4): - assert all( - [channels[i].miptype in MockTypes, type(channels[i]) == MockMipChannel] - ) + assert all([channels[i].miptype in MockTypes, isinstance(channels[i], MockMipChannel)]) # Test start def test_start(sample_imu: MockLordMicrostrainIMU): - assert all( - [ - sample_imu._connection == None, - not hasattr(sample_imu, "_node"), - sample_imu._is_streaming == False, - ] - ) + assert all([ + sample_imu._connection is None, + not hasattr(sample_imu, "_node"), + sample_imu._is_streaming is False, + ]) sample_imu.start() - assert all( - [ - type(sample_imu._connection) == MockConnection, - type(sample_imu._node) == MockNode, - sample_imu._is_streaming == True, - sample_imu._node.type == "mocktype", - sample_imu._node.datastream == "mocktype", - ] - ) + assert all([ + isinstance(sample_imu._connection, MockConnection), + isinstance(sample_imu._node, MockNode), + sample_imu._is_streaming is True, + sample_imu._node.type == "mocktype", + sample_imu._node.datastream == "mocktype", + ]) # Test stop def test_stop_not_started(sample_imu: MockLordMicrostrainIMU): with pytest.raises(SensorNotStreamingException) as e: sample_imu.stop() - assert ( - str(e.value) - == f"{sample_imu.__repr__()} is not streaming, please ensure that the connections are intact, power is on, and the start method is called." + assert str(e.value) == ( + f"{sample_imu.__repr__()} is not streaming, please ensure that the connections " + "are intact, power is on, and the start method is called." ) def test_stop(sample_imu: MockLordMicrostrainIMU): sample_imu.start() - assert sample_imu._is_streaming == True + assert sample_imu._is_streaming is True sample_imu.stop() - assert all([sample_imu._is_streaming == False, sample_imu._node.idle == True]) + assert all([sample_imu._is_streaming is False, sample_imu._node.idle is True]) # Test ping def test_ping_not_started(sample_imu: MockLordMicrostrainIMU): with pytest.raises(SensorNotStreamingException) as e: sample_imu.ping() - assert ( - str(e.value) - == f"{sample_imu.__repr__()} is not streaming, please ensure that the connections are intact, power is on, and the start method is called." + assert str(e.value) == ( + f"{sample_imu.__repr__()} is not streaming, please ensure that the connections " + "are intact, power is on, and the start method is called." ) @@ -260,12 +248,12 @@ def test_update(sample_imu: MockLordMicrostrainIMU): sample_imu.start() assert sample_imu._data == {} sample_imu.update(500, 2, False) - assert sample_imu._data == {"mockdata": 10, "mockdata": 20} + assert sample_imu._data == {"mockdata": 20} return_val = sample_imu.update(400, 3, True) assert len(return_val) == 3 for i in range(0, 3): - assert type(return_val[i]) == MockDataPockets + assert isinstance(return_val[i], MockDataPockets) # Test LordMicrostrainIMU repr @@ -297,7 +285,7 @@ def test_frequency(sample_imu: MockLordMicrostrainIMU): # Test is_streaming def test_is_streaming(sample_imu: MockLordMicrostrainIMU): sample_imu._is_streaming = True - assert sample_imu.is_streaming == True + assert sample_imu.is_streaming is True # Test roll diff --git a/tests/test_sensors/test_loadcell.py b/tests/test_sensors/test_loadcell.py index 76d8a9ce..3d963f2f 100644 --- a/tests/test_sensors/test_loadcell.py +++ b/tests/test_sensors/test_loadcell.py @@ -1,12 +1,8 @@ # Global Units Dictionary -import enum -import time -from dataclasses import dataclass import numpy as np import pytest from numpy import typing as npt -from smbus2 import SMBus from opensourceleg.sensors import loadcell @@ -17,7 +13,6 @@ def test_SRILoadcell_init(): - invalid_cal_matrix = np.ones(shape=(5, 6), dtype=np.double) with pytest.raises(TypeError): SRI = loadcell.SRILoadcell(calibration_matrix=invalid_cal_matrix) @@ -56,17 +51,13 @@ def test_SRILoadcell_init(): def test_SRILoadcell_reset(): - SRI = loadcell.SRILoadcell(calibration_matrix=DEFAULT_CAL_MATRIX) - SRI._calibration_offset == np.ones(shape=(1, 6), dtype=np.double) + SRI._calibration_offset = np.ones(shape=(1, 6), dtype=np.double) SRI.reset() - assert np.array_equal( - SRI._calibration_offset, np.zeros(shape=(1, 6), dtype=np.double) - ) + assert np.array_equal(SRI._calibration_offset, np.zeros(shape=(1, 6), dtype=np.double)) def test_SRILoadcell_update(): - # Test basic call execution SRI = loadcell.SRILoadcell(calibration_matrix=DEFAULT_CAL_MATRIX) SRI.update(data_callback=_read_data) @@ -105,10 +96,7 @@ def _update_calculations(SRI: loadcell.SRILoadcell, calibration_offset: float): test_data = _read_data() signed_data = ((test_data - SRI.OFFSET) / SRI.ADC_RANGE) * SRI._exc coupled_data = signed_data * 1000 / (SRI._exc * SRI._amp_gain) - data = ( - np.transpose(a=SRI._calibration_matrix.dot(b=np.transpose(a=coupled_data))) - - calibration_offset - ) + data = np.transpose(a=SRI._calibration_matrix.dot(b=np.transpose(a=coupled_data))) - calibration_offset return data diff --git a/tests/test_sensors/test_sensors_base.py b/tests/test_sensors/test_sensors_base.py index 6448f074..2074bc6d 100644 --- a/tests/test_sensors/test_sensors_base.py +++ b/tests/test_sensors/test_sensors_base.py @@ -2,7 +2,14 @@ import pytest -from opensourceleg.sensors.base import * +from opensourceleg.sensors.base import ( + EncoderBase, + IMUBase, + LoadcellBase, + SensorBase, + SensorNotStreamingException, + check_sensor_stream, +) # Test SensorNotStreamingException init @@ -10,16 +17,16 @@ def test_actuator_stream_exception(): test_str = "test" with pytest.raises(SensorNotStreamingException) as e: raise SensorNotStreamingException(test_str) - assert ( - str(e.value) - == f"{test_str} is not streaming, please ensure that the connections are intact, power is on, and the start method is called." + assert str(e.value) == ( + f"{test_str} is not streaming, please ensure that the connections are " + "intact, power is on, and the start method is called." ) with pytest.raises(SensorNotStreamingException) as e: raise SensorNotStreamingException() - assert ( - str(e.value) - == "Sensor is not streaming, please ensure that the connections are intact, power is on, and the start method is called." + assert str(e.value) == ( + "Sensor is not streaming, please ensure that the connections are intact, " + "power is on, and the start method is called." ) @@ -61,9 +68,9 @@ def mock_sensor(): def test_check_sensor_stream(mock_sensor: MockSensor): with pytest.raises(SensorNotStreamingException) as e: mock_sensor.test_sensor_stream() - assert ( - str(e.value) - == f"{mock_sensor.__repr__()} is not streaming, please ensure that the connections are intact, power is on, and the start method is called." + assert str(e.value) == ( + f"{mock_sensor.__repr__()} is not streaming, please ensure that the " + "connections are intact, power is on, and the start method is called." ) mock_sensor.set_is_streaming() diff --git a/tests/test_time/test_time.py b/tests/test_time/test_time.py index 2da06317..e8816dac 100644 --- a/tests/test_time/test_time.py +++ b/tests/test_time/test_time.py @@ -15,12 +15,12 @@ def test_loopkiller_init(): lk = LoopKiller() assert lk._fade_time == 0.0 - assert lk._soft_kill_time == None + assert lk._soft_kill_time == 0.0 lk1 = LoopKiller(fade_time=1.0) assert lk1._fade_time == 1.0 - assert lk1._soft_kill_time == None - assert lk1._kill_now == False - assert lk1._kill_soon == False + assert lk1._soft_kill_time == 0.0 + assert lk1._kill_now is False + assert lk1._kill_soon is False def test_loopkiller_handle_signal(): @@ -32,9 +32,9 @@ def test_loopkiller_handle_signal(): """ lkhs = LoopKiller() - assert lkhs.kill_now == False + assert lkhs.kill_now is False lkhs.handle_signal(signum=None, frame=None) - assert lkhs.kill_now == True + assert lkhs.kill_now is True @pytest.fixture @@ -70,11 +70,11 @@ def test_loopkiller_kill_now_prop(patch_time_time2): lkknp = LoopKiller(fade_time=1.0) lkknp._kill_soon = True lkknp._soft_kill_time = 0.0 - assert lkknp.kill_now == False + assert lkknp.kill_now is False lkknp._soft_kill_time = 0.5 - assert lkknp.kill_now == False - assert lkknp.kill_now == True - assert lkknp.kill_now == True + assert lkknp.kill_now is False + assert lkknp.kill_now is True + assert lkknp.kill_now is True def test_loopkiller_kill_now_setter(patch_time_time2): @@ -83,19 +83,19 @@ def test_loopkiller_kill_now_setter(patch_time_time2): lkkns._kill_soon = True lkkns._soft_kill_time = 0.0 lkkns.kill_now = False - assert lkkns._kill_now == False - assert lkkns._kill_soon == False - assert lkkns._soft_kill_time == None + assert lkkns._kill_now is False + assert lkkns._kill_soon is False + assert lkkns._soft_kill_time == 0.0 lkkns.kill_now = True - assert lkkns._kill_soon == True + assert lkkns._kill_soon is True assert lkkns._soft_kill_time == 0.0 - assert lkkns._kill_now == False + assert lkkns._kill_now is False lkkns.kill_now = True - assert lkkns._kill_now == True + assert lkkns._kill_now is True lkkns.kill_now = False lkkns._fade_time = 0.0 lkkns.kill_now = True - assert lkkns._kill_now == True + assert lkkns._kill_now is True def test_softrealtimeloop_init(patch_time_time2): @@ -103,12 +103,12 @@ def test_softrealtimeloop_init(patch_time_time2): assert srtl.t0 == 0.0 assert srtl.t1 == 0.0 assert isinstance(srtl.killer, LoopKiller) - assert srtl.ttarg == None + assert srtl.ttarg is None assert srtl.sum_err == 0.0 assert srtl.sum_var == 0.0 assert srtl.sleep_t_agg == 0.0 assert srtl.n == 0 - assert srtl.report == False + assert srtl.report is False @pytest.fixture @@ -137,7 +137,6 @@ def test_softrealtimeloop_iter(patch_time_time2): def test_softrealtimeloop_fade_prop(patch_time_time3): - srtlf = SoftRealtimeLoop(fade=1.0) assert srtlf.fade == 1.0 srtlf.killer._kill_soon = True @@ -149,9 +148,9 @@ def test_softrealtimeloop_fade_prop(patch_time_time3): def test_softrealtimeloop_stop(patch_time_time2): srtls = SoftRealtimeLoop() srtls.killer._kill_soon = True - assert srtls.killer._kill_now == False + assert srtls.killer._kill_now is False srtls.stop() - assert srtls.killer.kill_now == True + assert srtls.killer.kill_now is True def test_softrealtimeloop_time(patch_time_time2): diff --git a/tests/test_units/test_units.py b/tests/test_units/test_units.py index fc2727f5..7c8392fb 100644 --- a/tests/test_units/test_units.py +++ b/tests/test_units/test_units.py @@ -1,11 +1,23 @@ # Global Units Dictionary -import enum -from dataclasses import dataclass import numpy as np import pytest -from opensourceleg.units import * +from opensourceleg.units import ( + Acceleration, + Current, + Damping, + Force, + Length, + Mass, + Position, + Stiffness, + Torque, + Velocity, + Voltage, + convert_from_default, + convert_to_default, +) # Values to iterate over for testing VALUES = np.append(np.array([0, 1, -1, 1000, -1000]), np.random.random(5)) @@ -14,37 +26,33 @@ def test_convert_to_from_default(): # Testing behaviour when invalid input type is passed to convert_to_default & convert_from_default with pytest.raises(TypeError): - convert_to_default("TEST", force.kgf) + convert_to_default("TEST", Force.kgf) with pytest.raises(TypeError): - convert_to_default("T", force.kgf) + convert_to_default("T", Force.kgf) with pytest.raises(TypeError): - convert_from_default("TEST", force.N) + convert_from_default("TEST", Force.N) with pytest.raises(TypeError): - convert_from_default("T", force.N) + convert_from_default("T", Force.N) # Testing behaviour on all possible unit conversions to default & from default categories = [ - force, - torque, - stiffness, - damping, - length, - position, - mass, - velocity, - acceleration, - time, - current, - voltage, + Force, + Torque, + Stiffness, + Damping, + Length, + Position, + Mass, + Velocity, + Acceleration, + Current, + Voltage, ] units = [] # Add all units to array for c in categories: - for unit in dir(c): - if not unit.startswith("__"): - value = getattr(c, unit) - print(value) - units.append(value) + for unit in c: + units.append(unit) # Iterate over all possible units and test converting to/from default for unit in units: @@ -54,99 +62,60 @@ def test_convert_to_from_default(): def test_force(): - - assert force.N == 1.0 - assert force.lbf == 4.4482216152605 - assert force.kgf == 9.80665 - # assert repr(force) == "force" + assert Force.N == 1.0 + assert Force.lbf == 4.4482216152605 + assert Force.kgf == 9.80665 def test_torque(): - - assert torque.N_m == 1.0 - assert torque.lbf_inch == 0.1129848290276167 - assert torque.kgf_cm == 0.0980665 - # assert repr(torque) == "torque" + assert Torque.N_m == 1.0 + assert Torque.lbf_inch == 0.1129848290276167 + assert Torque.kgf_cm == 0.0980665 def test_stiffness(): - - assert stiffness.N_m_per_rad == 1.0 - assert stiffness.N_m_per_deg == 0.017453292519943295 - - # assert repr(stiffness) == "stiffness" + assert Stiffness.N_m_per_rad == 1.0 + assert Stiffness.N_m_per_deg == 0.017453292519943295 def test_damping(): - - assert damping.N_m_per_rad_per_s == 1.0 - assert damping.N_m_per_deg_per_s == 0.017453292519943295 - - # assert repr(damping) == "damping" + assert Damping.N_m_per_rad_per_s == 1.0 + assert Damping.N_m_per_deg_per_s == 0.017453292519943295 def test_length(): - - assert length.m == 1.0 - assert length.cm == 0.01 - assert length.inch == 0.0254 - - # assert repr(length) == "length" + assert Length.m == 1.0 + assert Length.cm == 0.01 + assert Length.inch == 0.0254 def test_position(): - - assert position.rad == 1.0 - assert position.deg == 0.017453292519943295 - - # assert repr(position) == "position" + assert Position.rad == 1.0 + assert Position.deg == 0.017453292519943295 def test_mass(): - - assert mass.kg == 1.0 - assert mass.g == 0.001 - assert mass.lb == 0.45359237 - - # assert repr(mass) == "mass" + assert Mass.kg == 1.0 + assert Mass.g == 0.001 + assert Mass.lb == 0.45359237 def test_velocity(): - - assert velocity.rad_per_s == 1.0 - assert velocity.deg_per_s == 0.017453292519943295 - assert velocity.rpm == 0.10471975511965977 - - # assert repr(velocity) == "velocity" + assert Velocity.rad_per_s == 1.0 + assert Velocity.deg_per_s == 0.017453292519943295 + assert Velocity.rpm == 0.10471975511965977 def test_acceleration(): - - assert acceleration.rad_per_s2 == 1.0 - assert acceleration.deg_per_s2 == 0.017453292519943295 - - # assert repr(acceleration) == "acceleration" - - -def test_time(): - - assert time.s == 1.0 - assert time.ms == 0.001 - - # assert repr(time) == "time" + assert Acceleration.rad_per_s2 == 1.0 + assert Acceleration.deg_per_s2 == 0.017453292519943295 def test_current(): - - assert current.mA == 1 - assert current.A == 1000 - - # assert repr(current) == "current" + assert Current.mA == 1 + assert Current.A == 1000 def test_voltage(): - - assert voltage.mV == 1 - assert voltage.V == 1000 - - # assert repr(voltage) == "voltage" + assert Voltage.mV == 1 + assert Voltage.V == 1000 diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..a3d6929e --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[tox] +skipsdist = true +envlist = py39, py310, py311 + +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + +[testenv] +passenv = PYTHON_VERSION +allowlist_externals = poetry +commands = + poetry install -v + pytest --doctest-modules tests --cov --cov-config=pyproject.toml --cov-report=xml + mypy diff --git a/tutorials/actuators/README.md b/tutorials/actuators/README.md index b945a090..d2fd82ac 100644 --- a/tutorials/actuators/README.md +++ b/tutorials/actuators/README.md @@ -1,13 +1,13 @@ # Reference - Performance Evaluation for `DephyActpack` and `moteus-r4` -## Torque / Current Control +## Torque / Current Control ![](./images/torque_comp.png) -## Position Control +## Position Control ![](./images/position_comp.png) ## Velocity (`moteus-r4` only) -![](./images/velocity_moteus.png) \ No newline at end of file +![](./images/velocity_moteus.png) diff --git a/tutorials/actuators/dephy/current_control.py b/tutorials/actuators/dephy/current_control.py index ebdefb15..8c308965 100644 --- a/tutorials/actuators/dephy/current_control.py +++ b/tutorials/actuators/dephy/current_control.py @@ -1,4 +1,4 @@ -import time +import time import pandas as pd @@ -17,16 +17,13 @@ def main(): port="/dev/ttyACM0", gear_ratio=1.0, ) - current_data = pd.DataFrame( - { - "Time": [], - "Output_Current": [], - "Command_Current": [], - } - ) + current_data = pd.DataFrame({ + "Time": [], + "Output_Current": [], + "Command_Current": [], + }) clock = SoftRealtimeLoop(dt=DT) with actpack: - try: actpack.set_control_mode(mode=CONTROL_MODES.CURRENT) actpack.set_current_gains( @@ -34,7 +31,6 @@ def main(): ) for t in clock: - if t > TIME_TO_STEP: command_current = 275 actpack.set_motor_current(value=command_current) # in mA @@ -54,13 +50,11 @@ def main(): current_data = pd.concat( [ current_data, - pd.DataFrame( - { - "Time": [t], - "Output_Current": [actpack.motor_current], - "Command_Current": [command_current], - } - ), + pd.DataFrame({ + "Time": [t], + "Output_Current": [actpack.motor_current], + "Command_Current": [command_current], + }), ], ignore_index=True, ) diff --git a/tutorials/actuators/dephy/impedance_control.py b/tutorials/actuators/dephy/impedance_control.py index 3e3fb0b9..19b30d84 100644 --- a/tutorials/actuators/dephy/impedance_control.py +++ b/tutorials/actuators/dephy/impedance_control.py @@ -1,4 +1,4 @@ -import time +import time import numpy as np diff --git a/tutorials/actuators/dephy/position_control.py b/tutorials/actuators/dephy/position_control.py index 6aa20bcb..97fbd415 100644 --- a/tutorials/actuators/dephy/position_control.py +++ b/tutorials/actuators/dephy/position_control.py @@ -1,4 +1,4 @@ -import time +import time import numpy as np import pandas as pd @@ -18,13 +18,11 @@ def main(): gear_ratio=1.0, ) - position_data = pd.DataFrame( - { - "Time": [], - "Output_Position": [], - "Command_Position": [], - } - ) + position_data = pd.DataFrame({ + "Time": [], + "Output_Position": [], + "Command_Position": [], + }) clock = SoftRealtimeLoop(dt=DT) with actpack: try: @@ -39,7 +37,6 @@ def main(): current_position = actpack.output_position for t in clock: - if t > TIME_TO_STEP: command_position = current_position + np.pi actpack.set_output_position(value=command_position) @@ -59,13 +56,11 @@ def main(): position_data = pd.concat( [ position_data, - pd.DataFrame( - { - "Time": [t], - "Output_Position": [actpack.output_position], - "Command_Position": [command_position], - } - ), + pd.DataFrame({ + "Time": [t], + "Output_Position": [actpack.output_position], + "Command_Position": [command_position], + }), ], ignore_index=True, ) diff --git a/tutorials/actuators/dephy/sensor_reading.py b/tutorials/actuators/dephy/sensor_reading.py index c815300f..08d2bd47 100644 --- a/tutorials/actuators/dephy/sensor_reading.py +++ b/tutorials/actuators/dephy/sensor_reading.py @@ -1,4 +1,3 @@ -import sys import time import opensourceleg.actuators.dephy as Dephy diff --git a/tutorials/actuators/dephy/voltage_control.py b/tutorials/actuators/dephy/voltage_control.py index fea4ef84..8bef9fee 100644 --- a/tutorials/actuators/dephy/voltage_control.py +++ b/tutorials/actuators/dephy/voltage_control.py @@ -1,4 +1,4 @@ -# import time +# import time # import sys # from opensourceleg.actuators.base import CONTROL_MODES # import opensourceleg.actuators.dephy as Dephy @@ -45,21 +45,17 @@ def main(): port="/dev/ttyACM0", gear_ratio=1.0, ) - voltage_data = pd.DataFrame( - { - "Time": [], - "Output_Voltage": [], - "Command_Voltage": [], - } - ) + voltage_data = pd.DataFrame({ + "Time": [], + "Output_Voltage": [], + "Command_Voltage": [], + }) clock = SoftRealtimeLoop(dt=DT) with actpack: - try: actpack.set_control_mode(mode=CONTROL_MODES.VOLTAGE) for t in clock: - if t > TIME_TO_STEP: command_voltage = 3000 actpack.set_motor_voltage(value=command_voltage) # in mV @@ -79,13 +75,11 @@ def main(): voltage_data = pd.concat( [ voltage_data, - pd.DataFrame( - { - "Time": [t], - "Output_Voltage": [actpack.motor_voltage], - "Command_Voltage": [command_voltage], - } - ), + pd.DataFrame({ + "Time": [t], + "Output_Voltage": [actpack.motor_voltage], + "Command_Voltage": [command_voltage], + }), ], ignore_index=True, ) diff --git a/tutorials/actuators/moteus/position_control.py b/tutorials/actuators/moteus/position_control.py index 718848e4..be638fdc 100644 --- a/tutorials/actuators/moteus/position_control.py +++ b/tutorials/actuators/moteus/position_control.py @@ -1,8 +1,7 @@ -import asyncio +import asyncio import numpy as np import pandas as pd -from moteus import Register from opensourceleg.actuators.moteus import MoteusActuator from opensourceleg.logging.logger import LOGGER @@ -22,13 +21,11 @@ async def main(): gear_ratio=9.0, ) - position_data = pd.DataFrame( - { - "Time": [], - "Output_Position": [], - "Command_Position": [], - } - ) + position_data = pd.DataFrame({ + "Time": [], + "Output_Position": [], + "Command_Position": [], + }) clock = SoftRealtimeLoop(dt=DT) @@ -44,7 +41,6 @@ async def main(): await mc1.update() for t in clock: - # current_time = time.monotonic() if t > TIME_TO_STEP: @@ -56,29 +52,22 @@ async def main(): else: command_position = position - print(f"######") - LOGGER.info( - "".join( - f"Motor Position: {mc1.output_position}\t" - + f"Command_Position: {command_position}\t" - ) - ) + print("######") + LOGGER.info("".join(f"Motor Position: {mc1.output_position}\t" + f"Command_Position: {command_position}\t")) position_data = pd.concat( [ position_data, - pd.DataFrame( - { - "Time": [t], - "Output_Position": [mc1.output_position], - "Command_Position": [command_position], - } - ), + pd.DataFrame({ + "Time": [t], + "Output_Position": [mc1.output_position], + "Command_Position": [command_position], + }), ], ignore_index=True, ) - print(f"------") + print("------") await asyncio.sleep(DT) finally: diff --git a/tutorials/actuators/moteus/sensor_readings.py b/tutorials/actuators/moteus/sensor_readings.py index 8fd63723..8e7a3c4d 100644 --- a/tutorials/actuators/moteus/sensor_readings.py +++ b/tutorials/actuators/moteus/sensor_readings.py @@ -1,12 +1,9 @@ -import asyncio -import math +import asyncio -import numpy as np import pandas as pd from moteus import Register from opensourceleg.actuators.moteus import MoteusActuator -from opensourceleg.logging.logger import LOGGER async def main(): @@ -14,36 +11,30 @@ async def main(): servo_id=42, bus_id=3, ) - current_data = pd.DataFrame( - { - "Time": [], - "Output_Current": [], - "Command_Current": [], - } - ) + current_data = pd.DataFrame({ + "Time": [], + "Output_Current": [], + "Command_Current": [], + }) try: await mc1.start() - iter = 0 + iterator = 0 time_period = 0.001 while True: - iter += 1 + iterator += 1 await mc1.update() - print(f"######") + print("######") print(f"{mc1.case_temperature}") - print(f"------") + print("------") current_data = pd.concat( [ current_data, - pd.DataFrame( - { - "Time": [iter * time_period], - "Output_Current": [mc1._data[0].values[Register.Q_CURRENT]], - "Command_Current": [ - mc1._data[0].values[Register.COMMAND_Q_CURRENT] - ], - } - ), + pd.DataFrame({ + "Time": [iterator * time_period], + "Output_Current": [mc1._data[0].values[Register.Q_CURRENT]], + "Command_Current": [mc1._data[0].values[Register.COMMAND_Q_CURRENT]], + }), ], ignore_index=True, ) diff --git a/tutorials/actuators/moteus/torque_control.py b/tutorials/actuators/moteus/torque_control.py index d81cc67d..90e3cd88 100644 --- a/tutorials/actuators/moteus/torque_control.py +++ b/tutorials/actuators/moteus/torque_control.py @@ -1,7 +1,5 @@ -import asyncio -import math +import asyncio -import numpy as np import pandas as pd from moteus import Register @@ -22,13 +20,11 @@ async def main(): bus_id=3, gear_ratio=9.0, ) - torque_data = pd.DataFrame( - { - "Time": [], - "Output_Torque": [], - "Command_Torque": [], - } - ) + torque_data = pd.DataFrame({ + "Time": [], + "Output_Torque": [], + "Command_Torque": [], + }) clock = SoftRealtimeLoop(dt=DT) @@ -44,7 +40,6 @@ async def main(): await mc1.update() for t in clock: - # current_time = time.monotonic() if t > TIME_TO_STEP: mc1.set_motor_torque( @@ -52,29 +47,18 @@ async def main(): ) await mc1.update() - print(f"######") - LOGGER.info( - "".join( - f"Output Torque: {mc1._data[0].values[Register.TORQUE] * mc1.gear_ratio}\t" - ) - ) + print("######") + LOGGER.info("".join(f"Output Torque: {mc1._data[0].values[Register.TORQUE] * mc1.gear_ratio}\t")) - print(f"------") + print("------") torque_data = pd.concat( [ torque_data, - pd.DataFrame( - { - "Time": [t], - "Output_Torque": [ - mc1._data[0].values[Register.TORQUE] * mc1.gear_ratio - ], - "Command_Torque": [ - mc1._data[0].values[Register.COMMAND_FEEDFORWARD_TORQUE] - * mc1.gear_ratio - ], - } - ), + pd.DataFrame({ + "Time": [t], + "Output_Torque": [mc1._data[0].values[Register.TORQUE] * mc1.gear_ratio], + "Command_Torque": [mc1._data[0].values[Register.COMMAND_FEEDFORWARD_TORQUE] * mc1.gear_ratio], + }), ], ignore_index=True, ) diff --git a/tutorials/actuators/moteus/velocity_control.py b/tutorials/actuators/moteus/velocity_control.py index 69d140ec..39ecdd85 100644 --- a/tutorials/actuators/moteus/velocity_control.py +++ b/tutorials/actuators/moteus/velocity_control.py @@ -1,4 +1,4 @@ -import asyncio +import asyncio import numpy as np import pandas as pd @@ -23,13 +23,11 @@ async def main(): bus_id=3, gear_ratio=9.0, ) - velocity_data = pd.DataFrame( - { - "Time": [], - "Output_Velocity": [], - "Command_Velocity": [], - } - ) + velocity_data = pd.DataFrame({ + "Time": [], + "Output_Velocity": [], + "Command_Velocity": [], + }) clock = SoftRealtimeLoop(dt=DT) try: @@ -44,43 +42,37 @@ async def main(): await mc1.update() for t in clock: - # current_time = time.monotonic() if t > TIME_TO_STEP: - mc1.set_motor_velocity( value=np.pi * 2, ) await mc1.update() - print(f"######") + print("######") LOGGER.info( "".join( f"Motor Velocity: {mc1.motor_velocity}\t" - + f"Motor Velocity Command: {mc1._data[0].values[Register.COMMAND_VELOCITY] * 2 * np.pi / mc1.gear_ratio}\t" + + "Motor Velocity Command: " + + f"{mc1._data[0].values[Register.COMMAND_VELOCITY] * 2 * np.pi / mc1.gear_ratio}\t" ) ) velocity_data = pd.concat( [ velocity_data, - pd.DataFrame( - { - "Time": [t], - "Output_Velocity": [mc1.motor_velocity], - "Command_Velocity": [ - mc1._data[0].values[Register.COMMAND_VELOCITY] - * 2 - * np.pi - / mc1.gear_ratio - ], - } - ), + pd.DataFrame({ + "Time": [t], + "Output_Velocity": [mc1.motor_velocity], + "Command_Velocity": [ + mc1._data[0].values[Register.COMMAND_VELOCITY] * 2 * np.pi / mc1.gear_ratio + ], + }), ], ignore_index=True, ) - print(f"------") + print("------") await asyncio.sleep(DT) finally: diff --git a/tutorials/compiled_control/compiled_control.py b/tutorials/compiled_control/compiled_control.py index e8a362c1..25edf689 100644 --- a/tutorials/compiled_control/compiled_control.py +++ b/tutorials/compiled_control/compiled_control.py @@ -18,9 +18,7 @@ ("z", my_linalg.types.c_double), ], ) -my_linalg.define_inputs( - [("vector1", my_linalg.types.Vector3D), ("vector2", my_linalg.types.Vector3D)] -) +my_linalg.define_inputs([("vector1", my_linalg.types.Vector3D), ("vector2", my_linalg.types.Vector3D)]) my_linalg.define_outputs([("result", my_linalg.types.c_double)]) vector1 = my_linalg.types.Vector3D() @@ -33,8 +31,8 @@ vector2.y = 0.6716 vector2.z = -0.0460 -my_linalg.inputs.vector1 = vector1 # type: ignore -my_linalg.inputs.vector2 = vector2 # type: ignore +my_linalg.inputs.vector1 = vector1 +my_linalg.inputs.vector2 = vector2 outputs = my_linalg.run() diff --git a/tutorials/compiled_control/dot_product_3d.cpp b/tutorials/compiled_control/dot_product_3d.cpp index 13b08b4d..e9c42eb4 100644 --- a/tutorials/compiled_control/dot_product_3d.cpp +++ b/tutorials/compiled_control/dot_product_3d.cpp @@ -1,6 +1,6 @@ /** dot_product_3d.cpp An example cpp file to demonstrate the compiled controller functionality -in the opensourceleg library. +in the opensourceleg library. Kevin Best University of Michigan October 2023