diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
new file mode 100755
index 0000000..5a3baa2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -0,0 +1,8 @@
+---
+name: "🐛 Bug Report"
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
new file mode 100755
index 0000000..972de88
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -0,0 +1,8 @@
+---
+name: "🚀 Feature Request"
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
new file mode 100755
index 0000000..2c22aea
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,13 @@
+---
+name: "❓Question"
+about: Ask a general question
+title: ''
+labels: question
+assignees: ''
+
+---
+
+## ❔Question
+
+
+## Additional context
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..0af54de
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,13 @@
+### Description
+
+This PR ...
+
+### Checklist
+
+- [ ] Linked issues (if existing)
+- [ ] Amended changelog.md for large changes (and added myself there as contributor)
+- [ ] Added/modified tests
+- [ ] Used pre-commit hooks when committing to ensure that code is compliant with hooks. Install hooks with `pre-commit install`.
+ To run hooks independent of commit, execute `pre-commit run --all-files`
+
+Thank you for joining. Have fun coding!
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..f16fcfb
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,52 @@
+# This action runs GitHub's industry-leading static analysis engine, CodeQL, against a repository's source code to find security vulnerabilities.
+# https://github.com/github/codeql-action
+
+name: "CodeQL"
+
+on:
+ schedule:
+ - cron: '0 0 27 * *' # Runs at 00:00 UTC on the 27th of every month
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ['python']
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v2
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 https://git.io/JvXDl
+
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ #- run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..8acd81b
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,70 @@
+name: Lint
+
+on:
+ push:
+ branches: [master, main, dev]
+ pull_request:
+ branches: [master, main]
+
+jobs:
+ linter-black:
+ name: Check code formatting with Black
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+ - name: Install Black
+ run: pip install black[jupyter]
+ - name: Run Black
+ run: black --check .
+
+ imports-check-isort:
+ name: Check valid import formatting with isort
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+ - name: Install isort
+ run: pip install isort==5.6.4
+ - name: Run isort
+ run: isort --check-only --diff .
+
+ linter-flake8:
+ name: Check valid formatting with flake8
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+
+ - name: Install dependencies
+ run: pip install flake8==3.9.2
+ - name: Run checks
+ run: flake8
+
+ pre-commit-hooks:
+ name: Check that pre-commit hooks pass
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+ - name: Install dependencies
+ run: pip install pre-commit
+
+ - name: Run checks
+ run: pre-commit run --all-files
diff --git a/.github/workflows/pypi_release.yml b/.github/workflows/pypi_release.yml
new file mode 100644
index 0000000..a366f6c
--- /dev/null
+++ b/.github/workflows/pypi_release.yml
@@ -0,0 +1,39 @@
+# This workflows will upload a Python Package using Twine when a release is created
+# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
+
+name: PyPi Release
+
+on:
+ push:
+ branches: [main]
+ release:
+ types: [created]
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: '3.8'
+
+ - name: Install poetry
+ shell: bash
+ run: |
+ curl -sSL https://install.python-poetry.org | python3 -
+ python -m pip install poetry-dynamic-versioning[plugin]
+
+ - name: Set poetry path variable
+ run: echo "/Users/runner/.local/bin" >> $GITHUB_PATH
+
+ - name: Build
+ run: |
+ poetry build
+
+ - name: Publish distribution 📦 to PyPI
+ if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
+ run: |
+ poetry publish --username "${{ secrets.PYPI_USERNAME }}" --password "${{ secrets.PYPI_PASSWORD }}"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..b78dac0
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,125 @@
+# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Test
+
+on:
+ push:
+ branches: [master, main, dev]
+ pull_request:
+ branches: [master, main, dev]
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macOS-latest] # add windows-2019 when poetry allows installation with `-f` flag
+ python-version: [3.8, 3.9] # python 3.9 is not supported by all dependencies yet
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Get full Python version
+ id: full-python-version
+ shell: bash
+ run: echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))")
+
+ - name: Install poetry
+ shell: bash
+ run: |
+ curl -sSL https://install.python-poetry.org | python3 -
+ - name: Set poetry path variable
+ run: echo "/Users/runner/.local/bin" >> $GITHUB_PATH
+
+ - name: Configure poetry
+ shell: bash
+ run: poetry config virtualenvs.in-project true
+
+ - name: Set up cache
+ uses: actions/cache@v2
+ id: cache
+ with:
+ path: .venv
+ key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }}
+
+ - name: Ensure cache is healthy
+ if: steps.cache.outputs.cache-hit == 'true'
+ shell: bash
+ run: poetry run pip --version >/dev/null 2>&1 || rm -rf .venv
+
+ - name: Upgrade pip
+ shell: bash
+ run: poetry run python -m pip install pip -U
+
+ - name: Install dependencies
+ shell: bash
+ run: poetry install --no-interaction --no-root
+
+ - name: Run unittest
+ shell: bash
+ run: poetry run coverage run -m unittest discover -s ./tests -p 'test_*.py'
+
+ - name: Statistics
+ if: success()
+ run: |
+ poetry run coverage report -i
+ poetry run coverage xml -i
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v3
+ if: always()
+ continue-on-error: true
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: coverage.xml
+ flags: cpu, unittest
+ name: CPU-coverage
+ fail_ci_if_error: false
+
+ docs:
+ name: Test docs build
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out Git repository
+ uses: actions/checkout@v2
+
+ - name: Set up Python
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.8
+
+ - name: Cache pip
+ uses: actions/cache@v2
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('docs/requirements_docs.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update && sudo apt-get install -y pandoc
+ python -m pip install --upgrade pip
+ pip install -r docs/requirements_docs.txt
+ shell: bash
+
+ - name: Build sphinx documentation
+ run: |
+ cd docs
+ make clean
+ make html --debug --jobs 2 SPHINXOPTS="-W"
+
+ - name: Upload built docs
+ uses: actions/upload-artifact@v2
+ with:
+ name: docs-results-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.requires }}
+ path: docs/build/html/
+ # Use always() to always run this step to publish test results when there are test failures
+ if: success()
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..569e40e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,92 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# Sphinx documentation
+docs/_build/
+docs/source/api/
+docs/source/CHANGELOG.md
+
+# mkdocs documentation
+/site
+
+# pycharm
+.idea
+
+# vscode
+.vscode
+
+# checkpoints
+*.ckpt
+*.pkl
+.DS_Store
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# yuetan
+**/nohup.out
+/reference/
+/data/
+/weights/
+/reference/jobshoppro/*
+/business/*
+/examples/*.xlsx
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..ae55273
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,29 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.1.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-ast
+ - repo: https://github.com/PyCQA/flake8
+ rev: "3.9.2"
+ hooks:
+ - id: flake8
+ - repo: https://github.com/pre-commit/mirrors-isort
+ rev: v5.10.1
+ hooks:
+ - id: isort
+ - repo: https://github.com/psf/black
+ rev: 22.3.0
+ hooks:
+ - id: black
+ - repo: https://github.com/nbQA-dev/nbQA
+ rev: 1.2.3
+ hooks:
+ - id: nbqa-black
+ - id: nbqa-isort
+ - id: nbqa-flake8
+ - id: nbqa-check-ast
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..3a16e74
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,26 @@
+# .readthedocs.yml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Build documentation in the docs/ directory with Sphinx
+# reference: https://docs.readthedocs.io/en/stable/config-file/v2.html#sphinx
+sphinx:
+ configuration: docs/source/conf.py
+ fail_on_warning: false
+
+# Build documentation with MkDocs
+#mkdocs:
+# configuration: mkdocs.yml
+
+# Optionally build your docs in additional formats such as PDF and ePub
+formats:
+ - htmlzip
+
+# Optionally set the version of Python and requirements required to build your docs
+python:
+ version: 3.8
+ install:
+ - requirements: docs/requirements_docs.txt
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4eb3502
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,18 @@
+# Release notes
+
+## v0.0.1 Initial release (15/10/2022)
+
+### Added
+- solver support
+ - dispatching rules
+ - spt
+ - fifo
+ - edd
+ - forward scheduling
+ - backward scheduling
+ - meta heuristics
+ - local search
+ - genetic
+
+### Contributor
+- LongxingTan
diff --git a/LICENSE b/LICENSE
new file mode 100755
index 0000000..e9f1ff4
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2023] [Longxing Tan]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5b96002
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,22 @@
+.PHONY: style test docs
+
+check_dirs := lekin examples tests
+
+# run checks on all files and potentially modifies some of them
+
+style:
+ black $(check_dirs)
+ isort $(check_dirs)
+ flake8 $(check_dirs)
+ pre-commit run --files $(check_dirs)
+
+# run tests for the library
+
+test:
+ python -m unittest
+
+# run tests for the docs
+
+docs:
+ make -C docs clean M=$(shell pwd)
+ make -C docs html M=$(shell pwd)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0e8d2a2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,110 @@
+[license-image]: https://img.shields.io/badge/License-Apache%202.0-blue.svg
+[license-url]: https://opensource.org/licenses/Apache-2.0
+[pypi-image]: https://badge.fury.io/py/lekin.svg
+[pypi-url]: https://pypi.python.org/pypi/lekin
+[pepy-image]: https://pepy.tech/badge/lekin/month
+[pepy-url]: https://pepy.tech/project/lekin
+[build-image]: https://github.com/LongxingTan/python-lekin/actions/workflows/test.yml/badge.svg?branch=master
+[build-url]: https://github.com/LongxingTan/python-lekin/actions/workflows/test.yml?query=branch%3Amaster
+[lint-image]: https://github.com/LongxingTan/python-lekin/actions/workflows/lint.yml/badge.svg?branch=master
+[lint-url]: https://github.com/LongxingTan/python-lekin/actions/workflows/lint.yml?query=branch%3Amaster
+[docs-image]: https://readthedocs.org/projects/python-lekin/badge/?version=latest
+[docs-url]: https://python-lekin.readthedocs.io/en/latest/
+[coverage-image]: https://codecov.io/gh/longxingtan/python-lekin/branch/master/graph/badge.svg
+[coverage-url]: https://codecov.io/github/longxingtan/python-lekin?branch=master
+[codeql-image]: https://github.com/longxingtan/python-lekin/actions/workflows/codeql-analysis.yml/badge.svg
+[codeql-url]: https://github.com/longxingtan/python-lekin/actions/workflows/codeql-analysis.yml
+
+
+
+
+
+[![LICENSE][license-image]][license-url]
+[![PyPI Version][pypi-image]][pypi-url]
+[![Download][pepy-image]][pepy-url]
+[![Build Status][build-image]][build-url]
+[![Lint Status][lint-image]][lint-url]
+[![Docs Status][docs-image]][docs-url]
+[![Code Coverage][coverage-image]][coverage-url]
+[![CodeQL Status][codeql-image]][codeql-url]
+
+**[Documentation](https://python-lekin.readthedocs.io)** | **[Tutorials](https://python-lekin.readthedocs.io/en/latest/tutorials.html)** | **[Release Notes](https://python-lekin.readthedocs.io/en/latest/CHANGELOG.html)** | **[中文](https://github.com/LongxingTan/python-lekin/blob/master/README_zh_CN.md)**
+
+**python-lekin** is a rapid-to-implement and easy-to-use Flexible Job Shop Scheduler Library, named after and inspired by [Lekin](https://web-static.stern.nyu.edu/om/software/lekin/). As a core function in **APS (advanced planning and scheduler)**, it helps manufacturers optimize the allocation of materials and production capacity optimally to balance demand and capacity.
+
+- accelerate by
+- Changeover Optimization
+- Ready for demo, research and maybe production
+
+# **DEVELOPING - NOT FINISHED AND DON'T USE IT NOW!**
+
+## Feature
+
+- constrained optimization
+ - route
+ - production
+ - material kit
+ - together
+
+- soft constrained optimization
+ - objective
+
+
+## Tutorial
+
+**Installation**
+
+``` shell
+$ pip install lekin
+```
+
+**Usage**
+
+``` python
+from lekin import Heuristics, Rule
+from lekin import Scheduler
+
+solver = Rule('SPT')
+scheduler = Scheduler(solver)
+scheduler.solve(job_list, machine_list)
+
+scheduler.draw()
+```
+
+## Examples
+
+In real world, Lekin integrates with MES to deploy production plans on the shop floor. Integration with ERP system is also required to exchange information on demand, inventory, and production
+
+- Exhaustive search
+ - branch and bound
+
+- Construction heuristics
+ - [SPT]()
+ - [critical path]()
+
+- Meta heuristics
+ - [local search]()
+ - [hill climbing]()
+ - [tabu search]()
+ - [evolutionary algorithms]()
+ - [genetic algorithms]()
+
+- Operation search
+ - [or-tools]()
+
+- Reinforcement learning
+
+Metaheuristics combined with Construction
+Heuristics to initialize is the recommended choice.
+
+## Citation
+```
+@misc{python-lekin2022,
+ author = {Yue Tan},
+ title = {python lekin},
+ year = {2020},
+ publisher = {GitHub},
+ journal = {GitHub repository},
+ howpublished = {\url{https://github.com/yuetan1988/python-lekin}},
+}
+```
diff --git a/README_zh_CN.md b/README_zh_CN.md
new file mode 100644
index 0000000..cf97c5f
--- /dev/null
+++ b/README_zh_CN.md
@@ -0,0 +1,86 @@
+[license-image]: https://img.shields.io/badge/License-Apache%202.0-blue.svg
+[license-url]: https://opensource.org/licenses/Apache-2.0
+[pypi-image]: https://badge.fury.io/py/python-lekin.svg
+[pypi-url]: https://pypi.python.org/pypi/python-lekin
+[pepy-image]: https://pepy.tech/badge/lekin/month
+[pepy-url]: https://pepy.tech/project/lekin
+[build-image]: https://github.com/LongxingTan/python-lekin/actions/workflows/test.yml/badge.svg?branch=master
+[build-url]: https://github.com/LongxingTan/python-lekin/actions/workflows/test.yml?query=branch%3Amaster
+[lint-image]: https://github.com/LongxingTan/python-lekin/actions/workflows/lint.yml/badge.svg?branch=master
+[lint-url]: https://github.com/LongxingTan/python-lekin/actions/workflows/lint.yml?query=branch%3Amaster
+[docs-image]: https://readthedocs.org/projects/python-lekin/badge/?version=latest
+[docs-url]: https://python-lekin.readthedocs.io/en/latest/
+[coverage-image]: https://codecov.io/gh/longxingtan/python-lekin/branch/master/graph/badge.svg
+[coverage-url]: https://codecov.io/github/longxingtan/python-lekin?branch=master
+
+
+
+
+
+[![LICENSE][license-image]][license-url]
+[![PyPI Version][pypi-image]][pypi-url]
+[![Download][pepy-image]][pepy-url]
+[![Build Status][build-image]][build-url]
+[![Lint Status][lint-image]][lint-url]
+[![Docs Status][docs-image]][docs-url]
+[![Code Coverage][coverage-image]][coverage-url]
+
+**[文档](https://python-lekin.readthedocs.io)** | **[教程](https://python-lekin.readthedocs.io/en/latest/tutorials.html)** | **[发布日志](https://python-lekin.readthedocs.io/en/latest/CHANGELOG.html)** | **[English](https://github.com/LongxingTan/python-lekin/blob/master/README.md)**
+
+**python-lekin**是一个APS智能排产调度工具,名字来源于[Lekin](https://web-static.stern.nyu.edu/om/software/lekin/)。在考虑实际约束的前提下,实现动态调整计划排程,高效响应客户订单承诺。
+
+
+- 支持工艺路线约束
+- 支持产能约束
+- 支持物料齐套约束
+- 支持顺排、倒排等排产方法
+- 支持遗传算法排产
+- 支持强化学习排产
+
+# **开发中- 目前请不要使用包,可用代码跑和学习!**
+
+## 快速入门
+
+### 安装
+
+``` shell
+$ pip install lekin
+```
+
+### 使用
+
+``` python
+from lekin import Heuristics, Genetics
+from lekin import Scheduler
+
+solver = Heuristics()
+scheduler = Scheduler(solver)
+scheduler.solve(jobs, machines)
+
+scheduler.draw()
+```
+
+## 示例
+在实际APS系统开发中,
+
+- 按工艺路线拆分工序
+- 按BOM拆分物料
+
+### 数据准备
+- Job
+- Task
+- Machine
+- Route
+
+## 引用
+
+```
+@misc{python-lekin2022,
+ author = {Yue Tan},
+ title = {python lekin},
+ year = {2020},
+ publisher = {GitHub},
+ journal = {GitHub repository},
+ howpublished = {\url{https://github.com/yuetan1988/python-lekin}},
+}
+```
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..cb9489b
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,8 @@
+coverage:
+ precision: 2
+ round: down
+ range: "70...100"
+ status:
+ project:
+ default:
+ threshold: 0.2%
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000..8e9b12b
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,9 @@
+FROM ubuntu:18.04
+
+RUN apt-get update
+RUN apt-get install -y wget vim python3.8
+
+RUN pip install --no-cache-dir lekin
+
+# Set the default command to python3.
+CMD ["python3"]
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..d0c3cbf
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# 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 = source
+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/make.bat b/docs/make.bat
new file mode 100644
index 0000000..6247f7e
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=source
+set BUILDDIR=build
+
+if "%1" == "" goto help
+
+%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.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/docs/requirements_docs.txt b/docs/requirements_docs.txt
new file mode 100644
index 0000000..b1acf8e
--- /dev/null
+++ b/docs/requirements_docs.txt
@@ -0,0 +1,13 @@
+recommonmark>=0.7.1
+nbconvert>=6.3.0
+pandoc>=1.0
+ipython
+sphinx>3.2
+nbsphinx==0.8.8
+sphinx_markdown_tables==0.0.17
+pydata_sphinx_theme==0.8.0
+docutils
+sphinx-autobuild
+
+pandas
+numpy
diff --git a/docs/source/_static/backward-scheduling.png b/docs/source/_static/backward-scheduling.png
new file mode 100644
index 0000000..99a9b8f
Binary files /dev/null and b/docs/source/_static/backward-scheduling.png differ
diff --git a/docs/source/_static/forward-scheduling.png b/docs/source/_static/forward-scheduling.png
new file mode 100644
index 0000000..3eaf98d
Binary files /dev/null and b/docs/source/_static/forward-scheduling.png differ
diff --git a/docs/source/_static/logo.svg b/docs/source/_static/logo.svg
new file mode 100644
index 0000000..bf2fc9c
--- /dev/null
+++ b/docs/source/_static/logo.svg
@@ -0,0 +1,4 @@
+
+
+
+python-lekin Text is not SVG - cannot display
diff --git a/docs/source/_static/network-backward-algorithm.svg b/docs/source/_static/network-backward-algorithm.svg
new file mode 100644
index 0000000..8247950
--- /dev/null
+++ b/docs/source/_static/network-backward-algorithm.svg
@@ -0,0 +1 @@
+13342
diff --git a/docs/source/_static/simple-backward-algorithm.svg b/docs/source/_static/simple-backward-algorithm.svg
new file mode 100644
index 0000000..8c9f3c9
--- /dev/null
+++ b/docs/source/_static/simple-backward-algorithm.svg
@@ -0,0 +1 @@
+13332
diff --git a/docs/source/_templates/custom-module-template.rst b/docs/source/_templates/custom-module-template.rst
new file mode 100644
index 0000000..8617479
--- /dev/null
+++ b/docs/source/_templates/custom-module-template.rst
@@ -0,0 +1,67 @@
+{{ fullname.split(".")[-1] | escape | underline}}
+
+.. automodule:: {{ fullname }}
+
+ {% block attributes %}
+ {% if attributes %}
+ .. rubric:: Module Attributes
+
+ .. autosummary::
+ :toctree:
+ {% for item in attributes %}
+ {{ item }}
+ {%- endfor %}
+ {% endif %}
+ {% endblock %}
+
+ {% block functions %}
+ {% if functions %}
+ .. rubric:: {{ _('Functions') }}
+
+ .. autosummary::
+ :toctree:
+ :template: custom-base-template.rst
+ {% for item in functions %}
+ {{ item }}
+ {%- endfor %}
+ {% endif %}
+ {% endblock %}
+
+ {% block classes %}
+ {% if classes %}
+ .. rubric:: {{ _('Classes') }}
+
+ .. autosummary::
+ :toctree:
+ :template: custom-class-template.rst
+ {% for item in classes %}
+ {{ item }}
+ {%- endfor %}
+ {% endif %}
+ {% endblock %}
+
+ {% block exceptions %}
+ {% if exceptions %}
+ .. rubric:: {{ _('Exceptions') }}
+
+ .. autosummary::
+ :toctree:
+ {% for item in exceptions %}
+ {{ item }}
+ {%- endfor %}
+ {% endif %}
+ {% endblock %}
+
+{% block modules %}
+{% if modules %}
+.. rubric:: Modules
+
+.. autosummary::
+ :toctree:
+ :template: custom-module-template.rst
+ :recursive:
+{% for item in modules %}
+ {{ item }}
+{%- endfor %}
+{% endif %}
+{% endblock %}
diff --git a/docs/source/api.rst b/docs/source/api.rst
new file mode 100644
index 0000000..1fc7aaa
--- /dev/null
+++ b/docs/source/api.rst
@@ -0,0 +1,13 @@
+API
+====
+
+.. currentmodule:: lekin
+
+.. autosummary::
+ :toctree: api
+ :template: custom-module-template.rst
+ :recursive:
+
+ scheduler
+ solver
+ lekin_struct
diff --git a/docs/source/application.rst b/docs/source/application.rst
new file mode 100644
index 0000000..d84d113
--- /dev/null
+++ b/docs/source/application.rst
@@ -0,0 +1,146 @@
+Application
+===========
+
+基本概念
+----------------
+均衡生产:heijunka
+
+
+数据
+----------------
+
+MRP: Material Requirements Planning
+
+
+BOM: Bill Of Materials
+
+
+功能
+------------------------
+
+一个完善的APS系统包含以下模块。
+- 需求中心
+- 排程中心
+- 排程工作台
+- 物料中心
+
+
+建模
+----------
+
+Activities represent operations with time and resource requirements
+Resources have calendars defining availability
+Demand represents customer orders to fulfill
+
+
+过程
+----------
+
+实际APS会涉及到各个车间,各个工序的复杂区分,以及BOM中涉及到多工厂的部分。
+- 其实和APS关系不大,APS只要把最后一层工序展开,最后一层BOM展开,按实际的资源约束进行计算,最后只是展现形式上的区别
+
+首先解决m个工序、n个机器的车间排产问题,然后把实际问题往车间问题靠。
+
+1、根据一定的规则(产品总工作时长、工序B的最早开工时间等)获得产品的优先级
+2、初始化任务的初始状态,除了每个产品的工序A为可开工状态其余皆为不可开工状态;
+3、根据优先级对工序A对应的任务进行加工,并更新任务的状态及紧后工序的状态;
+4、对机器的空闲时间进行排序,取最早可开工机器k;
+5、根据机器k的空闲开始时间以及任务状态检索任务,存储为任务列表R;
+6、判断任务列表R是否为空,是则k=k+1,返回步骤五,否则进行下一步;
+7、根据任务的最早可加工时间进行排序,选择最早开始的任务进行加工,更新机器状态、任务状态及后续的工序状态;
+ ·确定任务的开工时间及结束时间
+ ·更新机器的释放时间
+ ·更新当前任务的状态、开工时间、完工时间
+ ·更新当前任务后续节点的最早开工时间,若当前任务为产品的最后一个工序则无须更新
+8、判断所有任务是否均已完工,是则结束,否则返回步骤四。
+
+解决冲突的过程,即是一个顺排的过程。把所有分布在该资源上的任务根据顺序进行顺排
+
+车间过程1-倒排+顺排
+-------------------
+
+先分发冻结期,按锁定期
+- 关键问题:成组后的工序部分处于冻结期;job的工序不是完整的工序
+- 只assign资源和日历,由于不完整进行顺排。
+- 检查物料齐套约束,如果物料不齐套,按齐套排程并给出报警消息
+
+
+再排锁定任务
+- 只倒排,如果倒排不可行则返回错误。按优先级
+ - 非关键工序: 按lead_time(倒排lead_time,顺排lag_time)、节拍往前排,不考虑齐套时间。
+ - 关键工序: 如有锁定资源,则按资源情况进行编排。不考虑齐套时间,因为锁定任务是为了确保优先级,物料通过缺料去人工追料
+ - 共享工序: 共享工序本身取多个job中靠前的时间断。
+ - 原本已经排过的其他job,前序工序需要以此为新的往前推
+ - 同时拥有共享工序的job排序适时进行调整,尽量避免以上修改
+ - 在冻结期且物料约束不满足: 按最早齐套时间进行排产,同时已排工序进行
+
+
+再根据优先级排产其他任务
+- 先倒排
+ - 第一道关键工序前的非关键工序,先按lead time进行排,后面需要二次更新。
+ - 关键工序倒排,(task分割会有的)非关键工序则按lead time前推
+ - 倒排中时间约束都是最晚时间,但物料约束是最早开始时间。如果时间不足以排产,则该工序及之后的工序都转为顺排重排
+ - 另一个思路是,先直接道排到第一个工序,后续一起按照可开始时间进行顺排。甚至等到推紧时一起顺排是否可行呢
+ - 遇到共享task,如果之前的共享task被安排的时间更晚,那么剩余工序也转为顺排重排
+ - 第一步剩余的非关键工序后拉
+ - 如果任务一步中,资源日历不足则进入下一步
+- 倒排有问题则顺排
+ - 按各个资源最早可用日期开始排 (此时应该选可以最早的资源),非关键工序按lead time排,并需要进行二次更新
+ - 关键工序顺排
+ - 如果遇到共享task不满足时间约束
+ - 第一步剩余的非关键工序前拉
+ - 如果任何一步中,资源日历不足则返回错误
+- 关键工序完全没有设置的job,按无限产能倒排
+
+
+任务推紧规整
+- 所有任务都采用顺排,类似按资源排产的方法。
+- 按关键工序设置,“是否可以挪动”。每一个资源的第一道关键工序都可以向前,并跟新后续的可挪动状态与开始时间约束
+- 迭代更新
+
+
+未排任务再次尝试
+- 推紧之后,再次尝试将之前未排的任务进行排产
+
+
+委外/外协的排产
+- 委外的指定是针对供应商,排到日历中
+- 根据关键工序的产能,按优先级将各委外的部分进行排产
+
+
+车间过程2-倒排+顺排2
+-------------------
+仍然是先排锁定任务
+
+把所有任务按照交期和最早开工日期进行倒排或顺排,不考虑资源的约束本身 【带来的问题是:资源优先级的选择】
+
+
+顺排的时候,按照job优先级 【指定 > 优先级】
+- 每一个job都按第一道工序其最早开工日期开始,
+
+
+
+车间过程3-按资源增量排产
+---------------------
+输入: 排产任务(MO+计划单)
+输出: 各工序的排产资源与结果
+1. 筛选出主工单与部件工单,建立子部件的属性联系
+2. 筛选出主工单中的关键工序与非关键工序
+3. 初始化历史已排且其资源仍存在的关键工序的资源队列
+4. 对于新任务计划单或资源不存在的情况下, 重新分配任务. 完成资源中任务队列初始化
+5. 资源中任务队列重排
+6. 主工单非关键工序的前推后拉
+7. 部件工单和工序的前推
+
+
+可视化
+------------
+- 资源在时间线上的计划情况
+- 按订单,在时间线上的操作情况
+
+
+可视化重排
+-------------------
+输入: 资源和资源任务队列顺序
+输出:
+1. 初始化到增量排产队列任务
diff --git a/docs/source/conf.py b/docs/source/conf.py
new file mode 100644
index 0000000..4953ee3
--- /dev/null
+++ b/docs/source/conf.py
@@ -0,0 +1,146 @@
+# 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
+from pathlib import Path
+import shutil
+import sys
+
+from sphinx.application import Sphinx
+from sphinx.ext.autosummary import Autosummary
+from sphinx.pycode import ModuleAnalyzer
+
+SOURCE_PATH = Path(os.path.dirname(__file__)) # noqa # docs source
+PROJECT_PATH = SOURCE_PATH.joinpath("../..") # noqa # project root
+
+sys.path.insert(0, str(PROJECT_PATH)) # noqa
+
+import lekin # isort:skip
+
+# -- Project information -----------------------------------------------------
+
+project = "python-lekin"
+copyright = "2022, Longxing Tan"
+author = "Longxing Tan"
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ "nbsphinx",
+ "recommonmark",
+ "sphinx_markdown_tables",
+ "sphinx.ext.autodoc",
+ "sphinx.ext.autosummary",
+ "sphinx.ext.doctest",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.mathjax",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.githubpages",
+ "sphinx.ext.napoleon",
+]
+
+# 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 = []
+
+
+# -- 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 = "alabaster"
+
+
+# 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"]
+
+
+# setup configuration
+def skip(app, what, name, obj, skip, options):
+ """
+ Document __init__ methods
+ """
+ if name == "__init__":
+ return True
+ return skip
+
+
+apidoc_output_folder = SOURCE_PATH.joinpath("api")
+PACKAGES = [lekin.__name__]
+
+
+def get_by_name(string: str):
+ """
+ Import by name and return imported module/function/class
+ Args:
+ string (str): module/function/class to import, e.g. 'pandas.read_csv' will return read_csv function as
+ defined by pandas
+ Returns:
+ imported object
+ """
+ class_name = string.split(".")[-1]
+ module_name = ".".join(string.split(".")[:-1])
+
+ if module_name == "":
+ return getattr(sys.modules[__name__], class_name)
+
+ mod = __import__(module_name, fromlist=[class_name])
+ return getattr(mod, class_name)
+
+
+class ModuleAutoSummary(Autosummary):
+ def get_items(self, names):
+ new_names = []
+ for name in names:
+ mod = sys.modules[name]
+ mod_items = getattr(mod, "__all__", mod.__dict__)
+ for t in mod_items:
+ if "." not in t and not t.startswith("_"):
+ obj = get_by_name(f"{name}.{t}")
+ if hasattr(obj, "__module__"):
+ mod_name = obj.__module__
+ t = f"{mod_name}.{t}"
+ if t.startswith("pytorch_forecasting"):
+ new_names.append(t)
+ new_items = super().get_items(sorted(new_names))
+ return new_items
+
+
+def setup(app: Sphinx):
+ app.add_css_file("custom.css")
+ app.connect("autodoc-skip-member", skip)
+ app.add_directive("moduleautosummary", ModuleAutoSummary)
+ app.add_js_file("https://buttons.github.io/buttons.js", **{"async": "async"})
+
+
+# autosummary
+autosummary_generate = True
+shutil.rmtree(SOURCE_PATH.joinpath("api"), ignore_errors=True)
+
+
+# copy changelog
+shutil.copy(
+ "../../CHANGELOG.md",
+ "CHANGELOG.md",
+)
diff --git a/docs/source/demand.rst b/docs/source/demand.rst
new file mode 100644
index 0000000..a57a0af
--- /dev/null
+++ b/docs/source/demand.rst
@@ -0,0 +1,3 @@
+demand
+========================================
+
diff --git a/docs/source/heuristics.rst b/docs/source/heuristics.rst
new file mode 100644
index 0000000..3171229
--- /dev/null
+++ b/docs/source/heuristics.rst
@@ -0,0 +1,28 @@
+heuristics
+============
+
+
+禁忌搜索
+------------
+
+
+遗传算法
+-------------
+
+遗传算法应用在排产中的关键就是如何将排产结果进行编码、以及如何计算fitness。
+
+每一个可行解被称为一个染色体,一个染色体由多个元素构成,这个元素称为基因。
+
+遗传算法应用在排产问题时,以及TSP、VRP等经典问题,模型的解不表示数量,而表示顺序。
+对于m个job,n个机器的排产问题。基因序列的长度是 m * n,因为每个job都需要在n台机器上加工。
+
+两个序列表示模型的解,一个是工序的OS,一个是机器的MS。
+- 其中,OS基因序列的数值表示第i个job,这个数值第几次出现决定的是该job的第几道工序。
+- MS同理表示的选择的机器。
+
+
+
+强化学习
+-------------
+
+Q-learning方法的关键是在agent探索过程中保存一个状态收益表。
diff --git a/docs/source/index.rst b/docs/source/index.rst
new file mode 100644
index 0000000..276d1b3
--- /dev/null
+++ b/docs/source/index.rst
@@ -0,0 +1,75 @@
+.. python-lekin documentation master file, created by
+ sphinx-quickstart on Fri Sep 30 17:57:17 2022.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+python-lekin documentation
+========================================
+.. raw:: html
+
+ GitHub
+
+
+**python-lekin** 是一个工厂智能排产调度工具,名字来源于`Lekin `_.
+
+组合优化基础
+-------------
+
+- 装箱问题(Bin Packing, BP)
+- 背包问题(Knapsack Problem, KP)
+- 车间调度问题(Job-shop Scheduling Problem, JSP)
+- 整数规划问题(Integer Programming, IP)
+
+- 旅行商问题(Traveling Salesman Problem, TSP)
+- 车辆路径问题(Vehicle Routing Problem, VRP)
+- 图着色问题(Graph Coloring, GC)
+- 图匹配问题(Graph Matching, GM)
+
+
+- 精确算法:分支定界法(Branch and Bound)和动态规划法(Dynamic Programming)
+
+- 近似算法:近似算法(Approximate Algorithms)和启发式算法(Heuristic Algorithms)
+ - 贪心算法、局部搜索算法、线性规划、松弛算法、序列算法
+ - 模拟退火算法、禁忌搜索、进化算法、蚁群优化算法、粒子群算法、迭代局部搜索、变邻域搜索
+
+
+车间排产快速入门
+---------------
+
+排产是一个分配任务,将有限的资源分配给需求。因此需求需要有优先级,约束主要有产能约束与物料约束。产能约束,将订单中的成品按工艺路线分解为工序,而每一道工序有对应的生产机器;物料约束,将订单的成品按BOM(bill of materials)展开为原材料需求,每一道工序开始前需要对应原材料齐套。
+
+下标,机器k kk加工为任务i ii后加工任务j jj
+
+其中,:math:`A_\text{c} = (\pi/4) d^2`
+
+
+subject to:
+
+.. math:: \alpha{}_t(i) = P(O_1, O_2, … O_t, q_t = S_i \lambda{})
+
+Flexible Job-Shop Scheduling problem(FJSP)包含两个任务
+- Machine assignment: 选择机器
+- Operation sequencing:工序顺序
+
+
+Finite Capacity Planning
+
+.. toctree::
+ :maxdepth: 2
+ :caption: Contents:
+
+ rules
+ heuristics
+ rl
+ application
+ demand
+ api
+ GitHub
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/source/rl.rst b/docs/source/rl.rst
new file mode 100644
index 0000000..2d0a87b
--- /dev/null
+++ b/docs/source/rl.rst
@@ -0,0 +1,2 @@
+rl
+=========
diff --git a/docs/source/rules.rst b/docs/source/rules.rst
new file mode 100644
index 0000000..ef8e91d
--- /dev/null
+++ b/docs/source/rules.rst
@@ -0,0 +1,71 @@
+Rules
+============
+
+分为rule-based、event-based、resource-based两种思路。
+
+SPT最短加工时间
+--------------------
+
+按任务所需工序时间长短,从短到长顺序排列.
+实现中,为保证工艺路径的先后约束关系,构造规则法通过循环的先后关系来保证满足约束。
+
+
+EDD最早预定交货期规则
+---------------------------
+
+按生产任务规定完成时刻(预定交货期)的先后,从先到后顺次排列
+
+SPT—EDD规则
+-----------------
+
+1)根据EDD规则安排D(max)为最小的方案。
+2)计算所有任务的总流程时间。
+3)查找方案中,预定交贷期(di)大于总流程时间的生产任务(不惟一),按SPT规则,将其中加工时间最大者排于最后。
+4)舍弃第3步能排定的最后任务者及其后序任务,回到第2步重复。
+
+
+关键路径法
+-------------
+
+关键路径是决定项目完成的最短时间,关键路径可能不止一条。
+
+其基本概念:
+最早开始时间 (Early start)
+最晚开始时间 (Late start)
+最早完成时间 (Early finish)
+最晚完成时间 (Late finish)
+松弛时间 (slack)
+
+正推方法确定每个任务的最早开始时间和最早完成时间,逆推方法确定每个任务的最晚完成时间和最晚开始时间。
+
+
+顺排
+-------------
+
+顺排和倒排,和其他规则启发式算法一样,一个工序集一个工序集的排。每排一个工序,工序job完成后,更新机器、job状态、后续job状态。
+顺排对下一道工序的约束是:最早开始时间
+
+.. code-block:: python
+ backward(operations, next_op_start_until, with_material_kitting_constraint, align_with_same_production_line, latest_start_time, latest_end_time) -> remaining_operations: list[operations],
+
+
+.. code-block:: python
+ assign_op(operation, is_critical, direction: str, ) -> chosen_resource, chosen_production_id, chosen_hours,
+
+在顺排中,排的比较紧密的资源往往就是瓶颈资源。
+
+倒排
+---------------
+
+每一个MO最早开始时间初始化:max(ESD, today)。确保开始时间不早于今天,或不早于资源日历最早开始时间
+倒排对下一道工序的约束是: 最晚结束时间
+
+倒排
+- 从业务上可以减少库存, just-in-time
+- 带来的结果是不连续
+- 影响连续排产的判断
+- 考虑物料齐套时,导致倒排可能需要一次次推倒重来
+
+
+.. code-block:: python
+ forward(operations, next_op_start_until, with_material_kitting_constraint, align_with_same_production_line, earliest_start_time, earliest_end_time)
diff --git a/examples/__init__.py b/examples/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/data/k1.json b/examples/data/k1.json
new file mode 100644
index 0000000..1fdeef5
--- /dev/null
+++ b/examples/data/k1.json
@@ -0,0 +1,129 @@
+{
+ "itineraries": [
+ {
+ "itineraryName": "Itinerary 1",
+ "tasksList": [
+ {
+ "taskName": "Task 1",
+ "taskMachine": {
+ "machineName": "M1"
+ },
+ "taskDuration": 10.0
+ },
+ {
+ "taskName": "Task 2",
+ "taskMachine": {
+ "machineName": "M2"
+ },
+ "taskDuration": 5.0
+ },
+ {
+ "taskName": "Task 3",
+ "taskMachine": {
+ "machineName": "M3"
+ },
+ "taskDuration": 35.0
+ }
+ ]
+ },
+ {
+ "itineraryName": "Itinerary 2",
+ "tasksList": [
+ {
+ "taskName": "Task 1",
+ "taskMachine": {
+ "machineName": "M2"
+ },
+ "taskDuration": 25.0
+ },
+ {
+ "taskName": "Task 2",
+ "taskMachine": {
+ "machineName": "M1"
+ },
+ "taskDuration": 5.0
+ },
+ {
+ "taskName": "Task 3",
+ "taskMachine": {
+ "machineName": "M3"
+ },
+ "taskDuration": 30.0
+ },
+ {
+ "taskName": "Task 4",
+ "taskMachine": {
+ "machineName": "M4"
+ },
+ "taskDuration": 15.0
+ }
+ ]
+ },
+ {
+ "itineraryName": "Itinerary 3",
+ "tasksList": [
+ {
+ "taskName": "Task 1",
+ "taskMachine": {
+ "machineName": "M2"
+ },
+ "taskDuration": 5.0
+ },
+ {
+ "taskName": "Task 2",
+ "taskMachine": {
+ "machineName": "M4"
+ },
+ "taskDuration": 10.0
+ }
+ ]
+ },
+ {
+ "itineraryName": "Itinerary 4",
+ "tasksList": [
+ {
+ "taskName": "Task 1",
+ "taskMachine": {
+ "machineName": "M2"
+ },
+ "taskDuration": 15.0
+ },
+ {
+ "taskName": "Task 2",
+ "taskMachine": {
+ "machineName": "M3"
+ },
+ "taskDuration": 10.0
+ },
+ {
+ "taskName": "Task 3",
+ "taskMachine": {
+ "machineName": "M4"
+ },
+ "taskDuration": 20.0
+ },
+ {
+ "taskName": "Task 4",
+ "taskMachine": {
+ "machineName": "M1"
+ },
+ "taskDuration": 10.0
+ }
+ ]
+ }
+ ],
+ "machines": [
+ {
+ "machineName": "M1"
+ },
+ {
+ "machineName": "M2"
+ },
+ {
+ "machineName": "M3"
+ },
+ {
+ "machineName": "M4"
+ }
+ ]
+}
diff --git a/examples/genetic_example.py b/examples/genetic_example.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/rule_example.py b/examples/rule_example.py
new file mode 100644
index 0000000..46d0cb3
--- /dev/null
+++ b/examples/rule_example.py
@@ -0,0 +1,101 @@
+import json
+import logging
+
+from lekin.dashboard.gantt import get_scheduling_res_from_all_jobs, plot_gantt_chart
+from lekin.lekin_struct import (
+ Job,
+ JobCollector,
+ Operation,
+ OperationCollector,
+ Resource,
+ ResourceCollector,
+ Route,
+ RouteCollector,
+)
+from lekin.solver.construction_heuristics import BackwardScheduler, ForwardScheduler, LPSTScheduler, SPTScheduler
+
+logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.DEBUG)
+
+
+def prepare_data(file_path="./data/k1.json"):
+ with open(file_path, "r", encoding="utf8") as file: # read file from path
+ data = json.loads(file.read())
+
+ job_collector = JobCollector()
+ # operation_collector = OperationCollector()
+ route_collector = RouteCollector()
+ resource_collector = ResourceCollector()
+
+ if list(data.keys()) == ["itineraries", "machines"]:
+ resources = data["machines"] # is first level structure is correct, then split
+ routes = data["itineraries"]
+
+ # parse the resource
+ for re in resources:
+ re_name = re["machineName"]
+ re_id = int(re_name.replace("M", ""))
+ resource = Resource(resource_id=re_id, resource_name=re_name)
+ resource_collector.add_resource_dict({re_id: resource})
+ # print([i.resource_id for i in resource_collector.get_all_resources()])
+ # print(resource_collector.get_all_resources()[0].available_hours)
+
+ # parse the job and route
+ for ro in routes:
+ # ro_name = ro["itineraryName"]
+ ro_id = int(ro["itineraryName"].replace("Itinerary ", ""))
+ route = Route(route_id=ro_id)
+ operations_sequence = []
+ for ta in ro["tasksList"]:
+ op_name = ta["taskName"]
+ op_id = ta["taskName"].replace("Task ", "")
+
+ op_pt = ta["taskDuration"]
+
+ op_tm = []
+ if isinstance(ta["taskMachine"], list):
+ for re in ta["taskMachine"]:
+ re_name = re["machineName"]
+ re_id = int(re_name.replace("M", ""))
+ op_tm.append(resource_collector.get_resource_by_id(re_id))
+ else:
+ re_name = ta["taskMachine"]["machineName"]
+ re_id = int(re_name.replace("M", ""))
+ op_tm.append(resource_collector.get_resource_by_id(re_id))
+
+ operations_sequence.append(
+ Operation(
+ operation_id=op_id,
+ operation_name=op_name,
+ quantity=1,
+ processing_time=op_pt,
+ parent_job_id=ro_id, # route defines job here
+ available_resource=op_tm,
+ )
+ )
+
+ route.operations_sequence = operations_sequence
+ route_collector.add_route(route)
+
+ job_collector.add_job(Job(job_id=ro_id, assigned_route_id=ro_id))
+
+ # print(resources)
+ # print(routes)
+
+ return job_collector, resource_collector, route_collector
+
+
+def run_scheduling(job_collector, resource_collector, route_collector):
+ # scheduler = ForwardScheduler(job_collector, resource_collector, route_collector)
+ scheduler = BackwardScheduler(job_collector, resource_collector, route_collector)
+
+ scheduler.run()
+ return
+
+
+if __name__ == "__main__":
+ job_collector, resource_collector, route_collector = prepare_data(file_path="./data/k1.json")
+ run_scheduling(job_collector, resource_collector, route_collector)
+
+ scheduling_res = get_scheduling_res_from_all_jobs(job_collector)
+ print(scheduling_res)
+ plot_gantt_chart(job_collector, scheduling_res)
diff --git a/lekin/__init__.py b/lekin/__init__.py
new file mode 100644
index 0000000..a743725
--- /dev/null
+++ b/lekin/__init__.py
@@ -0,0 +1,12 @@
+from lekin.datasets.get_data import get_data
+
+# from lekin.lekin_struct.job import Job
+# from lekin.lekin_struct.machine import Machine
+# from lekin.lekin_struct.operation import Operation
+# from lekin.lekin_struct.route import Route
+from lekin.scheduler import Scheduler
+from lekin.solver.meta_heuristics import Heuristics
+
+__all__ = ["Job", "Machine", "Route", "Operation", "Scheduler", "get_data"]
+
+__version__ = "0.0.0"
diff --git a/lekin/dashboard/__init__.py b/lekin/dashboard/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lekin/dashboard/gantt.py b/lekin/dashboard/gantt.py
new file mode 100644
index 0000000..c3caabc
--- /dev/null
+++ b/lekin/dashboard/gantt.py
@@ -0,0 +1,70 @@
+"""
+Gantt
+"""
+
+import logging
+from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, Union
+
+from matplotlib import ticker
+import matplotlib.patches as patches
+import matplotlib.pyplot as plt
+import pandas as pd
+
+logging.getLogger("matplotlib.font_manager").disabled = True
+
+
+def get_scheduling_res_from_all_jobs(job_collector):
+ ops = []
+ for job in job_collector.job_list:
+ ops += job.operations
+
+ scheduling_res = []
+ for op in ops:
+ scheduling_res.append(
+ [
+ op.operation_id,
+ op.parent_job_id,
+ op.quantity,
+ op.assigned_resource.resource_id,
+ min(op.assigned_hours),
+ max(op.assigned_hours),
+ ]
+ )
+ scheduling_res = pd.DataFrame(scheduling_res, columns=["Operation", "Job", "Quantity", "Resource", "Start", "End"])
+ scheduling_res["Duration"] = scheduling_res["End"] - scheduling_res["Start"] # + 1
+ return scheduling_res
+
+
+def plot_gantt_chart(job_collector, scheduling_res):
+ color_dict = job_collector.generate_color_list_for_jobs()
+
+ # gantt
+ resource_list = []
+ for resource, group in scheduling_res.groupby("Resource"):
+ resource_list.append(resource)
+ start_tuple = []
+ color_tuple = []
+ for _, op in group.iterrows():
+ start_tuple.append(op[["Start", "Duration"]].tolist())
+ color_tuple.append(color_dict.get(op["Job"]))
+
+ plt.gca().broken_barh(start_tuple, ((resource + 1) * 10, 9), facecolors=color_tuple)
+
+ # legend
+ legends_colors = []
+ for job in job_collector.job_list:
+ legends_colors.append(patches.Patch(color=color_dict.get(job.job_id), label=f"job{job.job_id}"))
+ plt.legend(handles=legends_colors, fontsize=8)
+
+ # resource tick
+ resources = resource_list # list(reversed(resource_list))
+ resource_ticks = [15]
+ for i in range(len(resources)):
+ resource_ticks.append(resource_ticks[i] + 10) # machine increase + 10
+ plt.yticks(resource_ticks[1:], resources)
+
+ plt.grid(True)
+ plt.xlabel("Time")
+ plt.ylabel("Resources")
+ plt.title("Gantt Chart - Scheduling Result")
+ plt.show()
diff --git a/lekin/dashboard/pages.py b/lekin/dashboard/pages.py
new file mode 100644
index 0000000..51f2925
--- /dev/null
+++ b/lekin/dashboard/pages.py
@@ -0,0 +1,3 @@
+from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, Union
+
+import streamlit as st
diff --git a/lekin/datasets/__init__.py b/lekin/datasets/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lekin/datasets/check_data.py b/lekin/datasets/check_data.py
new file mode 100644
index 0000000..c1c8cab
--- /dev/null
+++ b/lekin/datasets/check_data.py
@@ -0,0 +1,15 @@
+"""Check the input job shop format and necessary information"""
+
+import logging
+
+
+def check_data(data):
+ if data.keys() != ["routes", "machines"]:
+ logging.error("key")
+
+ if len(data["machines"]) < 1:
+ logging.error("machine")
+ if len(data["routes"]) < 1:
+ logging.error("route")
+
+ return
diff --git a/lekin/datasets/get_data.py b/lekin/datasets/get_data.py
new file mode 100644
index 0000000..715ca63
--- /dev/null
+++ b/lekin/datasets/get_data.py
@@ -0,0 +1,13 @@
+"""Generate the example jobshoppro"""
+
+import json
+import logging
+
+
+def get_data(name):
+ # machine_list = []
+ # route_list = []
+ if name == "simple":
+ pass
+
+ return
diff --git a/lekin/datasets/parse_data.py b/lekin/datasets/parse_data.py
new file mode 100644
index 0000000..e69de29
diff --git a/lekin/forecast/__init__.py b/lekin/forecast/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lekin/lekin_struct/__init__.py b/lekin/lekin_struct/__init__.py
new file mode 100644
index 0000000..10496be
--- /dev/null
+++ b/lekin/lekin_struct/__init__.py
@@ -0,0 +1,15 @@
+"""
+https://gitee.com/EnCode/APS?_from=gitee_search#%E5%BB%BA%E6%A8%A1
+
+工厂:订单
+车间:工单
+班组:工序
+操作动作:动作
+
+"""
+
+from lekin.lekin_struct.job import Job, JobCollector # 成品需求
+from lekin.lekin_struct.operation import Operation, OperationCollector # 工序
+from lekin.lekin_struct.resource import Resource, ResourceCollector # 机器
+from lekin.lekin_struct.route import Route, RouteCollector
+from lekin.lekin_struct.timeslot import TimeSlot
diff --git a/lekin/lekin_struct/job.py b/lekin/lekin_struct/job.py
new file mode 100644
index 0000000..88667f8
--- /dev/null
+++ b/lekin/lekin_struct/job.py
@@ -0,0 +1,154 @@
+"""
+Struct Job/订单作业
+ - a job could finish one product while finished
+ - job/mo/operation/activity
+"""
+
+from datetime import datetime
+import random
+from typing import Any, Callable, Dict, List, Optional, Tuple
+
+from lekin.lekin_struct.operation import Operation
+
+random.seed(315)
+
+
+class Job(object):
+ def __init__(
+ self,
+ job_id: str,
+ priority: int = None,
+ quantity: int = None,
+ demand_date: datetime = None,
+ job_type: str = None,
+ earliest_start_time=None,
+ assigned_route_id=None,
+ assigned_bom_id=None,
+ **kwargs: Dict,
+ ) -> None:
+ self.job_id: str = job_id
+ self.priority: int = priority
+ self.demand_date: datetime = demand_date
+ self.quantity: int = quantity
+ self.job_type: str = job_type
+ self.earliest_start_time: datetime = earliest_start_time # Material constraint
+ # cached scheduling result until the whole job is finished
+ self.cached_scheduling: dict = {}
+ self.assigned_route_id: str = assigned_route_id # Route object assigned to this job
+ self.assigned_bom_id: str = assigned_bom_id
+ self.current_operation_index: int = 0 # Record the current processing operation
+ self._operations_sequence: List[Operation] = [] # List of Operation objects for this job
+
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def assign_route(self, route_id):
+ self.assigned_route_id = route_id
+
+ @property
+ def operations(self):
+ return self._operations_sequence
+
+ def ger_next_operation(self):
+ if self.current_operation_index < len(self._operations_sequence):
+ return self._operations_sequence[self.current_operation_index]
+ else:
+ return None
+
+ def assign_cached_scheduling(self):
+ """assign all cached scheduling officially while all ops are fine"""
+ pass
+
+ def clear_cached_scheduling(self, all, start, dir):
+ """clear the cached scheduling result"""
+ pass
+
+ @operations.setter
+ def operations(self, operations_sequence):
+ self._operations_sequence = operations_sequence
+
+ def __eq__(self, other):
+ return
+
+ def __hash__(self):
+ return
+
+ def __str__(self):
+ return f"{self.job_id}"
+
+
+class JobCollector:
+ def __init__(self):
+ self.job_list = [] # List to store Job objects
+ self.color_dict = dict() # List to store colors for job
+ self.index = -1
+ # self.route_list = []
+ # self.operation_list = []
+ # self.resource_list = []
+ # self.time_slot_list = []
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ self.index += 1
+ if self.index < len(self.job_list):
+ return self.job_list[self.index]
+ else:
+ raise StopIteration("Stop")
+
+ def add_job(self, job: Job) -> None:
+ self.job_list.append(job)
+
+ def get_job_by_id(self, job_id: str):
+ for job in self.job_list:
+ if job.job_id == job_id:
+ return job
+ return None
+
+ def sort_jobs(self, jobs):
+ # Custom sorting function based on priority and continuity
+ def custom_sort(job):
+ priority_weight = (job.priority, job.demand_date)
+ # continuity_weight = -self.calculate_gap_time(job)
+ return priority_weight # + continuity_weight
+
+ # jobs = sorted(jobs, key=custom_sort, reverse=True)
+ return [i[0] for i in sorted(enumerate(jobs), key=lambda x: custom_sort(x[1]), reverse=False)]
+
+ def get_schedule(self):
+ schedule = {}
+
+ for resource in self.resource_list:
+ scheduled_operations = []
+ for operation in self.operation_list:
+ if operation.resource == resource:
+ scheduled_operations.append({"operation_id": operation.id, "start_time": operation.start_time})
+
+ if scheduled_operations:
+ schedule[resource.id] = scheduled_operations
+
+ return schedule
+
+ def generate_color_list_for_jobs(self, pastel_factor=0.5):
+ for job in self.job_list:
+ max_distance = None
+ best_color = None
+ for i in range(0, 100):
+ color = [
+ (x + pastel_factor) / (1.0 + pastel_factor) for x in [random.uniform(0, 1.0) for i in [1, 2, 3]]
+ ]
+ if color not in self.color_dict.values():
+ best_color = color
+ break
+ else:
+ best_distance = min([self.color_distance(color, c) for c in self.color_dict.values()])
+ if not max_distance or best_distance > max_distance:
+ max_distance = best_distance
+ best_color = color
+ self.color_dict.update({job.job_id: best_color})
+ return self.color_dict
+
+ @staticmethod
+ def color_distance(c1, c2):
+ return sum([abs(x[0] - x[1]) for x in zip(c1, c2)])
diff --git a/lekin/lekin_struct/material.py b/lekin/lekin_struct/material.py
new file mode 100644
index 0000000..5501117
--- /dev/null
+++ b/lekin/lekin_struct/material.py
@@ -0,0 +1,7 @@
+"""
+material struct
+- 物料属于需求的属性,连续排产时根据物料进行判断
+"""
+
+a = 1
+print(f"Schedule Job {a}/{a} backwards, " f"Num of ops: {a}, " "Demand date {a}, " "Priority {a}, " "Material {a}")
diff --git a/lekin/lekin_struct/operation.py b/lekin/lekin_struct/operation.py
new file mode 100644
index 0000000..2979741
--- /dev/null
+++ b/lekin/lekin_struct/operation.py
@@ -0,0 +1,146 @@
+"""
+Operation Struct
+op: 单个工单或新增需求的某一道工序
+GroupOP: 单个需求或多个需求必须在一起的一道工序
+MaterialOP: 同一物料多个需求的一道工序
+"""
+
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+
+class Operation:
+ def __init__(
+ self,
+ operation_id: str,
+ operation_name: str,
+ quantity: int,
+ beat_time: Union[int, List[int], float, List[float]],
+ processing_time: Union[int, List[int], float, List[float]],
+ pre_time: float = 0, # setup times
+ post_time: float = 0,
+ lead_time: float = 0,
+ lag_time: float = 0,
+ route_constraint=None,
+ available_resource=None,
+ available_resource_priority=None,
+ parent_job_id=None,
+ prev_operation_ids=None,
+ next_operation_ids=None,
+ **kwargs,
+ ):
+ self.operation_id = operation_id
+ self.operation_name = operation_name
+ self.quantity = quantity
+ self.beat_time = beat_time
+ self.processing_time = processing_time
+ self.pre_time = pre_time
+ self.post_time = post_time
+ self.lead_time = lead_time
+ self.lag_time = lag_time
+ # self.demand_time = demand_time
+ self.route_constraint = route_constraint
+ self.available_resource = available_resource
+ self.available_resource_priority = available_resource_priority
+ self.parent_job_id = parent_job_id
+ self.prev_operation_ids = prev_operation_ids # predecessors
+ self.next_operation_ids = next_operation_ids # successors
+
+ self.earliest_start_time = None
+ self.latest_start_time = None
+ self.earliest_end_time = None
+ self.latest_end_time = None
+
+ self.statue = "none" # none -> waiting -> pending -> done
+ self.assigned_resource = None # Track the assigned resource
+ self.assigned_time_slot = None # Track the assigned time slot
+
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ # Add a method to calculate granularity metric based on processing time and available time slot
+ def calculate_granularity_metric(self, available_time_slot):
+ # Calculate the granularity metric based on processing time and available time slot
+ pass
+
+ def is_finished(self):
+ return self.assigned_resource is not None
+
+ def __str__(self):
+ return f"{self.operation_id}-{self.operation_name}"
+
+
+class JobOperations(object):
+ """Operations from same job"""
+ def __init__(self, operation_list):
+ pass
+
+ @property
+ def job(self):
+ return
+
+
+class MaterialOperation(object):
+ """
+ 排产时, MaterialOperation, JobOperation, Operation是三层抽象结构
+ resource.assigned_material_op = list[MaterialOperation]
+ """
+ def __init__(self, job_operation_list):
+ pass
+
+ @property
+ def material(self):
+ return
+
+
+class OperationCollector:
+ def __init__(self):
+ self.operation_list = [] # List to store Operation objects
+
+ def add_operation(self, operation):
+ self.operation_list.append(operation)
+
+ def get_operation_by_id(self, operation_id):
+ for operation in self.operation_list:
+ if operation.operation_id == operation_id:
+ return operation
+ return None
+
+ def get_operations_by_job_and_route(self, job_list, route_list):
+ assert len(job_list) == len(route_list)
+
+ for job, route in zip(job_list, route_list):
+ # Get the operations for the current job and route
+ # route = route_list[route_index]
+ job_operations = route.get_operations()
+
+ # Fill in the job_id for each operation
+ for i, operation in enumerate(job_operations):
+ if i > 0:
+ operation.parent_operation_id = job_operations[i - 1].operation_id
+ if i < len(job_operations) - 1:
+ operation.next_operation_id = job_operations[i + 1].operation_id
+
+ operation.job_id = job.job_id
+
+ # Extend the list of all operations with the current job's operations
+ self.operation_list.extend(job_operations)
+
+ # Assign the operations to the current job
+ job.operations = job_operations
+ return self.operation_list
+
+ # def get_operations_by_job_and_route(self, job_list, route_list):
+ # assert len(job_list) == len(route_list)
+ # operation_list = []
+ # for operation in self.operations:
+ # if operation.parent_operation_id is None and operation.route_id == route_id:
+ # # If the operation is the first operation in the route
+ # current_operation = operation
+ # while current_operation:
+ # if current_operation.job_id == job_id:
+ # job_operations.append(current_operation)
+ # next_operation_id = current_operation.next_operation_id
+ # current_operation = next(
+ # (op for op in self.operations if op.operation_id == next_operation_id), None
+ # )
+ # return job_operations
diff --git a/lekin/lekin_struct/relation.py b/lekin/lekin_struct/relation.py
new file mode 100644
index 0000000..113bb77
--- /dev/null
+++ b/lekin/lekin_struct/relation.py
@@ -0,0 +1,18 @@
+"""
+工序之间的关系(工业工程)
+ES: 前工序完成后,后工序开始
+ES-Split: 前工序分割后,后工序应在所有分割工作完成后开始
+SS: 前后工序可以同时开始。即进程的“异步”关系
+SS-Split: 前工序分割后,后工序和最后一个分割工作同时开始
+EE: 后工序制造时间(自定义制造时间短)小于前工序,前后工序一起结束
+EE-SS: 后工序制造时间(自定义制造时间长)大于前工序,后工序开始时间不能超过前工序开始时间,所以前后工序一起开始后,无法保证前后工序一起结束。
+EE-Split: 后工序制造时间和前分割工序时间长度进行比较
+"""
+
+
+class OpRelation(object):
+ def __init__(self) -> None:
+ pass
+
+ def calculate(self, type: str):
+ return
diff --git a/lekin/lekin_struct/resource.py b/lekin/lekin_struct/resource.py
new file mode 100644
index 0000000..0e517d1
--- /dev/null
+++ b/lekin/lekin_struct/resource.py
@@ -0,0 +1,265 @@
+"""
+Resource/Machine Struct
+"""
+
+import math
+from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, Union
+
+import numpy as np
+import pandas as pd
+
+from lekin.lekin_struct.timeslot import TimeSlot
+
+
+class Resource:
+ def __init__(self, resource_id, resource_name=None, max_tasks=1, **kwargs):
+ self.resource_id = resource_id
+ self.resource_name = resource_name
+ self.max_tasks = max_tasks # maximum task can be done in same time, capacity
+ self.tasks = {time_slot: None for time_slot in range(1, max_tasks + 1)}
+ self._available_timeslots = []
+ self._available_hours = []
+
+ self.assigned_operations = []
+ self.assigned_time_slots = []
+ self.assigned_hours = []
+ self.pending_list = [] # 临时缓冲区
+ self.changeover_number = None # number of times
+ self.changeover_time = None # total time costs
+
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def add_timeslot(self, start_time, end_time):
+ self._available_timeslots.append(TimeSlot(start_time, end_time))
+ self._available_hours += TimeSlot(start_time, end_time).hours
+
+ def add_available_hours(self, hours):
+ self._available_hours = hours
+
+ @property
+ def available_hours(self):
+ return self._available_hours
+
+ @available_hours.setter
+ def available_hours(self, available_hours):
+ self._available_hours = available_hours
+
+ def init_job_op_from_op(self):
+ """op-> job_op"""
+ pass
+
+ def init_material_op_from_group_op(self):
+ """group_op-> material_op"""
+ pass
+
+ def update_op_from_material_op(self):
+ """material_op -> op"""
+ pass
+
+ def get_available_timeslot_for_op(self, start=None, end=None, periods=None, freq="1H", forward=True):
+ self.update_continuous_empty_hours()
+ select_hours = [i for i in self.available_hours if i <= end]
+
+ assert len(self.available_hours) == len(self.continuous_empty_hours)
+ front_available = self.continuous_empty_hours[: len(select_hours)]
+
+ chosen_hours_index = self.find_last_index_larger(periods, front_available)
+
+ if not chosen_hours_index:
+ back_available = self.continuous_empty_hours[len(select_hours) :]
+ chosen_hours_index = self.find_first_index_larger(periods, back_available)
+
+ if chosen_hours_index:
+ chosen_hours = self.available_hours[int(chosen_hours_index - periods) : chosen_hours_index]
+ return chosen_hours
+ else:
+ return []
+
+ def get_earliest_available_time(self, duration=None, start=None):
+ if len(self.available_hours) > len(self.assigned_hours):
+ return min(set(self.available_hours).difference(set(self.assigned_hours)))
+ else:
+ return None
+
+ def get_latest_available_time(self, duration=None, end=None):
+ self.update_continuous_empty_hours()
+ return max([i + 1 for (i, v) in enumerate(self.continuous_empty_hours[:end]) if v >= duration])
+
+ def update_continuous_empty_hours(self):
+ if len(self.available_hours) != len(self._available_timeslots):
+ pass
+ if len(self.assigned_hours) != len(self.assigned_time_slots):
+ pass
+
+ # for hours_list in self.available_hours:
+ empty_hours = []
+ continuous_hours = 0
+
+ for hour in self.available_hours:
+ if hour in self.assigned_hours: # Hour is not available
+ continuous_hours = 0
+ else:
+ continuous_hours += 1
+ empty_hours.append(continuous_hours)
+ self.continuous_empty_hours = empty_hours
+
+ def find_first_index_larger(self, input_value, lists):
+ for j, value in enumerate(lists):
+ if value > input_value:
+ return j
+ return None # If no value is larger than the input
+
+ def find_last_index_larger(self, input_value, lists):
+ lists = lists[::-1]
+ for j, value in enumerate(lists):
+ if value > input_value:
+ return len(lists) - j
+ return None
+
+ def get_available_time_slots_within_time(self, start=None, end=None, periods=None, freq="1H", forward=True):
+ available_hours = []
+
+ # check_periods = pd.date_range(start=start, end=end, periods=periods, freq=freq)
+ # if not forward:
+ # check_periods = check_periods[::-1]
+ # occupied_periods = [i.hours for i in self.assigned_time_slot]
+ # for period in check_periods:
+ # if period not in occupied_periods:
+ # available_hours.append(period)
+ # else: # considering the continuous assignment
+ # break
+
+ # all_available_end_time = [slot.end_time for slot in self.available_timeslots]
+ # just_in_time_end_time_slot = max([i for i in all_available_end_time if i <= end])
+ if forward:
+ selected_time_slot = [i for i in self.available_timeslots if i.start_time >= start]
+ else:
+ selected_time_slot = [i for i in self.available_timeslots if i.end_time <= end][::-1]
+
+ # print(just_in_time_end_time_slot)
+ # print([i.start_time for i in self.available_timeslots])
+ # print([i.end_time for i in self.available_timeslots])
+ # print(self.available_timeslots[0].hours)
+
+ left_periods = periods
+
+ for time_slot in selected_time_slot:
+ if left_periods <= 0:
+ break
+
+ if start and time_slot.start_time < start:
+ continue
+
+ if end and time_slot.start_time >= end:
+ break
+
+ # if forward:
+ # current_time = max(start, time_slot.start_time)
+ # else:
+ # current_time = min(end, time_slot.end_time)
+
+ if time_slot in self.assigned_time_slot:
+ break
+
+ if time_slot.duration_of_hours < left_periods:
+ available_hours += time_slot.hours
+ left_periods -= time_slot.duration_of_hours
+
+ else:
+ available_hours += time_slot.hours[-math.ceil(left_periods) :]
+ break
+ #
+ # print(left_periods, available_hours)
+
+ # for current_time in range(max(start, time_slot.start_time),
+ # min(end, time_slot.end_time - period) + 1):
+ # if all(self.is_available(current_time + offset, current_time +offset+1) for offset in range(periods)):
+ # available_hours.append(current_time)
+
+ return available_hours
+
+ def is_available(self, start_time, end_time):
+ for assigned_time_slot in self.assigned_time_slots:
+ if not (end_time <= assigned_time_slot.start_time or start_time >= assigned_time_slot.end_time):
+ return False
+ return True
+
+ def get_unoccupied_time_slots_within_time(self):
+ unoccupied_slots = []
+ prev_end_time = None
+ for time_slot in self.time_slots:
+ if not time_slot.is_occupied():
+ if prev_end_time:
+ # Check if there is a gap between unoccupied time slots
+ if time_slot.start_time > prev_end_time:
+ unoccupied_slots.append(TimeSlot(prev_end_time, time_slot.start_time))
+ unoccupied_slots.append(time_slot)
+ prev_end_time = time_slot.end_time
+ return unoccupied_slots
+
+ def merge_schedules(self):
+ # Sort timeslots based on start time
+ self.timeslots.sort(key=lambda x: x.start_time)
+
+ merged_slots = []
+ current_slot = None
+
+ for slot in self.timeslots:
+ if not current_slot:
+ current_slot = slot
+ else:
+ # If the current slot and the next slot overlap, merge them
+ if current_slot.end_time >= slot.start_time:
+ current_slot.end_time = max(current_slot.end_time, slot.end_time)
+ else:
+ merged_slots.append(current_slot)
+ current_slot = slot
+
+ if current_slot:
+ merged_slots.append(current_slot)
+
+ self.timeslots = merged_slots
+
+ def __hash__(self):
+ return hash(str(self))
+
+ def __str__(self):
+ return f"{self.resource_id}"
+
+ def __eq__(self, other):
+ return self.resource_id == other.resource_id
+
+ def __lt__(self, other):
+ return self.resource_id < other.resource_id
+
+
+class ResourceCollector:
+ def __init__(self):
+ self.resources = {}
+ self.index = -1
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ self.index += 1
+ if self.index < len(self.resources):
+ return list(self.resources.values())[self.index]
+ else:
+ raise StopIteration("Stop")
+
+ def add_resource_dict(self, resource: Resource):
+ self.resources.update({resource.resource_id: resource})
+
+ def get_resource_by_id(self, resource_id):
+ return self.resources.get(resource_id)
+
+ def get_all_resources(self):
+ return list(self.resources.values())
+
+ def get_unoccupied_time_slots(self):
+ unoccupied_slots = []
+ for resource in self.get_all_resources():
+ unoccupied_slots.extend(resource.get_unoccupied_time_slots())
+ return unoccupied_slots
diff --git a/lekin/lekin_struct/route.py b/lekin/lekin_struct/route.py
new file mode 100644
index 0000000..a4ea487
--- /dev/null
+++ b/lekin/lekin_struct/route.py
@@ -0,0 +1,59 @@
+"""
+Route map Struct
+"""
+
+from typing import Any, Callable, Dict, List, Optional, Tuple
+
+from lekin.lekin_struct.operation import Operation
+
+
+class Route:
+ def __init__(self, route_id, operations_sequence=None, available_resources=None, **kwargs):
+ self.route_id = route_id
+ self.operations_sequence = operations_sequence # List of Operation objects
+ self.available_resources = available_resources # List of Resource objects representing available machines,
+ # When assigning operations to resources, check for resource availability and consider resource capacities
+ self.available_time_slots = [] # List of time slots when machines are available
+
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def add_operation(self, operation: Operation):
+ self.operations_sequence.append(operation)
+
+ def get_operations(self) -> List[Operation]:
+ return self.operations_sequence
+
+ def add_resource(self, resource):
+ self.available_resources.append(resource)
+
+ def add_time_slot(self, time_slot):
+ self.available_time_slots.append(time_slot)
+
+ def __repr__(self):
+ return (
+ f"Route(route_id={self.route_id}, operation_ids={self.operations_sequence},"
+ f" resources_available={self.available_resources})"
+ )
+
+
+class RouteCollector:
+ def __init__(self):
+ self.routes = {}
+ self.index = -1
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ self.index += 1
+ if self.index < len(self.routes):
+ return list(self.routes.values())[self.index]
+ else:
+ raise StopIteration("Stop")
+
+ def add_route(self, route):
+ self.routes[route.route_id] = route
+
+ def get_route_by_id(self, route_id):
+ return self.routes.get(route_id, None)
diff --git a/lekin/lekin_struct/timeslot.py b/lekin/lekin_struct/timeslot.py
new file mode 100644
index 0000000..861f309
--- /dev/null
+++ b/lekin/lekin_struct/timeslot.py
@@ -0,0 +1,44 @@
+"""
+Calendar for resource Struct
+"""
+
+from datetime import datetime, timedelta
+from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, Union
+
+import pandas as pd
+
+
+class TimeSlot(object):
+ def __init__(self, start_time, end_time, **kwargs):
+ self.start_time = start_time
+ self.end_time = end_time
+ self.duration = end_time - start_time
+ self.assigned_operation = None
+
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def assign_operation(self, operation, processing_time):
+ self.assigned_operation = operation
+ self.end_time = self.start_time + timedelta(hours=processing_time)
+
+ def is_occupied(self):
+ return self.assigned_operation is not None
+
+ @property
+ def hours(self):
+ return pd.date_range(start=self.start_time, end=self.end_time, freq="1H").tolist()[:-1]
+
+ @property
+ def duration_of_hours(self):
+ return len(pd.date_range(start=self.start_time, end=self.end_time, freq="1H")) - 1
+
+ def overlaps_with(self, timeslot):
+ overlap_start = max(self.start_time, timeslot.start_time)
+ overlap_end = min(self.end_time, timeslot.end_time)
+
+ if overlap_start < overlap_end:
+ overlap_hours = (overlap_end - overlap_start).total_seconds() / 3600
+ return overlap_hours
+ else:
+ return 0
diff --git a/lekin/objective/__init__.py b/lekin/objective/__init__.py
new file mode 100644
index 0000000..428b4b1
--- /dev/null
+++ b/lekin/objective/__init__.py
@@ -0,0 +1,2 @@
+from lekin.objective.makespan import calculate_makespan
+from lekin.objective.tardiness import calculate_tardiness
diff --git a/lekin/objective/makespan.py b/lekin/objective/makespan.py
new file mode 100644
index 0000000..c354ece
--- /dev/null
+++ b/lekin/objective/makespan.py
@@ -0,0 +1,19 @@
+def calculate_makespan(job_collector):
+ for job in job_collector.job_list:
+ op = job.operations
+ job.makespan = op.assigned_hours[-1]
+
+ if job.demand_date is not None:
+ job.tardiness = job.makespan - job.demand_date
+ return
+
+
+def calculate_changeover_time(schedule_result, job_collector):
+ changeover_time = 0
+ for resource in job_collector.resources:
+ previous_end_time = 0
+ for operation in schedule_result:
+ if operation.resource == resource:
+ changeover_time += max(0, operation.start_time - previous_end_time)
+ previous_end_time = operation.end_time
+ return changeover_time
diff --git a/lekin/objective/tardiness.py b/lekin/objective/tardiness.py
new file mode 100644
index 0000000..785ea5e
--- /dev/null
+++ b/lekin/objective/tardiness.py
@@ -0,0 +1,30 @@
+"""Tardiness total/maximum/weighted"""
+
+
+def calculate_tardiness(schedule_result, job):
+ end_time = schedule_result[job.route.operations[-1]][1]
+ return max(0, end_time - job.demand_date)
+
+
+def calculate_total_tardiness(schedule_result, jobs):
+ total_tardiness = 0
+ for job in jobs:
+ total_tardiness += calculate_tardiness(schedule_result, job)
+ return total_tardiness
+
+
+def calculate_total_late_jobs(schedule_result, jobs):
+ total_late_jobs = 0
+ for job in jobs:
+ if calculate_tardiness(schedule_result, job) > 0:
+ total_late_jobs += 1
+ return total_late_jobs
+
+
+def calculate_total_late_time(schedule_result, jobs):
+ total_late_time = 0
+ for job in jobs:
+ tardiness = calculate_tardiness(schedule_result, job)
+ if tardiness > 0:
+ total_late_time += tardiness
+ return total_late_time
diff --git a/lekin/scheduler.py b/lekin/scheduler.py
new file mode 100644
index 0000000..4662266
--- /dev/null
+++ b/lekin/scheduler.py
@@ -0,0 +1,25 @@
+"""Flexible job shop scheduler
+Rescheduler due to inserted order or default machine
+"""
+
+from collections import OrderedDict
+import logging
+from typing import Any, Callable, Dict, List, Optional, Tuple
+
+from lekin.datasets.check_data import check_data
+
+
+class Scheduler(object):
+ def __init__(self, objective, solver, max_operations, **kwargs):
+ self.objective = objective
+ self.solver = solver
+ self.max_operations = max_operations
+
+ def run(self, jobs, machines):
+ self.solver.solve(jobs, machines)
+
+ def evaluate(self):
+ pass
+
+ def plot(self):
+ pass
diff --git a/lekin/solver/__init__.py b/lekin/solver/__init__.py
new file mode 100644
index 0000000..cd5f2f6
--- /dev/null
+++ b/lekin/solver/__init__.py
@@ -0,0 +1,363 @@
+"""
+交付
+连续
+均衡
+"""
+
+from datetime import datetime, timedelta
+import heapq
+from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, Union
+
+import pandas as pd
+
+from lekin.lekin_struct.operation import Operation
+
+
+class CTPSolver(object):
+ def __init__(self, optimizers=None):
+ self.optimizers = optimizers
+
+ def run(self, job_collector, route_list, resource_collector, operation_resource):
+ sorted_index = sort_jobs(job_collector.job_list)
+ job_list = [job_collector.job_list[i] for i in sorted_index]
+
+ for i, job in enumerate(job_list):
+ route_id = job.assigned_route_id
+ route = None
+ for r in route_list:
+ if r.route_id == route_id:
+ route = r
+ break
+ if not route:
+ print(f"Route with ID '{job.assigned_route_id}' not found for Job ID '{job.job_id}'. Skipping job.")
+ continue
+
+ operations_sequence = route.operations_sequence[::-1] # Reverse the operations in the route
+ print([i.operation_id for i in operations_sequence])
+
+ current_end_time = job.demand_date
+
+ for operation in operations_sequence:
+ job_id = job.job_id
+ operation_id = operation.operation_id
+
+ temp = operation_resource.loc[
+ (operation_resource["产品ID"] == job_id) & (operation_resource["工序ID"] == operation_id)
+ ]
+
+ operation.available_resource = [
+ resource_collector.get_resource_by_id(i) for i in temp["资源需求组合ID"].tolist()
+ ]
+ operation.processing_time = temp["任务加工时长"].tolist()
+ operation.quantity = job.quantity
+
+ print(
+ operation.operation_id,
+ operation.available_resource,
+ operation.processing_time,
+ operation.quantity,
+ current_end_time,
+ )
+
+ # 对于不同工序依赖进行更新 & calculate the time constraint
+ # Calculate earliest and latest start times based on demand time and route constraint
+ latest_end_time = current_end_time
+ if operation.next_operation_ids is not None:
+ pass
+ # get_operation_time_constraint()
+
+ resource, start_time, end_time = find_best_resource_and_timeslot_for_operation(
+ operation, latest_end_time=latest_end_time
+ )
+
+ def reschedule_operation(self, operation):
+ # TODO: Implement dynamic rescheduling logic here, considering conflicts with other operations,
+ # resource availability, and any other relevant constraints.
+ pass
+
+
+def sort_jobs(jobs):
+ # Custom sorting function based on priority and continuity
+ def custom_sort(job):
+ priority_weight = job.priority # You may adjust the weight based on your requirements
+ # continuity_weight = -self.calculate_gap_time(job)
+ return priority_weight # + continuity_weight
+
+ # jobs = sorted(jobs, key=custom_sort, reverse=True)
+ return [i[0] for i in sorted(enumerate(jobs), key=lambda x: custom_sort(x[1]), reverse=False)]
+
+
+def get_operation_time_constraint():
+ # calculate the earliest start time and latest end time
+ return
+
+
+def find_best_resource_and_timeslot_for_operation(
+ operation: Operation, earliest_start_time=None, latest_end_time=None, allowed_conflict=False
+):
+ # assign operation
+ resource_timeslots_pq: List = [] # Create a priority queue to store the possible resource-time slot pairs
+ available_resource = operation.available_resource
+ # if operation.required_resource_priority is not None:
+ # required_resource.sort() # Sort by resource priority
+
+ for i, resource in enumerate(available_resource):
+ # start_time = max(operation.prev_operation.end_time, earliest_start_time)
+ # end_time = min(start_time + operation.processing_time, latest_end_time)
+
+ resource_all_available_end_time = [slot.end_time for slot in resource.available_timeslots]
+ just_in_time_end_time_slot = max([i for i in resource_all_available_end_time if i <= latest_end_time])
+ print(latest_end_time, just_in_time_end_time_slot)
+
+ if hasattr(operation.beat_time, "__iter__"):
+ working_hours = (operation.beat_time[i] * operation.quantity) / 60
+
+ unoccupied_hours = resource.get_available_time_slots_within_time()
+ if unoccupied_hours >= working_hours:
+ # assign
+ pass
+ else:
+ pass
+
+ # required processing time and time slot
+
+ just_in_time_slot = find_time_slots(resource, operation, "just_in_time")
+ if just_in_time_slot:
+ evaluation_score = calculate_evaluation_score(operation, just_in_time_slot, resource)
+ heapq.heappush(resource_timeslots_pq, (evaluation_score, resource, just_in_time_slot))
+
+ # If no "just in time" time slot is found, search for available time slots in the past
+ if not resource_timeslots_pq:
+ past_time_slots = find_time_slots(resource, operation, "history")
+ for timeslot in past_time_slots:
+ evaluation_score = calculate_evaluation_score(operation, timeslot, resource)
+ heapq.heappush(resource_timeslots_pq, (evaluation_score, resource, timeslot))
+
+ # If still no suitable time slot is found, look for available time slots in the future
+ # if in future, all the next operation need to be rescheduling forwards
+ if not resource_timeslots_pq:
+ future_time_slots = find_time_slots(resource, operation, "future")
+ for timeslot in future_time_slots:
+ evaluation_score = calculate_evaluation_score(operation, timeslot, resource)
+ heapq.heappush(resource_timeslots_pq, (evaluation_score, resource, timeslot))
+
+ if resource_timeslots_pq:
+ _, chosen_resource, chosen_timeslot = heapq.heappop(resource_timeslots_pq)
+ operation.assigned_resource = chosen_resource
+ operation.assigned_timeslot = chosen_timeslot
+ resource.available_timeslots -= chosen_timeslot
+ return True
+
+
+def find_just_in_time_slots(resource, num_time_slots, duration_hours, job_demand_date):
+ available_slots = resource.get_available_time_slots()
+ just_in_time_slots = []
+
+ for slot in available_slots:
+ slot_end = slot + duration_hours
+ if slot_end <= job_demand_date:
+ just_in_time_slots.append(slot)
+ if len(just_in_time_slots) >= num_time_slots:
+ break
+
+ return just_in_time_slots
+
+
+def find_time_slots(resource, operation, time_slot_type, latest_end_time, working_hours):
+ # Find available time slots in the resource for the operation.
+ # The time_slot_type can be 'just_in_time', 'history', or 'future'.
+ end_time = [slot.end_time for slot in resource.time_slots]
+ time_slots = []
+ if time_slot_type == "just_in_time":
+ # Find a "just in time" time slot where the operation can finish just before the job's demand date.
+
+ resource_all_available_end_time = [slot.end_time for slot in resource.available_timeslots]
+ just_in_time_end_time_slot = max([i for i in resource_all_available_end_time if i <= latest_end_time])
+ unoccupied_hours = resource.get_available_time_slots_within_time(just_in_time_end_time_slot)
+ if unoccupied_hours >= working_hours:
+ duration_hours = 0
+ for slot in unoccupied_hours[::-1]:
+ time_slots.append(slot)
+ duration_hours += len(slot.hours)
+
+ if duration_hours >= working_hours:
+ break
+
+ elif time_slot_type == "history":
+ # Find available time slots in the past that allow the operation to finish before the demand date.
+ past_time_slots = []
+ for slot in resource.timeslots:
+ if slot.end_time <= end_time:
+ past_time_slots.append(slot)
+ return past_time_slots
+ elif time_slot_type == "future":
+ # Find available time slots in the future that allow the operation to finish before the demand date.
+ future_time_slots = []
+ for slot in resource.timeslots:
+ if slot.start_time >= end_time:
+ future_time_slots.append(slot)
+ return future_time_slots
+ else:
+ raise ValueError("Invalid time_slot_type. It should be 'just_in_time', 'history', or 'future'.")
+
+
+def calculate_evaluation_score(self, operation, timeslots, resource):
+ # Implement evaluation function here, consider factors like changeover time, priority, lead time, etc.
+ # The evaluation function should consider conflicts with other operations and
+ # prioritize the best resource and timeslot sequence
+ priority_score = operation.priority
+ changeover_score = self.calculate_total_changeover_score(operation, timeslots, resource)
+
+ return priority_score + changeover_score
+
+
+def calculate_conflicts_hours(resource, operation):
+ for op in resource.assigned_operation:
+ if operation.time_slot.overlaps_with(op.time_slot):
+ # Handle conflict: Apply resolution strategy
+ # adjust_start_times(operations[i], operations[j])
+ pass
+ return
+
+
+def calculate_delay_hours(operation):
+ pass
+
+
+def calculate_total_changeover_score(resource, operation, timeslots):
+ # Implement your total changeover calculation
+ # calculate the changeover time between the current operation and the previous one in the resource's task sequence
+
+ # Example: A simple total changeover calculation based on processing time of the previous operation
+ total_changeover_time = 0
+ if resource.assigned_operation:
+ previous_operation = resource.task_sequence[-1].operation
+ for timeslot in timeslots:
+ total_changeover_time += abs(previous_operation.processing_time - operation.processing_time)
+
+ return total_changeover_time
+
+
+class ConflictResolver:
+ def __init__(self, resources):
+ self.resources = resources
+
+ def merge_and_resolve(self):
+ # Merge backward and forward schedules
+ for resource in self.resources:
+ resource.merge_schedules()
+
+ # Resolve conflicts
+ for resource in self.resources:
+ self.resolve_conflicts(resource)
+
+ def resolve_conflicts(self, resource):
+ # Find overlapping operations on the same resource at the same time
+ overlapping_ops = self.find_overlapping_operations(resource)
+
+ # Resolve conflicts
+ for op1, op2 in overlapping_ops:
+ # Adjust the start or end time of operations to resolve conflicts
+ self.adjust_times_for_conflict(op1, op2)
+
+ def find_overlapping_operations(self, resource):
+ overlapping_ops = []
+ for time_slot in resource.schedules:
+ ops_in_timeslot = resource.schedules[time_slot]
+ if len(ops_in_timeslot) > 1:
+ # Multiple operations in the same time slot indicate a conflict
+ for i in range(len(ops_in_timeslot)):
+ for j in range(i + 1, len(ops_in_timeslot)):
+ overlapping_ops.append((ops_in_timeslot[i], ops_in_timeslot[j]))
+ return overlapping_ops
+
+ def adjust_times_for_conflict(self, op1, op2):
+ # Implement your logic to adjust the start or end time of operations
+ # based on your specific requirements and constraints.
+ # You can consider factors like priority, changeovers, lead times, etc.
+ # For example, you might want to adjust the end time of op1 or the start time of op2.
+ # Check if op2 starts before op1 ends
+ if op2.start_time < op1.end_time:
+ # Calculate the time difference between op2's start time and op1's end time
+ time_difference = op1.end_time - op2.start_time
+
+ # Update op2's start time to be after op1's end time
+ op2.start_time += time_difference
+
+ # Adjust the end time of the job if it exceeds the demand date
+ if op2.end_time > op2.job.demand_date:
+ op2.job.demand_date = op2.end_time
+
+ def resolve_conflicts2(self):
+ # Sort operations by their route requirements and processing time
+ self.operations.sort(key=lambda op: (op.route.priority, op.processing_time))
+
+ for operation in self.operations:
+ # Find a suitable time slot for the operation
+ suitable_time_slot = self.find_suitable_time_slot(operation)
+
+ if suitable_time_slot is not None:
+ # Assign the operation to the resource and time slot
+ operation.resource = suitable_time_slot.resource
+ operation.time_slot = suitable_time_slot
+
+ # Update resource's assigned operations
+ suitable_time_slot.resource.assign_operation(operation)
+
+ def find_suitable_time_slot(self, operation):
+ # Sort available time slots based on priority, earliest start time, and resource availability
+ available_time_slots = sorted(
+ self.resources,
+ key=lambda resource: (
+ resource.priority,
+ resource.get_earliest_start_time(),
+ resource.get_latest_end_time(),
+ ),
+ )
+
+ for time_slot in available_time_slots:
+ # Check if the resource's time slot is compatible with the operation's route
+ if time_slot.resource.is_compatible_route(operation.route):
+ # Check if there are any conflicts with other operations in the time slot
+ if not time_slot.has_conflicts(operation.processing_time):
+ return time_slot
+
+ return None # No suitable time slot found
+
+
+# --
+#
+# for start_index in range(len(resource.timeslots) - operation.required_time_slots + 1):
+# # Check if consecutive time slots can accommodate the entire operation
+# can_accommodate = True
+# for i in range(operation.required_time_slots):
+# timeslot = resource.timeslots[start_index + i]
+# if self.check_timeslot_conflict(resource, timeslot):
+# can_accommodate = False
+# break
+#
+# if can_accommodate:
+# end_index = start_index + operation.required_time_slots - 1
+# evaluation_score = self.calculate_evaluation_score(operation,
+# resource.timeslots[start_index:end_index + 1],
+# resource)
+# heappush(priority_queue, (evaluation_score, resource, resource.timeslots[start_index:end_index + 1]))
+#
+# if end_time >= start_time + processing_time:
+# delay_time = max(0, operation.job.demand_date - end_time)
+# changeover_time = self.calculate_changeover_time(last_operation, resource)
+# priority_score = delay_time + changeover_time + resource.priority
+# heapq.heappush(resource_timeslots, (priority_score, resource, start_time, end_time))
+#
+# while resource_timeslots:
+# _, resource, start_time, end_time = heapq.heappop(resource_timeslots)
+#
+# # Create a temporary timeslot for the operation
+# temp_timeslot = Timeslot(start_time, end_time)
+#
+# # Check if the time slot overlaps with any of the previous task's timeslots
+# if not self.check_timeslot_conflict(resource, temp_timeslot):
+# # Assign the operation to the current resource and timeslot
+# operation.assigned_resource = resource
+# operation.assigned_timeslot = temp_timeslot
+# return
diff --git a/lekin/solver/construction_heuristics/__init__.py b/lekin/solver/construction_heuristics/__init__.py
new file mode 100644
index 0000000..e2497db
--- /dev/null
+++ b/lekin/solver/construction_heuristics/__init__.py
@@ -0,0 +1,9 @@
+"""Dispatching rules"""
+
+from lekin.solver.construction_heuristics.atcs import ATCScheduler
+from lekin.solver.construction_heuristics.backward import BackwardScheduler
+from lekin.solver.construction_heuristics.forward import ForwardScheduler
+from lekin.solver.construction_heuristics.lpst import LPSTScheduler
+from lekin.solver.construction_heuristics.spt import SPTScheduler
+
+__all__ = [ATCScheduler, LPSTScheduler, SPTScheduler, ForwardScheduler, BackwardScheduler]
diff --git a/lekin/solver/construction_heuristics/atcs.py b/lekin/solver/construction_heuristics/atcs.py
new file mode 100644
index 0000000..171208c
--- /dev/null
+++ b/lekin/solver/construction_heuristics/atcs.py
@@ -0,0 +1,38 @@
+"""Apparent Tardiness Cost"""
+
+import logging
+
+from lekin.solver.construction_heuristics.base import BaseScheduler
+
+
+class ATCScheduler(object):
+ def __init__(self, jobs, routes):
+ self.jobs = jobs
+ self.routes = routes
+
+ def calculate_tardiness_cost(self, operation):
+ # Calculate the tardiness cost for an operation based on its finish time and due date
+ if operation.end_time > operation.due_date:
+ return operation.end_time - operation.due_date
+ else:
+ return 0
+
+ def schedule_job(self, job):
+ # Schedule the operations of a job using ATC method
+ for operation in job.route.operations:
+ operation.start_time = max(operation.available_time, operation.earliest_start_time)
+ operation.end_time = operation.start_time + operation.processing_time
+
+ # Sort the operations by their tardiness cost in descending order
+ sorted_operations = sorted(job.route.operations, key=self.calculate_tardiness_cost, reverse=True)
+
+ # Reschedule the operations based on their tardiness cost
+ current_time = 0
+ for operation in sorted_operations:
+ operation.start_time = max(current_time, operation.earliest_start_time)
+ operation.end_time = operation.start_time + operation.processing_time
+ current_time = operation.end_time
+
+ def run(self):
+ for job in self.jobs:
+ self.schedule_job(job)
diff --git a/lekin/solver/construction_heuristics/base.py b/lekin/solver/construction_heuristics/base.py
new file mode 100644
index 0000000..9dca977
--- /dev/null
+++ b/lekin/solver/construction_heuristics/base.py
@@ -0,0 +1,19 @@
+import logging
+
+
+class BaseScheduler(object):
+ def __init__(self, job_collector, resource_collector, **kwargs):
+ self.job_collector = job_collector
+ self.resource_collector = resource_collector
+
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def run(self):
+ raise NotImplementedError
+
+ def scheduling_job(self, job, **kwargs):
+ raise NotImplementedError
+
+ def find_best_resource_and_timeslot_for_operation(self, operation, **kwargs):
+ raise NotImplementedError
diff --git a/lekin/solver/construction_heuristics/cr.py b/lekin/solver/construction_heuristics/cr.py
new file mode 100644
index 0000000..46a735c
--- /dev/null
+++ b/lekin/solver/construction_heuristics/cr.py
@@ -0,0 +1,30 @@
+"""Critical ratio rule"""
+
+import logging
+
+from lekin.solver.construction_heuristics.base import BaseScheduler
+
+
+class CRScheduler(object):
+ def __init__(self, jobs, routes):
+ self.jobs = jobs
+ self.routes = routes
+
+ def calculate_critical_ratio(self, operation):
+ time_remaining = operation.job.due_date - operation.start_time
+ return time_remaining / operation.processing_time
+
+ def schedule_job(self, job):
+ # Schedule the operations of a job using CR method
+ current_time = 0
+ for operation in job.route.operations:
+ operation.start_time = max(current_time, operation.available_time)
+ operation.end_time = operation.start_time + operation.processing_time
+ current_time = operation.end_time
+
+ # Sort the operations based on critical ratio
+ job.route.operations.sort(key=self.calculate_critical_ratio, reverse=True)
+
+ def run(self):
+ for job in self.jobs:
+ self.schedule_job(job)
diff --git a/lekin/solver/construction_heuristics/edd.py b/lekin/solver/construction_heuristics/edd.py
new file mode 100644
index 0000000..07e1cfe
--- /dev/null
+++ b/lekin/solver/construction_heuristics/edd.py
@@ -0,0 +1,30 @@
+"""Earliest Due Date"""
+
+import logging
+
+from lekin.solver.construction_heuristics.base import BaseScheduler
+
+
+class EDDScheduler(object):
+ def __init__(self, jobs, routes):
+ self.jobs = jobs
+ self.routes = routes
+
+ def schedule_job(self, job):
+ # Schedule the operations of a job using EDD method
+ current_time = 0
+ for operation in job.route.operations:
+ operation.start_time = max(current_time, operation.available_time)
+ operation.end_time = operation.start_time + operation.processing_time
+ current_time = operation.end_time
+
+ def run(self):
+ for job in self.jobs:
+ self.schedule_job(job)
+
+
+class MS(object):
+ """Variation of EDD"""
+
+ def __init__(self):
+ pass
diff --git a/lekin/solver/construction_heuristics/epst.py b/lekin/solver/construction_heuristics/epst.py
new file mode 100644
index 0000000..926a109
--- /dev/null
+++ b/lekin/solver/construction_heuristics/epst.py
@@ -0,0 +1,109 @@
+"""Earliest Possible Start Time
+Forward scheduler
+正排"""
+
+import logging
+import math
+
+from lekin.solver.construction_heuristics.base import BaseScheduler
+
+
+class EPSTScheduler(BaseScheduler):
+ def __init__(self, job_collector, resource_collector, route_collector=None, **kwargs):
+ super().__init__(job_collector, resource_collector, **kwargs)
+ self.job_collector = job_collector
+ self.resource_collector = resource_collector
+ self.route_collector = route_collector
+
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def run(self):
+ for i, job in enumerate(self.job_collector.job_list):
+ self.scheduling_job(job, self.resource_collector, self.route_collector)
+ logging.info("First Scheduling Done")
+
+ for i, job in enumerate(self.job_collector.job_list):
+ self.rescheduling_job_to_resolve_conflict(job)
+ logging.info("ReScheduling Done")
+ return
+
+ def scheduling_job(self, job, resource_collector, route_collector):
+ logging.info(f"\nAssign Job {job.job_id}")
+
+ if route_collector is not None:
+ route_id = job.assigned_route_id
+ route = None
+ for r in route_collector:
+ if r.route_id == route_id:
+ route = r
+ break
+ if not route:
+ print(f"Route with ID '{job.assigned_route_id}' not found for Job ID '{job.job_id}'. Skipping job.")
+
+ job.operations = route.operations_sequence
+
+ op_earliest_start = 0
+ for operation in job.operations:
+ logging.info(f"\tAssign Operation {operation.operation_id} of Job {job.job_id}")
+ chosen_resource, chosen_timeslot_hour = self.find_best_resource_and_timeslot_for_operation(
+ operation, op_earliest_start
+ )
+
+ if chosen_resource and chosen_timeslot_hour:
+ logging.info(
+ f"\tOperation {operation.operation_id} assigned in: resource"
+ f" {chosen_resource.resource_id}, {min(chosen_timeslot_hour)} -"
+ f" {max(chosen_timeslot_hour)}"
+ )
+
+ # assign
+ operation.assigned_resource = chosen_resource
+ operation.assigned_hours = chosen_timeslot_hour
+ chosen_resource.assigned_operations.append(operation)
+ chosen_resource.assigned_hours += chosen_timeslot_hour
+
+ op_earliest_start = chosen_timeslot_hour[-1] + 1
+ return
+
+ def find_best_resource_and_timeslot_for_operation(self, operation, op_earliest_start, **kwargs):
+ available_resource = operation.available_resource
+
+ earliest_index = 0
+ resource_earliest_time = float("inf")
+ for i, resource in enumerate(available_resource):
+ resource_time = resource.get_earliest_available_time(duration=operation.processing_time)
+
+ if resource_time < resource_earliest_time:
+ earliest_index = i
+ resource_earliest_time = resource_time
+
+ chosen_resource = available_resource[earliest_index]
+ earliest_time = int(max(op_earliest_start, resource_earliest_time))
+ chosen_hours = list(range(earliest_time, earliest_time + math.ceil(operation.processing_time)))
+ return chosen_resource, chosen_hours
+
+ def rescheduling_job_to_resolve_conflict(self, job):
+ op_earliest_start = 0
+ for operation in job.operations:
+ logging.info(f"Rescheduling {job.job_id}/ {operation.operation_id}")
+ assigned_resource = operation.assigned_resource
+ if operation.assigned_hours[0] < op_earliest_start:
+ delta = op_earliest_start - operation.assigned_hours[0]
+ operation.assigned_hours = [i + delta for i in operation.assigned_hours]
+
+ pivot_assigned_hours = operation.assigned_hours
+ op_earliest_start = pivot_assigned_hours[-1] + 1
+
+ ops_in_same_resource = assigned_resource.assigned_operations
+ ops_in_same_resource.sort(key=lambda x: x.assigned_hours[0], reverse=False)
+ logging.info([i.parent_job_id for i in ops_in_same_resource])
+ for op in ops_in_same_resource:
+ if op != operation:
+ op_start = op.assigned_hours[0]
+ if set(pivot_assigned_hours).intersection(set(op.assigned_hours)):
+ logging.info(
+ f"\tRescheduling {job.job_id}/ {op.operation_id} in {assigned_resource.resource_id}"
+ )
+ op.assigned_hours = [i + pivot_assigned_hours[-1] - op_start + 1 for i in op.assigned_hours]
+ return
diff --git a/lekin/solver/construction_heuristics/fifo.py b/lekin/solver/construction_heuristics/fifo.py
new file mode 100644
index 0000000..232f840
--- /dev/null
+++ b/lekin/solver/construction_heuristics/fifo.py
@@ -0,0 +1,31 @@
+"""First In First Out"""
+
+import logging
+
+from lekin.solver.construction_heuristics.base import BaseScheduler
+
+
+class FCFSScheduler:
+ """
+ - 初始化,记录各个机器前的任务等待序列。模拟时间进度
+ """
+
+ def __init__(self, jobs, routes):
+ self.jobs = jobs
+ self.routes = routes
+
+ def init(self):
+ """ """
+ pass
+
+ def schedule_job(self, job):
+ # Schedule the operations of a job in FCFS order
+ current_time = 0
+ for operation in job.route.operations:
+ operation.start_time = max(current_time, operation.available_time)
+ operation.end_time = operation.start_time + operation.processing_time
+ current_time = operation.end_time
+
+ def run(self):
+ for job in self.jobs:
+ self.schedule_job(job)
diff --git a/lekin/solver/construction_heuristics/lpst.py b/lekin/solver/construction_heuristics/lpst.py
new file mode 100644
index 0000000..c43dd97
--- /dev/null
+++ b/lekin/solver/construction_heuristics/lpst.py
@@ -0,0 +1,146 @@
+"""Latest Possible Start Time
+Backward scheduler
+倒排"""
+
+import logging
+import math
+
+from lekin.lekin_struct.job import Job, JobCollector
+from lekin.lekin_struct.operation import Operation
+from lekin.lekin_struct.resource import ResourceCollector
+from lekin.lekin_struct.route import RouteCollector
+from lekin.lekin_struct.timeslot import TimeSlot
+from lekin.solver.construction_heuristics.base import BaseScheduler
+
+
+class LPSTScheduler(object):
+ def __init__(
+ self,
+ job_collector: JobCollector,
+ resource_collector: ResourceCollector,
+ route_collector: RouteCollector = None,
+ **kwargs,
+ ) -> None:
+ self.job_collector = job_collector
+ self.resource_collector = resource_collector
+ self.route_collector = route_collector
+
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def run(self) -> None:
+ for i, job in enumerate(self.job_collector.job_list):
+ self.scheduling_job(job, self.resource_collector, self.route_collector)
+ logging.info("First Scheduling Done")
+ return
+
+ def scheduling_job(self, job: Job, resource_collector, route_collector: RouteCollector) -> None:
+ logging.info(f"\nAssign Job {job.job_id}")
+
+ if route_collector is not None:
+ route_id = job.assigned_route_id
+ route = None
+ for r in route_collector:
+ if r.route_id == route_id:
+ route = r
+ break
+ if not route:
+ print(f"Route with ID '{job.assigned_route_id}' not found for Job ID '{job.job_id}'. Skipping job.")
+
+ job.operations = route.operations_sequence
+
+ op_earliest_start = 0 # forward constraint
+ op_latest_end = 150 # backward constraint
+ for operation in job.operations[::-1]: # inverse
+ logging.info(f"\tAssign Operation {operation.operation_id} of Job {job.job_id}")
+ chosen_resource, chosen_timeslot_hour = self.find_best_resource_and_timeslot_for_operation(
+ operation, op_latest_end, op_earliest_start
+ )
+
+ if chosen_resource and chosen_timeslot_hour:
+ logging.info(
+ f"\tOperation {operation.operation_id} assigned in: resource"
+ f" {chosen_resource.resource_id}, {min(chosen_timeslot_hour)} -"
+ f" {max(chosen_timeslot_hour)}"
+ )
+
+ # assign
+ operation.assigned_resource = chosen_resource
+ operation.assigned_hours = chosen_timeslot_hour
+ chosen_resource.assigned_operations.append(operation)
+ chosen_resource.assigned_hours += chosen_timeslot_hour
+
+ # op_earliest_start = chosen_timeslot_hour[-1] + 1
+ op_latest_end = chosen_timeslot_hour[0] - 1
+ return
+
+ def find_best_resource_and_timeslot_for_operation(
+ self, operation: Operation, op_latest_end=None, op_earliest_start=None, **kwargs
+ ):
+ available_resource = operation.available_resource
+
+ latest_index = float("inf")
+ resource_latest_time = 0
+ for i, resource in enumerate(available_resource):
+ resource_time = resource.get_latest_available_time(duration=operation.processing_time, end=op_latest_end)
+
+ if resource_time > resource_latest_time:
+ latest_index = i
+ resource_latest_time = resource_time
+
+ chosen_resource = available_resource[latest_index]
+ latest_time = int(min(op_latest_end, resource_latest_time))
+ chosen_hours = list(range(latest_time - math.ceil(operation.processing_time), latest_time + 0))
+ return chosen_resource, chosen_hours
+
+ def assign_operation(self, operation: Operation, start_time, end_time, resources):
+ timeslot = TimeSlot(start_time, end_time)
+ self.timeslots.append(timeslot)
+ for resource in resources:
+ # Add timeslot to resource's schedule
+ resource.schedule.append(timeslot)
+ # Link operation to scheduled timeslot
+ operation.scheduled_timeslot = timeslot
+
+ def select_resources(self, job: Job, operation: Operation):
+ available_slots = self.find_available_timeslots(job, operation)
+
+ selected_resources = []
+ for slot in available_slots:
+ resources = slot.available_resources()
+ resource = self.optimize_resource_selection(resources, operation)
+ selected_resources.append((slot, resource))
+ return selected_resources
+
+ def find_available_timeslots(self, job, operation):
+ # Search timeslots and filter based on:
+ # - operation duration
+ # - predecessor timeslots
+ # - resource requirements
+
+ slots = []
+ # for ts in job.schedule.timeslots:
+ # if ts.end - ts.start >= operation.duration:
+ # if all(pred in job.predecessors(ts)):
+ # if ts.meets_resource_needs(operation):
+ # slots.append(ts)
+ return slots
+
+ def optimize_resource_selection(self, resources, operation):
+ # Score and prioritize resources based on:
+ # - Capacity
+ # - Changeover time
+ # - Utilization
+
+ scored = []
+ for resource in resources:
+ score = 0
+ if resource.capacity >= operation.required_capacity:
+ score += 1
+ if resource.type in operation.preferred_resources:
+ score += 1
+ # Prioritize resources with less adjacent timeslots
+ score -= len(resource.adjacent_timeslots(operation))
+ scored.append((score, resource))
+ best = max(scored, key=lambda x: x[0])
+ return best[1]
diff --git a/lekin/solver/construction_heuristics/lsf.py b/lekin/solver/construction_heuristics/lsf.py
new file mode 100644
index 0000000..ce25ed0
--- /dev/null
+++ b/lekin/solver/construction_heuristics/lsf.py
@@ -0,0 +1,46 @@
+"""L"""
+
+import logging
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+from lekin.solver.construction_heuristics.base import BaseScheduler
+
+
+class LSTScheduler(object):
+ def __init__(self, jobs, routes):
+ self.jobs = jobs
+ self.routes = routes
+
+ def calculate_slack_time(self, operation, current_time):
+ # Calculate the slack time for an operation based on its due date and current time
+ return max(0, operation.due_date - current_time)
+
+ def select_next_operation(self, available_operations, current_time):
+ # Select the operation with the longest slack time from the available operations
+ selected_operation = None
+ max_slack_time = float("-inf")
+
+ for operation in available_operations:
+ slack_time = self.calculate_slack_time(operation, current_time)
+ if slack_time > max_slack_time:
+ max_slack_time = slack_time
+ selected_operation = operation
+
+ return selected_operation
+
+ def schedule_job(self, job, start_time):
+ # Schedule the operations of a job based on LST
+ current_time = start_time
+
+ for operation in job.route.operations:
+ slack_time = self.calculate_slack_time(operation, current_time)
+ print(slack_time)
+ operation.start_time = current_time
+ operation.end_time = current_time + operation.processing_time
+ current_time += operation.processing_time
+
+ job.completion_time = current_time
+
+ def run(self):
+ for job in self.jobs:
+ self.schedule_job(job, 0)
diff --git a/lekin/solver/construction_heuristics/mix.py b/lekin/solver/construction_heuristics/mix.py
new file mode 100644
index 0000000..a74a1e7
--- /dev/null
+++ b/lekin/solver/construction_heuristics/mix.py
@@ -0,0 +1,72 @@
+import heapq
+
+
+class JobScheduler:
+ def __init__(self, available_slots, jobs, routes, operations, resources):
+ self.available_slots = available_slots
+ heapq.heapify(self.available_slots) # Convert the list into a min heap
+ self.jobs = jobs
+ self.routes = routes
+ self.operations = operations
+ self.resources = resources
+
+ def backward_schedule(self):
+ # Implement your initial backward scheduling pass here
+ pass
+
+ def assign_resource(self, operation, available_resources):
+ # Implement resource assignment logic based on availability and scoring
+ pass
+
+ def analyze_schedule_density(self):
+ # Implement schedule density analysis
+ pass
+
+ def identify_bottleneck_resources(self):
+ # Identify bottleneck resources limiting density
+ pass
+
+ def push_operations_closer(self, bottleneck_resources):
+ # Push operations closer on bottleneck resources
+ pass
+
+ def rescore_operations(self):
+ # Rescore operations based on priority and slack time
+ pass
+
+ def reassign_operations(self):
+ # Reassign operations to reduce gaps
+ pass
+
+ def reevaluate_routes(self, critical_jobs):
+ # Re-evaluate routes for critical jobs
+ pass
+
+ def reschedule_operations(self, critical_jobs):
+ # Reschedule operations on preferred resources for critical jobs
+ pass
+
+ def push_dense(self):
+ # Iteratively push operations closer system-wide
+ # while True:
+ # self.backward_schedule()
+ # density = self.analyze_schedule_density()
+ # if density is not improved:
+ # break
+ pass
+
+ def final_tweaking(self):
+ # Fine-tune schedules of critical jobs and leverage flexibilities
+ pass
+
+ def optimize_schedule(self):
+ self.push_dense()
+ critical_jobs = self.identify_critical_jobs()
+ self.reevaluate_routes(critical_jobs)
+ self.reschedule_operations(critical_jobs)
+ self.final_tweaking()
+ return self.schedule
+
+ def identify_critical_jobs(self):
+ # Identify critical jobs on the schedule
+ pass
diff --git a/lekin/solver/construction_heuristics/spt.py b/lekin/solver/construction_heuristics/spt.py
new file mode 100644
index 0000000..bd47e82
--- /dev/null
+++ b/lekin/solver/construction_heuristics/spt.py
@@ -0,0 +1,102 @@
+"""Shortest Processing Time
+"""
+
+from collections import OrderedDict
+import logging
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
+
+from lekin.solver.construction_heuristics.base import BaseScheduler
+
+
+class SPTScheduler(object):
+ def __init__(self):
+ self.time = {} # global时间队列
+ self.waiting_operations = {} # 记录每个机器的任务等待队列
+ self.jobs_list_to_export = []
+
+ def setup(self, job_list: List, machine_list: List):
+ for machine in machine_list:
+ # init for machine start time
+ self.current_time_on_machines[machine.name] = 0
+
+ # init for waiting list of machines
+ self.waiting_operations[machine.name] = 0
+ for job in job_list:
+ if job.operation_id == 1 and machine.name in job.machine:
+ if len(job.machine) == 1:
+ self.waiting_operations[machine.name].append(job)
+
+ self.waiting_operations[machine.name].sort(key=lambda j: j.duration)
+ return
+
+ def solve(self, job_list: List, machine_list: List):
+ self.setup(job_list, machine_list)
+
+ self.time[0] = self.waiting_operations
+
+ for key_mach, operations in self.waiting_operations.items():
+ # for each waiting task in front of machine, set time to 0
+ if len(operations):
+ operations[0].start_time = 0
+ operations[0].completed = True
+ operations[0].assigned_machine = key_mach
+
+ self.jobs_list_to_export.append(operations[0])
+ self.current_time_on_machines[key_mach] = operations[0].get_end_time()
+ self.time[self.current_time_on_machines[key_mach]] = {}
+
+ while len(self.jobs_list_to_export) != len(job_list):
+ for t, operations in self.time.items():
+ operations = self.get_waiting_operations(
+ job_list, float(t), machine_list, self.current_time_on_machines
+ )
+
+ for key_mach, tasks in operations.items():
+ if len(tasks):
+ if float(t) < self.current_time_on_machines[key_mach]:
+ continue
+
+ tasks[0].start_time = float(t)
+ tasks[0].completed = True
+ tasks[0].assigned_machine = key_mach
+
+ self.jobs_list_to_export.append(tasks[0])
+ self.current_time_on_machines[key_mach] = tasks[0].get_end_time()
+ self.time[self.current_time_on_machines[key_mach]] = {}
+
+ del self.time[t]
+ break
+ self.time = OrderedDict(self.time)
+ return self.jobs_list_to_export
+
+ def get_waiting_operations(self, job_list, time, machine_list, current_time_on_machines):
+ incoming_operations = {}
+
+ for mach in machine_list:
+ assigned_jobs_for_machine = []
+ for job in job_list:
+ if job.completed is False and mach.name in job.machine:
+ if len(job.machine) == 1:
+ assigned_jobs_for_machine.append(job)
+
+ incoming_operations[mach.name] = []
+ for j in assigned_jobs_for_machine:
+ if j.id_operation == 1:
+ incoming_operations[mach.name].append(j)
+ else:
+ previous_task = [
+ job
+ for job in job_list
+ if job.route_id == j.route_id
+ and job.id_operation == (j.id_operation - 1)
+ and job.end_time <= time
+ ]
+ if len(previous_task):
+ if previous_task[0].completed:
+ incoming_operations[mach.name].append(j)
+
+ incoming_operations[mach.name].sort(key=lambda j: j.duration)
+ return incoming_operations
+
+ def run(self):
+ return
diff --git a/lekin/solver/meta_heuristics/__init__.py b/lekin/solver/meta_heuristics/__init__.py
new file mode 100644
index 0000000..e8ba164
--- /dev/null
+++ b/lekin/solver/meta_heuristics/__init__.py
@@ -0,0 +1,6 @@
+"""Heuristics"""
+
+
+class Heuristics(object):
+ def __init__(self, name):
+ pass
diff --git a/lekin/solver/meta_heuristics/branch_and_bound.py b/lekin/solver/meta_heuristics/branch_and_bound.py
new file mode 100644
index 0000000..0734e06
--- /dev/null
+++ b/lekin/solver/meta_heuristics/branch_and_bound.py
@@ -0,0 +1,57 @@
+from datetime import datetime, timedelta
+from itertools import permutations
+
+from lekin.lekin_struct import TimeSlot
+
+
+class BranchAndBoundScheduler:
+ def __init__(self, job_list, resource_list):
+ self.job_list = job_list
+ self.resource_list = resource_list
+ self.best_schedule = None
+ self.best_cost = float("inf")
+
+ def find_available_time_slots(self, operation, resource, current_time):
+ # Implement the logic to find available time slots for a given operation and resource
+ available_time_slots = []
+ for slot in resource.available_time_slots:
+ if slot.start_time >= current_time + operation.processing_time:
+ available_time_slots.append(slot)
+ return available_time_slots
+
+ def assign_operation(self, job, operation, resource, start_time):
+ # Implement the logic to assign an operation to a resource at a specific start time
+ end_time = start_time + operation.processing_time
+ resource.available_time_slots.append(TimeSlot(end_time, float("inf")))
+ return end_time
+
+ def calculate_cost(self, schedule):
+ # Implement the logic to calculate the cost of the current schedule
+ # For example, you can calculate makespan or any other relevant metric
+ makespan = max(slot.end_time for slot in schedule)
+ return makespan
+
+ def branch_and_bound(self, current_job_idx, current_time, schedule):
+ # Implement the Branch and Bound algorithm for scheduling
+ if current_job_idx == len(self.job_list):
+ cost = self.calculate_cost(schedule)
+ if cost < self.best_cost:
+ self.best_cost = cost
+ self.best_schedule = schedule.copy()
+ return
+
+ job = self.job_list[current_job_idx]
+ route = self.resource_list[job.assigned_route_id]
+
+ for resource_id in route.operations[0].resource_ids:
+ resource = self.resource_list[resource_id]
+ time_slots = self.find_available_time_slots(route.operations[0], resource, current_time)
+ for slot in time_slots:
+ new_schedule = schedule + [slot]
+ new_time = self.assign_operation(job, route.operations[0], resource, slot.start_time)
+ self.branch_and_bound(current_job_idx + 1, new_time, new_schedule)
+
+ def get_schedule(self):
+ # Implement the main function to get the final schedule using Branch and Bound
+ self.branch_and_bound(0, datetime(2023, 1, 1), [])
+ return self.best_schedule
diff --git a/lekin/solver/meta_heuristics/critical_path.py b/lekin/solver/meta_heuristics/critical_path.py
new file mode 100644
index 0000000..4f88e1a
--- /dev/null
+++ b/lekin/solver/meta_heuristics/critical_path.py
@@ -0,0 +1,70 @@
+"""Critical path"""
+
+import copy
+from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, Union
+
+
+class CriticalPathScheduler:
+ def __init__(self, job_collector):
+ self.job_collector = job_collector
+
+ def schedule(self):
+ # Generate the forward and backward pass times
+ forward_pass_times = self.forward_pass()
+ backward_pass_times = self.backward_pass()
+
+ # Find the critical path
+ critical_path = self.find_critical_path(forward_pass_times, backward_pass_times)
+
+ # Assign start times for each operation in the critical path
+ critical_path_schedule = {}
+ current_time = 0
+ for job_id, operation_id in critical_path:
+ operation = self.job_collector.get_operation_by_id(job_id, operation_id)
+ critical_path_schedule[operation] = current_time
+ current_time += operation.processing_time
+
+ return critical_path_schedule
+
+ def forward_pass(self):
+ forward_pass_times: Dict[str, Dict[str, float]] = {}
+ for job in self.job_collector.jobs:
+ forward_pass_times[job.id] = {}
+ for operation in job.route.operations:
+ if operation.id == 0:
+ forward_pass_times[job.id][operation.id] = 0
+ else:
+ predecessors = operation.get_predecessors()
+ max_predecessor_time = max(
+ [forward_pass_times[job.id][pred.id] + pred.processing_time for pred in predecessors]
+ )
+ forward_pass_times[job.id][operation.id] = max_predecessor_time
+
+ return forward_pass_times
+
+ def backward_pass(self):
+ backward_pass_times: Dict[str, Dict[str, float]] = {}
+ for job in self.job_collector.jobs:
+ backward_pass_times[job.id] = {}
+ for operation in reversed(job.route.operations):
+ if operation.id == len(job.route.operations) - 1:
+ backward_pass_times[job.id][operation.id] = operation.processing_time
+ else:
+ successors = operation.get_successors()
+ min_successor_time = min(
+ [backward_pass_times[job.id][succ.id] + succ.processing_time for succ in successors]
+ )
+ backward_pass_times[job.id][operation.id] = min_successor_time
+
+ return backward_pass_times
+
+ def find_critical_path(self, forward_pass_times, backward_pass_times):
+ critical_path = []
+ for job in self.job_collector.jobs:
+ for operation in job.route.operations:
+ start_time = forward_pass_times[job.id][operation.id]
+ end_time = backward_pass_times[job.id][operation.id]
+ if start_time + operation.processing_time == end_time:
+ critical_path.append((job.id, operation.id))
+
+ return critical_path
diff --git a/lekin/solver/meta_heuristics/genetic.py b/lekin/solver/meta_heuristics/genetic.py
new file mode 100644
index 0000000..0dba9ba
--- /dev/null
+++ b/lekin/solver/meta_heuristics/genetic.py
@@ -0,0 +1,98 @@
+"""Genetic scheduler"""
+
+import copy
+import random
+
+from lekin.lekin_struct import JobCollector, ResourceCollector, RouteCollector
+
+
+class GeneticScheduler:
+ def __init__(
+ self,
+ job_collector: JobCollector,
+ resource_collector: ResourceCollector,
+ route_collector: RouteCollector = None,
+ initial_schedule=None,
+ population_size=50,
+ generations=1000,
+ crossover_rate=0.8,
+ mutation_rate=0.2,
+ **kwargs,
+ ):
+ self.job_collector = job_collector
+ self.initial_schedule = initial_schedule
+ self.population_size = population_size
+ self.generations = generations
+ self.crossover_rate = crossover_rate
+ self.mutation_rate = mutation_rate
+
+ def run(self):
+ population = self.initialize_population()
+
+ for generation in range(self.generations):
+ selected_individuals = self.selection(population)
+ new_population = []
+
+ while len(new_population) < self.population_size:
+ parent1 = random.choice(selected_individuals)
+ parent2 = random.choice(selected_individuals)
+
+ if random.random() < self.crossover_rate:
+ offspring1, offspring2 = self.crossover(parent1, parent2)
+ else:
+ offspring1, offspring2 = parent1, parent2
+
+ if random.random() < self.mutation_rate:
+ offspring1 = self.mutation(offspring1)
+ if random.random() < self.mutation_rate:
+ offspring2 = self.mutation(offspring2)
+
+ new_population.append(offspring1)
+ new_population.append(offspring2)
+
+ population = new_population
+
+ # Find the best solution in the final population
+ best_solution = min(population, key=lambda chromosome: self.fitness(chromosome)[0])
+
+ # Return the best schedule
+ return self.job_collector.create_schedule_from_operations(best_solution)
+
+ def initialize_population(self):
+ population = []
+ for _ in range(self.population_size):
+ # Shuffle the operations for each job to create a random chromosome
+ chromosome = copy.deepcopy(self.job_collector.get_operations())
+ for job_operations in chromosome.values():
+ random.shuffle(job_operations)
+ population.append(chromosome)
+ return population
+
+ def fitness(self, chromosome):
+ # Calculate the fitness of a chromosome based on the scheduling criteria (e.g., makespan, tardiness)
+ # The lower the fitness value, the better the solution
+ # Return a tuple with the fitness value and the schedule
+ schedule = self.job_collector.create_schedule_from_operations(chromosome)
+ fitness_value = 0
+ return fitness_value, schedule
+
+ def selection(self, population):
+ # Select the best individuals based on their fitness values
+ # You can use tournament selection, rank-based selection, or other methods
+ # Return the selected individuals
+ selected_individuals = 0
+ return selected_individuals
+
+ def crossover(self, parent1, parent2):
+ # Perform crossover (recombination) between two parents to create two offspring
+ # You can use one-point crossover, two-point crossover, or other methods
+ # Return the two offspring
+ offspring1, offspring2 = 0, 0
+ return offspring1, offspring2
+
+ def mutation(self, chromosome):
+ # Introduce random changes in the chromosome to add diversity
+ # You can swap two operations for a randomly selected job
+ # Return the mutated chromosome
+ mutated_chromosome = 0
+ return mutated_chromosome
diff --git a/lekin/solver/meta_heuristics/hill_climbing.py b/lekin/solver/meta_heuristics/hill_climbing.py
new file mode 100644
index 0000000..dac23ae
--- /dev/null
+++ b/lekin/solver/meta_heuristics/hill_climbing.py
@@ -0,0 +1,60 @@
+"""Hill climbing"""
+
+import random
+
+
+class HillClimbingScheduler:
+ def __init__(self, job_collector):
+ self.job_collector = job_collector
+
+ def schedule(self, max_iterations=1000):
+ current_solution = self.random_solution()
+ current_score = self.evaluate_solution(current_solution)
+
+ for _ in range(max_iterations):
+ neighbors = self.get_neighbors(current_solution)
+ if not neighbors:
+ break
+
+ next_solution = max(neighbors, key=lambda neighbor: self.evaluate_solution(neighbor))
+ next_score = self.evaluate_solution(next_solution)
+
+ if next_score <= current_score:
+ current_solution = next_solution
+ current_score = next_score
+ else:
+ break
+
+ return current_solution
+
+ def random_solution(self):
+ return {
+ operation: random.randint(0, operation.get_latest_start_time())
+ for job in self.job_collector.jobs
+ for operation in job.route.operations
+ }
+
+ def get_neighbors(self, solution):
+ neighbors = []
+ for operation, start_time in solution.items():
+ for t in range(start_time - 1, operation.get_latest_start_time() + 1):
+ neighbor = solution.copy()
+ neighbor[operation] = t
+ neighbors.append(neighbor)
+ return neighbors
+
+ def evaluate_solution(self, solution):
+ end_times = {}
+ for job in self.job_collector.jobs:
+ for operation in job.route.operations:
+ if operation in solution:
+ start_time = solution[operation]
+ else:
+ start_time = operation.get_latest_start_time()
+
+ end_time = start_time + operation.processing_time
+ if operation.id not in end_times or end_time > end_times[operation.id]:
+ end_times[operation.id] = end_time
+
+ makespan = max(end_times.values())
+ return makespan
diff --git a/lekin/solver/meta_heuristics/nsga3.py b/lekin/solver/meta_heuristics/nsga3.py
new file mode 100644
index 0000000..e25a2ee
--- /dev/null
+++ b/lekin/solver/meta_heuristics/nsga3.py
@@ -0,0 +1,87 @@
+import random
+from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, Union
+
+
+class NSGA3Scheduler:
+ def __init__(self, jobs, routes, num_generations, population_size):
+ self.jobs = jobs
+ self.routes = routes
+ self.num_generations = num_generations
+ self.population_size = population_size
+ self.population = []
+ self.fronts = []
+
+ def initialize_population(self):
+ # Generate an initial random population
+ for _ in range(self.population_size):
+ solution = self.generate_random_solution()
+ self.population.append(solution)
+
+ def generate_random_solution(self):
+ # Generate a random solution representing start times for operations
+ solution: Dict = {}
+ for job in self.jobs:
+ solution[job] = {}
+ for operation in job.route.operations:
+ solution[job][operation] = random.randint(0, 100) # Random start time
+ return solution
+
+ def evaluate_objectives(self, solution):
+ # Evaluate the objectives for a given solution
+ makespan = self.calculate_makespan(solution)
+ total_tardiness = self.calculate_total_tardiness(solution)
+ resource_utilization = self.calculate_resource_utilization(solution)
+ return makespan, total_tardiness, resource_utilization
+
+ def calculate_makespan(self, solution):
+ # Calculate the makespan for a given solution
+ # Implementation specific to your job shop scheduling problem
+ pass
+
+ def calculate_total_tardiness(self, solution):
+ # Calculate the total tardiness for a given solution
+ # Implementation specific to your job shop scheduling problem
+ pass
+
+ def calculate_resource_utilization(self, solution):
+ # Calculate the resource utilization for a given solution
+ # Implementation specific to your job shop scheduling problem
+ pass
+
+ def run(self):
+ self.initialize_population()
+ for generation in range(self.num_generations):
+ self.fast_nondominated_sort()
+ self.crowding_distance()
+ self.selection()
+ self.crossover()
+ self.mutation()
+
+ def fast_nondominated_sort(self):
+ # Implement NSGA-III's fast non-dominated sorting
+ # Categorize solutions into different fronts based on their dominance relationships
+ pass
+
+ def crowding_distance(self):
+ # Implement NSGA-III's crowding distance calculation
+ # Calculate the crowding distance for each solution in each front
+ pass
+
+ def selection(self):
+ # Implement NSGA-III's selection mechanism
+ # Select the best solutions based on their non-dominated ranks and crowding distances
+ pass
+
+ def crossover(self):
+ # Implement NSGA-III's crossover operator
+ # Perform crossover to create offspring solutions
+ pass
+
+ def mutation(self):
+ # Implement NSGA-III's mutation operator
+ # Perform mutation to introduce diversity in the population
+ pass
+
+ def get_pareto_front(self):
+ # Get the Pareto front solutions after the algorithm has run
+ return self.fronts[0]
diff --git a/lekin/solver/meta_heuristics/pso.py b/lekin/solver/meta_heuristics/pso.py
new file mode 100644
index 0000000..e0ce0ce
--- /dev/null
+++ b/lekin/solver/meta_heuristics/pso.py
@@ -0,0 +1 @@
+"""particle swarm optimization"""
diff --git a/lekin/solver/meta_heuristics/shifting_bottle_neck.py b/lekin/solver/meta_heuristics/shifting_bottle_neck.py
new file mode 100644
index 0000000..8dd031d
--- /dev/null
+++ b/lekin/solver/meta_heuristics/shifting_bottle_neck.py
@@ -0,0 +1,63 @@
+"""Shifting bottle neck meta_heuristics"""
+
+import copy
+
+
+class ShiftingBottleneckScheduler:
+ def __init__(self, job_collector):
+ self.job_collector = job_collector
+
+ def schedule(self):
+ best_solution = self.generate_initial_solution()
+ best_cost = self.calculate_cost(best_solution)
+
+ for _ in range(1000): # Number of iterations
+ current_solution = copy.deepcopy(best_solution)
+ bottleneck_job, bottleneck_op = self.find_bottleneck_operation(current_solution)
+
+ # Move bottleneck operation to different time slots
+ for time_slot in range(self.job_collector.max_time):
+ current_solution[bottleneck_job.id][bottleneck_op.id] = time_slot
+ current_cost = self.calculate_cost(current_solution)
+
+ if current_cost < best_cost:
+ best_solution = current_solution
+ best_cost = current_cost
+
+ return best_solution
+
+ def generate_initial_solution(self):
+ # Randomly assign operations to time slots
+ solution = {}
+ for job in self.job_collector.jobs:
+ solution[job.id] = {op.id: 0 for op in job.route.operations}
+ return solution
+
+ def find_bottleneck_operation(self, solution):
+ # Find the operation with the longest processing time in the schedule
+ max_processing_time = 0
+ bottleneck_job = None
+ bottleneck_op = None
+
+ for job in self.job_collector.jobs:
+ for operation in job.route.operations:
+ processing_time = operation.processing_time
+ start_time = solution[job.id][operation.id]
+ end_time = start_time + processing_time
+
+ if end_time > max_processing_time:
+ max_processing_time = end_time
+ bottleneck_job = job
+ bottleneck_op = operation
+
+ return bottleneck_job, bottleneck_op
+
+ def calculate_cost(self, solution):
+ # Calculate the makespan of the schedule
+ max_end_time = 0
+ for job in self.job_collector.jobs:
+ end_time = max(
+ [solution[job.id][operation.id] + operation.processing_time for operation in job.route.operations]
+ )
+ max_end_time = max(max_end_time, end_time)
+ return max_end_time
diff --git a/lekin/solver/meta_heuristics/tabu_search.py b/lekin/solver/meta_heuristics/tabu_search.py
new file mode 100644
index 0000000..025d936
--- /dev/null
+++ b/lekin/solver/meta_heuristics/tabu_search.py
@@ -0,0 +1,74 @@
+"""
+Local search
+https://towardsdatascience.com/optimization-techniques-tabu-search-36f197ef8e25
+"""
+
+
+import math
+import random
+
+
+class TabuSearchScheduler:
+ def __init__(self, job_collector, max_iterations=1000, tabu_size=20):
+ self.job_collector = job_collector
+ self.max_iterations = max_iterations
+ self.tabu_size = tabu_size
+
+ def schedule(self):
+ best_solution = self.generate_initial_solution()
+ best_cost = self.calculate_cost(best_solution)
+
+ tabu_list = [best_solution]
+ current_solution = best_solution
+
+ for iteration in range(self.max_iterations):
+ neighbors = self.generate_neighbors(current_solution)
+ non_tabu_neighbors = [neighbor for neighbor in neighbors if neighbor not in tabu_list]
+
+ if not non_tabu_neighbors:
+ break
+
+ current_solution = random.choice(non_tabu_neighbors)
+ current_cost = self.calculate_cost(current_solution)
+
+ if current_cost < best_cost:
+ best_solution = current_solution
+ best_cost = current_cost
+
+ tabu_list.append(current_solution)
+ if len(tabu_list) > self.tabu_size:
+ tabu_list.pop(0)
+
+ return best_solution
+
+ def generate_initial_solution(self):
+ # Randomly assign operations to time slots
+ solution = {}
+ for job in self.job_collector.jobs:
+ for operation in job.route.operations:
+ solution[(job.id, operation.id)] = random.randint(
+ 0, self.job_collector.max_time - operation.processing_time
+ )
+ return solution
+
+ def generate_neighbors(self, solution):
+ neighbors = []
+ for i in range(len(solution)):
+ neighbor = solution.copy()
+ job_id, operation_id = list(neighbor.keys())[i]
+ operation = self.job_collector.get_operation_by_id(job_id, operation_id)
+ neighbor[(job_id, operation_id)] = random.randint(
+ 0, self.job_collector.max_time - operation.processing_time
+ )
+ neighbors.append(neighbor)
+ return neighbors
+
+ def calculate_cost(self, solution):
+ # Calculate the makespan of the schedule
+ max_end_time = 0
+ for job in self.job_collector.jobs:
+ end_time = max(
+ [solution[(job.id, operation.id)] + operation.processing_time for operation in job.route.operations]
+ )
+ max_end_time = max(max_end_time, end_time)
+ return max_end_time
diff --git a/lekin/solver/meta_heuristics/variable_neighborhood_search.py b/lekin/solver/meta_heuristics/variable_neighborhood_search.py
new file mode 100644
index 0000000..57ae2d8
--- /dev/null
+++ b/lekin/solver/meta_heuristics/variable_neighborhood_search.py
@@ -0,0 +1,89 @@
+"""Variable neighborhood search"""
+
+import random
+
+
+class VNSScheduler:
+ def __init__(self, jobs, routes, max_iterations, neighborhood_size):
+ self.jobs = jobs
+ self.routes = routes
+ self.max_iterations = max_iterations
+ self.neighborhood_size = neighborhood_size
+
+ def initialize_solution(self):
+ # Generate an initial random solution representing start times for operations
+ solution = {}
+ for job in self.jobs:
+ solution[job] = {}
+ for operation in job.route.operations:
+ solution[job][operation] = random.randint(0, 100) # Random start time
+ return solution
+
+ def evaluate_objectives(self, solution):
+ # Evaluate the objectives for a given solution
+ makespan = self.calculate_makespan(solution)
+ total_tardiness = self.calculate_total_tardiness(solution)
+ resource_utilization = self.calculate_resource_utilization(solution)
+ return makespan, total_tardiness, resource_utilization
+
+ def calculate_makespan(self, solution):
+ # Calculate the makespan for a given solution
+ # Implementation specific to your job shop scheduling problem
+ pass
+
+ def calculate_total_tardiness(self, solution):
+ # Calculate the total tardiness for a given solution
+ # Implementation specific to your job shop scheduling problem
+ pass
+
+ def calculate_resource_utilization(self, solution):
+ # Calculate the resource utilization for a given solution
+ # Implementation specific to your job shop scheduling problem
+ pass
+
+ def generate_neighbor(self, current_solution):
+ # Generate a neighbor solution by perturbing the current solution
+ # You can use different perturbation techniques like swap, insert, etc.
+ # based on your specific problem requirements
+ neighbor_solution = current_solution.copy()
+ # Implement your perturbation here
+ return neighbor_solution
+
+ def accept_neighbor(self, current_solution, neighbor_solution, temperature):
+ # Decide whether to accept the neighbor solution based on acceptance criteria
+ # For VNS, you can use simulated annealing-like acceptance probability
+ # based on the difference in objective values and the current temperature
+ current_objectives = self.evaluate_objectives(current_solution)
+ neighbor_objectives = self.evaluate_objectives(neighbor_solution)
+ current_cost = self.calculate_cost(current_objectives)
+ neighbor_cost = self.calculate_cost(neighbor_objectives)
+
+ if neighbor_cost < current_cost:
+ return True
+ else:
+ acceptance_prob = min(1, pow(2.71, -(neighbor_cost - current_cost) / temperature))
+ return random.random() < acceptance_prob
+
+ def calculate_cost(self, objectives):
+ # Calculate the cost for a set of objectives
+ # You can define a weighted sum, weighted sum of ranks, or other measures
+ # based on your specific problem requirements and preferences
+ # For example, you can use a weighted sum of makespan and total tardiness
+ weight_makespan = 1
+ weight_tardiness = 1
+ return weight_makespan * objectives[0] + weight_tardiness * objectives[1]
+
+ def run(self):
+ current_solution = self.initialize_solution()
+ temperature = 100 # Initial temperature for simulated annealing
+ iteration = 0
+
+ while iteration < self.max_iterations:
+ for _ in range(self.neighborhood_size):
+ neighbor_solution = self.generate_neighbor(current_solution)
+ if self.accept_neighbor(current_solution, neighbor_solution, temperature):
+ current_solution = neighbor_solution
+ temperature *= 0.95 # Reduce temperature for simulated annealing
+ iteration += 1
+
+ return current_solution
diff --git a/lekin/solver/operation_research/__init__.py b/lekin/solver/operation_research/__init__.py
new file mode 100644
index 0000000..8b878dd
--- /dev/null
+++ b/lekin/solver/operation_research/__init__.py
@@ -0,0 +1 @@
+"""Operation research"""
diff --git a/lekin/solver/operation_research/ortool.py b/lekin/solver/operation_research/ortool.py
new file mode 100644
index 0000000..5d1c26a
--- /dev/null
+++ b/lekin/solver/operation_research/ortool.py
@@ -0,0 +1,69 @@
+from ortools.sat.python import cp_model
+
+
+class ORToolsScheduler:
+ def __init__(self, job_collector):
+ self.job_collector = job_collector
+ self.model = cp_model.CpModel()
+ self.vars = {}
+
+ def schedule(self):
+ self.create_variables()
+ self.add_constraints()
+ self.add_objective()
+ solver = cp_model.CpSolver()
+ status = solver.Solve(self.model)
+ if status == cp_model.OPTIMAL:
+ return self.get_schedule(solver)
+ else:
+ return None
+
+ def create_variables(self):
+ for job in self.job_collector.jobs:
+ for operation in job.route.operations:
+ self.vars[operation.id] = self.model.NewIntVar(
+ 0, self.job_collector.max_time, f"Operation_{operation.id}_Start"
+ )
+
+ def add_constraints(self):
+ for job in self.job_collector.jobs:
+ for i, operation in enumerate(job.route.operations):
+ # Constraint: Each operation starts after the end of its parent operation
+ if i > 0:
+ parent_operation = job.route.operations[i - 1]
+ self.model.Add(
+ self.vars[operation.id] >= self.vars[parent_operation.id] + parent_operation.processing_time
+ )
+
+ # Constraint: Each operation must be finished before the job's demand date
+ self.model.Add(self.vars[operation.id] + operation.processing_time <= job.demand_date)
+
+ for resource in self.job_collector.resources:
+ for timeslot in resource.timeslots:
+ for job in self.job_collector.jobs:
+ for operation in job.route.operations:
+ # Constraint: The operation must start within the resource's available timeslots
+ self.model.Add(self.vars[operation.id] >= timeslot.start_time).OnlyEnforceIf(timeslot.is_used)
+ self.model.Add(
+ self.vars[operation.id] <= timeslot.end_time - operation.processing_time
+ ).OnlyEnforceIf(timeslot.is_used)
+
+ def add_objective(self):
+ objective_var = self.model.NewIntVar(0, self.job_collector.max_time, "Makespan")
+ self.model.AddMaxEquality(
+ objective_var,
+ [
+ self.vars[operation.id] + operation.processing_time
+ for job in self.job_collector.jobs
+ for operation in job.route.operations
+ ],
+ )
+ self.model.Minimize(objective_var)
+
+ def get_schedule(self, solver):
+ schedule = {}
+ for job in self.job_collector.jobs:
+ for operation in job.route.operations:
+ start_time = solver.Value(self.vars[operation.id])
+ schedule[(job.id, operation.id)] = start_time
+ return schedule
diff --git a/lekin/solver/reinforcement_learning/__init__.py b/lekin/solver/reinforcement_learning/__init__.py
new file mode 100644
index 0000000..47e826e
--- /dev/null
+++ b/lekin/solver/reinforcement_learning/__init__.py
@@ -0,0 +1 @@
+"""Reinforcement learning"""
diff --git a/lekin/solver/reinforcement_learning/dqn.py b/lekin/solver/reinforcement_learning/dqn.py
new file mode 100644
index 0000000..751fc42
--- /dev/null
+++ b/lekin/solver/reinforcement_learning/dqn.py
@@ -0,0 +1,99 @@
+import random
+
+import numpy as np
+import tensorflow as tf
+
+
+class DeepQLearningScheduler:
+ def __init__(
+ self,
+ job_collector,
+ state_shape,
+ action_shape,
+ learning_rate=0.001,
+ gamma=0.99,
+ epsilon=1.0,
+ epsilon_decay=0.995,
+ epsilon_min=0.01,
+ ):
+ self.job_collector = job_collector
+ self.state_shape = state_shape
+ self.action_shape = action_shape
+ self.learning_rate = learning_rate
+ self.gamma = gamma
+ self.epsilon = epsilon
+ self.epsilon_decay = epsilon_decay
+ self.epsilon_min = epsilon_min
+
+ self.memory = []
+ self.q_network = self.build_q_network()
+ self.target_q_network = self.build_q_network()
+ self.update_target_network()
+
+ def build_q_network(self):
+ model = tf.keras.Sequential(
+ [
+ tf.keras.layers.Dense(64, activation="relu", input_shape=self.state_shape),
+ tf.keras.layers.Dense(64, activation="relu"),
+ tf.keras.layers.Dense(np.prod(self.action_shape), activation="linear"),
+ ]
+ )
+ model.compile(optimizer=tf.keras.optimizers.Adam(lr=self.learning_rate), loss="mse")
+ return model
+
+ def update_target_network(self):
+ self.target_q_network.set_weights(self.q_network.get_weights())
+
+ def get_action(self, state):
+ if np.random.rand() <= self.epsilon:
+ return np.random.randint(0, np.prod(self.action_shape))
+ return np.argmax(self.q_network.predict(np.array([state])))
+
+ def remember(self, state, action, reward, next_state, done):
+ self.memory.append((state, action, reward, next_state, done))
+
+ def replay(self, batch_size):
+ if len(self.memory) < batch_size:
+ return
+
+ batch = random.sample(self.memory, batch_size)
+
+ states = np.array([experience[0] for experience in batch])
+ actions = np.array([experience[1] for experience in batch])
+ rewards = np.array([experience[2] for experience in batch])
+ next_states = np.array([experience[3] for experience in batch])
+ dones = np.array([experience[4] for experience in batch])
+
+ targets = rewards + self.gamma * (1 - dones) * np.amax(self.target_q_network.predict(next_states), axis=1)
+ target_f = self.q_network.predict(states)
+ target_f[np.arange(batch_size), actions] = targets
+
+ self.q_network.fit(states, target_f, epochs=1, verbose=0)
+
+ if self.epsilon > self.epsilon_min:
+ self.epsilon *= self.epsilon_decay
+
+ def train(self, episodes, batch_size):
+ for episode in range(episodes):
+ state = self.job_collector.get_state()
+ done = False
+
+ while not done:
+ action = self.get_action(state)
+ next_state, reward, done = self.job_collector.step(action)
+ self.remember(state, action, reward, next_state, done)
+ state = next_state
+
+ if len(self.memory) > batch_size:
+ self.replay(batch_size)
+
+ if episode % 10 == 0:
+ self.update_target_network()
+
+ def get_schedule(self):
+ state = self.job_collector.get_state()
+ done = False
+ while not done:
+ action = np.argmax(self.q_network.predict(np.array([state])))
+ state, _, done = self.job_collector.step(action)
+ return self.job_collector.get_schedule()
diff --git a/lekin/solver/utils/__init__.py b/lekin/solver/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lekin/solver/utils/push_dense.py b/lekin/solver/utils/push_dense.py
new file mode 100644
index 0000000..425528e
--- /dev/null
+++ b/lekin/solver/utils/push_dense.py
@@ -0,0 +1,28 @@
+def push_dense(schedule):
+ changed = True
+
+ while changed:
+ changed = False
+
+ for resource in schedule.resources:
+ slots = sorted(resource.slots, key=lambda x: x.start_time)
+
+ for i in range(len(slots) - 1):
+ curr_slot = slots[i]
+ next_slot = slots[i + 1]
+
+ if curr_slot.end_time < next_slot.start_time:
+ # Gap exists between operations
+
+ gap = next_slot.start_time - curr_slot.end_time
+
+ if next_slot.operation.can_start_early(gap):
+ # Update slots
+ next_slot.start_time -= gap
+ next_slot.end_time -= gap
+
+ curr_slot.end_time = next_slot.start_time
+
+ changed = True
+
+ return schedule
diff --git a/lekin/utils/__init__.py b/lekin/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lekin/utils/group_op_ds.py b/lekin/utils/group_op_ds.py
new file mode 100644
index 0000000..0f27e13
--- /dev/null
+++ b/lekin/utils/group_op_ds.py
@@ -0,0 +1,67 @@
+"""
+新加入的时候可以快速找到该节点,因此之前用了dict[id, MaterialOP]
+
+移动的时候, 先标记一些candidate
+
+找到一个candidate, 往前找之前的插入位置
+
+重新整理
+
+"""
+
+
+class DictNode:
+ def __init__(self, key, value):
+ self.key = key
+ self.value = value
+ self.prev = None
+ self.next = None
+
+
+from collections import OrderedDict
+
+class IndexedList:
+ def __init__(self):
+ self.ordered_dict = OrderedDict()
+ self.order_list = []
+
+ def insert_at_index(self, index, key, value):
+ if index < 0 or index > len(self.order_list):
+ raise IndexError("Index out of bounds")
+
+ self.ordered_dict[key] = value
+ self.order_list.insert(index, key)
+
+ def insert_after(self, key, new_key, new_value):
+ if key is None:
+ # Insert at the beginning
+ self.ordered_dict[new_key] = new_value
+ elif key in self.ordered_dict:
+ # Insert after the specified key
+ items = list(self.ordered_dict.items())
+ index = next((i for i, (k, v) in enumerate(items) if k == key), -1)
+ if index != -1:
+ items.insert(index + 1, (new_key, new_value))
+ self.ordered_dict = OrderedDict(items)
+ else:
+ raise KeyError(f"Key '{key}' not found in the indexed list")
+ else:
+ raise KeyError(f"Key '{key}' not found in the indexed list")
+
+ def display(self):
+ for key, value in self.ordered_dict.items():
+ print(f"({key}: {value})", end=" ")
+ print()
+
+
+# Example Usage:
+indexed_list = IndexedList()
+indexed_list.insert_at_index(0, 'a', 1)
+indexed_list.insert_at_index(1, 'b', 2)
+indexed_list.insert_at_index(1, 'c', 3)
+indexed_list.display() # Output: (a: 1) (c: 3) (b: 2)
+
+indexed_list.insert_at_index(2, 'd', 4)
+indexed_list.display() # Output: (a: 1) (c: 3) (d: 4) (b: 2)
+
+
diff --git a/lekin/utils/push_dense.py b/lekin/utils/push_dense.py
new file mode 100644
index 0000000..a3de2b8
--- /dev/null
+++ b/lekin/utils/push_dense.py
@@ -0,0 +1,17 @@
+# push dense
+# for i, (id, resource) in enumerate(resource_collector.resources_dict.items()):
+# logging.info(
+# f'Start to push {i + 1}/{len(resource_collector.resources_dict)} resources'
+# )
+# assigned_ops = resource.assigned_operations
+# assigned_ops.sort()
+# for op in assigned_ops:
+# buffer_push = op.buffer
+# if buffer_push > resource.available_capacity:
+# buffer_push = resource.available_capacity
+
+# op.start_time -= buffer_push
+
+# # last chance for remaining jobs
+# for job in self.unassigned_jobs:
+# logging.warning(f'Abnornal! No scheduling for job {job.job_id}')
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..d0daceb
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,2669 @@
+# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+
+[[package]]
+name = "accessible-pygments"
+version = "0.0.4"
+description = "A collection of accessible pygments styles"
+optional = false
+python-versions = "*"
+files = [
+ {file = "accessible-pygments-0.0.4.tar.gz", hash = "sha256:e7b57a9b15958e9601c7e9eb07a440c813283545a20973f2574a5f453d0e953e"},
+ {file = "accessible_pygments-0.0.4-py2.py3-none-any.whl", hash = "sha256:416c6d8c1ea1c5ad8701903a20fcedf953c6e720d64f33dc47bfb2d3f2fa4e8d"},
+]
+
+[package.dependencies]
+pygments = ">=1.5"
+
+[[package]]
+name = "alabaster"
+version = "0.7.13"
+description = "A configurable sidebar-enabled Sphinx theme"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"},
+ {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"},
+]
+
+[[package]]
+name = "appnope"
+version = "0.1.3"
+description = "Disable App Nap on macOS >= 10.9"
+optional = false
+python-versions = "*"
+files = [
+ {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"},
+ {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"},
+]
+
+[[package]]
+name = "astroid"
+version = "2.11.7"
+description = "An abstract syntax tree for Python with inference support."
+optional = false
+python-versions = ">=3.6.2"
+files = [
+ {file = "astroid-2.11.7-py3-none-any.whl", hash = "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b"},
+ {file = "astroid-2.11.7.tar.gz", hash = "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946"},
+]
+
+[package.dependencies]
+lazy-object-proxy = ">=1.4.0"
+setuptools = ">=20.0"
+typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""}
+typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
+wrapt = ">=1.11,<2"
+
+[[package]]
+name = "attrs"
+version = "23.1.0"
+description = "Classes Without Boilerplate"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
+ {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
+]
+
+[package.dependencies]
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[package.extras]
+cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
+dev = ["attrs[docs,tests]", "pre-commit"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
+tests = ["attrs[tests-no-zope]", "zope-interface"]
+tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+
+[[package]]
+name = "babel"
+version = "2.12.1"
+description = "Internationalization utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"},
+ {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"},
+]
+
+[package.dependencies]
+pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""}
+
+[[package]]
+name = "backcall"
+version = "0.2.0"
+description = "Specifications for callback functions passed in to an API"
+optional = false
+python-versions = "*"
+files = [
+ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
+ {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
+]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.12.2"
+description = "Screen-scraping library"
+optional = false
+python-versions = ">=3.6.0"
+files = [
+ {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"},
+ {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"},
+]
+
+[package.dependencies]
+soupsieve = ">1.2"
+
+[package.extras]
+html5lib = ["html5lib"]
+lxml = ["lxml"]
+
+[[package]]
+name = "black"
+version = "23.3.0"
+description = "The uncompromising code formatter."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"},
+ {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"},
+ {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"},
+ {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"},
+ {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"},
+ {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"},
+ {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"},
+ {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"},
+ {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"},
+ {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"},
+ {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"},
+ {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"},
+ {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"},
+ {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"},
+ {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"},
+ {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"},
+ {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"},
+ {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"},
+ {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"},
+ {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"},
+ {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"},
+ {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"},
+ {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"},
+ {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"},
+ {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+ipython = {version = ">=7.8.0", optional = true, markers = "extra == \"jupyter\""}
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tokenize-rt = {version = ">=3.2.0", optional = true, markers = "extra == \"jupyter\""}
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
+typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "bleach"
+version = "6.0.0"
+description = "An easy safelist-based HTML-sanitizing tool."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "bleach-6.0.0-py3-none-any.whl", hash = "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4"},
+ {file = "bleach-6.0.0.tar.gz", hash = "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414"},
+]
+
+[package.dependencies]
+six = ">=1.9.0"
+webencodings = "*"
+
+[package.extras]
+css = ["tinycss2 (>=1.1.0,<1.2)"]
+
+[[package]]
+name = "certifi"
+version = "2023.7.22"
+description = "Python package for providing Mozilla's CA Bundle."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
+ {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
+]
+
+[[package]]
+name = "cffi"
+version = "1.15.1"
+description = "Foreign Function Interface for Python calling C code."
+optional = false
+python-versions = "*"
+files = [
+ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
+ {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
+ {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
+ {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
+ {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
+ {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
+ {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
+ {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
+ {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
+ {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
+ {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
+ {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
+ {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
+ {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
+ {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
+ {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
+ {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
+ {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
+ {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
+ {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
+ {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
+ {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
+ {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
+ {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
+ {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
+ {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
+ {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
+ {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
+ {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
+ {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
+ {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
+ {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
+ {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
+ {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
+ {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
+ {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
+ {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
+ {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
+ {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
+ {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
+ {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
+]
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+name = "cfgv"
+version = "3.3.1"
+description = "Validate configuration and produce human readable error messages."
+optional = false
+python-versions = ">=3.6.1"
+files = [
+ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
+ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.2.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.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"},
+ {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"},
+ {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"},
+ {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"},
+ {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"},
+ {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"},
+ {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"},
+]
+
+[[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\""}
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[[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 = "comm"
+version = "0.1.4"
+description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "comm-0.1.4-py3-none-any.whl", hash = "sha256:6d52794cba11b36ed9860999cd10fd02d6b2eac177068fdd585e1e2f8a96e67a"},
+ {file = "comm-0.1.4.tar.gz", hash = "sha256:354e40a59c9dd6db50c5cc6b4acc887d82e9603787f83b68c01a80a923984d15"},
+]
+
+[package.dependencies]
+traitlets = ">=4"
+
+[package.extras]
+lint = ["black (>=22.6.0)", "mdformat (>0.7)", "mdformat-gfm (>=0.3.5)", "ruff (>=0.0.156)"]
+test = ["pytest"]
+typing = ["mypy (>=0.990)"]
+
+[[package]]
+name = "commonmark"
+version = "0.9.1"
+description = "Python parser for the CommonMark Markdown spec"
+optional = false
+python-versions = "*"
+files = [
+ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
+ {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
+]
+
+[package.extras]
+test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
+
+[[package]]
+name = "coverage"
+version = "7.2.7"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8"},
+ {file = "coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2"},
+ {file = "coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353"},
+ {file = "coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495"},
+ {file = "coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818"},
+ {file = "coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850"},
+ {file = "coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f"},
+ {file = "coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f"},
+ {file = "coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97"},
+ {file = "coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a"},
+ {file = "coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a"},
+ {file = "coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562"},
+ {file = "coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01"},
+ {file = "coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de"},
+ {file = "coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d"},
+ {file = "coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511"},
+ {file = "coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3"},
+ {file = "coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9"},
+ {file = "coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959"},
+ {file = "coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02"},
+ {file = "coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f"},
+ {file = "coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0"},
+ {file = "coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5"},
+ {file = "coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6"},
+ {file = "coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5"},
+ {file = "coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f"},
+ {file = "coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e"},
+ {file = "coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c"},
+ {file = "coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9"},
+ {file = "coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e"},
+ {file = "coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250"},
+ {file = "coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2"},
+ {file = "coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb"},
+ {file = "coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27"},
+ {file = "coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d"},
+ {file = "coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59"},
+]
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "cycler"
+version = "0.11.0"
+description = "Composable style cycles"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"},
+ {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"},
+]
+
+[[package]]
+name = "debugpy"
+version = "1.7.0"
+description = "An implementation of the Debug Adapter Protocol for Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "debugpy-1.7.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:17ad9a681aca1704c55b9a5edcb495fa8f599e4655c9872b7f9cf3dc25890d48"},
+ {file = "debugpy-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1285920a3f9a75f5d1acf59ab1b9da9ae6eb9a05884cd7674f95170c9cafa4de"},
+ {file = "debugpy-1.7.0-cp310-cp310-win32.whl", hash = "sha256:a6f43a681c5025db1f1c0568069d1d1bad306a02e7c36144912b26d9c90e4724"},
+ {file = "debugpy-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e9571d831ad3c75b5fb6f3efcb71c471cf2a74ba84af6ac1c79ce00683bed4b"},
+ {file = "debugpy-1.7.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:538765a41198aa88cc089295b39c7322dd598f9ef1d52eaae12145c63bf9430a"},
+ {file = "debugpy-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7e8cf91f8f3f9b5fad844dd88427b85d398bda1e2a0cd65d5a21312fcbc0c6f"},
+ {file = "debugpy-1.7.0-cp311-cp311-win32.whl", hash = "sha256:18a69f8e142a716310dd0af6d7db08992aed99e2606108732efde101e7c65e2a"},
+ {file = "debugpy-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7515a5ba5ee9bfe956685909c5f28734c1cecd4ee813523363acfe3ca824883a"},
+ {file = "debugpy-1.7.0-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:bc8da67ade39d9e75608cdb8601d07e63a4e85966e0572c981f14e2cf42bcdef"},
+ {file = "debugpy-1.7.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5036e918c6ba8fc4c4f1fd0207d81db634431a02f0dc2ba51b12fd793c8c9de"},
+ {file = "debugpy-1.7.0-cp37-cp37m-win32.whl", hash = "sha256:d5be95b3946a4d7b388e45068c7b75036ac5a610f41014aee6cafcd5506423ad"},
+ {file = "debugpy-1.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0e90314a078d4e3f009520c8387aba8f74c3034645daa7a332a3d1bb81335756"},
+ {file = "debugpy-1.7.0-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:1565fd904f9571c430adca597771255cff4f92171486fced6f765dcbdfc8ec8d"},
+ {file = "debugpy-1.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6516f36a2e95b3be27f171f12b641e443863f4ad5255d0fdcea6ae0be29bb912"},
+ {file = "debugpy-1.7.0-cp38-cp38-win32.whl", hash = "sha256:2b0e489613bc066051439df04c56777ec184b957d6810cb65f235083aef7a0dc"},
+ {file = "debugpy-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:7bf0b4bbd841b2397b6a8de15da9227f1164f6d43ceee971c50194eaed930a9d"},
+ {file = "debugpy-1.7.0-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:ad22e1095b9977af432465c1e09132ba176e18df3834b1efcab1a449346b350b"},
+ {file = "debugpy-1.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f625e427f21423e5874139db529e18cb2966bdfcc1cb87a195538c5b34d163d1"},
+ {file = "debugpy-1.7.0-cp39-cp39-win32.whl", hash = "sha256:18bca8429d6632e2d3435055416d2d88f0309cc39709f4f6355c8d412cc61f24"},
+ {file = "debugpy-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:dc8a12ac8b97ef3d6973c6679a093138c7c9b03eb685f0e253269a195f651559"},
+ {file = "debugpy-1.7.0-py2.py3-none-any.whl", hash = "sha256:f6de2e6f24f62969e0f0ef682d78c98161c4dca29e9fb05df4d2989005005502"},
+ {file = "debugpy-1.7.0.zip", hash = "sha256:676911c710e85567b17172db934a71319ed9d995104610ce23fd74a07f66e6f6"},
+]
+
+[[package]]
+name = "decorator"
+version = "5.1.1"
+description = "Decorators for Humans"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
+ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
+]
+
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+description = "XML bomb protection for Python stdlib modules"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
+ {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
+]
+
+[[package]]
+name = "dill"
+version = "0.3.7"
+description = "serialize all of Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "dill-0.3.7-py3-none-any.whl", hash = "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e"},
+ {file = "dill-0.3.7.tar.gz", hash = "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"},
+]
+
+[package.extras]
+graph = ["objgraph (>=1.7.2)"]
+
+[[package]]
+name = "distlib"
+version = "0.3.7"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+files = [
+ {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
+ {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
+]
+
+[[package]]
+name = "docutils"
+version = "0.19"
+description = "Docutils -- Python Documentation Utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"},
+ {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"},
+]
+
+[[package]]
+name = "entrypoints"
+version = "0.4"
+description = "Discover and load entry points from installed packages."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"},
+ {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"},
+]
+
+[[package]]
+name = "fastjsonschema"
+version = "2.18.0"
+description = "Fastest Python implementation of JSON schema"
+optional = false
+python-versions = "*"
+files = [
+ {file = "fastjsonschema-2.18.0-py3-none-any.whl", hash = "sha256:128039912a11a807068a7c87d0da36660afbfd7202780db26c4aa7153cfdc799"},
+ {file = "fastjsonschema-2.18.0.tar.gz", hash = "sha256:e820349dd16f806e4bd1467a138dced9def4bc7d6213a34295272a6cac95b5bd"},
+]
+
+[package.extras]
+devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"]
+
+[[package]]
+name = "filelock"
+version = "3.12.2"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"},
+ {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "flake8"
+version = "3.9.2"
+description = "the modular source code checker: pep8 pyflakes and co"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+files = [
+ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"},
+ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"},
+]
+
+[package.dependencies]
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+mccabe = ">=0.6.0,<0.7.0"
+pycodestyle = ">=2.7.0,<2.8.0"
+pyflakes = ">=2.3.0,<2.4.0"
+
+[[package]]
+name = "fonttools"
+version = "4.38.0"
+description = "Tools to manipulate font files"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "fonttools-4.38.0-py3-none-any.whl", hash = "sha256:820466f43c8be8c3009aef8b87e785014133508f0de64ec469e4efb643ae54fb"},
+ {file = "fonttools-4.38.0.zip", hash = "sha256:2bb244009f9bf3fa100fc3ead6aeb99febe5985fa20afbfbaa2f8946c2fbdaf1"},
+]
+
+[package.extras]
+all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=14.0.0)", "xattr", "zopfli (>=0.1.4)"]
+graphite = ["lz4 (>=1.7.4.2)"]
+interpolatable = ["munkres", "scipy"]
+lxml = ["lxml (>=4.0,<5)"]
+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 (>=14.0.0)"]
+woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
+
+[[package]]
+name = "identify"
+version = "2.5.24"
+description = "File identification library for Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"},
+ {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "idna"
+version = "3.4"
+description = "Internationalized Domain Names in Applications (IDNA)"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
+ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+]
+
+[[package]]
+name = "imagesize"
+version = "1.4.1"
+description = "Getting image size from png/jpeg/jpeg2000/gif file"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"},
+ {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"},
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "6.7.0"
+description = "Read metadata from Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5"},
+ {file = "importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+perf = ["ipython"]
+testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
+
+[[package]]
+name = "importlib-resources"
+version = "5.12.0"
+description = "Read resources from Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"},
+ {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"},
+]
+
+[package.dependencies]
+zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+
+[[package]]
+name = "invoke"
+version = "2.2.0"
+description = "Pythonic task execution"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820"},
+ {file = "invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5"},
+]
+
+[[package]]
+name = "ipykernel"
+version = "6.16.2"
+description = "IPython Kernel for Jupyter"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ipykernel-6.16.2-py3-none-any.whl", hash = "sha256:67daf93e5b52456cd8eea87a8b59405d2bb80ae411864a1ea206c3631d8179af"},
+ {file = "ipykernel-6.16.2.tar.gz", hash = "sha256:463f3d87a92e99969b1605cb7a5b4d7b36b7145a0e72d06e65918a6ddefbe630"},
+]
+
+[package.dependencies]
+appnope = {version = "*", markers = "platform_system == \"Darwin\""}
+debugpy = ">=1.0"
+ipython = ">=7.23.1"
+jupyter-client = ">=6.1.12"
+matplotlib-inline = ">=0.1"
+nest-asyncio = "*"
+packaging = "*"
+psutil = "*"
+pyzmq = ">=17"
+tornado = ">=6.1"
+traitlets = ">=5.1.0"
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt"]
+test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "ipython"
+version = "7.34.0"
+description = "IPython: Productive Interactive Computing"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ipython-7.34.0-py3-none-any.whl", hash = "sha256:c175d2440a1caff76116eb719d40538fbb316e214eda85c5515c303aacbfb23e"},
+ {file = "ipython-7.34.0.tar.gz", hash = "sha256:af3bdb46aa292bce5615b1b2ebc76c2080c5f77f54bda2ec72461317273e7cd6"},
+]
+
+[package.dependencies]
+appnope = {version = "*", markers = "sys_platform == \"darwin\""}
+backcall = "*"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+decorator = "*"
+jedi = ">=0.16"
+matplotlib-inline = "*"
+pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""}
+pickleshare = "*"
+prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0"
+pygments = "*"
+setuptools = ">=18.5"
+traitlets = ">=4.2"
+
+[package.extras]
+all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"]
+doc = ["Sphinx (>=1.3)"]
+kernel = ["ipykernel"]
+nbconvert = ["nbconvert"]
+nbformat = ["nbformat"]
+notebook = ["ipywidgets", "notebook"]
+parallel = ["ipyparallel"]
+qtconsole = ["qtconsole"]
+test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments", "requests", "testpath"]
+
+[[package]]
+name = "ipywidgets"
+version = "8.1.1"
+description = "Jupyter interactive widgets"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "ipywidgets-8.1.1-py3-none-any.whl", hash = "sha256:2b88d728656aea3bbfd05d32c747cfd0078f9d7e159cf982433b58ad717eed7f"},
+ {file = "ipywidgets-8.1.1.tar.gz", hash = "sha256:40211efb556adec6fa450ccc2a77d59ca44a060f4f9f136833df59c9f538e6e8"},
+]
+
+[package.dependencies]
+comm = ">=0.1.3"
+ipython = ">=6.1.0"
+jupyterlab-widgets = ">=3.0.9,<3.1.0"
+traitlets = ">=4.3.1"
+widgetsnbextension = ">=4.0.9,<4.1.0"
+
+[package.extras]
+test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"]
+
+[[package]]
+name = "isort"
+version = "5.11.5"
+description = "A Python utility / library to sort Python imports."
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "isort-5.11.5-py3-none-any.whl", hash = "sha256:ba1d72fb2595a01c7895a5128f9585a5cc4b6d395f1c8d514989b9a7eb2a8746"},
+ {file = "isort-5.11.5.tar.gz", hash = "sha256:6be1f76a507cb2ecf16c7cf14a37e41609ca082330be4e3436a18ef74add55db"},
+]
+
+[package.extras]
+colors = ["colorama (>=0.4.3,<0.5.0)"]
+pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
+plugins = ["setuptools"]
+requirements-deprecated-finder = ["pip-api", "pipreqs"]
+
+[[package]]
+name = "jedi"
+version = "0.19.0"
+description = "An autocompletion tool for Python that can be used for text editors."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "jedi-0.19.0-py2.py3-none-any.whl", hash = "sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e"},
+ {file = "jedi-0.19.0.tar.gz", hash = "sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4"},
+]
+
+[package.dependencies]
+parso = ">=0.8.3,<0.9.0"
+
+[package.extras]
+docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"]
+qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"]
+testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"]
+
+[[package]]
+name = "jinja2"
+version = "3.1.2"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
+ {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "jsonschema"
+version = "4.17.3"
+description = "An implementation of JSON Schema validation for Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jsonschema-4.17.3-py3-none-any.whl", hash = "sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6"},
+ {file = "jsonschema-4.17.3.tar.gz", hash = "sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d"},
+]
+
+[package.dependencies]
+attrs = ">=17.4.0"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""}
+pkgutil-resolve-name = {version = ">=1.3.10", markers = "python_version < \"3.9\""}
+pyrsistent = ">=0.14.0,<0.17.0 || >0.17.0,<0.17.1 || >0.17.1,<0.17.2 || >0.17.2"
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
+
+[package.extras]
+format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"]
+format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"]
+
+[[package]]
+name = "jupyter-client"
+version = "7.4.9"
+description = "Jupyter protocol implementation and client libraries"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyter_client-7.4.9-py3-none-any.whl", hash = "sha256:214668aaea208195f4c13d28eb272ba79f945fc0cf3f11c7092c20b2ca1980e7"},
+ {file = "jupyter_client-7.4.9.tar.gz", hash = "sha256:52be28e04171f07aed8f20e1616a5a552ab9fee9cbbe6c1896ae170c3880d392"},
+]
+
+[package.dependencies]
+entrypoints = "*"
+jupyter-core = ">=4.9.2"
+nest-asyncio = ">=1.5.4"
+python-dateutil = ">=2.8.2"
+pyzmq = ">=23.0"
+tornado = ">=6.2"
+traitlets = "*"
+
+[package.extras]
+doc = ["ipykernel", "myst-parser", "sphinx (>=1.3.6)", "sphinx-rtd-theme", "sphinxcontrib-github-alt"]
+test = ["codecov", "coverage", "ipykernel (>=6.12)", "ipython", "mypy", "pre-commit", "pytest", "pytest-asyncio (>=0.18)", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "jupyter-core"
+version = "4.12.0"
+description = "Jupyter core package. A base package on which Jupyter projects rely."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyter_core-4.12.0-py3-none-any.whl", hash = "sha256:a54672c539333258495579f6964144924e0aa7b07f7069947bef76d7ea5cb4c1"},
+ {file = "jupyter_core-4.12.0.tar.gz", hash = "sha256:87f39d7642412ae8a52291cc68e71ac01dfa2c735df2701f8108251d51b4f460"},
+]
+
+[package.dependencies]
+pywin32 = {version = ">=1.0", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""}
+traitlets = "*"
+
+[package.extras]
+test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "jupyterlab-pygments"
+version = "0.2.2"
+description = "Pygments theme using JupyterLab CSS variables"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyterlab_pygments-0.2.2-py2.py3-none-any.whl", hash = "sha256:2405800db07c9f770863bcf8049a529c3dd4d3e28536638bd7c1c01d2748309f"},
+ {file = "jupyterlab_pygments-0.2.2.tar.gz", hash = "sha256:7405d7fde60819d905a9fa8ce89e4cd830e318cdad22a0030f7a901da705585d"},
+]
+
+[[package]]
+name = "jupyterlab-widgets"
+version = "3.0.9"
+description = "Jupyter interactive widgets for JupyterLab"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "jupyterlab_widgets-3.0.9-py3-none-any.whl", hash = "sha256:3cf5bdf5b897bf3bccf1c11873aa4afd776d7430200f765e0686bd352487b58d"},
+ {file = "jupyterlab_widgets-3.0.9.tar.gz", hash = "sha256:6005a4e974c7beee84060fdfba341a3218495046de8ae3ec64888e5fe19fdb4c"},
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.5"
+description = "A fast implementation of the Cassowary constraint solver"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"},
+ {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"},
+ {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"},
+ {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"},
+ {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"},
+ {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"},
+ {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"},
+ {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"},
+ {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"},
+ {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"},
+ {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"},
+]
+
+[package.dependencies]
+typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "lazy-object-proxy"
+version = "1.9.0"
+description = "A fast and thorough lazy object proxy."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"},
+ {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"},
+ {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"},
+ {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"},
+ {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"},
+ {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"},
+]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.3"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
+ {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
+ {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
+ {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
+ {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
+ {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
+ {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
+ {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.5.3"
+description = "Python plotting package"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "matplotlib-3.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a206a1b762b39398efea838f528b3a6d60cdb26fe9d58b48265787e29cd1d693"},
+ {file = "matplotlib-3.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cd45a6f3e93a780185f70f05cf2a383daed13c3489233faad83e81720f7ede24"},
+ {file = "matplotlib-3.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d62880e1f60e5a30a2a8484432bcb3a5056969dc97258d7326ad465feb7ae069"},
+ {file = "matplotlib-3.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ab29589cef03bc88acfa3a1490359000c18186fc30374d8aa77d33cc4a51a4a"},
+ {file = "matplotlib-3.5.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2886cc009f40e2984c083687251821f305d811d38e3df8ded414265e4583f0c5"},
+ {file = "matplotlib-3.5.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c995f7d9568f18b5db131ab124c64e51b6820a92d10246d4f2b3f3a66698a15b"},
+ {file = "matplotlib-3.5.3-cp310-cp310-win32.whl", hash = "sha256:6bb93a0492d68461bd458eba878f52fdc8ac7bdb6c4acdfe43dba684787838c2"},
+ {file = "matplotlib-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:2e6d184ebe291b9e8f7e78bbab7987d269c38ea3e062eace1fe7d898042ef804"},
+ {file = "matplotlib-3.5.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6ea6aef5c4338e58d8d376068e28f80a24f54e69f09479d1c90b7172bad9f25b"},
+ {file = "matplotlib-3.5.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:839d47b8ead7ad9669aaacdbc03f29656dc21f0d41a6fea2d473d856c39c8b1c"},
+ {file = "matplotlib-3.5.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b4fa56159dc3c7f9250df88f653f085068bcd32dcd38e479bba58909254af7f"},
+ {file = "matplotlib-3.5.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:94ff86af56a3869a4ae26a9637a849effd7643858a1a04dd5ee50e9ab75069a7"},
+ {file = "matplotlib-3.5.3-cp37-cp37m-win32.whl", hash = "sha256:35a8ad4dddebd51f94c5d24bec689ec0ec66173bf614374a1244c6241c1595e0"},
+ {file = "matplotlib-3.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:43e9d3fa077bf0cc95ded13d331d2156f9973dce17c6f0c8b49ccd57af94dbd9"},
+ {file = "matplotlib-3.5.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:22227c976ad4dc8c5a5057540421f0d8708c6560744ad2ad638d48e2984e1dbc"},
+ {file = "matplotlib-3.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf618a825deb6205f015df6dfe6167a5d9b351203b03fab82043ae1d30f16511"},
+ {file = "matplotlib-3.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9befa5954cdbc085e37d974ff6053da269474177921dd61facdad8023c4aeb51"},
+ {file = "matplotlib-3.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3840c280ebc87a48488a46f760ea1c0c0c83fcf7abbe2e6baf99d033fd35fd8"},
+ {file = "matplotlib-3.5.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dacddf5bfcec60e3f26ec5c0ae3d0274853a258b6c3fc5ef2f06a8eb23e042be"},
+ {file = "matplotlib-3.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b428076a55fb1c084c76cb93e68006f27d247169f056412607c5c88828d08f88"},
+ {file = "matplotlib-3.5.3-cp38-cp38-win32.whl", hash = "sha256:874df7505ba820e0400e7091199decf3ff1fde0583652120c50cd60d5820ca9a"},
+ {file = "matplotlib-3.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:b28de401d928890187c589036857a270a032961411934bdac4cf12dde3d43094"},
+ {file = "matplotlib-3.5.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3211ba82b9f1518d346f6309df137b50c3dc4421b4ed4815d1d7eadc617f45a1"},
+ {file = "matplotlib-3.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6fe807e8a22620b4cd95cfbc795ba310dc80151d43b037257250faf0bfcd82bc"},
+ {file = "matplotlib-3.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c096363b206a3caf43773abebdbb5a23ea13faef71d701b21a9c27fdcef72f4"},
+ {file = "matplotlib-3.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcdfcb0f976e1bac6721d7d457c17be23cf7501f977b6a38f9d38a3762841f7"},
+ {file = "matplotlib-3.5.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e64ac9be9da6bfff0a732e62116484b93b02a0b4d4b19934fb4f8e7ad26ad6a"},
+ {file = "matplotlib-3.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:73dd93dc35c85dece610cca8358003bf0760d7986f70b223e2306b4ea6d1406b"},
+ {file = "matplotlib-3.5.3-cp39-cp39-win32.whl", hash = "sha256:879c7e5fce4939c6aa04581dfe08d57eb6102a71f2e202e3314d5fbc072fd5a0"},
+ {file = "matplotlib-3.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:ab8d26f07fe64f6f6736d635cce7bfd7f625320490ed5bfc347f2cdb4fae0e56"},
+ {file = "matplotlib-3.5.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:99482b83ebf4eb6d5fc6813d7aacdefdd480f0d9c0b52dcf9f1cc3b2c4b3361a"},
+ {file = "matplotlib-3.5.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f814504e459c68118bf2246a530ed953ebd18213dc20e3da524174d84ed010b2"},
+ {file = "matplotlib-3.5.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57f1b4e69f438a99bb64d7f2c340db1b096b41ebaa515cf61ea72624279220ce"},
+ {file = "matplotlib-3.5.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d2484b350bf3d32cae43f85dcfc89b3ed7bd2bcd781ef351f93eb6fb2cc483f9"},
+ {file = "matplotlib-3.5.3.tar.gz", hash = "sha256:339cac48b80ddbc8bfd05daae0a3a73414651a8596904c2a881cfd1edb65f26c"},
+]
+
+[package.dependencies]
+cycler = ">=0.10"
+fonttools = ">=4.22.0"
+kiwisolver = ">=1.0.1"
+numpy = ">=1.17"
+packaging = ">=20.0"
+pillow = ">=6.2.0"
+pyparsing = ">=2.2.1"
+python-dateutil = ">=2.7"
+
+[[package]]
+name = "matplotlib-inline"
+version = "0.1.6"
+description = "Inline Matplotlib backend for Jupyter"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"},
+ {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"},
+]
+
+[package.dependencies]
+traitlets = "*"
+
+[[package]]
+name = "mccabe"
+version = "0.6.1"
+description = "McCabe checker, plugin for flake8"
+optional = false
+python-versions = "*"
+files = [
+ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
+ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
+]
+
+[[package]]
+name = "mistune"
+version = "3.0.1"
+description = "A sane and fast Markdown parser with useful plugins and renderers"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mistune-3.0.1-py3-none-any.whl", hash = "sha256:b9b3e438efbb57c62b5beb5e134dab664800bdf1284a7ee09e8b12b13eb1aac6"},
+ {file = "mistune-3.0.1.tar.gz", hash = "sha256:e912116c13aa0944f9dc530db38eb88f6a77087ab128f49f84a48f4c05ea163c"},
+]
+
+[[package]]
+name = "mypy"
+version = "1.4.1"
+description = "Optional static typing for Python"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"},
+ {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"},
+ {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"},
+ {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"},
+ {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"},
+ {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"},
+ {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"},
+ {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"},
+ {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"},
+ {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"},
+ {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"},
+ {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"},
+ {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"},
+ {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"},
+ {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"},
+ {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"},
+ {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"},
+ {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"},
+ {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"},
+ {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"},
+ {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"},
+ {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"},
+ {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"},
+ {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"},
+ {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"},
+ {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"},
+]
+
+[package.dependencies]
+mypy-extensions = ">=1.0.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""}
+typing-extensions = ">=4.1.0"
+
+[package.extras]
+dmypy = ["psutil (>=4.0)"]
+install-types = ["pip"]
+python2 = ["typed-ast (>=1.4.0,<2)"]
+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 = "nbclient"
+version = "0.7.4"
+description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor."
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "nbclient-0.7.4-py3-none-any.whl", hash = "sha256:c817c0768c5ff0d60e468e017613e6eae27b6fa31e43f905addd2d24df60c125"},
+ {file = "nbclient-0.7.4.tar.gz", hash = "sha256:d447f0e5a4cfe79d462459aec1b3dc5c2e9152597262be8ee27f7d4c02566a0d"},
+]
+
+[package.dependencies]
+jupyter-client = ">=6.1.12"
+jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0"
+nbformat = ">=5.1"
+traitlets = ">=5.3"
+
+[package.extras]
+dev = ["pre-commit"]
+docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"]
+test = ["flaky", "ipykernel", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"]
+
+[[package]]
+name = "nbconvert"
+version = "7.6.0"
+description = "Converting Jupyter Notebooks"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "nbconvert-7.6.0-py3-none-any.whl", hash = "sha256:5a445c6794b0791984bc5436608fe2c066cb43c83920c7bc91bde3b765e9a264"},
+ {file = "nbconvert-7.6.0.tar.gz", hash = "sha256:24fcf27efdef2b51d7f090cc5ce5a9b178766a55be513c4ebab08c91899ab550"},
+]
+
+[package.dependencies]
+beautifulsoup4 = "*"
+bleach = "!=5.0.0"
+defusedxml = "*"
+importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""}
+jinja2 = ">=3.0"
+jupyter-core = ">=4.7"
+jupyterlab-pygments = "*"
+markupsafe = ">=2.0"
+mistune = ">=2.0.3,<4"
+nbclient = ">=0.5.0"
+nbformat = ">=5.7"
+packaging = "*"
+pandocfilters = ">=1.4.1"
+pygments = ">=2.4.1"
+tinycss2 = "*"
+traitlets = ">=5.1"
+
+[package.extras]
+all = ["nbconvert[docs,qtpdf,serve,test,webpdf]"]
+docs = ["ipykernel", "ipython", "myst-parser", "nbsphinx (>=0.2.12)", "pydata-sphinx-theme", "sphinx (==5.0.2)", "sphinxcontrib-spelling"]
+qtpdf = ["nbconvert[qtpng]"]
+qtpng = ["pyqtwebengine (>=5.15)"]
+serve = ["tornado (>=6.1)"]
+test = ["ipykernel", "ipywidgets (>=7)", "pre-commit", "pytest", "pytest-dependency"]
+webpdf = ["pyppeteer (>=1,<1.1)"]
+
+[[package]]
+name = "nbformat"
+version = "5.8.0"
+description = "The Jupyter Notebook format"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "nbformat-5.8.0-py3-none-any.whl", hash = "sha256:d910082bd3e0bffcf07eabf3683ed7dda0727a326c446eeb2922abe102e65162"},
+ {file = "nbformat-5.8.0.tar.gz", hash = "sha256:46dac64c781f1c34dfd8acba16547024110348f9fc7eab0f31981c2a3dc48d1f"},
+]
+
+[package.dependencies]
+fastjsonschema = "*"
+importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.8\""}
+jsonschema = ">=2.6"
+jupyter-core = "*"
+traitlets = ">=5.1"
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
+test = ["pep440", "pre-commit", "pytest", "testpath"]
+
+[[package]]
+name = "nbsphinx"
+version = "0.9.3"
+description = "Jupyter Notebook Tools for Sphinx"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "nbsphinx-0.9.3-py3-none-any.whl", hash = "sha256:6e805e9627f4a358bd5720d5cbf8bf48853989c79af557afd91a5f22e163029f"},
+ {file = "nbsphinx-0.9.3.tar.gz", hash = "sha256:ec339c8691b688f8676104a367a4b8cf3ea01fd089dc28d24dec22d563b11562"},
+]
+
+[package.dependencies]
+docutils = "*"
+jinja2 = "*"
+nbconvert = "!=5.4"
+nbformat = "*"
+sphinx = ">=1.8"
+traitlets = ">=5"
+
+[[package]]
+name = "nest-asyncio"
+version = "1.5.7"
+description = "Patch asyncio to allow nested event loops"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "nest_asyncio-1.5.7-py3-none-any.whl", hash = "sha256:5301c82941b550b3123a1ea772ba9a1c80bad3a182be8c1a5ae6ad3be57a9657"},
+ {file = "nest_asyncio-1.5.7.tar.gz", hash = "sha256:6a80f7b98f24d9083ed24608977c09dd608d83f91cccc24c9d2cba6d10e01c10"},
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.8.0"
+description = "Node.js virtual environment builder"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+files = [
+ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
+ {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
+[[package]]
+name = "numpy"
+version = "1.21.6"
+description = "NumPy is the fundamental package for array computing with Python."
+optional = false
+python-versions = ">=3.7,<3.11"
+files = [
+ {file = "numpy-1.21.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8737609c3bbdd48e380d463134a35ffad3b22dc56295eff6f79fd85bd0eeeb25"},
+ {file = "numpy-1.21.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fdffbfb6832cd0b300995a2b08b8f6fa9f6e856d562800fea9182316d99c4e8e"},
+ {file = "numpy-1.21.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3820724272f9913b597ccd13a467cc492a0da6b05df26ea09e78b171a0bb9da6"},
+ {file = "numpy-1.21.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f17e562de9edf691a42ddb1eb4a5541c20dd3f9e65b09ded2beb0799c0cf29bb"},
+ {file = "numpy-1.21.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f30427731561ce75d7048ac254dbe47a2ba576229250fb60f0fb74db96501a1"},
+ {file = "numpy-1.21.6-cp310-cp310-win32.whl", hash = "sha256:d4bf4d43077db55589ffc9009c0ba0a94fa4908b9586d6ccce2e0b164c86303c"},
+ {file = "numpy-1.21.6-cp310-cp310-win_amd64.whl", hash = "sha256:d136337ae3cc69aa5e447e78d8e1514be8c3ec9b54264e680cf0b4bd9011574f"},
+ {file = "numpy-1.21.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6aaf96c7f8cebc220cdfc03f1d5a31952f027dda050e5a703a0d1c396075e3e7"},
+ {file = "numpy-1.21.6-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:67c261d6c0a9981820c3a149d255a76918278a6b03b6a036800359aba1256d46"},
+ {file = "numpy-1.21.6-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a6be4cb0ef3b8c9250c19cc122267263093eee7edd4e3fa75395dfda8c17a8e2"},
+ {file = "numpy-1.21.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c4068a8c44014b2d55f3c3f574c376b2494ca9cc73d2f1bd692382b6dffe3db"},
+ {file = "numpy-1.21.6-cp37-cp37m-win32.whl", hash = "sha256:7c7e5fa88d9ff656e067876e4736379cc962d185d5cd808014a8a928d529ef4e"},
+ {file = "numpy-1.21.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bcb238c9c96c00d3085b264e5c1a1207672577b93fa666c3b14a45240b14123a"},
+ {file = "numpy-1.21.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:82691fda7c3f77c90e62da69ae60b5ac08e87e775b09813559f8901a88266552"},
+ {file = "numpy-1.21.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:643843bcc1c50526b3a71cd2ee561cf0d8773f062c8cbaf9ffac9fdf573f83ab"},
+ {file = "numpy-1.21.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:357768c2e4451ac241465157a3e929b265dfac85d9214074985b1786244f2ef3"},
+ {file = "numpy-1.21.6-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f411b2c3f3d76bba0865b35a425157c5dcf54937f82bbeb3d3c180789dd66a6"},
+ {file = "numpy-1.21.6-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4aa48afdce4660b0076a00d80afa54e8a97cd49f457d68a4342d188a09451c1a"},
+ {file = "numpy-1.21.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a96eef20f639e6a97d23e57dd0c1b1069a7b4fd7027482a4c5c451cd7732f4"},
+ {file = "numpy-1.21.6-cp38-cp38-win32.whl", hash = "sha256:5c3c8def4230e1b959671eb959083661b4a0d2e9af93ee339c7dada6759a9470"},
+ {file = "numpy-1.21.6-cp38-cp38-win_amd64.whl", hash = "sha256:bf2ec4b75d0e9356edea834d1de42b31fe11f726a81dfb2c2112bc1eaa508fcf"},
+ {file = "numpy-1.21.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4391bd07606be175aafd267ef9bea87cf1b8210c787666ce82073b05f202add1"},
+ {file = "numpy-1.21.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:67f21981ba2f9d7ba9ade60c9e8cbaa8cf8e9ae51673934480e45cf55e953673"},
+ {file = "numpy-1.21.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee5ec40fdd06d62fe5d4084bef4fd50fd4bb6bfd2bf519365f569dc470163ab0"},
+ {file = "numpy-1.21.6-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1dbe1c91269f880e364526649a52eff93ac30035507ae980d2fed33aaee633ac"},
+ {file = "numpy-1.21.6-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d9caa9d5e682102453d96a0ee10c7241b72859b01a941a397fd965f23b3e016b"},
+ {file = "numpy-1.21.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58459d3bad03343ac4b1b42ed14d571b8743dc80ccbf27444f266729df1d6f5b"},
+ {file = "numpy-1.21.6-cp39-cp39-win32.whl", hash = "sha256:7f5ae4f304257569ef3b948810816bc87c9146e8c446053539947eedeaa32786"},
+ {file = "numpy-1.21.6-cp39-cp39-win_amd64.whl", hash = "sha256:e31f0bb5928b793169b87e3d1e070f2342b22d5245c755e2b81caa29756246c3"},
+ {file = "numpy-1.21.6-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd1c8f6bd65d07d3810b90d02eba7997e32abbdf1277a481d698969e921a3be0"},
+ {file = "numpy-1.21.6.zip", hash = "sha256:ecb55251139706669fdec2ff073c98ef8e9a84473e51e716211b41aa0f18e656"},
+]
+
+[[package]]
+name = "packaging"
+version = "23.1"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
+ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
+]
+
+[[package]]
+name = "pandocfilters"
+version = "1.5.0"
+description = "Utilities for writing pandoc filters in python"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"},
+ {file = "pandocfilters-1.5.0.tar.gz", hash = "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38"},
+]
+
+[[package]]
+name = "parso"
+version = "0.8.3"
+description = "A Python Parser"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
+ {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"},
+]
+
+[package.extras]
+qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
+testing = ["docopt", "pytest (<6.0.0)"]
+
+[[package]]
+name = "pathspec"
+version = "0.11.2"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
+ {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
+]
+
+[[package]]
+name = "pexpect"
+version = "4.8.0"
+description = "Pexpect allows easy control of interactive console applications."
+optional = false
+python-versions = "*"
+files = [
+ {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
+ {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
+]
+
+[package.dependencies]
+ptyprocess = ">=0.5"
+
+[[package]]
+name = "pickleshare"
+version = "0.7.5"
+description = "Tiny 'shelve'-like database with concurrency support"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
+ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
+]
+
+[[package]]
+name = "pillow"
+version = "9.5.0"
+description = "Python Imaging Library (Fork)"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"},
+ {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"},
+ {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"},
+ {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"},
+ {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"},
+ {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"},
+ {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"},
+ {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"},
+ {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"},
+ {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"},
+ {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"},
+ {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"},
+ {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"},
+ {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"},
+ {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"},
+ {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"},
+ {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"},
+ {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"},
+ {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"},
+ {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"},
+ {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"},
+ {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"},
+ {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"},
+ {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"},
+ {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"},
+ {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"},
+ {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"},
+ {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"},
+ {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"},
+ {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"},
+ {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"},
+ {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"},
+ {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"},
+ {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"},
+ {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"},
+ {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"},
+ {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"},
+ {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"},
+ {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"},
+ {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"},
+ {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"},
+ {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"},
+ {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"},
+ {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"},
+ {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"},
+ {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"},
+ {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"},
+ {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"},
+ {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"},
+ {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"},
+ {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"},
+ {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"},
+ {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"},
+ {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"},
+ {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"},
+ {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"},
+ {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"},
+ {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"},
+ {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"},
+ {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"},
+ {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"},
+ {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"},
+ {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"},
+ {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"},
+ {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"},
+ {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"},
+]
+
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
+tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "pkgutil-resolve-name"
+version = "1.3.10"
+description = "Resolve a name to an object."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pkgutil_resolve_name-1.3.10-py3-none-any.whl", hash = "sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e"},
+ {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "3.10.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"},
+ {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.7.1", markers = "python_version < \"3.8\""}
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
+
+[[package]]
+name = "pre-commit"
+version = "2.21.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"},
+ {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.39"
+description = "Library for building powerful interactive command lines in Python"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"},
+ {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"},
+]
+
+[package.dependencies]
+wcwidth = "*"
+
+[[package]]
+name = "psutil"
+version = "5.9.5"
+description = "Cross-platform lib for process and system monitoring in Python."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "psutil-5.9.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:be8929ce4313f9f8146caad4272f6abb8bf99fc6cf59344a3167ecd74f4f203f"},
+ {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ab8ed1a1d77c95453db1ae00a3f9c50227ebd955437bcf2a574ba8adbf6a74d5"},
+ {file = "psutil-5.9.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:4aef137f3345082a3d3232187aeb4ac4ef959ba3d7c10c33dd73763fbc063da4"},
+ {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ea8518d152174e1249c4f2a1c89e3e6065941df2fa13a1ab45327716a23c2b48"},
+ {file = "psutil-5.9.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:acf2aef9391710afded549ff602b5887d7a2349831ae4c26be7c807c0a39fac4"},
+ {file = "psutil-5.9.5-cp27-none-win32.whl", hash = "sha256:5b9b8cb93f507e8dbaf22af6a2fd0ccbe8244bf30b1baad6b3954e935157ae3f"},
+ {file = "psutil-5.9.5-cp27-none-win_amd64.whl", hash = "sha256:8c5f7c5a052d1d567db4ddd231a9d27a74e8e4a9c3f44b1032762bd7b9fdcd42"},
+ {file = "psutil-5.9.5-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3c6f686f4225553615612f6d9bc21f1c0e305f75d7d8454f9b46e901778e7217"},
+ {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a7dd9997128a0d928ed4fb2c2d57e5102bb6089027939f3b722f3a210f9a8da"},
+ {file = "psutil-5.9.5-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89518112647f1276b03ca97b65cc7f64ca587b1eb0278383017c2a0dcc26cbe4"},
+ {file = "psutil-5.9.5-cp36-abi3-win32.whl", hash = "sha256:104a5cc0e31baa2bcf67900be36acde157756b9c44017b86b2c049f11957887d"},
+ {file = "psutil-5.9.5-cp36-abi3-win_amd64.whl", hash = "sha256:b258c0c1c9d145a1d5ceffab1134441c4c5113b2417fafff7315a917a026c3c9"},
+ {file = "psutil-5.9.5-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c607bb3b57dc779d55e1554846352b4e358c10fff3abf3514a7a6601beebdb30"},
+ {file = "psutil-5.9.5.tar.gz", hash = "sha256:5410638e4df39c54d957fc51ce03048acd8e6d60abc0f5107af51e5fb566eb3c"},
+]
+
+[package.extras]
+test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+description = "Run a subprocess in a pseudo terminal"
+optional = false
+python-versions = "*"
+files = [
+ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
+ {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
+]
+
+[[package]]
+name = "pycodestyle"
+version = "2.7.0"
+description = "Python style guide checker"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"},
+ {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"},
+]
+
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
+ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
+]
+
+[[package]]
+name = "pydata-sphinx-theme"
+version = "0.13.3"
+description = "Bootstrap-based Sphinx theme from the PyData community"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pydata_sphinx_theme-0.13.3-py3-none-any.whl", hash = "sha256:bf41ca6c1c6216e929e28834e404bfc90e080b51915bbe7563b5e6fda70354f0"},
+ {file = "pydata_sphinx_theme-0.13.3.tar.gz", hash = "sha256:827f16b065c4fd97e847c11c108bf632b7f2ff53a3bca3272f63f3f3ff782ecc"},
+]
+
+[package.dependencies]
+accessible-pygments = "*"
+Babel = "*"
+beautifulsoup4 = "*"
+docutils = "!=0.17.0"
+packaging = "*"
+pygments = ">=2.7"
+sphinx = ">=4.2"
+typing-extensions = "*"
+
+[package.extras]
+dev = ["nox", "pre-commit", "pydata-sphinx-theme[doc,test]", "pyyaml"]
+doc = ["ablog (>=0.11.0rc2)", "colorama", "ipyleaflet", "jupyter_sphinx", "linkify-it-py", "matplotlib", "myst-nb", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap", "sphinx-togglebutton", "sphinxcontrib-youtube", "sphinxext-rediraffe", "xarray"]
+test = ["codecov", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "pyflakes"
+version = "2.3.1"
+description = "passive checker of Python programs"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"},
+ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"},
+]
+
+[[package]]
+name = "pygments"
+version = "2.16.1"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"},
+ {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
+]
+
+[package.extras]
+plugins = ["importlib-metadata"]
+
+[[package]]
+name = "pylint"
+version = "2.13.9"
+description = "python code static checker"
+optional = false
+python-versions = ">=3.6.2"
+files = [
+ {file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"},
+ {file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"},
+]
+
+[package.dependencies]
+astroid = ">=2.11.5,<=2.12.0-dev0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+dill = ">=0.2"
+isort = ">=4.2.5,<6"
+mccabe = ">=0.6,<0.8"
+platformdirs = ">=2.2.0"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+testutil = ["gitpython (>3)"]
+
+[[package]]
+name = "pyparsing"
+version = "3.1.1"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+optional = false
+python-versions = ">=3.6.8"
+files = [
+ {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"},
+ {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"},
+]
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
+
+[[package]]
+name = "pyrsistent"
+version = "0.19.3"
+description = "Persistent/Functional/Immutable data structures"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pyrsistent-0.19.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a"},
+ {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64"},
+ {file = "pyrsistent-0.19.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf"},
+ {file = "pyrsistent-0.19.3-cp310-cp310-win32.whl", hash = "sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a"},
+ {file = "pyrsistent-0.19.3-cp310-cp310-win_amd64.whl", hash = "sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da"},
+ {file = "pyrsistent-0.19.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9"},
+ {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393"},
+ {file = "pyrsistent-0.19.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19"},
+ {file = "pyrsistent-0.19.3-cp311-cp311-win32.whl", hash = "sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3"},
+ {file = "pyrsistent-0.19.3-cp311-cp311-win_amd64.whl", hash = "sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8"},
+ {file = "pyrsistent-0.19.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28"},
+ {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf"},
+ {file = "pyrsistent-0.19.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9"},
+ {file = "pyrsistent-0.19.3-cp37-cp37m-win32.whl", hash = "sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1"},
+ {file = "pyrsistent-0.19.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b"},
+ {file = "pyrsistent-0.19.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8"},
+ {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a"},
+ {file = "pyrsistent-0.19.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c"},
+ {file = "pyrsistent-0.19.3-cp38-cp38-win32.whl", hash = "sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c"},
+ {file = "pyrsistent-0.19.3-cp38-cp38-win_amd64.whl", hash = "sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7"},
+ {file = "pyrsistent-0.19.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc"},
+ {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2"},
+ {file = "pyrsistent-0.19.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3"},
+ {file = "pyrsistent-0.19.3-cp39-cp39-win32.whl", hash = "sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2"},
+ {file = "pyrsistent-0.19.3-cp39-cp39-win_amd64.whl", hash = "sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98"},
+ {file = "pyrsistent-0.19.3-py3-none-any.whl", hash = "sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64"},
+ {file = "pyrsistent-0.19.3.tar.gz", hash = "sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440"},
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.8.2"
+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.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
+ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "pytz"
+version = "2023.3.post1"
+description = "World timezone definitions, modern and historical"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"},
+ {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"},
+]
+
+[[package]]
+name = "pywin32"
+version = "306"
+description = "Python for Window Extensions"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"},
+ {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"},
+ {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"},
+ {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"},
+ {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"},
+ {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"},
+ {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"},
+ {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"},
+ {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"},
+ {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"},
+ {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"},
+ {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"},
+ {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"},
+ {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"},
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+ {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+ {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
+[[package]]
+name = "pyzmq"
+version = "25.1.1"
+description = "Python bindings for 0MQ"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pyzmq-25.1.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:381469297409c5adf9a0e884c5eb5186ed33137badcbbb0560b86e910a2f1e76"},
+ {file = "pyzmq-25.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:955215ed0604dac5b01907424dfa28b40f2b2292d6493445dd34d0dfa72586a8"},
+ {file = "pyzmq-25.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:985bbb1316192b98f32e25e7b9958088431d853ac63aca1d2c236f40afb17c83"},
+ {file = "pyzmq-25.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:afea96f64efa98df4da6958bae37f1cbea7932c35878b185e5982821bc883369"},
+ {file = "pyzmq-25.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76705c9325d72a81155bb6ab48d4312e0032bf045fb0754889133200f7a0d849"},
+ {file = "pyzmq-25.1.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:77a41c26205d2353a4c94d02be51d6cbdf63c06fbc1295ea57dad7e2d3381b71"},
+ {file = "pyzmq-25.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:12720a53e61c3b99d87262294e2b375c915fea93c31fc2336898c26d7aed34cd"},
+ {file = "pyzmq-25.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:57459b68e5cd85b0be8184382cefd91959cafe79ae019e6b1ae6e2ba8a12cda7"},
+ {file = "pyzmq-25.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:292fe3fc5ad4a75bc8df0dfaee7d0babe8b1f4ceb596437213821f761b4589f9"},
+ {file = "pyzmq-25.1.1-cp310-cp310-win32.whl", hash = "sha256:35b5ab8c28978fbbb86ea54958cd89f5176ce747c1fb3d87356cf698048a7790"},
+ {file = "pyzmq-25.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:11baebdd5fc5b475d484195e49bae2dc64b94a5208f7c89954e9e354fc609d8f"},
+ {file = "pyzmq-25.1.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:d20a0ddb3e989e8807d83225a27e5c2eb2260eaa851532086e9e0fa0d5287d83"},
+ {file = "pyzmq-25.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e1c1be77bc5fb77d923850f82e55a928f8638f64a61f00ff18a67c7404faf008"},
+ {file = "pyzmq-25.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d89528b4943d27029a2818f847c10c2cecc79fa9590f3cb1860459a5be7933eb"},
+ {file = "pyzmq-25.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90f26dc6d5f241ba358bef79be9ce06de58d477ca8485e3291675436d3827cf8"},
+ {file = "pyzmq-25.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2b92812bd214018e50b6380ea3ac0c8bb01ac07fcc14c5f86a5bb25e74026e9"},
+ {file = "pyzmq-25.1.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2f957ce63d13c28730f7fd6b72333814221c84ca2421298f66e5143f81c9f91f"},
+ {file = "pyzmq-25.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:047a640f5c9c6ade7b1cc6680a0e28c9dd5a0825135acbd3569cc96ea00b2505"},
+ {file = "pyzmq-25.1.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7f7e58effd14b641c5e4dec8c7dab02fb67a13df90329e61c869b9cc607ef752"},
+ {file = "pyzmq-25.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c2910967e6ab16bf6fbeb1f771c89a7050947221ae12a5b0b60f3bca2ee19bca"},
+ {file = "pyzmq-25.1.1-cp311-cp311-win32.whl", hash = "sha256:76c1c8efb3ca3a1818b837aea423ff8a07bbf7aafe9f2f6582b61a0458b1a329"},
+ {file = "pyzmq-25.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:44e58a0554b21fc662f2712814a746635ed668d0fbc98b7cb9d74cb798d202e6"},
+ {file = "pyzmq-25.1.1-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:e1ffa1c924e8c72778b9ccd386a7067cddf626884fd8277f503c48bb5f51c762"},
+ {file = "pyzmq-25.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1af379b33ef33757224da93e9da62e6471cf4a66d10078cf32bae8127d3d0d4a"},
+ {file = "pyzmq-25.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cff084c6933680d1f8b2f3b4ff5bbb88538a4aac00d199ac13f49d0698727ecb"},
+ {file = "pyzmq-25.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2400a94f7dd9cb20cd012951a0cbf8249e3d554c63a9c0cdfd5cbb6c01d2dec"},
+ {file = "pyzmq-25.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d81f1ddae3858b8299d1da72dd7d19dd36aab654c19671aa8a7e7fb02f6638a"},
+ {file = "pyzmq-25.1.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:255ca2b219f9e5a3a9ef3081512e1358bd4760ce77828e1028b818ff5610b87b"},
+ {file = "pyzmq-25.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a882ac0a351288dd18ecae3326b8a49d10c61a68b01419f3a0b9a306190baf69"},
+ {file = "pyzmq-25.1.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:724c292bb26365659fc434e9567b3f1adbdb5e8d640c936ed901f49e03e5d32e"},
+ {file = "pyzmq-25.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ca1ed0bb2d850aa8471387882247c68f1e62a4af0ce9c8a1dbe0d2bf69e41fb"},
+ {file = "pyzmq-25.1.1-cp312-cp312-win32.whl", hash = "sha256:b3451108ab861040754fa5208bca4a5496c65875710f76789a9ad27c801a0075"},
+ {file = "pyzmq-25.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:eadbefd5e92ef8a345f0525b5cfd01cf4e4cc651a2cffb8f23c0dd184975d787"},
+ {file = "pyzmq-25.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:db0b2af416ba735c6304c47f75d348f498b92952f5e3e8bff449336d2728795d"},
+ {file = "pyzmq-25.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c133e93b405eb0d36fa430c94185bdd13c36204a8635470cccc200723c13bb"},
+ {file = "pyzmq-25.1.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:273bc3959bcbff3f48606b28229b4721716598d76b5aaea2b4a9d0ab454ec062"},
+ {file = "pyzmq-25.1.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cbc8df5c6a88ba5ae385d8930da02201165408dde8d8322072e3e5ddd4f68e22"},
+ {file = "pyzmq-25.1.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:18d43df3f2302d836f2a56f17e5663e398416e9dd74b205b179065e61f1a6edf"},
+ {file = "pyzmq-25.1.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:73461eed88a88c866656e08f89299720a38cb4e9d34ae6bf5df6f71102570f2e"},
+ {file = "pyzmq-25.1.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c850ce7976d19ebe7b9d4b9bb8c9dfc7aac336c0958e2651b88cbd46682123"},
+ {file = "pyzmq-25.1.1-cp36-cp36m-win32.whl", hash = "sha256:d2045d6d9439a0078f2a34b57c7b18c4a6aef0bee37f22e4ec9f32456c852c71"},
+ {file = "pyzmq-25.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:458dea649f2f02a0b244ae6aef8dc29325a2810aa26b07af8374dc2a9faf57e3"},
+ {file = "pyzmq-25.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7cff25c5b315e63b07a36f0c2bab32c58eafbe57d0dce61b614ef4c76058c115"},
+ {file = "pyzmq-25.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1579413ae492b05de5a6174574f8c44c2b9b122a42015c5292afa4be2507f28"},
+ {file = "pyzmq-25.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3d0a409d3b28607cc427aa5c30a6f1e4452cc44e311f843e05edb28ab5e36da0"},
+ {file = "pyzmq-25.1.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21eb4e609a154a57c520e3d5bfa0d97e49b6872ea057b7c85257b11e78068222"},
+ {file = "pyzmq-25.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:034239843541ef7a1aee0c7b2cb7f6aafffb005ede965ae9cbd49d5ff4ff73cf"},
+ {file = "pyzmq-25.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f8115e303280ba09f3898194791a153862cbf9eef722ad8f7f741987ee2a97c7"},
+ {file = "pyzmq-25.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1a5d26fe8f32f137e784f768143728438877d69a586ddeaad898558dc971a5ae"},
+ {file = "pyzmq-25.1.1-cp37-cp37m-win32.whl", hash = "sha256:f32260e556a983bc5c7ed588d04c942c9a8f9c2e99213fec11a031e316874c7e"},
+ {file = "pyzmq-25.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:abf34e43c531bbb510ae7e8f5b2b1f2a8ab93219510e2b287a944432fad135f3"},
+ {file = "pyzmq-25.1.1-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:87e34f31ca8f168c56d6fbf99692cc8d3b445abb5bfd08c229ae992d7547a92a"},
+ {file = "pyzmq-25.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c9c6c9b2c2f80747a98f34ef491c4d7b1a8d4853937bb1492774992a120f475d"},
+ {file = "pyzmq-25.1.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5619f3f5a4db5dbb572b095ea3cb5cc035335159d9da950830c9c4db2fbb6995"},
+ {file = "pyzmq-25.1.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5a34d2395073ef862b4032343cf0c32a712f3ab49d7ec4f42c9661e0294d106f"},
+ {file = "pyzmq-25.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f0e6b78220aba09815cd1f3a32b9c7cb3e02cb846d1cfc526b6595f6046618"},
+ {file = "pyzmq-25.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3669cf8ee3520c2f13b2e0351c41fea919852b220988d2049249db10046a7afb"},
+ {file = "pyzmq-25.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2d163a18819277e49911f7461567bda923461c50b19d169a062536fffe7cd9d2"},
+ {file = "pyzmq-25.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:df27ffddff4190667d40de7beba4a950b5ce78fe28a7dcc41d6f8a700a80a3c0"},
+ {file = "pyzmq-25.1.1-cp38-cp38-win32.whl", hash = "sha256:a382372898a07479bd34bda781008e4a954ed8750f17891e794521c3e21c2e1c"},
+ {file = "pyzmq-25.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:52533489f28d62eb1258a965f2aba28a82aa747202c8fa5a1c7a43b5db0e85c1"},
+ {file = "pyzmq-25.1.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:03b3f49b57264909aacd0741892f2aecf2f51fb053e7d8ac6767f6c700832f45"},
+ {file = "pyzmq-25.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:330f9e188d0d89080cde66dc7470f57d1926ff2fb5576227f14d5be7ab30b9fa"},
+ {file = "pyzmq-25.1.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2ca57a5be0389f2a65e6d3bb2962a971688cbdd30b4c0bd188c99e39c234f414"},
+ {file = "pyzmq-25.1.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d457aed310f2670f59cc5b57dcfced452aeeed77f9da2b9763616bd57e4dbaae"},
+ {file = "pyzmq-25.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c56d748ea50215abef7030c72b60dd723ed5b5c7e65e7bc2504e77843631c1a6"},
+ {file = "pyzmq-25.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8f03d3f0d01cb5a018debeb412441996a517b11c5c17ab2001aa0597c6d6882c"},
+ {file = "pyzmq-25.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:820c4a08195a681252f46926de10e29b6bbf3e17b30037bd4250d72dd3ddaab8"},
+ {file = "pyzmq-25.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:17ef5f01d25b67ca8f98120d5fa1d21efe9611604e8eb03a5147360f517dd1e2"},
+ {file = "pyzmq-25.1.1-cp39-cp39-win32.whl", hash = "sha256:04ccbed567171579ec2cebb9c8a3e30801723c575601f9a990ab25bcac6b51e2"},
+ {file = "pyzmq-25.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:e61f091c3ba0c3578411ef505992d356a812fb200643eab27f4f70eed34a29ef"},
+ {file = "pyzmq-25.1.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ade6d25bb29c4555d718ac6d1443a7386595528c33d6b133b258f65f963bb0f6"},
+ {file = "pyzmq-25.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0c95ddd4f6e9fca4e9e3afaa4f9df8552f0ba5d1004e89ef0a68e1f1f9807c7"},
+ {file = "pyzmq-25.1.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48e466162a24daf86f6b5ca72444d2bf39a5e58da5f96370078be67c67adc978"},
+ {file = "pyzmq-25.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abc719161780932c4e11aaebb203be3d6acc6b38d2f26c0f523b5b59d2fc1996"},
+ {file = "pyzmq-25.1.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ccf825981640b8c34ae54231b7ed00271822ea1c6d8ba1090ebd4943759abf5"},
+ {file = "pyzmq-25.1.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c2f20ce161ebdb0091a10c9ca0372e023ce24980d0e1f810f519da6f79c60800"},
+ {file = "pyzmq-25.1.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:deee9ca4727f53464daf089536e68b13e6104e84a37820a88b0a057b97bba2d2"},
+ {file = "pyzmq-25.1.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aa8d6cdc8b8aa19ceb319aaa2b660cdaccc533ec477eeb1309e2a291eaacc43a"},
+ {file = "pyzmq-25.1.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:019e59ef5c5256a2c7378f2fb8560fc2a9ff1d315755204295b2eab96b254d0a"},
+ {file = "pyzmq-25.1.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:b9af3757495c1ee3b5c4e945c1df7be95562277c6e5bccc20a39aec50f826cd0"},
+ {file = "pyzmq-25.1.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:548d6482dc8aadbe7e79d1b5806585c8120bafa1ef841167bc9090522b610fa6"},
+ {file = "pyzmq-25.1.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:057e824b2aae50accc0f9a0570998adc021b372478a921506fddd6c02e60308e"},
+ {file = "pyzmq-25.1.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2243700cc5548cff20963f0ca92d3e5e436394375ab8a354bbea2b12911b20b0"},
+ {file = "pyzmq-25.1.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79986f3b4af059777111409ee517da24a529bdbd46da578b33f25580adcff728"},
+ {file = "pyzmq-25.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:11d58723d44d6ed4dd677c5615b2ffb19d5c426636345567d6af82be4dff8a55"},
+ {file = "pyzmq-25.1.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:49d238cf4b69652257db66d0c623cd3e09b5d2e9576b56bc067a396133a00d4a"},
+ {file = "pyzmq-25.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fedbdc753827cf014c01dbbee9c3be17e5a208dcd1bf8641ce2cd29580d1f0d4"},
+ {file = "pyzmq-25.1.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc16ac425cc927d0a57d242589f87ee093884ea4804c05a13834d07c20db203c"},
+ {file = "pyzmq-25.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11c1d2aed9079c6b0c9550a7257a836b4a637feb334904610f06d70eb44c56d2"},
+ {file = "pyzmq-25.1.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e8a701123029cc240cea61dd2d16ad57cab4691804143ce80ecd9286b464d180"},
+ {file = "pyzmq-25.1.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:61706a6b6c24bdece85ff177fec393545a3191eeda35b07aaa1458a027ad1304"},
+ {file = "pyzmq-25.1.1.tar.gz", hash = "sha256:259c22485b71abacdfa8bf79720cd7bcf4b9d128b30ea554f01ae71fdbfdaa23"},
+]
+
+[package.dependencies]
+cffi = {version = "*", markers = "implementation_name == \"pypy\""}
+
+[[package]]
+name = "recommonmark"
+version = "0.7.1"
+description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects."
+optional = false
+python-versions = "*"
+files = [
+ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"},
+ {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"},
+]
+
+[package.dependencies]
+commonmark = ">=0.8.1"
+docutils = ">=0.11"
+sphinx = ">=1.3.1"
+
+[[package]]
+name = "requests"
+version = "2.31.0"
+description = "Python HTTP for Humans."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
+ {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+]
+
+[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 = "setuptools"
+version = "68.0.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "setuptools-68.0.0-py3-none-any.whl", hash = "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f"},
+ {file = "setuptools-68.0.0.tar.gz", hash = "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+
+[[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 = "snowballstemmer"
+version = "2.2.0"
+description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
+optional = false
+python-versions = "*"
+files = [
+ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
+ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.4.1"
+description = "A modern CSS selector implementation for Beautiful Soup."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"},
+ {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"},
+]
+
+[[package]]
+name = "sphinx"
+version = "5.3.0"
+description = "Python documentation generator"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"},
+ {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"},
+]
+
+[package.dependencies]
+alabaster = ">=0.7,<0.8"
+babel = ">=2.9"
+colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
+docutils = ">=0.14,<0.20"
+imagesize = ">=1.3"
+importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""}
+Jinja2 = ">=3.0"
+packaging = ">=21.0"
+Pygments = ">=2.12"
+requests = ">=2.5.0"
+snowballstemmer = ">=2.0"
+sphinxcontrib-applehelp = "*"
+sphinxcontrib-devhelp = "*"
+sphinxcontrib-htmlhelp = ">=2.0.0"
+sphinxcontrib-jsmath = "*"
+sphinxcontrib-qthelp = "*"
+sphinxcontrib-serializinghtml = ">=1.1.5"
+
+[package.extras]
+docs = ["sphinxcontrib-websupport"]
+lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"]
+test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"]
+
+[[package]]
+name = "sphinxcontrib-applehelp"
+version = "1.0.2"
+description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
+ {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
+]
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-devhelp"
+version = "1.0.2"
+description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
+ {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
+]
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-htmlhelp"
+version = "2.0.0"
+description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"},
+ {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"},
+]
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+test = ["html5lib", "pytest"]
+
+[[package]]
+name = "sphinxcontrib-jsmath"
+version = "1.0.1"
+description = "A sphinx extension which renders display math in HTML via JavaScript"
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
+ {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
+]
+
+[package.extras]
+test = ["flake8", "mypy", "pytest"]
+
+[[package]]
+name = "sphinxcontrib-qthelp"
+version = "1.0.3"
+description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
+ {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
+]
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+test = ["pytest"]
+
+[[package]]
+name = "sphinxcontrib-serializinghtml"
+version = "1.1.5"
+description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
+ {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"},
+]
+
+[package.extras]
+lint = ["docutils-stubs", "flake8", "mypy"]
+test = ["pytest"]
+
+[[package]]
+name = "tinycss2"
+version = "1.2.1"
+description = "A tiny CSS parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"},
+ {file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"},
+]
+
+[package.dependencies]
+webencodings = ">=0.4"
+
+[package.extras]
+doc = ["sphinx", "sphinx_rtd_theme"]
+test = ["flake8", "isort", "pytest"]
+
+[[package]]
+name = "tokenize-rt"
+version = "5.0.0"
+description = "A wrapper around the stdlib `tokenize` which roundtrips."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tokenize_rt-5.0.0-py2.py3-none-any.whl", hash = "sha256:c67772c662c6b3dc65edf66808577968fb10badfc2042e3027196bed4daf9e5a"},
+ {file = "tokenize_rt-5.0.0.tar.gz", hash = "sha256:3160bc0c3e8491312d0485171dea861fc160a240f5f5766b72a1165408d10740"},
+]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "tornado"
+version = "6.2"
+description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
+optional = false
+python-versions = ">= 3.7"
+files = [
+ {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"},
+ {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"},
+ {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"},
+ {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"},
+ {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"},
+ {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"},
+ {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"},
+ {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"},
+ {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"},
+ {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"},
+ {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"},
+]
+
+[[package]]
+name = "traitlets"
+version = "5.9.0"
+description = "Traitlets Python configuration system"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"},
+ {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"},
+]
+
+[package.extras]
+docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"]
+test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"]
+
+[[package]]
+name = "typed-ast"
+version = "1.5.5"
+description = "a fork of Python 2 and 3 ast modules with type comment support"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"},
+ {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"},
+ {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"},
+ {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"},
+ {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"},
+ {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"},
+ {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"},
+ {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"},
+ {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"},
+ {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"},
+ {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"},
+ {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"},
+ {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"},
+ {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"},
+ {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"},
+ {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"},
+ {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"},
+ {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"},
+ {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"},
+ {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"},
+ {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"},
+ {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"},
+ {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"},
+ {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"},
+ {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"},
+ {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"},
+ {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"},
+ {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"},
+ {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"},
+ {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"},
+ {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"},
+ {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"},
+ {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"},
+ {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"},
+ {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"},
+ {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"},
+ {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"},
+ {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"},
+ {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"},
+ {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"},
+ {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"},
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.7.1"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
+ {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
+]
+
+[[package]]
+name = "urllib3"
+version = "2.0.4"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "urllib3-2.0.4-py3-none-any.whl", hash = "sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"},
+ {file = "urllib3-2.0.4.tar.gz", hash = "sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11"},
+]
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
+socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[[package]]
+name = "virtualenv"
+version = "20.24.5"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"},
+ {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+importlib-metadata = {version = ">=6.6", markers = "python_version < \"3.8\""}
+platformdirs = ">=3.9.1,<4"
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "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 = "wcwidth"
+version = "0.2.6"
+description = "Measures the displayed width of unicode strings in a terminal"
+optional = false
+python-versions = "*"
+files = [
+ {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"},
+ {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"},
+]
+
+[[package]]
+name = "webencodings"
+version = "0.5.1"
+description = "Character encoding aliases for legacy web content"
+optional = false
+python-versions = "*"
+files = [
+ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
+ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
+]
+
+[[package]]
+name = "widgetsnbextension"
+version = "4.0.9"
+description = "Jupyter interactive widgets for Jupyter Notebook"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "widgetsnbextension-4.0.9-py3-none-any.whl", hash = "sha256:91452ca8445beb805792f206e560c1769284267a30ceb1cec9f5bcc887d15175"},
+ {file = "widgetsnbextension-4.0.9.tar.gz", hash = "sha256:3c1f5e46dc1166dfd40a42d685e6a51396fd34ff878742a3e47c6f0cc4a2a385"},
+]
+
+[[package]]
+name = "wrapt"
+version = "1.15.0"
+description = "Module for decorators, wrappers and monkey patching."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+files = [
+ {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"},
+ {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"},
+ {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"},
+ {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"},
+ {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"},
+ {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"},
+ {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"},
+ {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"},
+ {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"},
+ {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"},
+ {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"},
+ {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"},
+ {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"},
+ {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"},
+ {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"},
+ {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"},
+ {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"},
+ {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"},
+ {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"},
+ {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"},
+ {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"},
+ {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"},
+ {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"},
+ {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"},
+ {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"},
+ {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"},
+ {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"},
+ {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"},
+ {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"},
+ {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"},
+ {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"},
+ {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"},
+ {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"},
+ {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"},
+ {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"},
+ {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"},
+ {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"},
+ {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"},
+ {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"},
+ {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"},
+ {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"},
+ {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"},
+ {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"},
+ {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"},
+ {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"},
+ {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"},
+ {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"},
+ {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"},
+ {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"},
+ {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"},
+ {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"},
+ {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"},
+ {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"},
+ {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"},
+ {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"},
+ {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"},
+ {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"},
+ {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"},
+ {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"},
+ {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"},
+ {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"},
+ {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"},
+ {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"},
+ {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"},
+ {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"},
+ {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"},
+]
+
+[[package]]
+name = "zipp"
+version = "3.15.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
+ {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = ">=3.7,<3.11"
+content-hash = "c5b23e5b5be0d2103faf86cd3e0380fb6bfcd5e5b56e90dda009eb15e84fb619"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..cb6dae0
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,91 @@
+[tool.black]
+line-length = 120
+include = '\.pyi?$'
+exclude = '''
+(
+ /(
+ \.eggs # exclude a few common directories in the
+ | \.git # root of the project
+ | \.hg
+ | \.mypy_cache
+ | \.tox
+ | \.venv
+ | _build
+ | buck-out
+ | build
+ | dist
+ )/
+ | docs/build/
+ | node_modules/
+ | venve/
+ | .venv/
+)
+'''
+
+[tool.nbqa.mutate]
+isort = 1
+black = 1
+
+[tool.poetry]
+name = "lekin"
+readme = "README.md" # Markdown files are supported
+version = "0.0.2" # is being replaced automatically
+
+authors = ["Longxing Tan"]
+classifiers = [
+ "Intended Audience :: Developers",
+ "Intended Audience :: Science/Research",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Topic :: Scientific/Engineering",
+ "Topic :: Scientific/Engineering :: Mathematics",
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
+ "Topic :: Software Development",
+ "Topic :: Software Development :: Libraries",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "License :: OSI Approved :: MIT License"]
+description = "Flexible job shop scheduler in Python"
+repository = "https://github.com/LongxingTan/python-lekin"
+documentation = "https://python-lekin.readthedocs.io"
+homepage = "https://python-lekin.readthedocs.io"
+
+[tool.poetry.dependencies]
+python = ">=3.7,<3.11"
+#pandas = "^1.1.0"
+matplotlib = "*"
+
+[tool.poetry.dev-dependencies]
+# checks and make tools
+pre-commit = "^2.20.0"
+
+invoke = "*"
+flake8 = "*"
+mypy = "*"
+pylint = "*"
+isort = "*"
+coverage = "*"
+
+# jupyter notebook
+ipykernel = "*"
+black = { version = "*", allow-prereleases = true, extras = ["jupyter"] }
+
+# documentatation
+sphinx = "*"
+pydata-sphinx-theme = "*"
+nbsphinx = "*"
+# pandoc = "*"
+recommonmark = "*"
+ipywidgets = "^8.0.1"
+
+[tool.poetry-dynamic-versioning]
+enable = true
+vcs = "git"
+dirty = false
+style = "semver" # semantic versioning
+
+[build-system] # make the package pip installable
+requires = ["poetry-core>=1.0.7", "poetry-dynamic-versioning>=0.13.1"]
+build-backend = "poetry.core.masonry.api"
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..f0b8151
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,69 @@
+[flake8]
+max-line-length = 120
+show-source = true
+ignore =
+ # space before : (needed for how black formats slicing)
+ E203,
+ # line break before binary operator
+ W503,
+ # line break after binary operator
+ W504,
+ # module level import not at top of file
+ E402,
+ # do not assign a lambda expression, use a def
+ E731,
+ # ignore not easy to read variables like i l I etc.
+ E741,
+ # Unnecessary list literal - rewrite as a dict literal.
+ C406,
+ # Unnecessary dict call - rewrite as a literal.
+ C408,
+ # Unnecessary list passed to tuple() - rewrite as a tuple literal.
+ C409,
+ # found modulo formatter (incorrect picks up mod operations)
+ S001,
+ # unused imports
+ F401
+exclude = docs/build/*.py,
+ node_modules/*.py,
+ .eggs/*.py,
+ versioneer.py,
+ venv/*,
+ .venv/*,
+ .git/*
+ .history/*
+
+[isort]
+profile = black
+honor_noqa = true
+line_length = 120
+combine_as_imports = true
+force_sort_within_sections = true
+known_first_party = pytorch_forecasting
+
+[tool:pytest]
+addopts =
+ -rsxX
+ -vv
+ --last-failed
+ --cov=pytorch_forecasting
+ --cov-report=html
+ --cov-config=setup.cfg
+ --cov-report=term-missing:skip-covered
+ --no-cov-on-fail
+ -n0
+testpaths = tests/
+log_cli_level = ERROR
+markers =
+
+[coverage:report]
+ignore_errors = False
+show_missing = true
+
+
+[mypy]
+ignore_missing_imports = true
+no_implicit_optional = true
+check_untyped_defs = true
+
+cache_dir = .cache/mypy/
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dashboard/__init__.py b/tests/test_dashboard/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_dashboard/test_gantt.py b/tests/test_dashboard/test_gantt.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_datasets/__init__.py b/tests/test_datasets/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_lekin_struct/__init__.py b/tests/test_lekin_struct/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_lekin_struct/test_job.py b/tests/test_lekin_struct/test_job.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_lekin_struct/test_operation.py b/tests/test_lekin_struct/test_operation.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_lekin_struct/test_resource.py b/tests/test_lekin_struct/test_resource.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_lekin_struct/test_route.py b/tests/test_lekin_struct/test_route.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_lekin_struct/test_timeslot.py b/tests/test_lekin_struct/test_timeslot.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_objective/__init__.py b/tests/test_objective/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_solver/__init__.py b/tests/test_solver/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_solver/test_construction_heuristics/__init__.py b/tests/test_solver/test_construction_heuristics/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_solver/test_construction_heuristics/test_lpst.py b/tests/test_solver/test_construction_heuristics/test_lpst.py
new file mode 100644
index 0000000..2435285
--- /dev/null
+++ b/tests/test_solver/test_construction_heuristics/test_lpst.py
@@ -0,0 +1,3 @@
+import unittest
+
+from lekin.solver.construction_heuristics.lpst import LPSTScheduler
diff --git a/tests/test_solver/test_construction_heuristics/test_rule.py b/tests/test_solver/test_construction_heuristics/test_rule.py
new file mode 100644
index 0000000..f6df27e
--- /dev/null
+++ b/tests/test_solver/test_construction_heuristics/test_rule.py
@@ -0,0 +1,3 @@
+import pandas as pd
+
+from lekin.lekin_struct import Job, JobCollector
diff --git a/tests/test_solver/test_construction_heuristics/test_spt.py b/tests/test_solver/test_construction_heuristics/test_spt.py
new file mode 100644
index 0000000..ccb7ab4
--- /dev/null
+++ b/tests/test_solver/test_construction_heuristics/test_spt.py
@@ -0,0 +1,3 @@
+import unittest
+
+from lekin.solver.construction_heuristics.spt import SPTScheduler
diff --git a/tests/test_solver/test_meta_heuristics/__init__.py b/tests/test_solver/test_meta_heuristics/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_solver/test_meta_heuristics/test_branch_and_bound.py b/tests/test_solver/test_meta_heuristics/test_branch_and_bound.py
new file mode 100644
index 0000000..3a87ac3
--- /dev/null
+++ b/tests/test_solver/test_meta_heuristics/test_branch_and_bound.py
@@ -0,0 +1,26 @@
+from datetime import datetime, timedelta
+import unittest
+
+from lekin.lekin_struct import Job, Operation, Resource, Route, TimeSlot
+from lekin.solver.meta_heuristics.branch_and_bound import BranchAndBoundScheduler
+
+# class BranchAndBoundSchedulerTest(unittest.TestCase):
+# def test_schedule(self):
+# job1 = Job(1, datetime(2023, 1, 10), 2, 1)
+# job2 = Job(2, datetime(2023, 1, 20), 1, 1)
+#
+# op1 = Operation(1, timedelta(hours=2), 2, None, [1])
+# op2 = Operation(2, timedelta(hours=3), None, 1, [1])
+#
+# route1 = Route(1, [op1, op2])
+# print(route1)
+#
+# resource1 = Resource(1, [TimeSlot(datetime(2023, 1, 1), datetime(2023, 1, 3))])
+#
+# job_list = [job1, job2]
+# resource_list = [resource1]
+#
+# scheduler = BranchAndBoundScheduler(job_list, resource_list)
+# schedule = scheduler.get_schedule()
+# for idx, slot in enumerate(schedule):
+# print(f"Job {idx + 1} will start at {slot.start_time} and end at {slot.end_time}")
diff --git a/tests/test_solver/test_meta_heuristics/test_genetic.py b/tests/test_solver/test_meta_heuristics/test_genetic.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_solver/test_meta_heuristics/test_nsga3.py b/tests/test_solver/test_meta_heuristics/test_nsga3.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_solver/test_meta_heuristics/test_tabu_search.py b/tests/test_solver/test_meta_heuristics/test_tabu_search.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_solver/test_reinforcement_learning/__init__.py b/tests/test_solver/test_reinforcement_learning/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_solver/test_reinforcement_learning/test_dqn.py b/tests/test_solver/test_reinforcement_learning/test_dqn.py
new file mode 100644
index 0000000..e69de29