summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoberto Bertó <463349+robertoberto@users.noreply.github.com>2025-09-18 13:08:55 -0300
committerGitHub <noreply@github.com>2025-09-18 13:08:55 -0300
commit9a69a954a086df321640dfa65a2ac0ac35a15095 (patch)
tree3c8e41a08dab6e47e4639e09eebcc37c4549b914
parent85e4714c53b662c45a1f6ee4b6cb0089ad29cc7b (diff)
parentf25d28ab90a266c03fd966ddabc752088da080d6 (diff)
downloadpyvyos-9a69a954a086df321640dfa65a2ac0ac35a15095.tar.gz
pyvyos-9a69a954a086df321640dfa65a2ac0ac35a15095.zip
Merge pull request #18 from eduardormorais/release/0.3.0
Updating version release 0.3.0
-rw-r--r--.github/workflows/python-pr-validation.yml31
-rw-r--r--.gitignore2
-rw-r--r--CONTRIB.md12
-rw-r--r--docs/source/conf.py24
-rw-r--r--example.py109
-rw-r--r--poetry.lock494
-rw-r--r--pyproject.toml34
-rw-r--r--pyvyos/__init__.py2
-rw-r--r--pyvyos/device.py278
-rw-r--r--pyvyos/rest.py316
-rw-r--r--tests/conftest.py22
-rw-r--r--tests/modules/__init__.py0
-rw-r--r--tests/modules/test_vy_device.py353
-rw-r--r--tests/test_vy_device.py116
-rw-r--r--vagrant/.env.example9
-rw-r--r--vagrant/VAGRANT.md10
-rw-r--r--vagrant/Vagrantfile84
17 files changed, 1381 insertions, 515 deletions
diff --git a/.github/workflows/python-pr-validation.yml b/.github/workflows/python-pr-validation.yml
new file mode 100644
index 0000000..30f9969
--- /dev/null
+++ b/.github/workflows/python-pr-validation.yml
@@ -0,0 +1,31 @@
+name: Python Pull Request Validation Workflow
+
+on:
+ pull_request:
+ branches: [ "main", "master" ]
+
+jobs:
+ Validation:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Set up Python 3.12
+ uses: actions/setup-python@v2
+ with:
+ python-version: "3.12"
+ architecture: "x64"
+
+ - name: Install dependencies
+ run: |
+ python3 -m pip install --upgrade pip
+ pip install poetry
+ pip install pytest
+ poetry lock
+ poetry config virtualenvs.create false
+ poetry install --with dev --no-interaction --no-ansi
+
+ - name: Python Run Tests
+ run: |
+ pytest -v
diff --git a/.gitignore b/.gitignore
index 9f1438a..3c79534 100644
--- a/.gitignore
+++ b/.gitignore
@@ -165,4 +165,4 @@ packer/
*.code-workspace
.env
dev/
-
+.idea
diff --git a/CONTRIB.md b/CONTRIB.md
index fda65c5..296cb44 100644
--- a/CONTRIB.md
+++ b/CONTRIB.md
@@ -65,3 +65,15 @@ By contributing to pyvyos, you agree that your contributions will be licensed un
Thank you for considering contributing to pyvyos. Your efforts are what make this project great!
+## Unit Tests
+Ensure you've installed development dependencies first:
+```bash
+ poetry install --with dev
+```
+
+To view test coverage:
+```bash
+ pytest --cov
+```
+
+**Note:** Tests should run against a development environment or mocked device to prevent production changes.
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 4b3b786..bfa4675 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -6,33 +6,31 @@
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
-project = 'pyvyos'
-copyright = '2024, Roberto Berto'
-author = 'Roberto Berto'
-release = '0.2.1'
+project = "pyvyos"
+copyright = "2024, Roberto Berto"
+author = "Roberto Berto"
+release = "0.3.0"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
-templates_path = ['_templates']
+templates_path = ["_templates"]
exclude_patterns = []
-
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
-html_theme = 'sphinx_rtd_theme'
-html_static_path = ['_static']
+html_theme = "sphinx_rtd_theme"
+html_static_path = ["_static"]
import os
import sys
-sys.path.insert(0, os.path.abspath('../../pyvyos'))
-extensions = [
- 'sphinx.ext.autodoc',
- 'sphinx_rtd_theme',
+sys.path.insert(0, os.path.abspath("../../pyvyos"))
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx_rtd_theme",
]
-
diff --git a/example.py b/example.py
index 2796329..e48983a 100644
--- a/example.py
+++ b/example.py
@@ -1,12 +1,12 @@
# importing modules
import warnings
+
warnings.filterwarnings("ignore", category=RuntimeWarning)
-import sys
import os
+
# adding pyvyos to sys.path
-#sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__))))
+# sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__))))
-import unittest
from dotenv import load_dotenv
import pprint
import random
@@ -14,53 +14,100 @@ import string
# importing pyvyos modules
from pyvyos.device import VyDevice
-from pyvyos.device import ApiResponse
-
# getting env variables
load_dotenv()
-hostname = os.getenv('VYDEVICE_HOSTNAME')
-apikey = os.getenv('VYDEVICE_APIKEY')
-port = os.getenv('VYDEVICE_PORT')
-protocol = os.getenv('VYDEVICE_PROTOCOL')
-verify = os.getenv('VYDEVICE_VERIFY_SSL')
+hostname = os.getenv("VYDEVICE_HOSTNAME")
+apikey = os.getenv("VYDEVICE_APIKEY")
+port = os.getenv("VYDEVICE_PORT")
+protocol = os.getenv("VYDEVICE_PROTOCOL")
+verify = os.getenv("VYDEVICE_VERIFY_SSL")
+
if verify == "False":
verify = False
else:
verify = True
# running example
-if __name__ == '__main__':
+if __name__ == "__main__":
# preparing connection to vyos device
- device = VyDevice(hostname=hostname, apikey=apikey, port=port, protocol=protocol, verify=verify)
+ device = VyDevice(
+ hostname=hostname,
+ apikey=apikey,
+ port=port,
+ protocol=protocol,
+ verify=verify,
+ timeout=60,
+ )
+
+ response = device.retrieve_show_config(["system"])
+ pprint.pprint(response)
+ response = device.configure_set(
+ path=["interfaces", "dummy", "dum1", "address", "192.168.56.100/24"]
+ )
+ pprint.pprint(response)
+
+ response = device.retrieve_return_values(
+ path=["interfaces", "dummy", "dum1", "address"]
+ )
+ pprint.pprint(response)
+
+ response = device.configure_delete(path=["interfaces", "dummy", "dum1"])
+ pprint.pprint(response)
+ randstring = "".join(
+ random.choice(string.ascii_letters + string.digits) for _ in range(20)
+ )
+ keyrand = f"/tmp/key_{randstring}"
+ response = device.generate(path=["ssh", "client-key", keyrand])
+ pprint.pprint(response)
+ response = device.show(path=["system", "image"])
+ pprint.pprint(response)
- #response = device.retrieve_show_config(['system'])
- #pprint.pprint(response)
+ response = device.reset(path=["conntrack-sync", "internal-cache"])
+ pprint.pprint(response)
- #print("### Generating ssh key ###")
- #randstring = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(20))
- #keyrand = f'/tmp/key_{randstring}'
- #response = device.generate(path=["ssh", "client-key", keyrand])
- #pprint.pprint(response)
+ response = device.config_file_save(file="/config/test300.config")
+ pprint.pprint(response)
+ response = device.config_file_load(file="/config/test300.config")
+ pprint.pprint(response)
+
+ response = device.configure_multiple_op(
+ op_path=[
+ {
+ "op": "set",
+ "path": ["interfaces", "dummy", "dum1", "address", "192.168.56.100/24"],
+ },
+ {
+ "op": "delete",
+ "path": ["interfaces", "dummy", "dum1"],
+ },
+ ]
+ )
+ pprint.pprint(response)
+ # print("### Generating ssh key ###")
+ # randstring = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(20))
+ # keyrand = f'/tmp/key_{randstring}'
+ # response = device.generate(path=["ssh", "client-key", keyrand])
+ # pprint.pprint(response)
- #response = device.retrieve_return_values(path=["interfaces", "ethernet", "eth0", "address"])
- #pprint.pprint(response)
+ # response = device.retrieve_return_values(path=["interfaces", "ethernet", "eth0", "address"])
+ # pprint.pprint(response)
- #response = device.reset(path=["conntrack-sync", "internal-cache"])
- #pprint.pprint(response)
+ # response = device.reset(path=["conntrack-sync", "internal-cache"])
+ # pprint.pprint(response)
- #response = device.reboot(path=["now"])
- #pprint.pprint(response)
+ # response = device.reboot(path=["now"])
+ # pprint.pprint(response)
- #response = device.shutdown(path=["now"])
- #pprint.pprint(response)
+ # response = device.shutdown(path=["now"])
+ # pprint.pprint(response)
- #response = device.image_add(url="https://github.com/vyos/vyos-rolling-nightly-builds/releases/download/1.5-rolling-202312130023/vyos-1.5-rolling-202312130023-amd64.iso")
- #pprint.pprint(response)
+ # response = device.image_add(url="https://github.com/vyos/vyos-rolling-nightly-builds/releases/download/1.5-rolling-202312130023/vyos-1.5-rolling-202312130023-amd64.iso")
+ # pprint.pprint(response)
- response = device.image_delete(name="foo")
- pprint.pprint(response)
+ # response = device.image_delete(name="foo")
+ # pprint.pprint(response)
diff --git a/poetry.lock b/poetry.lock
index cd92fd7..0f793f4 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,127 +1,349 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.0 and should not be changed by hand.
+
+[[package]]
+name = "atomicwrites"
+version = "1.4.1"
+description = "Atomic file writes."
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+files = [
+ {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"},
+]
+
+[[package]]
+name = "attrs"
+version = "25.3.0"
+description = "Classes Without Boilerplate"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"},
+ {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"},
+]
+
+[package.extras]
+benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"]
+tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
[[package]]
name = "certifi"
-version = "2024.7.4"
+version = "2025.8.3"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
files = [
- {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
- {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
+ {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"},
+ {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"},
]
[[package]]
name = "charset-normalizer"
-version = "3.3.2"
+version = "3.4.3"
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.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
- {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
- {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
- {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
- {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
- {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
- {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
- {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
+python-versions = ">=3.7"
+files = [
+ {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"},
+ {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"},
+ {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"},
+ {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"},
+ {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"},
+ {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"},
+ {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"},
+ {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"},
+ {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"},
+ {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"},
+ {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"},
+ {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"},
+ {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"},
+ {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"},
+ {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"},
+ {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"},
+ {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"},
+ {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"},
+ {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"},
+ {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"},
+ {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"},
+ {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"},
+ {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"},
+ {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"},
+ {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"},
+ {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"},
+ {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"},
+ {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"},
+ {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"},
+ {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"},
+ {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"},
+ {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"},
+ {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"},
+ {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"},
+ {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"},
+ {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"},
+ {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"},
+ {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"},
+ {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"},
+ {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"},
+ {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"},
+ {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"},
+ {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"},
+ {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"},
+ {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"},
+ {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"},
+ {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"},
+ {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"},
+ {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"},
+ {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"},
+ {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"},
+ {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"},
+ {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"},
+ {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"},
+ {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"},
+ {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"},
+ {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"},
+ {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"},
+ {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"},
+ {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"},
+ {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"},
+ {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"},
+ {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"},
+ {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"},
+ {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"},
+ {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"},
+ {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"},
+ {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"},
+ {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"},
+ {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"},
+ {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"},
+ {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"},
+ {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"},
+ {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"},
+ {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"},
+ {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"},
+ {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"},
+ {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"},
+ {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"},
+]
+
+[[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 = "coverage"
+version = "7.6.1"
+description = "Code coverage measurement for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"},
+ {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"},
+ {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"},
+ {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"},
+ {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"},
+ {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"},
+ {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"},
+ {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"},
+ {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"},
+ {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"},
+ {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"},
+ {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"},
+ {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"},
+ {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"},
+ {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"},
+ {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"},
+ {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"},
+ {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"},
+ {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"},
+ {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"},
+ {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"},
+ {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"},
+ {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"},
+ {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"},
+ {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"},
+ {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"},
+ {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"},
+ {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"},
+ {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"},
+ {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"},
+ {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"},
+ {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"},
+ {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"},
+ {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"},
+ {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"},
+ {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"},
+ {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"},
+ {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"},
+ {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"},
+ {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"},
+ {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"},
+ {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"},
+ {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"},
+ {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"},
+ {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"},
+ {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"},
+ {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"},
+ {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"},
+ {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"},
+ {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"},
+ {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"},
+ {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"},
+ {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"},
+ {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"},
+ {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"},
+ {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"},
+ {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"},
+ {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"},
+ {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"},
+ {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"},
+ {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"},
+ {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"},
+ {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"},
+ {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"},
+ {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"},
+ {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"},
+ {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"},
+ {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"},
+ {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"},
+ {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"},
+ {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"},
+ {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"},
]
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
[[package]]
name = "idna"
-version = "3.7"
+version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
-python-versions = ">=3.5"
+python-versions = ">=3.6"
+files = [
+ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
+ {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
+]
+
+[package.extras]
+all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+description = "brain-dead simple config-ini parsing"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
+ {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.8"
files = [
- {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
- {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
+ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
+ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
]
[[package]]
+name = "pluggy"
+version = "1.5.0"
+description = "plugin and hook calling mechanisms for python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+ {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
+]
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "py"
+version = "1.11.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+files = [
+ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
+]
+
+[[package]]
+name = "pytest"
+version = "6.2.5"
+description = "pytest: simple powerful testing with Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
+ {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
+]
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+py = ">=1.8.2"
+toml = "*"
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "4.1.0"
+description = "Pytest plugin for measuring coverage."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
+ {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
+]
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[package]]
+name = "pytest-env"
+version = "0.6.2"
+description = "py.test plugin that allows you to add environment variables."
+optional = false
+python-versions = "*"
+files = [
+ {file = "pytest-env-0.6.2.tar.gz", hash = "sha256:7e94956aef7f2764f3c147d216ce066bf6c42948bb9e293169b1b1c880a580c2"},
+]
+
+[package.dependencies]
+pytest = ">=2.6.0"
+
+[[package]]
name = "python-dotenv"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
@@ -137,18 +359,18 @@ cli = ["click (>=5.0)"]
[[package]]
name = "requests"
-version = "2.32.3"
+version = "2.32.4"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
files = [
- {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
- {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
+ {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"},
+ {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"},
]
[package.dependencies]
certifi = ">=2017.4.17"
-charset-normalizer = ">=2,<4"
+charset_normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
@@ -157,14 +379,66 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+description = "A lil' TOML parser"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
+ {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
+ {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
+ {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
+ {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
+ {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
+ {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
+ {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
+ {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
+ {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
+ {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
+ {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
+ {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
+ {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
+ {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
+ {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
+ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
+ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
+]
+
+[[package]]
name = "urllib3"
-version = "2.2.2"
+version = "2.2.3"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.8"
files = [
- {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
- {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
+ {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"},
+ {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"},
]
[package.extras]
@@ -176,4 +450,4 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.8"
-content-hash = "f223c5fdd1d6f40fab9f9dc2320a2ee4aab1f2a82eea8a9b020e63621a887845"
+content-hash = "7047f60901f32c32cd305def611a8a0ca351289b401fd7738be3a39302488856"
diff --git a/pyproject.toml b/pyproject.toml
index 6eee47d..1aa3254 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "pyvyos"
-version = "0.2.2"
+version = "0.3.0"
authors = [
{ name="Roberto Berto", email="463349+robertoberto@users.noreply.github.com" },
]
@@ -26,18 +26,40 @@ dependencies = [
"requests>=2.31.0,<3.0",
"python-dotenv>=1.0.1,<2.0"
]
-dev-dependencies = [
- "pytest>=6.2.2,<7.0"
-]
[tool.poetry]
name = "pyvyos"
description = "Python SDK for interacting with VyOS API"
authors = ["Roberto Berto <463349+robertoberto@users.noreply.github.com>"]
-version = "0.2.2"
+version = "0.3.0"
[tool.poetry.dependencies]
python = ">=3.8"
-requests = "^2.31.0"
+requests = "^2.32.0"
python-dotenv = "^1.0.1"
+[tool.poetry.group.dev.dependencies]
+pytest = "^6.2.5"
+pytest-cov = "^4.1.0"
+pytest-env = "^0.6.2"
+
+[tool.pytest.ini_options]
+testpaths = ["./tests"]
+log_cli = false
+log_cli_level = "DEBUG"
+filterwarnings = [
+ "ignore::DeprecationWarning:_pytest.assertion.rewrite",
+ "ignore::DeprecationWarning:ast"
+]
+env = [
+ "VYDEVICE_APIKEY='api_key'",
+ "VYDEVICE_HOSTNAME=192.168.56.100",
+ "VYDEVICE_PORT=443",
+ "VYDEVICE_PROTOCOL=https",
+ "VYDEVICE_VERIFY_SSL=False",
+]
+
+[tool.coverage.run]
+omit = [
+ "tests/*",
+] \ No newline at end of file
diff --git a/pyvyos/__init__.py b/pyvyos/__init__.py
index 7c327e5..44bd563 100644
--- a/pyvyos/__init__.py
+++ b/pyvyos/__init__.py
@@ -1,2 +1,2 @@
from .device import VyDevice
-from .device import ApiResponse \ No newline at end of file
+from .device import ApiResponse
diff --git a/pyvyos/device.py b/pyvyos/device.py
index d168b65..246a913 100644
--- a/pyvyos/device.py
+++ b/pyvyos/device.py
@@ -1,26 +1,10 @@
-import urllib3
-import requests
-import json
-import pprint
-from dataclasses import dataclass
-
-@dataclass
-class ApiResponse:
- """
- Represents an API response.
+import warnings
+from typing import List, Literal
- Attributes:
- status (int): The HTTP status code of the response.
- request (dict): The request payload sent to the API.
- result (dict): The data result of the API response.
- error (str): Any error message in case of a failed response.
- """
- status: int
- request: dict
- result: dict
- error: str
+from .rest import ApiResponse, RestClient
-class VyDevice:
+
+class VyDevice(RestClient):
"""
Represents a device for interacting with the VyOS API.
@@ -51,8 +35,8 @@ class VyDevice:
image_delete(name, url=None, file=None, path=[]): Delete a specific image.
show(path=[]): Show configuration information.
generate(path=[]): Generate configuration based on specified path.
- configure_set(path=[]): Sets configuration based on the specified path. This method is versatile, accepting
- either a single configuration path or a list of configuration paths. This flexibility
+ configure_set(path=[]): Sets configuration based on the specified path. This method is versatile, accepting
+ either a single configuration path or a list of configuration paths. This flexibility
allows for setting both individual and multiple configurations in a single operation.
configure_delete(path=[]): Delete configuration based on specified path.
config_file_save(file=None): Save the configuration to a file.
@@ -61,140 +45,37 @@ class VyDevice:
poweroff(path=["now"]): Power off the device.
"""
- def __init__(self, hostname, apikey, protocol='https', port=443, verify=True, timeout=10):
- """
- Initializes a VyDevice instance.
-
- Args:
- hostname (str): The hostname or IP address of the VyOS device.
- apikey (str): The API key for authentication.
- protocol (str, optional): The protocol to use (default is 'https').
- port (int, optional): The port to use (default is 443).
- verify (bool, optional): Whether to verify SSL certificates (default is True).
- timeout (int, optional): The request timeout in seconds (default is 10).
- """
- self.hostname = hostname
- self.apikey = apikey
- self.protocol = protocol
- self.port = port
- self.verify = verify
- self.timeout = timeout
-
- def _get_url(self, command):
- """
- Get the full URL for a specific API command.
-
- Args:
- command (str): The API command to construct the URL for.
-
- Returns:
- str: The full URL for the API command.
- """
- return f"{self.protocol}://{self.hostname}:{self.port}/{command}"
-
- def _get_payload(self, op, path=[], file=None, url=None, name=None):
- """
- Generate the payload for an API request.
-
- Args:
- op (str): The operation to perform in the API request.
- path (list, optional): The path elements for the API request. This can be a single list for a single
- configuration path or a list of lists for multiple configuration paths.
- file (str, optional): The file to include in the request (default is None).
- url (str, optional): The URL to include in the request (default is None).
- name (str, optional): The name to include in the request (default is None).
-
- Returns:
- dict: The payload for the API request.
- """
- # Adjusting the data structure based on whether path is single or multiple
- if isinstance(path[0], list): # Handling multiple paths
- data = [{'op': op, 'path': p} for p in path]
- else: # Handling a single path
- data = {'op': op, 'path': path}
-
- # Including the optional parameters if provided
- if file:
- if isinstance(data, list): # If data is a list of dicts (multiple paths)
- for d in data:
- d['file'] = file
- else: # If data is a single dict (single path)
- data['file'] = file
-
- if url:
- if isinstance(data, list):
- for d in data:
- d['url'] = url
- else:
- data['url'] = url
-
- if name:
- if isinstance(data, list):
- for d in data:
- d['name'] = name
- else:
- data['name'] = name
-
- payload = {
- 'data': json.dumps(data),
- 'key': self.apikey
- }
-
- return payload
-
-
- def _api_request(self, command, op, path=[], method='POST', file=None, url=None, name=None):
- """
- Make an API request.
-
- Args:
- command (str): The API command to execute.
- op (str): The operation to perform in the API request.
- path (list, optional): The path elements for the API request (default is an empty list).
- method (str, optional): The HTTP method to use for the request (default is 'POST').
- file (str, optional): The file to include in the request (default is None).
- url (str, optional): The URL to include in the request (default is None).
- name (str, optional): The name to include in the request (default is None).
-
- Returns:
- ApiResponse: An ApiResponse object representing the API response.
- """
- url = self._get_url(command)
- payload = self._get_payload(op, path=path, file=file, url=url, name=name)
-
- headers = {}
- error = False
- result = {}
-
- try:
- resp = requests.post(url, verify=self.verify, data=payload, timeout=self.timeout, headers=headers)
-
- if resp.status_code == 200:
- try:
- resp_decoded = resp.json()
-
- if resp_decoded['success'] == True:
- result = resp_decoded['data']
- error = False
- else:
- error = resp_decoded['error']
-
- except json.JSONDecodeError:
- error = 'json decode error'
- else:
- error = 'http error'
-
- status = resp.status_code
-
- except requests.exceptions.ConnectionError as e:
- error = 'connection error: ' + str(e)
- status = 0
-
- # Removing apikey from payload for security reasons
- del(payload['key'])
- return ApiResponse(status=status, request=payload, result=result, error=error)
-
- def retrieve_show_config(self, path=[]):
+ def __init__(
+ self,
+ hostname: str,
+ apikey: str,
+ protocol: Literal["http", "https"] = "https",
+ port: int = 443,
+ verify: bool = True,
+ timeout: int = 10,
+ ):
+ super().__init__(
+ hostname, apikey, protocol, int(port), bool(verify), int(timeout)
+ )
+ self._validate_params()
+
+ def _validate_params(
+ self,
+ ) -> None:
+ """Validação centralizada de parâmetros"""
+ if not isinstance(self.hostname, str) or len(self.hostname) < 3:
+ raise ValueError("Invalid hostname")
+
+ if self.protocol not in ("http", "https"):
+ raise ValueError("The protocol must be http or https")
+
+ if not 1 <= self.port <= 65535:
+ raise ValueError("Port out of valid range (1-65535)")
+
+ if self.timeout and self.timeout < 1:
+ warnings.warn("Timeout below 1s may cause instability", UserWarning)
+
+ def retrieve_show_config(self, path: List = None):
"""
Retrieve and show the device configuration.
@@ -204,9 +85,12 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="retrieve", op='showConfig', path=path, method="POST")
- def retrieve_return_values(self, path=[]):
+ return self._api_request(
+ command="retrieve", op="showConfig", path=path, method="POST"
+ )
+
+ def retrieve_return_values(self, path: List = None):
"""
Retrieve and return specific configuration values.
@@ -216,9 +100,11 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="retrieve", op='returnValues', path=path, method="POST")
+ return self._api_request(
+ command="retrieve", op="returnValues", path=path, method="POST"
+ )
- def reset(self, path=[]):
+ def reset(self, path: List = None):
"""
Reset a specific configuration element.
@@ -228,7 +114,7 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="reset", op='reset', path=path, method="POST")
+ return self._api_request(command="reset", op="reset", path=path, method="POST")
def image_add(self, url=None, file=None, path=[]):
"""
@@ -242,7 +128,7 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="image", op='add', url=url, method="POST")
+ return self._api_request(command="image", op="add", url=url, method="POST")
def image_delete(self, name, url=None, file=None, path=[]):
"""
@@ -257,9 +143,9 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="image", op='delete', name=name, method="POST")
+ return self._api_request(command="image", op="delete", name=name, method="POST")
- def show(self, path=[]):
+ def show(self, path: List = None):
"""
Show configuration information.
@@ -269,9 +155,9 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="show", op='show', path=path, method="POST")
+ return self._api_request(command="show", op="show", path=path, method="POST")
- def generate(self, path=[]):
+ def generate(self, path: List = None):
"""
Generate configuration based on the given path.
@@ -281,9 +167,11 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="generate", op='generate', path=path, method="POST")
+ return self._api_request(
+ command="generate", op="generate", path=path, method="POST"
+ )
- def configure_set(self, path=[]):
+ def configure_set(self, path: List = None):
"""
Set configuration based on the given path.
@@ -293,10 +181,11 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="configure", op='set', path=path, method="POST")
+ return self._api_request(
+ command="configure", op="set", path=path, method="POST"
+ )
-
- def configure_delete(self, path=[]):
+ def configure_delete(self, path: List = None):
"""
Delete configuration based on the given path.
@@ -306,7 +195,22 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="configure", op='delete', path=path, method="POST")
+ return self._api_request(
+ command="configure", op="delete", path=path, method="POST"
+ )
+
+ def configure_multiple_op(self, op_path: List = None):
+ """
+ Set configuration based on the given {operation : path} for multiple operation.
+
+ Args:
+ op_path (list): The path elements for configuration deletion or/and setting.
+ eg: [{'op': 'delete', 'path': [...]}, {'op': 'set', 'path': [...]}]
+
+ Returns:
+ ApiResponse: An ApiResponse object representing the API response.
+ """
+ return self._api_request(command="configure", op="", path=op_path)
def config_file_save(self, file=None):
"""
@@ -318,7 +222,9 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="config-file", op='save', file=file, method="POST")
+ return self._api_request(
+ command="config-file", op="save", file=file, method="POST"
+ )
def config_file_load(self, file=None):
"""
@@ -330,9 +236,11 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="config-file", op='load', file=file, method="POST")
+ return self._api_request(
+ command="config-file", op="load", file=file, method="POST"
+ )
- def reboot(self, path=["now"]):
+ def reboot(self, path: List = None):
"""
Reboot the device.
@@ -342,9 +250,14 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="reboot", op='reboot', path=path, method="POST")
-
- def poweroff(self, path=["now"]):
+ if path is None:
+ path = ["now"]
+
+ return self._api_request(
+ command="reboot", op="reboot", path=path, method="POST"
+ )
+
+ def poweroff(self, path: List = None):
"""
Power off the device.
@@ -354,4 +267,9 @@ class VyDevice:
Returns:
ApiResponse: An ApiResponse object representing the API response.
"""
- return self._api_request(command="poweroff", op='poweroff', path=path, method="POST")
+ if path is None:
+ path = ["now"]
+
+ return self._api_request(
+ command="poweroff", op="poweroff", path=path, method="POST"
+ )
diff --git a/pyvyos/rest.py b/pyvyos/rest.py
new file mode 100644
index 0000000..04c786e
--- /dev/null
+++ b/pyvyos/rest.py
@@ -0,0 +1,316 @@
+import json
+from abc import ABC
+from dataclasses import dataclass
+from typing import Tuple, List, Union, Dict, Any, Optional
+
+import requests
+from requests import Response
+from requests.exceptions import (
+ HTTPError,
+ ConnectionError,
+ Timeout,
+ RequestException,
+ JSONDecodeError,
+)
+
+
+@dataclass
+class ApiResponse:
+ """
+ Represents an API response.
+
+ Attributes:
+ status (int): The HTTP status code of the response.
+ request (dict): The request payload sent to the API.
+ result (dict): The data result of the API response.
+ error (str): Any error message in case of a failed response.
+ """
+
+ status: int
+ request: dict
+ result: dict
+ error: str
+
+
+class RestClient(ABC):
+ """Secure REST client for integration with VyOS device APIs"""
+
+ hostname: str
+ apikey: str
+ protocol: str
+ port: int
+ verify: bool
+ timeout: int
+
+ def __init__(
+ self,
+ hostname: str,
+ apikey: str,
+ protocol: str = "https",
+ port: int = 443,
+ verify: bool = False,
+ timeout: int = 10,
+ ):
+ """
+ Args:
+ hostname: VyOS device address
+ apikey: API key for authentication
+ protocol: Protocol (http/https)
+ port: Access port
+ verify: Verify SSL certificates
+ timeout: Request timeout in seconds
+ """
+ super().__init__()
+ self.hostname = hostname
+ self.apikey = apikey
+ self.protocol = protocol
+ self.port = port
+ self.verify = verify
+ self.timeout = timeout
+
+ def _get_url(self, command):
+ """
+ Get the full URL for a specific API command.
+
+ Args:
+ command (str): The API command to construct the URL for.
+
+ Returns:
+ str: The full URL for the API command.
+ """
+ return f"{self.protocol}://{self.hostname}:{self.port}/{command}"
+
+ def _get_payload(
+ self,
+ op: Optional[str] = None,
+ path: Union[List[str], List[List[str]]] = None,
+ file: Optional[str] = None,
+ url: Optional[str] = None,
+ name: Optional[str] = None,
+ ) -> Dict[str, Any]:
+ """
+ Generates API request payload based on specified operations and parameters.
+
+ Parameters:
+ op (str, optional): Operation to perform (e.g., 'set', 'delete')
+ path (Union[List[str], List[List[str]]], optional):
+ Configuration path(s) for the API. Can be:
+ - Single path as string list
+ - Multiple paths as list of string lists
+ file (str, optional): File path for upload
+ url (str, optional): External resource URL
+ name (str, optional): Resource name
+
+ Returns:
+ Dict: Formatted API payload containing:
+ - data: JSON-serialized operations
+ - key: API key
+
+ Raises:
+ ValueError: If required parameters are missing or invalid
+ """
+
+ def _create_operations() -> Union[List[Dict], Dict]:
+ """Creates operation structure based on parameters."""
+ if not op:
+ if not all(isinstance(p, dict) for p in path):
+ raise ValueError(
+ "Path must contain dictionaries when no operation is specified"
+ )
+ return path
+
+ normalized_paths = path or []
+ is_multiple = (
+ isinstance(normalized_paths[0], list) if normalized_paths else False
+ )
+
+ if is_multiple:
+ return [{"op": op, "path": p} for p in normalized_paths]
+ return {"op": op, "path": normalized_paths}
+
+ def _add_optional_params(
+ data: Union[List[Dict], Dict], params: Dict[str, str]
+ ) -> Union[List[Dict], Dict]:
+ """Adds optional parameters to operation structure."""
+ if isinstance(data, list):
+ return [{**item, **params} for item in data]
+ return {**data, **params}
+
+ # Initial validation
+ if not op and not path:
+ raise ValueError(
+ "Must provide either 'op' or pre-formatted operations in 'path'"
+ )
+
+ operations = _create_operations()
+ optional_params = {
+ k: v for k, v in zip(["file", "url", "name"], [file, url, name]) if v
+ }
+
+ if optional_params:
+ operations = _add_optional_params(operations, optional_params)
+
+ return {"data": json.dumps(operations), "key": self.apikey}
+
+ def _api_request(
+ self,
+ command: str,
+ op: Optional[str] = None,
+ path: Optional[List[str]] = None,
+ method: str = "POST",
+ file: Optional[str] = None,
+ resource_url: Optional[str] = None,
+ name: Optional[str] = None,
+ ):
+ """
+ Executes an API request with proper error handling and security measures.
+
+ Parameters:
+ command (str): API endpoint command to execute
+ op (str, optional): Operation type (e.g., 'create', 'update', 'delete')
+ path (List[str], optional): Hierarchical path for resource location
+ method (str): HTTP method (GET/POST/PUT/DELETE). Default: POST
+ file (str, optional): Local file path for file uploads
+ resource_url (str, optional): External resource URL reference
+ name (str, optional): Resource identifier name
+
+ Returns:
+ ApiResponse: Structured response containing:
+ - status: HTTP status code
+ - request: Sanitized request payload
+ - result: Parsed response data
+ - error: Error message if applicable
+
+ Raises:
+ ConnectionError: Network communication failures
+ Timeout: Server response timeout
+ ValueError: Invalid parameter combinations
+ """
+
+ def _prepare_request() -> Dict[str, Any]:
+ """Constructs request components with validation."""
+ if not command:
+ raise ValueError("API command is required")
+ return {
+ "url": self._get_url(command),
+ "method": method,
+ "verify": self.verify,
+ "timeout": self.timeout,
+ "payload": self._get_payload(
+ op, path=path, file=file, url=resource_url, name=name
+ ),
+ "headers": {},
+ }
+
+ # Initialize mutable defaults safely
+ path = path or []
+
+ # Request execution flow
+ request_components = _prepare_request()
+ response = self._execute_request(**request_components)
+ status, result, error = self._validate_response(response)
+
+ # Sanitize sensitive data before returning
+ sanitized_payload = request_components["payload"].copy()
+ sanitized_payload.pop("key", None)
+
+ return ApiResponse(
+ status=status, request=sanitized_payload, result=result, error=error
+ )
+
+ @classmethod
+ def _execute_request(
+ cls,
+ url: str,
+ method: str,
+ verify: bool,
+ timeout: int,
+ payload: Dict,
+ headers: Dict,
+ ) -> requests.Response:
+ """Sends HTTP request with error handling."""
+ try:
+ return requests.request(
+ method=method.upper(),
+ url=url,
+ verify=verify,
+ data=payload,
+ timeout=timeout,
+ headers=headers,
+ )
+ except Timeout:
+ raise Timeout(f"Request timed out after {timeout} seconds")
+ except RequestException as e:
+ raise ConnectionError(f"Network error: {str(e)}")
+
+ @classmethod
+ def _validate_response(
+ cls, resp: Response
+ ) -> Tuple[Optional[int], Dict[str, Any], Union[str, bool]]:
+ """
+ Validates and processes API responses with comprehensive error handling.
+
+ Parameters:
+ resp (Response): HTTP response object from requests library
+
+ Returns:
+ Tuple containing:
+ - status (int | None): HTTP status code
+ - result (dict): Parsed successful response data
+ - error (str | bool): Error message (False indicates success)
+
+ Raises:
+ ValueError: For invalid response structures
+ RuntimeError: For unexpected parsing failures
+
+ Processing Flow:
+ 1. HTTP Status Code Validation
+ 2. Response Body Parsing
+ 3. API Success/Failure Flag Check
+ 4. Error Message Extraction
+ 5. Fallback Error Handling
+ """
+ status: Optional[int] = None
+ result: Dict[str, Any] = {}
+ error: Union[str, bool] = False
+
+ def _validate_schema(response_json: Dict[str, Any]) -> None:
+ """Validates response structure against API contract."""
+ required_keys = {"success", "data", "error"}
+ if isinstance(response_json, dict) and not required_keys.issubset(response_json.keys()):
+ missing = required_keys - response_json.keys()
+ raise ValueError(f"Invalid response structure. Missing keys: {missing}")
+
+ try:
+ # Validate HTTP status code
+ resp.raise_for_status()
+ status = resp.status_code
+
+ # Parse and validate JSON structure
+ resp_decoded = resp.json()
+ _validate_schema(resp_decoded)
+
+ # Process API business logic
+ if resp_decoded["success"]:
+ result = resp_decoded["data"]
+ else:
+ error = f"API Error {status}: {resp_decoded['error']}"
+
+ except JSONDecodeError as exc:
+ error = f"Invalid response format: {str(exc)}"
+ status = resp.status_code if resp is not None and isinstance(resp, Response) else 500
+
+
+ except HTTPError as exc:
+ response = exc.response
+ status = response.status_code if response is not None and isinstance(response, Response) else 500
+ error = f"HTTP Error {status}: {response.text[:200] if response else 'Unknown error'}"
+
+ except ValueError as exc:
+ error = f"Validation Error: {str(exc)}"
+
+ except Exception as exc:
+ error = f"Unexpected error: {str(exc)}"
+ status = 500
+
+ return status, result, error
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..5371206
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,22 @@
+import os
+
+import pytest
+
+from pyvyos import VyDevice
+
+
+@pytest.fixture(scope="module")
+def config_device():
+ return {
+ "hostname": os.getenv("VYDEVICE_HOSTNAME", "localhost"),
+ "apikey": os.getenv("VYDEVICE_APIKEY", "api_key"),
+ "port": os.getenv("VYDEVICE_PORT", 443),
+ "protocol": os.getenv("VYDEVICE_PROTOCOL", "https"),
+ "verify": os.getenv("VYDEVICE_VERIFY_SSL", False),
+ "timeout": 0,
+ }
+
+
+@pytest.fixture(scope="module")
+def test_device(config_device):
+ return VyDevice(**config_device)
diff --git a/tests/modules/__init__.py b/tests/modules/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/modules/__init__.py
diff --git a/tests/modules/test_vy_device.py b/tests/modules/test_vy_device.py
new file mode 100644
index 0000000..096e924
--- /dev/null
+++ b/tests/modules/test_vy_device.py
@@ -0,0 +1,353 @@
+import random
+import string
+
+import pytest
+import requests
+
+from pyvyos import VyDevice
+from pyvyos.rest import RestClient
+
+
+def test_device_invalid_configuration():
+ with pytest.raises(ValueError):
+ VyDevice(
+ "tst",
+ "123",
+ "123",
+ "123",
+ 2,
+ )
+
+
+def test_device_configuration(test_device):
+ assert isinstance(test_device.hostname, str)
+ assert isinstance(test_device.apikey, str)
+ assert isinstance(test_device.port, int)
+ assert isinstance(test_device.protocol, str)
+ assert isinstance(test_device.verify, bool)
+
+
+def test_device_retrieve_show_config(monkeypatch, test_device):
+ def mock_retrieve_show_config(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.json = lambda: {
+ "success": True,
+ "data": {
+ "config-management": {"commit-revisions": "100"},
+ "host-name": "pyvyos-device",
+ "name-server": "eth0",
+ },
+ "error": None,
+ }
+
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_retrieve_show_config,
+ )
+
+ api_resp = test_device.retrieve_show_config(["system"])
+ assert api_resp.result
+ assert isinstance(api_resp.result, dict)
+
+
+def test_device_configure_set(monkeypatch, test_device):
+ def mock_configure_set(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.json = lambda: {
+ "success": True,
+ "data": {},
+ "error": None,
+ }
+
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_configure_set,
+ )
+
+ api_resp = test_device.configure_set(
+ path=["interfaces", "dummy", "dum1", "address", "192.168.56.100/24"]
+ )
+ assert isinstance(api_resp.result, dict)
+
+
+def test_device_retrieve_return_values(monkeypatch, test_device):
+ def mock_retrieve_return_values(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.json = lambda: {
+ "success": True,
+ "data": ["192.168.56.100/24"],
+ "error": None,
+ }
+
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_retrieve_return_values,
+ )
+
+ api_resp = test_device.retrieve_return_values(
+ path=["interfaces", "dummy", "dum1", "address", "192.168.56.100/24"]
+ )
+ assert api_resp.result
+ assert isinstance(api_resp.result, list)
+
+
+def test_device_configure_delete(monkeypatch, test_device):
+ def mock_configure_delete(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.json = lambda: {
+ "success": True,
+ "data": {},
+ "error": None,
+ }
+
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_configure_delete,
+ )
+
+ api_resp = test_device.configure_delete(path=["interfaces", "dummy", "dum1"])
+ assert isinstance(api_resp.result, dict)
+
+
+def test_device_generate(monkeypatch, test_device):
+ def mock_configure_delete(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.json = lambda: {
+ "success": True,
+ "data": f"""'Generating public/private rsa key pair.\n'
+ 'Your identification has been saved in '
+ '/tmp/key_{keyrand}\n'
+ 'Your public key has been saved in '
+ '/tmp/key_39skBBzxc5NvIfE8GzQ7.pub\n'
+ 'The key fingerprint is:\n'
+ 'SHA256:7vfukWp093kyngUB+iH6'
+ 'root@pyvyos-device\n'
+ "The key's randomart image is:\n"
+ '+---[RSA 3072]----+\n'
+ '| . . |\n'
+ '| o + . - . |\n'
+ '| . = = = + |\n'
+ '| o + O - o.|\n'
+ '| . S B . .oo|\n'
+ '| o *. ..oo.|\n'
+ '| . ....o. Eo|\n'
+ '| . +...+ .o+o|\n'
+ '| ..o=o++.oo.|\n'
+ '+----[SHA256]-----+\n'""",
+ "error": None,
+ }
+
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_configure_delete,
+ )
+
+ randstring = "".join(
+ random.choice(string.ascii_letters + string.digits) for _ in range(20)
+ )
+ keyrand = f"/tmp/key_{randstring}"
+
+ api_resp = test_device.generate(path=["ssh", "client-key", keyrand])
+ assert isinstance(api_resp.result, str)
+ assert keyrand in api_resp.result
+
+
+def test_device_show(monkeypatch, test_device):
+ def mock_show(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.json = lambda: {
+ "success": True,
+ "data": """'Name Default boot Running \n'
+ '------------------------ -------------- --------- \n'
+ '1.5-rolling-202407300021 Yes Yes \n'
+ """,
+ "error": None,
+ }
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_show,
+ )
+
+ api_resp = test_device.show(path=["system", "image"])
+ assert isinstance(api_resp.result, str)
+
+
+def test_device_reset(monkeypatch, test_device):
+ def mock_reset(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.json = lambda: {
+ "success": True,
+ "data": """'conntrack-sync is not configured!\n'""",
+ "error": None,
+ }
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_reset,
+ )
+
+ api_resp = test_device.reset(path=["system", "image"])
+ assert isinstance(api_resp.result, str)
+
+
+def test_device_config_file_save(monkeypatch, test_device):
+ def mock_config_file_save(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.json = lambda: {
+ "success": True,
+ "data": "",
+ "error": None,
+ }
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_config_file_save,
+ )
+
+ api_resp = test_device.config_file_save(file="/config/teste.config")
+ assert isinstance(api_resp.result, str)
+
+
+def test_device_config_file_load(monkeypatch, test_device):
+ def mock_config_file_save(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.json = lambda: {
+ "success": True,
+ "data": None,
+ "error": None,
+ }
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_config_file_save,
+ )
+
+ api_resp = test_device.config_file_load(file="/config/teste.config")
+ assert api_resp
+
+
+def test_device_configure_multiple_op(monkeypatch, test_device):
+ def mock_config_file_save(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.json = lambda: {
+ "success": True,
+ "data": None,
+ "error": None,
+ }
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_config_file_save,
+ )
+
+ api_resp = test_device.configure_multiple_op(
+ op_path=[
+ {
+ "op": "set",
+ "path": ["interfaces", "dummy", "dum1", "address", "192.168.56.100/24"],
+ },
+ {
+ "op": "delete",
+ "path": ["interfaces", "dummy", "dum1"],
+ },
+ ]
+ )
+ assert api_resp
+
+
+def test_device_invalid_path_multiple_op(monkeypatch, test_device):
+ def mock_configure_set(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.json = lambda: {
+ "success": True,
+ "data": {},
+ "error": None,
+ }
+
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_configure_set,
+ )
+ with pytest.raises(ValueError):
+ test_device.configure_multiple_op(op_path="interfaces")
+
+
+def test_device_invalid_request(monkeypatch, test_device):
+ def mock_invalid_request(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 400
+ response.json = lambda: {
+ "success": False,
+ "data": None,
+ "error": "Bad Request",
+ }
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_invalid_request,
+ )
+
+ api_resp = test_device.show(path=["invalid", "path"])
+ assert not api_resp.result
+ assert api_resp.status == 400
+ assert "HTTP Error" in api_resp.error
+
+
+def test_device_error_json_response(monkeypatch, test_device):
+ def mock_error_json_response(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response.data = "This is not a JSON response"
+ return response
+
+ monkeypatch.setattr(
+ RestClient,
+ "_execute_request",
+ mock_error_json_response,
+ )
+
+ api_resp = test_device.show(path=["system", "image"])
+ assert not api_resp.result
+ assert "Invalid response format" in api_resp.error \ No newline at end of file
diff --git a/tests/test_vy_device.py b/tests/test_vy_device.py
deleted file mode 100644
index 84ff0ed..0000000
--- a/tests/test_vy_device.py
+++ /dev/null
@@ -1,116 +0,0 @@
-import sys
-import os
-import unittest
-from pyvyos.device import VyDevice
-from pyvyos.device import ApiResponse
-from dotenv import load_dotenv
-import os
-import pprint
-import random, string
-
-load_dotenv()
-
-hostname = os.getenv('VYDEVICE_HOSTNAME')
-apikey = os.getenv('VYDEVICE_APIKEY')
-port = os.getenv('VYDEVICE_PORT')
-protocol = os.getenv('VYDEVICE_PROTOCOL')
-verify = os.getenv('VYDEVICE_VERIFY_SSL')
-if verify == "False":
- verify = False
-else:
- verify = True
-
-
-class TestVyDevice(unittest.TestCase):
- def setUp(self):
- self.device = VyDevice(hostname=hostname, apikey=apikey, port=port, protocol=protocol, verify=verify)
-
- def test_001_retrieve_show_config(self):
- response = self.device.retrieve_show_config([])
- pprint.pprint(response)
-
- self.assertEqual(response.status, 200)
- self.assertIsNotNone(response.result)
- self.assertFalse(response.error)
-
- def test_010_configure_set_interface(self):
- response = self.device.configure_set(path=["interfaces", "dummy", "dum1", "address", "192.168.140.1/24"])
- #pprint.pprint(response)
-
- self.assertEqual(response.status, 200)
- self.assertIsNone(response.result)
- self.assertFalse(response.error)
-
- def test_011_retrieve_return_values(self):
- response = self.device.retrieve_return_values(path=["interfaces", "dummy", "dum1", "address"])
- self.assertEqual(response.status, 200)
- self.assertIsNotNone(response.result)
- self.assertFalse(response.error)
- self.assertEqual(response.result, ['192.168.140.1/24'])
-
- def test_020_configure_delete_interface(self):
- response = self.device.configure_delete(path=["interfaces", "dummy", "dum1"])
- pprint.pprint(response)
-
- self.assertEqual(response.status, 200)
- self.assertIsNone(response.result)
- self.assertFalse(response.error)
-
- def test_050_generate(self):
- randstring = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(20))
- keyrand = f'/tmp/key_{randstring}'
- response = self.device.generate(path=["ssh", "client-key", keyrand])
- pprint.pprint(response)
-
- self.assertEqual(response.status, 200)
- self.assertIsNotNone(response.result)
- self.assertFalse(response.error)
-
- def test_100_show(self):
- response = self.device.show(path=["system", "image"])
- pprint.pprint(response)
-
- self.assertEqual(response.status, 200)
- self.assertIsNotNone(response.result)
- self.assertFalse(response.error)
-
- def test_200_reset(self):
- response = self.device.reset(path=["conntrack-sync", "internal-cache"])
- pprint.pprint(response)
-
- self.assertEqual(response.status, 200)
- self.assertIsNotNone(response.result)
- self.assertFalse(response.error)
-
- def test_300_config_file_save(self):
- response = self.device.config_file_save(file="/config/test300.config")
- pprint.pprint(response)
-
- self.assertEqual(response.status, 200)
- self.assertIsNotNone(response.result)
- self.assertFalse(response.error)
-
- def test_301_config_file_save(self):
- response = self.device.config_file_save()
- pprint.pprint(response)
-
- self.assertEqual(response.status, 200)
- self.assertIsNotNone(response.result)
- self.assertFalse(response.error)
-
-
- def test_302_config_file_load(self):
- response = self.device.config_file_load(file="/config/test300.config")
- pprint.pprint(response)
-
- self.assertEqual(response.status, 200)
- self.assertIsNone(response.result)
- self.assertFalse(response.error)
-
-
- def tearDown(self):
- pass
-
-if __name__ == '__main__':
- unittest.main()
-
diff --git a/vagrant/.env.example b/vagrant/.env.example
index 36bebf8..905f259 100644
--- a/vagrant/.env.example
+++ b/vagrant/.env.example
@@ -1,13 +1,12 @@
-
# vyos https api key
-VYDEVICE_APIKEY=your_api_key
+VYDEVICE_APIKEY="your_api_key"
# vyos network interface
VYDEVICE_IP=192.168.56.10
VYDEVICE_NETMASK=255.255.255.0
+VYDEVICE_HTTPS_PORT=443
# HOST_IP is the ip address of the host machine of the virtualbox
# use your windows network interface for virtualbox running on windows host and using wsl2 for vagrant
-HOST_IP=192.168.0.114
-# or use 127.0.0.1, virtualbox running on linux host or windows host
-# HOST_IP=127.0.0.1 \ No newline at end of file
+VYDEVICE_HOST_IP=192.168.0.114
+# use 127.0.0.1, virtualbox running on linux host or windows host
diff --git a/vagrant/VAGRANT.md b/vagrant/VAGRANT.md
index 69a28f5..e6312e2 100644
--- a/vagrant/VAGRANT.md
+++ b/vagrant/VAGRANT.md
@@ -13,7 +13,6 @@ If you want to only use pyvyos you dont need to install vagrant
3. Install Vagrant plugins
```
vagrant plugin install vagrant-vyos
-vagrant plugin install vagrant-dotenv
```
4. Install mkisofs
@@ -21,11 +20,16 @@ vagrant plugin install vagrant-dotenv
sudo apt install genisoimage
```
-5. Run vagrant up
+5. Create .env file
+```
+mv .env.example .env
+```
+
+6. Run vagrant up
```
vagrant up
```
-6. Run vagrant ssh
+7. Run vagrant ssh
```
vagrant ssh
```
diff --git a/vagrant/Vagrantfile b/vagrant/Vagrantfile
index 1b20c97..01dd23e 100644
--- a/vagrant/Vagrantfile
+++ b/vagrant/Vagrantfile
@@ -1,61 +1,47 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
-# Documentation:
+# Documentation:
# - read VAGRANT.md
# - need vagrant plugin install vagrant-vyos
-Vagrant.configure("2") do |config|
- # enable the environment variables
- # need vagrant plugin install vagrant-dotenv
- config.env.enable
-
- # vm to deploy tests
- config.vm.define "pyvyos" do |pyvyos|
- pyvyos.vm.box = "vyos/current"
- pyvyos.vm.hostname = "pyvyos"
-
- # network configuration of eth1
- pyvyos.vm.network "private_network", ip: ENV['VYDEVICE_IP'], netmask: ENV['VYDEVICE_NETMASK']
- pyvyos.ssh.host = ENV['HOST_IP']
- # nat port forwarding
- pyvyos.vm.network "forwarded_port", guest: 443, host: 8433, id: "https", auto_correct: true, protocol: "tcp", host_ip: ENV['HOST_IP']
- pyvyos.vm.network "forwarded_port", guest: 22, host: 2022, id: "ssh", auto_correct: true, protocol: "tcp", host_ip: ENV['HOST_IP']
-
- # ssh configuration default username and password of vyos/current is vyos / vyos
- # if you want to change the default password, you can change in provision script
- # also, you can disable ssh password in provision script and use only ssh key
- pyvyos.ssh.username = "vyos"
- pyvyos.ssh.password = "vyos"
+env_vars = File.readlines('.env').each_with_object({}) do |line, hash|
+ next if line.strip.empty? || line.start_with?('#')
+ key, value = line.strip.split('=', 2)
+ hash[key.strip] = value.strip.gsub(/(^['"]|['"]$)/, '')
+end
- # vagrant will insert the ssh key in the vm automatically, so password authentication after
- # first boot is not necessary
- pyvyos.ssh.insert_key = true
+HTTPS_PORT = env_vars['VYDEVICE_HTTPS_PORT']
+API_KEY = env_vars['VYDEVICE_APIKEY']
+NETMASK = env_vars['VYDEVICE_NETMASK']
+VYOS_VM_IP = env_vars['VYDEVICE_IP']
+VYOS_VM_HOST_IP = env_vars['VYDEVICE_HOST_IP']
- # mkdir /opt/vyatta/config/certs
- # chmod 0700 /opt/vyatta/config/certs
- # generate pki certificate self-signed file /opt/vyatta/config/certs/certself
- # set pki certificate certself certificate "$(cat /opt/vyatta/config/certs/certself.pem | tail -n +2 | head -n -1 | tr -d '\n')"
- # set pki certificate certself private key "$(cat /opt/vyatta/config/certs/certself.key | tail -n +2 | head -n -1 | tr -d '\n')"
+$script = <<-SHELL
+ cfg=/opt/vyatta/sbin/vyatta-cfg-cmd-wrapper
+ $cfg begin
+ $cfg set service https api debug
+ $cfg set service https api keys id apikey key #{API_KEY}
+ $cfg set service https listen-address $1
+ $cfg set service https port #{HTTPS_PORT}
+ $cfg commit
+ $cfg end
+SHELL
- # generate pki ca file /opt/vyatta/config/certs/certca
- # set pki ca certca certificate "$(cat /opt/vyatta/config/certs/certca.pem | tail -n +2 | head -n -1 | tr -d '\n')"
- # set pki ca certca private key "$(cat /opt/vyatta/config/certs/certca.key | tail -n +2 | head -n -1 | tr -d '\n')"
-
- # shell script to provision the vyos vm
- pyvyos.vm.provision "shell", inline: <<-SHELL
- #!/bin/vbash
- source /opt/vyatta/etc/functions/script-template
- configure
- set service https listen-address '#{ENV['VYDEVICE_IP']}'
- set service https api keys id 'apikey' key '#{ENV['VYDEVICE_APIKEY']}'
- set service https api debug
- set service https api strict
- commit
- save
- exit
- SHELL
- end
+Vagrant.configure("2") do |config|
+ # Device VyOS
+ config.vm.define "pyvyos_device" do |pyvyos|
+ pyvyos.vm.box = "vyos/current"
+ pyvyos.vm.hostname = "pyvyos-device"
+ pyvyos.vm.network "private_network", ip: VYOS_VM_IP , netmask: NETMASK
+ pyvyos.ssh.host = VYOS_VM_HOST_IP
+ pyvyos.vm.network "forwarded_port", guest: 443, host: 8433, id: "https", auto_correct: true, protocol: "tcp", host_ip: VYOS_VM_HOST_IP
+ pyvyos.vm.network "forwarded_port", guest: 22, host: 2022, id: "ssh", auto_correct: true, protocol: "tcp", host_ip: VYOS_VM_HOST_IP
+ pyvyos.ssh.username = "vyos"
+ pyvyos.ssh.password = "vyos"
+ pyvyos.ssh.insert_key = false
+ pyvyos.vm.provision "shell", inline: $script, args: [VYOS_VM_IP]
end
+end \ No newline at end of file