From fe25ca51258e52cd184256d83a4a52f66f0a9e21 Mon Sep 17 00:00:00 2001 From: soumo033 Date: Wed, 20 Aug 2025 20:39:19 +0530 Subject: [PATCH 1/2] poetry lock --- poetry.lock | 1145 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1145 insertions(+) create mode 100644 poetry.lock diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..b9c0ab5 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1145 @@ +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.5.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version < \"3.13\"" +files = [ + {file = "annotated_types-0.5.0-py3-none-any.whl", hash = "sha256:58da39888f92c276ad970249761ebea80ba544b77acddaa1a4d6cf78287d45fd"}, + {file = "annotated_types-0.5.0.tar.gz", hash = "sha256:47cdc3490d9ac1506ce92c7aaa76c579dc3509ff11e098fc867e5130ab7be802"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "cachetools" +version = "6.1.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e"}, + {file = "cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587"}, +] + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "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" +groups = ["dev"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\" or python_version >= \"3.13\"" +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 = "distlib" +version = "0.4.0" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.12.2" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] + +[package.extras] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "filelock" +version = "3.19.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, + {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, +] + +[[package]] +name = "flake8" +version = "5.0.4" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.6.1" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" + +[[package]] +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" + +[[package]] +name = "importlib-metadata" +version = "4.2.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +markers = "python_version == \"3.7\"" +files = [ + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, +] + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] +testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\" and python_version < \"3.10\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy ; platform_python_implementation != \"PyPy\" and python_version < \"3.10\""] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +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 = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.4.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8"}, + {file = "mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878"}, + {file = "mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd"}, + {file = "mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc"}, + {file = "mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462"}, + {file = "mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258"}, + {file = "mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2"}, + {file = "mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7"}, + {file = "mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01"}, + {file = "mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b"}, + {file = "mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b"}, + {file = "mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7"}, + {file = "mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042"}, + {file = "mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3"}, + {file = "mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6"}, + {file = "mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f"}, + {file = "mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828"}, + {file = "mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3"}, + {file = "mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816"}, + {file = "mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c"}, + {file = "mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f"}, + {file = "mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4"}, + {file = "mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "platformdirs" +version = "2.6.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "platformdirs-2.6.2-py3-none-any.whl", hash = "sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490"}, + {file = "platformdirs-2.6.2.tar.gz", hash = "sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + +[[package]] +name = "platformdirs" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "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.*" +groups = ["dev"] +markers = "python_version < \"3.13\"" +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 = "pycodestyle" +version = "2.9.1" +description = "Python style guide checker" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + +[[package]] +name = "pydantic" +version = "2.5.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version < \"3.13\"" +files = [ + {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, + {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +importlib-metadata = {version = "*", markers = "python_version == \"3.7\""} +pydantic-core = "2.14.6" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic" +version = "2.11.7" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.14.6" +description = "" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version < \"3.13\"" +files = [ + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, + {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, + {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, + {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, + {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, + {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, + {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, + {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, + {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, + {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, + {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, + {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, + {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, + {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, + {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, + {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pyflakes" +version = "2.5.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyproject-api" +version = "1.9.1" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948"}, + {file = "pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335"}, +] + +[package.dependencies] +packaging = ">=25" + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=3.2)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.3.5)", "pytest-cov (>=6.1.1)", "pytest-mock (>=3.14)", "setuptools (>=80.3.1)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest" +version = "8.4.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] +markers = {dev = "python_version < \"3.13\""} + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tox" +version = "3.28.0" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, + {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, +] + +[package.dependencies] +colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} +filelock = ">=3.0.0" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +packaging = ">=14" +pluggy = ">=0.12.0" +py = ">=1.4.17" +six = ">=1.14.0" +tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} +virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" + +[package.extras] +docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3) ; python_version < \"3.4\"", "psutil (>=5.6.1) ; platform_python_implementation == \"cpython\"", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] + +[[package]] +name = "tox" +version = "4.28.4" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "tox-4.28.4-py3-none-any.whl", hash = "sha256:8d4ad9ee916ebbb59272bb045e154a10fa12e3bbdcf94cc5185cbdaf9b241f99"}, + {file = "tox-4.28.4.tar.gz", hash = "sha256:b5b14c6307bd8994ff1eba5074275826620325ee1a4f61316959d562bfd70b9d"}, +] + +[package.dependencies] +cachetools = ">=6.1" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.18" +packaging = ">=25" +platformdirs = ">=4.3.8" +pluggy = ">=1.6" +pyproject-api = ">=1.9.1" +virtualenv = ">=20.31.2" + +[[package]] +name = "typed-ast" +version = "1.5.5" +description = "a fork of Python 2 and 3 ast modules with type comment support" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version == \"3.7\"" +files = [ + {file = "typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b"}, + {file = "typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769"}, + {file = "typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d"}, + {file = "typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d"}, + {file = "typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee"}, + {file = "typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88"}, + {file = "typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9"}, + {file = "typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8"}, + {file = "typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b"}, + {file = "typed_ast-1.5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:44f214394fc1af23ca6d4e9e744804d890045d1643dd7e8229951e0ef39429b5"}, + {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:118c1ce46ce58fda78503eae14b7664163aa735b620b64b5b725453696f2a35c"}, + {file = "typed_ast-1.5.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4919b808efa61101456e87f2d4c75b228f4e52618621c77f1ddcaae15904fa"}, + {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:fc2b8c4e1bc5cd96c1a823a885e6b158f8451cf6f5530e1829390b4d27d0807f"}, + {file = "typed_ast-1.5.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:16f7313e0a08c7de57f2998c85e2a69a642e97cb32f87eb65fbfe88381a5e44d"}, + {file = "typed_ast-1.5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2b946ef8c04f77230489f75b4b5a4a6f24c078be4aed241cfabe9cbf4156e7e5"}, + {file = "typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e"}, + {file = "typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2"}, + {file = "typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4"}, + {file = "typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a"}, + {file = "typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede"}, + {file = "typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6"}, + {file = "typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4"}, + {file = "typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10"}, + {file = "typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8"}, + {file = "typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a"}, + {file = "typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba"}, + {file = "typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155"}, + {file = "typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd"}, +] + +[[package]] +name = "types-python-dateutil" +version = "2.8.19.14" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, + {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20250809" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "types_python_dateutil-2.9.0.20250809-py3-none-any.whl", hash = "sha256:768890cac4f2d7fd9e0feb6f3217fce2abbfdfc0cadd38d11fba325a815e4b9f"}, + {file = "types_python_dateutil-2.9.0.20250809.tar.gz", hash = "sha256:69cbf8d15ef7a75c3801d65d63466e46ac25a0baa678d89d0a137fc31a608cc1"}, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +description = "Backported and Experimental Type Hints for Python 3.7+" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "urllib3" +version = "2.0.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version < \"3.13\"" +files = [ + {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, + {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "virtualenv" +version = "20.16.2" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "python_version < \"3.13\"" +files = [ + {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, + {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, +] + +[package.dependencies] +distlib = ">=0.3.1,<1" +filelock = ">=3.2,<4" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +platformdirs = ">=2,<3" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] + +[[package]] +name = "virtualenv" +version = "20.34.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}, + {file = "virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "zipp" +version = "3.15.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version == \"3.7\"" +files = [ + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8 ; python_version < \"3.12\"", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\""] + +[metadata] +lock-version = "2.1" +python-versions = "^3.7" +content-hash = "131cde68a6e4a2aa7dffb9d7dc622b0b20282f3813e9d097d058c26174c83868" From 795a3b1214e5647b81b2584de809c848e9c7adf2 Mon Sep 17 00:00:00 2001 From: soumo033 Date: Fri, 22 Aug 2025 02:18:41 +0530 Subject: [PATCH 2/2] added files for appmix-builder --- README.md | 106 ++- pyproject.toml | 3 + samples/README.md | 42 + samples/appmix_builder.py | 745 +++++++++++++++ samples/appmix_builder.py_bkp | 748 +++++++++++++++ .../capture_folder/mssql-db-encrypted.pcap | Bin 0 -> 3906 bytes samples/capture_folder/ssl.pcap | Bin 0 -> 564674 bytes samples/capture_replay.py | 461 +++++++++ samples/combined_report.csv | 87 ++ samples/dict_of_pan_app-id_to_cyperf_app.txt | 103 ++ samples/invert.py | 890 ++++++++++++++++++ samples/pan_app_id_to_cyperf_app_mappings.csv | 188 ++++ samples/sample_attack_based_script.py | 217 ----- samples/test_parameters.yml | 25 + .../sample_attacks_load_and_run.py | 0 .../sample_create_save_and_export_config.py | 3 +- .../sample_load_and_run_precanned_config.py | 2 +- .../sample_udp_streaming_run.py | 1 + samples/utils.py | 699 ++++++++++++++ 19 files changed, 4071 insertions(+), 249 deletions(-) create mode 100644 samples/README.md create mode 100644 samples/appmix_builder.py create mode 100644 samples/appmix_builder.py_bkp create mode 100644 samples/capture_folder/mssql-db-encrypted.pcap create mode 100644 samples/capture_folder/ssl.pcap create mode 100644 samples/capture_replay.py create mode 100644 samples/combined_report.csv create mode 100644 samples/dict_of_pan_app-id_to_cyperf_app.txt create mode 100644 samples/invert.py create mode 100644 samples/pan_app_id_to_cyperf_app_mappings.csv delete mode 100644 samples/sample_attack_based_script.py create mode 100644 samples/test_parameters.yml rename samples/{ => test_samples}/sample_attacks_load_and_run.py (100%) rename samples/{ => test_samples}/sample_create_save_and_export_config.py (99%) rename samples/{ => test_samples}/sample_load_and_run_precanned_config.py (99%) rename samples/{ => test_samples}/sample_udp_streaming_run.py (99%) create mode 100644 samples/utils.py diff --git a/README.md b/README.md index 87c4c80..b44ce28 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# cyperf +# CyPerf CyPerf REST API This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: @@ -8,20 +8,79 @@ This Python package is automatically generated by the [OpenAPI Generator](https: - Generator version: 7.7.0 - Build package: org.openapitools.codegen.languages.PythonClientCodegen -## Requirements. - -Python 3.11+ - ## Installation & Usage -### pip install - -You can install directly by doing: -```sh -pip install . -``` - -from the base of this repository. +# CyPerf API Wrapper Installation Instructions + +## Table of Contents + +* [Prerequisites](#prerequisites) +* [Step 1: Install Python](#step-1-install-python) +* [Step 2: Create a New Directory and Virtual Environment](#step-2-create-a-new-directory-and-virtual-environment) +* [Step 3: Clone the CyPerf API Wrapper Repository](#step-3-clone-the-cyperf-api-wrapper-repository) +* [Step 4: Install Dependencies and Build the Package](#step-4-install-dependencies-and-build-the-package) + +## Prerequisites + +* Python 3.11+ (check the [README.md file](https://github.com/Keysight/cyperf-api-wrapper/blob/main/README.md) for the latest requirement) + +## Step 1: Install Python + +* Download and install Python from the [official Python website](https://www.python.org/downloads/). +* Ensure necessary dependencies are installed: + + On Linux (Debian-based): + ```bash + sudo apt install libssl-dev libffi-dev python3-dev + +* If installing Python from source: + ```bash + sudo apt update + sudo apt upgrade + ./configure --enable-optimizations --with-ssl + make -j $(nproc) + sudo make altinstall + +* Verify the installation: + ```bash + python3.X --version + +## Step 2: Create a New Directory and Virtual Environment +* Create a new directory: + ```bash + mkdir cyperf-api +* Navigate to the directory: + ```bash + cd cyperf-api +* Create a virtual environment: + ```bash + python3.X -m venv env1 +* Activate the virtual environment: + ```bash + source env1/bin/activate + +## Step 3: Clone the CyPerf API Wrapper Repository and install tshark +* Clone the repository: + ```bash + git clone https://github.com/Keysight/cyperf-api-wrapper.git +* Navigate to the repository directory: + ```bash + cd cyperf-api-wrapper + sudo apt install tshark + +## Step 4: Install Dependencies and Build the Package +* Upgrade pip: + ```bash + python3 -m pip install --upgrade pip + python3 -m pip install pyshark +* Install poetry: + ```bash + python3 -m pip install poetry +* Install dependencies: + ```bash + poetry install +* Build the package: + ```bash + poetry build Then import the package: ```python @@ -117,7 +176,6 @@ Class | Method | HTTP request | Description *AgentsApi* | [**start_controllers_set_port_link_state**](docs/AgentsApi.md#start_controllers_set_port_link_state) | **POST** /api/v2/controllers/operations/set-port-link-state | *ApplicationResourcesApi* | [**delete_resources_capture**](docs/ApplicationResourcesApi.md#delete_resources_capture) | **DELETE** /api/v2/resources/captures/{captureId} | *ApplicationResourcesApi* | [**delete_resources_certificate**](docs/ApplicationResourcesApi.md#delete_resources_certificate) | **DELETE** /api/v2/resources/certificates/{certificateId} | -*ApplicationResourcesApi* | [**delete_resources_custom_fuzzing_script**](docs/ApplicationResourcesApi.md#delete_resources_custom_fuzzing_script) | **DELETE** /api/v2/resources/custom-fuzzing-scripts/{customFuzzingScriptId} | *ApplicationResourcesApi* | [**delete_resources_flow_library**](docs/ApplicationResourcesApi.md#delete_resources_flow_library) | **DELETE** /api/v2/resources/flow-library/{flowLibraryId} | *ApplicationResourcesApi* | [**delete_resources_global_playlist**](docs/ApplicationResourcesApi.md#delete_resources_global_playlist) | **DELETE** /api/v2/resources/global-playlists/{globalPlaylistId} | *ApplicationResourcesApi* | [**delete_resources_http_library**](docs/ApplicationResourcesApi.md#delete_resources_http_library) | **DELETE** /api/v2/resources/http-library/{httpLibraryId} | @@ -151,10 +209,6 @@ Class | Method | HTTP request | Description *ApplicationResourcesApi* | [**get_resources_certificate_content_file**](docs/ApplicationResourcesApi.md#get_resources_certificate_content_file) | **GET** /api/v2/resources/certificates/{certificateId}/contentFile | *ApplicationResourcesApi* | [**get_resources_certificates**](docs/ApplicationResourcesApi.md#get_resources_certificates) | **GET** /api/v2/resources/certificates | *ApplicationResourcesApi* | [**get_resources_certificates_upload_file_result**](docs/ApplicationResourcesApi.md#get_resources_certificates_upload_file_result) | **GET** /api/v2/resources/certificates/operations/uploadFile/{uploadFileId}/result | -*ApplicationResourcesApi* | [**get_resources_custom_fuzzing_script_by_id**](docs/ApplicationResourcesApi.md#get_resources_custom_fuzzing_script_by_id) | **GET** /api/v2/resources/custom-fuzzing-scripts/{customFuzzingScriptId} | -*ApplicationResourcesApi* | [**get_resources_custom_fuzzing_script_content_file**](docs/ApplicationResourcesApi.md#get_resources_custom_fuzzing_script_content_file) | **GET** /api/v2/resources/custom-fuzzing-scripts/{customFuzzingScriptId}/contentFile | -*ApplicationResourcesApi* | [**get_resources_custom_fuzzing_scripts**](docs/ApplicationResourcesApi.md#get_resources_custom_fuzzing_scripts) | **GET** /api/v2/resources/custom-fuzzing-scripts | -*ApplicationResourcesApi* | [**get_resources_custom_fuzzing_scripts_upload_file_result**](docs/ApplicationResourcesApi.md#get_resources_custom_fuzzing_scripts_upload_file_result) | **GET** /api/v2/resources/custom-fuzzing-scripts/operations/uploadFile/{uploadFileId}/result | *ApplicationResourcesApi* | [**get_resources_flow_library**](docs/ApplicationResourcesApi.md#get_resources_flow_library) | **GET** /api/v2/resources/flow-library | *ApplicationResourcesApi* | [**get_resources_flow_library_by_id**](docs/ApplicationResourcesApi.md#get_resources_flow_library_by_id) | **GET** /api/v2/resources/flow-library/{flowLibraryId} | *ApplicationResourcesApi* | [**get_resources_flow_library_content_file**](docs/ApplicationResourcesApi.md#get_resources_flow_library_content_file) | **GET** /api/v2/resources/flow-library/{flowLibraryId}/contentFile | @@ -223,9 +277,7 @@ Class | Method | HTTP request | Description *ApplicationResourcesApi* | [**poll_resources_captures_batch_delete**](docs/ApplicationResourcesApi.md#poll_resources_captures_batch_delete) | **GET** /api/v2/resources/captures/operations/batch-delete/{id} | *ApplicationResourcesApi* | [**poll_resources_captures_upload_file**](docs/ApplicationResourcesApi.md#poll_resources_captures_upload_file) | **GET** /api/v2/resources/captures/operations/uploadFile/{uploadFileId} | *ApplicationResourcesApi* | [**poll_resources_certificates_upload_file**](docs/ApplicationResourcesApi.md#poll_resources_certificates_upload_file) | **GET** /api/v2/resources/certificates/operations/uploadFile/{uploadFileId} | -*ApplicationResourcesApi* | [**poll_resources_config_export_user_defined_apps**](docs/ApplicationResourcesApi.md#poll_resources_config_export_user_defined_apps) | **GET** /api/v2/resources/configs/{configId}/operations/export-user-defined-apps/{id} | *ApplicationResourcesApi* | [**poll_resources_create_app**](docs/ApplicationResourcesApi.md#poll_resources_create_app) | **GET** /api/v2/resources/operations/create-app/{id} | -*ApplicationResourcesApi* | [**poll_resources_custom_fuzzing_scripts_upload_file**](docs/ApplicationResourcesApi.md#poll_resources_custom_fuzzing_scripts_upload_file) | **GET** /api/v2/resources/custom-fuzzing-scripts/operations/uploadFile/{uploadFileId} | *ApplicationResourcesApi* | [**poll_resources_edit_app**](docs/ApplicationResourcesApi.md#poll_resources_edit_app) | **GET** /api/v2/resources/operations/edit-app/{id} | *ApplicationResourcesApi* | [**poll_resources_find_param_matches**](docs/ApplicationResourcesApi.md#poll_resources_find_param_matches) | **GET** /api/v2/resources/operations/find-param-matches/{id} | *ApplicationResourcesApi* | [**poll_resources_flow_library_upload_file**](docs/ApplicationResourcesApi.md#poll_resources_flow_library_upload_file) | **GET** /api/v2/resources/flow-library/operations/uploadFile/{uploadFileId} | @@ -252,9 +304,7 @@ Class | Method | HTTP request | Description *ApplicationResourcesApi* | [**start_resources_captures_batch_delete**](docs/ApplicationResourcesApi.md#start_resources_captures_batch_delete) | **POST** /api/v2/resources/captures/operations/batch-delete | *ApplicationResourcesApi* | [**start_resources_captures_upload_file**](docs/ApplicationResourcesApi.md#start_resources_captures_upload_file) | **POST** /api/v2/resources/captures/operations/uploadFile | *ApplicationResourcesApi* | [**start_resources_certificates_upload_file**](docs/ApplicationResourcesApi.md#start_resources_certificates_upload_file) | **POST** /api/v2/resources/certificates/operations/uploadFile | -*ApplicationResourcesApi* | [**start_resources_config_export_user_defined_apps**](docs/ApplicationResourcesApi.md#start_resources_config_export_user_defined_apps) | **POST** /api/v2/resources/configs/{configId}/operations/export-user-defined-apps | *ApplicationResourcesApi* | [**start_resources_create_app**](docs/ApplicationResourcesApi.md#start_resources_create_app) | **POST** /api/v2/resources/operations/create-app | -*ApplicationResourcesApi* | [**start_resources_custom_fuzzing_scripts_upload_file**](docs/ApplicationResourcesApi.md#start_resources_custom_fuzzing_scripts_upload_file) | **POST** /api/v2/resources/custom-fuzzing-scripts/operations/uploadFile | *ApplicationResourcesApi* | [**start_resources_edit_app**](docs/ApplicationResourcesApi.md#start_resources_edit_app) | **POST** /api/v2/resources/operations/edit-app | *ApplicationResourcesApi* | [**start_resources_find_param_matches**](docs/ApplicationResourcesApi.md#start_resources_find_param_matches) | **POST** /api/v2/resources/operations/find-param-matches | *ApplicationResourcesApi* | [**start_resources_flow_library_upload_file**](docs/ApplicationResourcesApi.md#start_resources_flow_library_upload_file) | **POST** /api/v2/resources/flow-library/operations/uploadFile | @@ -352,6 +402,7 @@ Class | Method | HTTP request | Description *SessionsApi* | [**create_sessions**](docs/SessionsApi.md#create_sessions) | **POST** /api/v2/sessions | *SessionsApi* | [**delete_session**](docs/SessionsApi.md#delete_session) | **DELETE** /api/v2/sessions/{sessionId} | *SessionsApi* | [**delete_session_meta**](docs/SessionsApi.md#delete_session_meta) | **DELETE** /api/v2/sessions/{sessionId}/meta/{metaId} | +*SessionsApi* | [**get_appsec_ui_metadata**](docs/SessionsApi.md#get_appsec_ui_metadata) | **GET** /api/v2/appsec-ui-metadata | *SessionsApi* | [**get_config_docs**](docs/SessionsApi.md#get_config_docs) | **GET** /api/v2/sessions/{sessionId}/config/$docs | *SessionsApi* | [**get_config_granular_stats**](docs/SessionsApi.md#get_config_granular_stats) | **GET** /api/v2/sessions/{sessionId}/config/granular-stats | *SessionsApi* | [**get_config_granular_stats_filters**](docs/SessionsApi.md#get_config_granular_stats_filters) | **GET** /api/v2/sessions/{sessionId}/config/granular-stats-filters | @@ -365,8 +416,8 @@ Class | Method | HTTP request | Description *SessionsApi* | [**patch_session_meta**](docs/SessionsApi.md#patch_session_meta) | **PATCH** /api/v2/sessions/{sessionId}/meta/{metaId} | *SessionsApi* | [**patch_session_test**](docs/SessionsApi.md#patch_session_test) | **PATCH** /api/v2/sessions/{sessionId}/test | *SessionsApi* | [**poll_config_add_applications**](docs/SessionsApi.md#poll_config_add_applications) | **GET** /api/v2/sessions/{sessionId}/config/config/TrafficProfiles/{trafficProfileId}/operations/add-applications/{id} | +*SessionsApi* | [**poll_config_save**](docs/SessionsApi.md#poll_config_save) | **GET** /api/v2/sessions/{sessionId}/config/operations/save/{id} | *SessionsApi* | [**poll_session_config_granular_stats_default_dashboards**](docs/SessionsApi.md#poll_session_config_granular_stats_default_dashboards) | **GET** /api/v2/sessions/{sessionId}/config/operations/granular-stats-default-dashboards/{id} | -*SessionsApi* | [**poll_session_config_save**](docs/SessionsApi.md#poll_session_config_save) | **GET** /api/v2/sessions/{sessionId}/config/operations/save/{id} | *SessionsApi* | [**poll_session_load_config**](docs/SessionsApi.md#poll_session_load_config) | **GET** /api/v2/sessions/{sessionId}/operations/loadConfig/{id} | *SessionsApi* | [**poll_session_prepare_test**](docs/SessionsApi.md#poll_session_prepare_test) | **GET** /api/v2/sessions/{sessionId}/operations/prepareTest/{id} | *SessionsApi* | [**poll_session_test_end**](docs/SessionsApi.md#poll_session_test_end) | **GET** /api/v2/sessions/{sessionId}/operations/testEnd/{id} | @@ -463,7 +514,6 @@ Class | Method | HTTP request | Description - [ActivationCodeInfo](docs/ActivationCodeInfo.md) - [ActivationCodeListRequest](docs/ActivationCodeListRequest.md) - [ActivationCodeRequest](docs/ActivationCodeRequest.md) - - [AddActionInfo](docs/AddActionInfo.md) - [AddInput](docs/AddInput.md) - [AdvancedSettings](docs/AdvancedSettings.md) - [Agent](docs/Agent.md) @@ -497,15 +547,12 @@ Class | Method | HTTP request | Description - [AsyncOperationResponse](docs/AsyncOperationResponse.md) - [Attack](docs/Attack.md) - [AttackAction](docs/AttackAction.md) - - [AttackMetadata](docs/AttackMetadata.md) - - [AttackMetadataKeywordsInner](docs/AttackMetadataKeywordsInner.md) - [AttackObjectivesAndTimeline](docs/AttackObjectivesAndTimeline.md) - [AttackProfile](docs/AttackProfile.md) - [AttackTimelineSegment](docs/AttackTimelineSegment.md) - [AttackTrack](docs/AttackTrack.md) - [AuthMethodType](docs/AuthMethodType.md) - [AuthProfile](docs/AuthProfile.md) - - [AuthProfileMetadata](docs/AuthProfileMetadata.md) - [AuthSettings](docs/AuthSettings.md) - [Authenticate200Response](docs/Authenticate200Response.md) - [AuthenticationSettings](docs/AuthenticationSettings.md) @@ -532,6 +579,7 @@ Class | Method | HTTP request | Description - [ConfigCategory](docs/ConfigCategory.md) - [ConfigId](docs/ConfigId.md) - [ConfigMetadata](docs/ConfigMetadata.md) + - [ConfigMetadataConfigDataValue](docs/ConfigMetadataConfigDataValue.md) - [ConfigValidation](docs/ConfigValidation.md) - [Conflict](docs/Conflict.md) - [Connection](docs/Connection.md) @@ -541,7 +589,6 @@ Class | Method | HTTP request | Description - [CountedFeatureConsumer](docs/CountedFeatureConsumer.md) - [CountedFeatureStats](docs/CountedFeatureStats.md) - [CreateAppOperation](docs/CreateAppOperation.md) - - [CreateAppOrAttackOperationInput](docs/CreateAppOrAttackOperationInput.md) - [CustomDashboards](docs/CustomDashboards.md) - [CustomImportHandler](docs/CustomImportHandler.md) - [CustomStat](docs/CustomStat.md) @@ -850,6 +897,5 @@ Authentication schemes defined for the API: ## Author -support@keysight.com - +soumya.neogy@keysight.com diff --git a/pyproject.toml b/pyproject.toml index 0c4f9f6..8d8ea19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,9 @@ urllib3 = ">= 1.25.3" python-dateutil = ">=2.8.2" pydantic = ">=2" typing-extensions = ">=4.7.1" +pyshark = ">=0.6" +pyyaml = ">=6.0.2" +pandas = ">=2.3.1" [tool.poetry.dev-dependencies] pytest = ">=7.2.1" diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..b8ba503 --- /dev/null +++ b/samples/README.md @@ -0,0 +1,42 @@ +# AppMix Builder Script + +## Overview + +The appmix_builder.py script creates an application mix from a CSV file containing application names and their corresponding percentages or weights. + +## How it Works +The script reads the application names from the CSV file and searches for corresponding applications in the CyPerf library. If a match is found, the application is added to the CyPerf app mix using the percentage or weight for the corresponding application. + +The script provides a percentage coverage based on the matches. For example, if 10 applications were provided in the CSV and 5 were found in the CyPerf library, the percentage coverage would be (5/10) * 100 = 50%. + +## Features ++ Creates application mix from CSV file ++ Searches for applications in CyPerf library ++ Provides percentage coverage based on matches ++ Option to use capture-to-application converter to increase coverage percentage ++ Option to configure test or abort +## Capture-to-Application Converter + +The user can upload a set of captures (corresponding to applications that were not present in the CyPerf library) to a pre-configured location (folder) and the converter function will create applications and add them to the CyPerf library. This may help to increase the coverage percentage. +## Application Naming Convention +Applications created by the script are named CCA-. All applications can be found in the Resource Library section of the CyPerf controller. +## Steps to Run the Script +### Step 1: Configure Test Parameters + +1. Edit the test_parameters.yml file under the "cyperf-api-wrapper/samples" folder. +Set the variable named "location_of_folder_containing_captures" to the absolute path of the folder where all captures should be stored. + +2. Set the variable named "location_of_folder_containing_captures" to the absolute path of the folder where all captures should be stored. + +### Step 2: Run the Script + +1. Navigate to the folder: cyperf-api-wrapper. + +2. Run the script "appmix_builder.py" using the following command from the CLI: + ```bash + python3.x samples/appmix_builder.py --controller -- + license-server --user --password +Example: + ```bash + python3.13 samples/appmix_builder.py --controller 10.39.68.180 --license-server 10.39.68.180 --license-user admin --license-password CyPerf\&Keysight#1 + diff --git a/samples/appmix_builder.py b/samples/appmix_builder.py new file mode 100644 index 0000000..c20f56d --- /dev/null +++ b/samples/appmix_builder.py @@ -0,0 +1,745 @@ +import cyperf +import os +import re +import utils +import urllib3; urllib3.disable_warnings() +import argparse +import yaml +from pprint import pprint +import asyncio +import csv +import pandas as pd +from collections import Counter +import math + +file_path = 'samples/test_parameters.yml' + +def replace_zeros_with_ones(lst): + arr = np.array(lst) + arr[arr == 0] = 1 + return arr.tolist() + +def merge_duplicate_entries(csv_file_path): + # Read the CSV file + df = pd.read_csv(csv_file_path) + + # Group by the first column and sum the values + merged_df = df.groupby(df.columns[0])[df.columns[1]].sum().reset_index() + + return merged_df + +def convert_to_integers(numbers): + # Calculate the greatest common divisor (GCD) of the numbers + def gcd(a, b): + while b: + a, b = b, a % b + return a + + # Find the least common multiple (LCM) of the denominators + def lcm(a, b): + return a * b // gcd(a, b) + + # Convert floats to fractions + fractions = [] + for num in numbers: + if isinstance(num, int): + fractions.append((num, 1)) + else: + denominator = 10 ** len(str(num).split('.')[1]) + numerator = int(num * denominator) + gcd_val = gcd(numerator, denominator) + fractions.append((numerator // gcd_val, denominator // gcd_val)) + + # Find the LCM of the denominators + lcm_denominator = 1 + for _, denominator in fractions: + lcm_denominator = lcm(lcm_denominator, denominator) + + # Convert fractions to integers while maintaining ratios + integers = [] + for numerator, denominator in fractions: + integers.append(numerator * lcm_denominator // denominator) + + # Find the GCD of the integers + gcd_integers = integers[0] + for num in integers[1:]: + gcd_integers = gcd(gcd_integers, num) + + # Divide all integers by the GCD to get the smallest possible integers + integers = [num // gcd_integers for num in integers] + + #print("SOUMYA BEFORE LIMITING !") + #print(integers) + + # Limit the integer values to 10000 + + max_value = max(integers) + if max_value > 10000: #cyperf limits weights upto 10K + ratio = max_value / 10000 + integers = [int(num / ratio) for num in integers] + + #eliminate any zero weights as they are not configurable in CyPerf . Convert all zeros to ONES . + non_zero_integers=[1 if x == 0 else x for x in integers] + + return non_zero_integers + + +def get_file_names(folder_path): + file_names = [] + for filename in os.listdir(folder_path): + if os.path.isfile(os.path.join(folder_path, filename)): + file_name_without_extension = os.path.splitext(filename)[0] + file_names.append(file_name_without_extension) + return file_names + +def convert_to_next_highest(dictionary): + return {key: math.ceil(value) for key, value in dictionary.items()} + + +def remove_values(input_string, separator='-', values_to_remove=['ms', 'base', 'ds', 'as']): + words = input_string.split(separator) + filtered_words = [word for word in words if word not in values_to_remove] + return ' '.join(filtered_words) + +def find_key_by_value(dictionary, value): + for key, val in dictionary.items(): + if val == value: + return key + return None + +def extract_first_word(input_string): + return input_string.split(' ')[0] + +def check_string_position(input_string, target_string): + words = target_string.split() + if input_string == words[0]: + return "Start" + elif input_string == words[-1]: + return "Last" + elif input_string in words[1:-1]: + return "Middle" + else: + return "Not found" + +def display_menu(): + print(" Please select one of the following option [1/2/3] ") + print("[1] Manually upload the captures and use capture convertor to improve the coverage percentage") + print("[2] Try to Automatically find captures from BPS and use capture convertor to improve coverage percentage") + print("[3] Dont continue with Test execution") + +def get_user_choice(): + while True: + try: + choice = int(input("Please select an option (1, 2, or 3): ")) + if 1 <= choice <= 3: + return choice + else: + print("Invalid option. Please choose 1, 2, or 3.") + except ValueError: + print("Invalid input. Please enter a number.") + +def process_choice(choice): + if choice == 1: + print("You chose Option 1") + elif choice == 2: + print("You chose Option 2") + elif choice == 3: + print("You chose Option 3") + +def find_matching_string(search_str, string_list): + matching_strings = [string for string in string_list if search_str in string] + return matching_strings + + +def remove_suffix(string, suffix="-base"): + if string.endswith(suffix): + return string[:-len(suffix)] + return string + +def find_indices(my_list, target_string, exact_match=False): + if exact_match: + return [i for i, s in enumerate(my_list) if s == target_string] + else: + return [i for i, s in enumerate(my_list) if target_string in s] + +def common_items(dictionary): + # Check if dictionary is empty + if not dictionary: + return [] + + # Find the intersection of all lists + common = set(dictionary[list(dictionary.keys())[0]]) + for key in dictionary: + common = common.intersection(set(dictionary[key])) + print(common) + return list(common) + +def select_best_matching_app(dump_app_dict,cyperf_apps,first_word): + + list_common = common_items(dump_app_dict) + + if (not len(list_common)): + print("SOUMYA : There was no common item") + tmp_list=[] + for ele in cyperf_apps: + posi= check_string_position(first_word,ele) + #print(posi) + if posi == "Not found": + continue; + else: + tmp_list.append(ele) + print("tmp_list") + print(tmp_list) + print ("the return valr from func select_best_matching_app is ") + print (sorted(tmp_list, key=len, reverse=False)) + return sorted(tmp_list, key=len, reverse=False) + + else: + print("SOUMYA: Common item was found in intersection ") + print(sorted(list_common, key=len, reverse=False)) + sorted_list = sorted(list_common, key=len, reverse=False) + return sorted_list + + + +#This function needs refinement. This is crucial to matching the apps in our Library . +def convert_app_names_to_common_names(application_dict): + for item in application_dict: + app_name= item + #if the user-specified application name does not contain hyphen('-') then include it as-is in the list + if(app_name.find("-")==-1): + application_dict[item].append(app_name) + #if the user-specified application name contains suffix ( -base ) , prefix (ms-) ,etc remove them . This will help in search. + else: + temp_list= re.split(r"[- ]", app_name) + # strings_to_remove list needs to be updated on regular basis + strings_to_remove=["base","ms","as","ds"] + #list1 = [element for element in list1 if element not in list2] + #result = [s for s in temp_list if s not in strings_to_remove] + result = [s for s in temp_list if s not in strings_to_remove] + application_dict[item].extend(result) + return application_dict + + +#read a yml file and return a dictionary +def read_yaml_file(file_path): + + try: + with open(file_path, 'r') as file: + yaml_data = yaml.safe_load(file) + return yaml_data + except FileNotFoundError: + print(f"File not found: {file_path}") + return {} + except yaml.YAMLError as e: + print(f"Error parsing YAML file: {e}") + return {} + +#read the test_parameters from the yml file and populate into variables +yaml_dict = read_yaml_file(file_path) + +print(yaml_dict) + +#populate the variables with user inputs +capture_folder_path = yaml_dict["location_of_folder_containing_captures"] +name_of_existing_cyperf_configuration = yaml_dict["name_of_existing_cyperf_configuration"] +csv_path = yaml_dict["csv_path"] +exact_match = yaml_dict["exact_match"] +threshold_coverage_percentage = int(yaml_dict["threshold_coverage_percentage"]) +percentage = yaml_dict["percentage"] +dictionary_path = yaml_dict["dictionary_path"] + +#Load the configuration or check if the configuration is already loaded and having an active session id. +#Only the loading part is imlemented . In case the configuartion is already loaded and has an active session-id +#we need to delete the application-profile and create a new one based on the app_mix.csv ( this is TBD ) +class AppMixBuilderTest (object): + def __init__ (self, capture_folder_path ,name_of_existing_cyperf_configuration ,csv_path , agent_map={} , re_entry=0): + args, offline_token = utils.parse_cli_options() + self.utils = utils.Utils(args.controller, + username=args.user, + password=args.password, + refresh_token=offline_token, + license_server=args.license_server, + license_user=args.license_user, + license_password=args.license_password) + + + + self.capture_folder_path = capture_folder_path + self.name_of_existing_cyperf_configuration = name_of_existing_cyperf_configuration + self.csv_path = csv_path + self.agent_map = agent_map + self.local_stats = {} + self.re_entry = re_entry + + def __del__(self): + self._release() + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, exception_traceback): + self._release() + if exception_value: + raise (exception_value) + + def _release(self): + try: + if self.session: + print('Deleting session') + self.utils.delete_session(self.session) + print('Deleted session') + self.session = None + except AttributeError: + pass + + def _set_objective_and_timeline(self): + # Change the objective type to 'Simulated Users'. 'Throughput' is not yet supported for UDP Stream. + self.utils.set_objective_and_timeline(self.session, + objective_type=cyperf.ObjectiveType.SIMULATED_USERS, + objective_value=1000, + test_duration=self.test_duration) + + #function to read the input CSV file which contains 2 coulums - (a)Application Name (b) percentage + def read_csv_file(self): + data_dict = {} + with open(self.csv_path, 'r') as file: + reader = csv.reader(file) + for row in reader: + if len(row) == 2: + key, value = row + data_dict[key] = value + else: + print(f"Skipping row: {row}. Expected 2 columns, but got {len(row)}.") + return data_dict + + def csv_to_dict(self,filename): + data_dict = {} + with open(filename, 'r') as file: + reader = csv.reader(file) + next(reader) # Skip the header row + for row in reader: + if len(row) == 2: # Ensure the row has exactly two columns + data_dict[row[0]] = row[1] + return data_dict + + def sum_percentage_and_populate_applications(self): + try: + df = pd.read_csv(self.csv_path) + total_percentage = df['percentage'].sum() + applications = df['application'].tolist() + percentages = df['percentage'].tolist() + return total_percentage, applications,percentages + except pd.errors.EmptyDataError: + print("The CSV file is empty.") + return None, None, None + except KeyError as e: + print(f"The CSV file is missing the column: {e}") + return None, None, None + + def populate_applications_and_weights(self): + try: + df = pd.read_csv(self.csv_path) + applications = df['application'].tolist() + weights_raw = df['weight'].tolist() + #process the weights such that they are convered to integers and in propoer ratio + weights=convert_to_integers(weights_raw) + print(" I am inside populate application weights ") + print(applications) + print(weights) + return applications,weights + except pd.errors.EmptyDataError: + print("The CSV file is empty.") + return None, None + except KeyError as e: + print(f"The CSV file is missing the column: {e}") + return None, None + + + def percentages_to_weights(self, percentages): + total_percentage = sum(percentages) + weights = [round(p / total_percentage * 100) for p in percentages] + # Ensure no weight is zero + min_weight = min(weights) + if min_weight == 0: + zero_indices = [i for i, w in enumerate(weights) if w == 0] + for i in zero_indices: + weights[i] = 1 + # Adjust weights to ensure they sum up to 100 + diff = sum(weights) - 100 + weights[weights.index(max(weights))] -= diff + else: + # Adjust weights to ensure they sum up to 100 + diff = 100 - sum(weights) + weights[weights.index(max(weights))] += diff + return weights + + #Configure the Applications with their corresponding weights in CyPerf + def configure_appmix_test(self): + + #check if the input csv ( appmix name & % weights provided by user ) contains any duplicate entry in aplication + #coulum , In case it does we need to coaslese them into a single entry by adding weights + merged_df = merge_duplicate_entries(csv_path) + print(merged_df) + + # Save the merged DataFrame to a the CSV file + merged_df.to_csv(csv_path, index=False) + + if (percentage == "True"): + total_percentage, applications, percenatges = self.sum_percentage_and_populate_applications() + + if(total_percentage < 99 or total_percentage > 101): + print("You must fix the percentages such that they add upto 100 ") + else: + #if percentages of all the userinput adds close to 100 , then convert them into non-zero weights + weights = self.percentages_to_weights(percenatges) + else: + applications, weights = self.populate_applications_and_weights() + + + #create a dictionary to store the user-input applications and their corresponding weights + input_app_dict_tmp = dict(zip(applications, weights)) + + print(input_app_dict_tmp) + #covert the weights to the nearest integer ceiling value + input_app_dict=convert_to_next_highest(input_app_dict_tmp) + + #Search a configuation by name as provided in the test-aparmeters.yml file ( example - PANW-APPMIX) + if (self.utils.search_configuration_file(name_of_existing_cyperf_configuration)): + #load the configuration and create a test session + try: + session_appmix=self.utils.create_session_by_config_name(name_of_existing_cyperf_configuration) + print("The Base Configuration Template was loaded successfully !") + except Exception: + print("The Base Configuration Template was not loaded successfully !") + return False + + #create the CyPerf app-dictionary + app_dictionary= self.csv_to_dict(dictionary_path) + target_app_mix_dict={} + not_found_apps=[] + matching_matrix_dict={} + found=0 + for item in input_app_dict: + cyperf_app_name=app_dictionary.get(item) + if(cyperf_app_name): + found=found+1 + #check if it present earlier in the dictionary - in that case we need to modify weights + if (cyperf_app_name in target_app_mix_dict ): + adjusted_weight= target_app_mix_dict[cyperf_app_name] + input_app_dict[item] + input_app_dict[item]=adjusted_weight + + target_app_mix_dict.update({cyperf_app_name:input_app_dict[item]}) + matching_matrix_dict.update({item:cyperf_app_name}) + else: + + not_found_apps.append(item) + + + #Reports for users + print(" = = = = = = = = = = = = = = = = = = = = = = = = = = = == = = = = = = = = ") + print(f"Total applications provided in the CSV file = {len(input_app_dict.keys())}") + #pprint(new_app_dict.keys()) + #print(f"Total applications found in CyPerf Libarary = {len(target_app_mix_dict.keys())}") + print(f"Total applications found in CyPerf Libarary = {found}") + #pprint(target_app_mix_dict.keys()) + print(f"Total non matching application = {len(not_found_apps)}") + pprint(not_found_apps) + print("Matching matrix") + print(matching_matrix_dict) + print(" = = = = = = = = = = = = = = = = = = = = = = = = = = = == = = = = = = = = \n") + #Coverage Percenatge : (This is the ratio of applications found in CyPerf Library / Total number of application presented by the user ) + #A higher Covergae ratio incicates that most of the applications were found in the CyPerf Library + + coverage_percent = (len(target_app_mix_dict.keys())/len((input_app_dict.keys())))*100 + print(f"coverage Percentage = {coverage_percent}") + print(" = = = = = = = = = = = = = = = = = = = = = = = = = = = == = = = = = = = = \n") + + session_deleted=False + if(threshold_coverage_percentage == 0 ): + #Menu Driven option to the user + decision=input("Do you wish to continue with the existing Coverage percentage [Y/N]?: ") + if (decision.lower() == 'y'): + #start the configuration of the test and subsequently run the test + self.utils.add_apps_with_weights(session_appmix,target_app_mix_dict) + self.utils.set_objective_and_timeline(session_appmix,objective_type=cyperf.ObjectiveType.SIMULATED_USERS, + objective_unit=cyperf.ObjectiveUnit.EMPTY, + objective_value=100, + test_duration=30) + #self.utils.check_if_traffic_enabled(session) + print ("configuration complete !") + + elif (decision.lower() == 'n' and self.re_entry == 0 ) : + while True: + display_menu() + choice = get_user_choice() + process_choice(choice) + cont = input("Do you want to continue? (y/n): ") + if cont.lower() != 'y': + break + + + if(choice == 1): + #This part requires automation . TBD + print("Delete any existing capture files in the capture Folder & ensure the name of the capture file is same as the name of the app you provided in the CSV (application, weight) ") + user_input =input("Upload the capture files of missing applications manually to the capture folder and type -\"continue\" ") + if(user_input.lower()== "continue"): + #start updating the master dictionary after reading the capture folder + #read the capture folder and gather all the new capture names in a list + filenames=get_file_names(capture_folder_path) + #update the master csv - whic conyains teh dictionary mapping between user-input & cyPerf appnames + # Create a DataFrame from the list + df = pd.DataFrame({'app-id': filenames, 'cyperf-appname': ['CCA-'+ filename for filename in filenames]}) + + # Check if the CSV file exists + try: + existing_df = pd.read_csv(dictionary_path) + combined_df = pd.concat([existing_df, df]) + combined_df.to_csv(dictionary_path, index=False) + except FileNotFoundError: + df.to_csv(dictionary_path, index=False) + + #start creating the custom applications + asyncio.run(capture_convertor_and_custom_app_builder()) + print("Exiting the present Test . Reruning Test again and check coverage improvement") + self.utils.delete_session(session_appmix) + start_appmixbuilder_test(re_entry=1) + break; + else: + print("Invalid input ! ") + elif(choice == 2): + user_input =input("Trying to fetch captures from known repo & creating custom application") + #start creating the custom applications + print("This is under implementation ! ... exiting the test!") + if(self.utils.session_appmix.id): + self.utils.delete_session(session_appmix) + session_deleted=True + break; + elif(choice == 3): + print("Stopping the execution ! ") + #raise Exception(" TEST STOPPED ! ") + if(not session_deleted): + self.utils.delete_session(session_appmix) + break; + elif (decision.lower() == 'n' and self.re_entry == 1 ) : + print("Stopping the execution ! ") + #raise Exception(" TEST STOPPED ! ") + if(not session_deleted): + self.utils.delete_session(session_appmix) + + else: + print("Invalid ! option selected ") + + #If threshold percentage is non-zero the decision to configure and run the test is driven by the threshold percenatge & covergae percentage + else: + if(threshold_coverage_percentage <= coverage_percent): + #add_application-mix along with weights to the CyPerf + self.utils.add_apps_with_weights(session_appmix,target_app_mix_dict) + #self.utils.check_if_traffic_enabled(session) + print ("configuration complete !") + print ( "Starting test ") + #start the test + #self.utils.delete_session(session_appmix.id) + else: + #need to stop the test + #raise Exception ( " APPLICATION COVERAGE INADEQUATE FOR PROCEDDING WITH TEST Configuration !! ") + print(" APPLICATION COVERAGE INADEQUATE FOR PROCEDDING WITH TEST Configuration !!") + self.utils.delete_session(session_appmix.id) + + + def _set_objective_and_timeline(self): + # Change the objective type to 'Simulated Users'. 'Throughput' is not yet supported for UDP Stream. + self.utils.set_objective_and_timeline(self.session, + objective_type=cyperf.ObjectiveType.SIMULATED_USERS, + objective_value=1000, + test_duration=30) + +class CaptureReplayTest (object): + def __init__(self, capture_folder_path ,agent_map={}): + args, offline_token = utils.parse_cli_options() + self.utils = utils.Utils(args.controller, + username=args.user, + password=args.password, + refresh_token=offline_token, + license_server=args.license_server, + license_user=args.license_user, + license_password=args.license_password) + + + + self.capture_folder_path = capture_folder_path + self.agent_map = agent_map + self.test_duration = 60 + self.local_stats = {} + + def __del__(self): + self._release() + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, exception_traceback): + self._release() + if exception_value: + raise (exception_value) + + def _release(self): + try: + if self.session: + print('Deleting session') + self.utils.delete_session(self.session) + print('Deleted session') + self.session = None + except AttributeError: + pass + + def _set_objective_and_timeline(self): + # Change the objective type to 'Simulated Users'. 'Throughput' is not yet supported for UDP Stream. + self.utils.set_objective_and_timeline(self.session, + objective_type=cyperf.ObjectiveType.SIMULATED_USERS, + objective_value=1000, + test_duration=self.test_duration) + #soumo + def get_capture_file_paths(self): + try: + # Check if the folder exists + if not os.path.exists(self.capture_folder_path): + print(f"Error: Folder '{self.capture_folder_path}' does not exist.") + return [] + + # Get a list of all files in the folder + file_paths = [os.path.join(self.capture_folder_path, file) for file in os.listdir(self.capture_folder_path) if os.path.isfile(os.path.join(self.capture_folder_path, file))] + return file_paths + + except Exception as e: + print(f"An error occurred: {str(e)}") + return [] + + + + + async def configure(self): + print('Configuring ...') + #read the pcap files + list_of_paths_of_pcap_files = self.get_capture_file_paths() + list_of_paths_of_pcap_files.reverse() + print(list_of_paths_of_pcap_files) + #upload all the captures from the specified folder ( in yml file ) + #to CyPerf Resource Library for Captures + while(list_of_paths_of_pcap_files): + capture_file=list_of_paths_of_pcap_files.pop() + print("uploading capture - {} ".format(capture_file)) + await self.utils.upload_the_capture_file(capture_file) + #create an application from the uploaded captures + apps_created= self.utils.create_apps_from_captures() + print('Conversion from pcaps to Applications complete !!!.You may now use the created apps created from pcaps.\nThe custom apps are available under the Resource Library in CyPerf Controller') + + def _start(self): + print('Starting test ...') + self.utils.start_test(self.session) + print('Started test ...') + + def _process_stats(self, stats): + processed_stats = self.local_stats + for stat in stats: + if stat.snapshots: + processed_stats[stat.name] = {} + for snapshot in stat.snapshots: + time_stamp = snapshot.timestamp + processed_stats[stat.name][time_stamp] = [] + d = {} + for idx, stat_name in enumerate(stat.columns): + d[stat_name] = [val[idx].actual_instance for val in snapshot.values] + processed_stats[stat.name][time_stamp] = d + return processed_stats + + def _print_run_time_stats(self, test, time_from, time_to): + stat_names = ['client-streaming-rate', 'server-streaming-rate'] + return self.print_run_time_stats(test, time_from, time_to, stat_names) + + def print_run_time_stats(self, test, time_from, time_to, stat_names): + last_monitored_time_stamp = None + for stat_name in stat_names: + stats = self.utils.collect_stats(test, + stat_name, + time_from, + time_to, + self._process_stats) + if stat_name not in stats: + continue + + stats = stats[stat_name] + last_time_stamp = max(stats) + + if stat_name in self.last_recorded_time_stamps: + last_recorded_time_stamp = self.last_recorded_time_stamps[stat_name] + else: + last_recorded_time_stamp = 0 + + if last_time_stamp != last_recorded_time_stamp: + last_stats = stats[last_time_stamp] + + print(f'\n{stat_name} at {self.utils.format_milliseconds(last_time_stamp)}\n') + lines = self.utils.format_stats_dict_as_table(last_stats) + for line in lines: + print(line) + + self.last_recorded_time_stamps[stat_name] = last_time_stamp + + if last_monitored_time_stamp: + last_monitored_time_stamp = min (max(last_time_stamp, time_from), + last_monitored_time_stamp) + else: + last_monitored_time_stamp = max(last_time_stamp, time_from) + + return last_monitored_time_stamp + + def _wait_until_stopped(self): + self.last_recorded_time_stamps = {} + self.utils.wait_for_test_stop(self.session, self._print_run_time_stats) + print('Stopped test ...') + + def run(self): + self._start() + self._wait_until_stopped() + + def collect_final_stats(self): + print('Collecting final statistics ...') + stat_names = ['client-streaming-statistics', 'server-streaming-statistics'] + session_api = cyperf.SessionsApi(self.utils.api_client) + test = session_api.get_test(session_id=self.session.id) + self.print_run_time_stats(test, 0, -1, stat_names) + print('Collected final statistics ...') + + +def start_appmixbuilder_test(re_entry=0): + + agents = { + 'IP Network 1': ['10.39.69.98'], + 'IP Network 2': ['10.39.69.99'] + } + with AppMixBuilderTest( capture_folder_path,name_of_existing_cyperf_configuration,csv_path,agents,re_entry) as test1: + test1.configure_appmix_test() + #test1._set_objective_and_timeline() + #test.configure() + #test.run() + #test.collect_final_stats() + + + +#async def main(): +async def capture_convertor_and_custom_app_builder(): + agents = { + 'IP Network 1': ['10.39.68.192'], + 'IP Network 2': ['10.39.69.229'] + } + + with CaptureReplayTest(capture_folder_path,agents) as test: + await test.configure() + #test.configure() + #test.run() + #test.collect_final_stats() + +if __name__ == '__main__': + #asyncio.run(capture_convertor_and_custom_app_builder()) + start_appmixbuilder_test() diff --git a/samples/appmix_builder.py_bkp b/samples/appmix_builder.py_bkp new file mode 100644 index 0000000..181a208 --- /dev/null +++ b/samples/appmix_builder.py_bkp @@ -0,0 +1,748 @@ +import cyperf +import os +import re +import utils +import urllib3; urllib3.disable_warnings() +import argparse +import yaml +from pprint import pprint +import asyncio +import csv +import pandas as pd +from collections import Counter +import math + +file_path = 'samples/test_parameters.yml' + +def replace_zeros_with_ones(lst): + arr = np.array(lst) + arr[arr == 0] = 1 + return arr.tolist() + +def merge_duplicate_entries(csv_file_path): + # Read the CSV file + df = pd.read_csv(csv_file_path) + + # Group by the first column and sum the values + merged_df = df.groupby(df.columns[0])[df.columns[1]].sum().reset_index() + + return merged_df + +def convert_to_integers(numbers): + # Calculate the greatest common divisor (GCD) of the numbers + def gcd(a, b): + while b: + a, b = b, a % b + return a + + # Find the least common multiple (LCM) of the denominators + def lcm(a, b): + return a * b // gcd(a, b) + + # Convert floats to fractions + fractions = [] + for num in numbers: + if isinstance(num, int): + fractions.append((num, 1)) + else: + denominator = 10 ** len(str(num).split('.')[1]) + numerator = int(num * denominator) + gcd_val = gcd(numerator, denominator) + fractions.append((numerator // gcd_val, denominator // gcd_val)) + + # Find the LCM of the denominators + lcm_denominator = 1 + for _, denominator in fractions: + lcm_denominator = lcm(lcm_denominator, denominator) + + # Convert fractions to integers while maintaining ratios + integers = [] + for numerator, denominator in fractions: + integers.append(numerator * lcm_denominator // denominator) + + # Find the GCD of the integers + gcd_integers = integers[0] + for num in integers[1:]: + gcd_integers = gcd(gcd_integers, num) + + # Divide all integers by the GCD to get the smallest possible integers + integers = [num // gcd_integers for num in integers] + + #print("SOUMYA BEFORE LIMITING !") + #print(integers) + + # Limit the integer values to 10000 + + max_value = max(integers) + if max_value > 10000: #cyperf limits weights upto 10K + ratio = max_value / 10000 + integers = [int(num / ratio) for num in integers] + + #eliminate any zero weights as they are not configurable in CyPerf . Convert all zeros to ONES . + non_zero_integers=[1 if x == 0 else x for x in integers] + + return non_zero_integers + + +def get_file_names(folder_path): + file_names = [] + for filename in os.listdir(folder_path): + if os.path.isfile(os.path.join(folder_path, filename)): + file_name_without_extension = os.path.splitext(filename)[0] + file_names.append(file_name_without_extension) + return file_names + +def convert_to_next_highest(dictionary): + return {key: math.ceil(value) for key, value in dictionary.items()} + + +def remove_values(input_string, separator='-', values_to_remove=['ms', 'base', 'ds', 'as']): + words = input_string.split(separator) + filtered_words = [word for word in words if word not in values_to_remove] + return ' '.join(filtered_words) + +def find_key_by_value(dictionary, value): + for key, val in dictionary.items(): + if val == value: + return key + return None + +def extract_first_word(input_string): + return input_string.split(' ')[0] + +def check_string_position(input_string, target_string): + words = target_string.split() + if input_string == words[0]: + return "Start" + elif input_string == words[-1]: + return "Last" + elif input_string in words[1:-1]: + return "Middle" + else: + return "Not found" + +def display_menu(): + print(" Please select one of the following option [1/2/3] ") + print("[1] Manually upload the captures and use capture convertor to improve the coverage percentage") + print("[2] Try to Automatically find captures from BPS and use capture convertor to improve coverage percentage") + print("[3] Dont continue with Test execution") + +def get_user_choice(): + while True: + try: + choice = int(input("Please select an option (1, 2, or 3): ")) + if 1 <= choice <= 3: + return choice + else: + print("Invalid option. Please choose 1, 2, or 3.") + except ValueError: + print("Invalid input. Please enter a number.") + +def process_choice(choice): + if choice == 1: + print("You chose Option 1") + elif choice == 2: + print("You chose Option 2") + elif choice == 3: + print("You chose Option 3") + +def find_matching_string(search_str, string_list): + matching_strings = [string for string in string_list if search_str in string] + return matching_strings + + +def remove_suffix(string, suffix="-base"): + if string.endswith(suffix): + return string[:-len(suffix)] + return string + +def find_indices(my_list, target_string, exact_match=False): + if exact_match: + return [i for i, s in enumerate(my_list) if s == target_string] + else: + return [i for i, s in enumerate(my_list) if target_string in s] + +def common_items(dictionary): + # Check if dictionary is empty + if not dictionary: + return [] + + # Find the intersection of all lists + common = set(dictionary[list(dictionary.keys())[0]]) + for key in dictionary: + common = common.intersection(set(dictionary[key])) + print(common) + return list(common) + +def select_best_matching_app(dump_app_dict,cyperf_apps,first_word): + + list_common = common_items(dump_app_dict) + + if (not len(list_common)): + print("SOUMYA : There was no common item") + tmp_list=[] + for ele in cyperf_apps: + posi= check_string_position(first_word,ele) + #print(posi) + if posi == "Not found": + continue; + else: + tmp_list.append(ele) + print("tmp_list") + print(tmp_list) + print ("the return valr from func select_best_matching_app is ") + print (sorted(tmp_list, key=len, reverse=False)) + return sorted(tmp_list, key=len, reverse=False) + + else: + print("SOUMYA: Common item was found in intersection ") + print(sorted(list_common, key=len, reverse=False)) + sorted_list = sorted(list_common, key=len, reverse=False) + return sorted_list + + + +#This function needs refinement. This is crucial to matching the apps in our Library . +def convert_app_names_to_common_names(application_dict): + for item in application_dict: + app_name= item + #if the user-specified application name does not contain hyphen('-') then include it as-is in the list + if(app_name.find("-")==-1): + application_dict[item].append(app_name) + #if the user-specified application name contains suffix ( -base ) , prefix (ms-) ,etc remove them . This will help in search. + else: + temp_list= re.split(r"[- ]", app_name) + # strings_to_remove list needs to be updated on regular basis + strings_to_remove=["base","ms","as","ds"] + #list1 = [element for element in list1 if element not in list2] + #result = [s for s in temp_list if s not in strings_to_remove] + result = [s for s in temp_list if s not in strings_to_remove] + application_dict[item].extend(result) + return application_dict + + +#read a yml file and return a dictionary +def read_yaml_file(file_path): + + try: + with open(file_path, 'r') as file: + yaml_data = yaml.safe_load(file) + return yaml_data + except FileNotFoundError: + print(f"File not found: {file_path}") + return {} + except yaml.YAMLError as e: + print(f"Error parsing YAML file: {e}") + return {} + +#read the test_parameters from the yml file and populate into variables +yaml_dict = read_yaml_file(file_path) + +print(yaml_dict) + +#populate the variables with user inputs +capture_folder_path = yaml_dict["location_of_folder_containing_captures"] +name_of_existing_cyperf_configuration = yaml_dict["name_of_existing_cyperf_configuration"] +csv_path = yaml_dict["csv_path"] +exact_match = yaml_dict["exact_match"] +threshold_coverage_percentage = int(yaml_dict["threshold_coverage_percentage"]) +percentage = yaml_dict["percentage"] +dictionary_path = yaml_dict["dictionary_path"] + +#Load the configuration or check if the configuration is already loaded and having an active session id. +#Only the loading part is imlemented . In case the configuartion is already loaded and has an active session-id +#we need to delete the application-profile and create a new one based on the app_mix.csv ( this is TBD ) +class AppMixBuilderTest (object): + def __init__ (self, capture_folder_path ,name_of_existing_cyperf_configuration ,csv_path , agent_map={}): + args, offline_token = utils.parse_cli_options() + self.utils = utils.Utils(args.controller, + username=args.user, + password=args.password, + refresh_token=offline_token, + license_server=args.license_server, + license_user=args.license_user, + license_password=args.license_password) + + + + self.capture_folder_path = capture_folder_path + self.name_of_existing_cyperf_configuration = name_of_existing_cyperf_configuration + self.csv_path = csv_path + self.agent_map = agent_map + self.local_stats = {} + + def __del__(self): + self._release() + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, exception_traceback): + self._release() + if exception_value: + raise (exception_value) + + def _release(self): + try: + if self.session: + print('Deleting session') + self.utils.delete_session(self.session) + print('Deleted session') + self.session = None + except AttributeError: + pass + + def _set_objective_and_timeline(self): + # Change the objective type to 'Simulated Users'. 'Throughput' is not yet supported for UDP Stream. + self.utils.set_objective_and_timeline(self.session, + objective_type=cyperf.ObjectiveType.SIMULATED_USERS, + objective_value=1000, + test_duration=self.test_duration) + + #function to read the input CSV file which contains 2 coulums - (a)Application Name (b) percentage + def read_csv_file(self): + data_dict = {} + with open(self.csv_path, 'r') as file: + reader = csv.reader(file) + for row in reader: + if len(row) == 2: + key, value = row + data_dict[key] = value + else: + print(f"Skipping row: {row}. Expected 2 columns, but got {len(row)}.") + return data_dict + + def csv_to_dict(self,filename): + data_dict = {} + with open(filename, 'r') as file: + reader = csv.reader(file) + next(reader) # Skip the header row + for row in reader: + if len(row) == 2: # Ensure the row has exactly two columns + data_dict[row[0]] = row[1] + return data_dict + + def sum_percentage_and_populate_applications(self): + try: + df = pd.read_csv(self.csv_path) + total_percentage = df['percentage'].sum() + applications = df['application'].tolist() + percentages = df['percentage'].tolist() + return total_percentage, applications,percentages + except pd.errors.EmptyDataError: + print("The CSV file is empty.") + return None, None, None + except KeyError as e: + print(f"The CSV file is missing the column: {e}") + return None, None, None + + def populate_applications_and_weights(self): + try: + df = pd.read_csv(self.csv_path) + applications = df['application'].tolist() + weights_raw = df['weight'].tolist() + #process the weights such that they are convered to integers and in propoer ratio + weights=convert_to_integers(weights_raw) + print(" I am inside populate application weights ") + print(applications) + print(weights) + return applications,weights + except pd.errors.EmptyDataError: + print("The CSV file is empty.") + return None, None + except KeyError as e: + print(f"The CSV file is missing the column: {e}") + return None, None + + + def percentages_to_weights(self, percentages): + total_percentage = sum(percentages) + weights = [round(p / total_percentage * 100) for p in percentages] + # Ensure no weight is zero + min_weight = min(weights) + if min_weight == 0: + zero_indices = [i for i, w in enumerate(weights) if w == 0] + for i in zero_indices: + weights[i] = 1 + # Adjust weights to ensure they sum up to 100 + diff = sum(weights) - 100 + weights[weights.index(max(weights))] -= diff + else: + # Adjust weights to ensure they sum up to 100 + diff = 100 - sum(weights) + weights[weights.index(max(weights))] += diff + return weights + + #Configure the Applications with their corresponding weights in CyPerf + def configure_appmix_test(self): + + #check if the input csv ( appmix name & % weights provided by user ) contains any duplicate entry in aplication + #coulum , In case it does we need to coaslese them into a single entry by adding weights + merged_df = merge_duplicate_entries(csv_path) + print(merged_df) + + # Save the merged DataFrame to a the CSV file + merged_df.to_csv(csv_path, index=False) + + if (percentage == "True"): + total_percentage, applications, percenatges = self.sum_percentage_and_populate_applications() + + if(total_percentage < 99 or total_percentage > 101): + print("You must fix the percentages such that they add upto 100 ") + else: + #if percentages of all the userinput adds close to 100 , then convert them into non-zero weights + weights = self.percentages_to_weights(percenatges) + else: + applications, weights = self.populate_applications_and_weights() + + + #create a dictionary to store the user-input applications and their corresponding weights + input_app_dict_tmp = dict(zip(applications, weights)) + + print(input_app_dict_tmp) + #covert the weights to the nearest integer ceiling value + input_app_dict=convert_to_next_highest(input_app_dict_tmp) + + #Search a configuation by name as provided in the test-aparmeters.yml file ( example - PANW-APPMIX) + if (self.utils.search_configuration_file(name_of_existing_cyperf_configuration)): + #load the configuration and create a test session + try: + session_appmix=self.utils.create_session_by_config_name(name_of_existing_cyperf_configuration) + print("The Base Configuration Template was loaded successfully !") + except Exception: + print("The Base Configuration Template was not loaded successfully !") + return False + + #session_id = session_appmix.id + #print(session_id) + + #check if the user-input apps are present pre-canned in CyPerf + ##SOUMO + #create the CyPerf app-dictionary + app_dictionary= self.csv_to_dict(dictionary_path) + target_app_mix_dict={} + not_found_apps=[] + matching_matrix_dict={} + found=0 + for item in input_app_dict: + cyperf_app_name=app_dictionary.get(item) + if(cyperf_app_name): + found=found+1 + #check if it present earlier in the dictionary - in that case we need to modify weights + if (cyperf_app_name in target_app_mix_dict ): + adjusted_weight= target_app_mix_dict[cyperf_app_name] + input_app_dict[item] + input_app_dict[item]=adjusted_weight + + target_app_mix_dict.update({cyperf_app_name:input_app_dict[item]}) + matching_matrix_dict.update({item:cyperf_app_name}) + else: + + not_found_apps.append(item) + + + #Reports for users + print(" = = = = = = = = = = = = = = = = = = = = = = = = = = = == = = = = = = = = ") + print(f"Total applications provided in the CSV file = {len(input_app_dict.keys())}") + #pprint(new_app_dict.keys()) + #print(f"Total applications found in CyPerf Libarary = {len(target_app_mix_dict.keys())}") + print(f"Total applications found in CyPerf Libarary = {found}") + #pprint(target_app_mix_dict.keys()) + print(f"Total non matching application = {len(not_found_apps)}") + pprint(not_found_apps) + print("Matching matrix") + print(matching_matrix_dict) + print(" = = = = = = = = = = = = = = = = = = = = = = = = = = = == = = = = = = = = \n") + #Coverage Percenatge : (This is the ratio of applications found in CyPerf Library / Total number of application presented by the user ) + #A higher Covergae ratio incicates that most of the applications were found in the CyPerf Library + + coverage_percent = (len(target_app_mix_dict.keys())/len((input_app_dict.keys())))*100 + print(f"coverage Percentage = {coverage_percent}") + print(" = = = = = = = = = = = = = = = = = = = = = = = = = = = == = = = = = = = = \n") + + session_deleted=False + if(threshold_coverage_percentage == 0 ): + #Menu Driven option to the user + decision=input("Do you wish to continue with the existing Coverage percentage [Y/N]?: ") + if (decision.lower() == 'y'): + #start the configuration of the test and subsequently run the test + self.utils.add_apps_with_weights(session_appmix,target_app_mix_dict) + self.utils.set_objective_and_timeline(session_appmix,objective_type=cyperf.ObjectiveType.SIMULATED_USERS, + objective_unit=cyperf.ObjectiveUnit.EMPTY, + objective_value=100, + test_duration=30) + #self.utils.check_if_traffic_enabled(session) + print ("configuration complete !") + print ("Starting Test ....! ") + + elif (decision.lower() == 'n'): + while True: + display_menu() + choice = get_user_choice() + process_choice(choice) + cont = input("Do you want to continue? (y/n): ") + if cont.lower() != 'y': + break + + + if(choice == 1): + #This part requires automation . TBD + print("Delete any existing capture files in the capture Folder & ensure the name of the capture file is same as the name of the app in user-input CSV ") + user_input =input("Upload the capture files of missing applications manually to the capture folder and type -\"continue\" ") + if(user_input.lower()== "continue"): + + + #start updating the master dictionary after reading the capture folder + #read the capture folder and gather all the new capture names in a list + filenames=get_file_names(capture_folder_path) + #update the master csv - whic conyains teh dictionary mapping between user-input & cyPerf appnames + # Create a DataFrame from the list + df = pd.DataFrame({'app-id': filenames, 'cyperf-appname': ['CCA-'+ filename for filename in filenames]}) + + # Check if the CSV file exists + try: + existing_df = pd.read_csv(dictionary_path) + combined_df = pd.concat([existing_df, df]) + combined_df.to_csv(dictionary_path, index=False) + except FileNotFoundError: + df.to_csv(dictionary_path, index=False) + + + + #start creating the custom applications + asyncio.run(capture_convertor_and_custom_app_builder()) + print("Exiting the present Test . Rerun Test again and check coverage improvement") + self.utils.delete_session(session_appmix) + else: + print("Invalid input ! ") + elif(choice == 2): + user_input =input("Trying to fetch captures from known repo & creating custom application") + #start creating the custom applications + print("This is under implementation ! ... exiting the test!") + if(self.utils.session_appmix.id): + self.utils.delete_session(session_appmix) + session_deleted=True + break; + elif(choice == 3): + print("Stopping the execution ! ") + #raise Exception(" TEST STOPPED ! ") + if(session_deleted): + self.utils.delete_session(session_appmix) + break; + + else: + print("Invalid ! option selected ") + + #If threshold percentage is non-zero the decision to configure and run the test is driven by the threshold percenatge & covergae percentage + else: + if(threshold_coverage_percentage <= coverage_percent): + #add_application-mix along with weights to the CyPerf + self.utils.add_apps_with_weights(session_appmix,target_app_mix_dict) + #self.utils.check_if_traffic_enabled(session) + print ("configuration complete !") + print ( "Starting test ") + #start the test + #self.utils.delete_session(session_appmix.id) + else: + #need to stop the test + #raise Exception ( " APPLICATION COVERAGE INADEQUATE FOR PROCEDDING WITH TEST Configuration !! ") + print(" APPLICATION COVERAGE INADEQUATE FOR PROCEDDING WITH TEST Configuration !!") + self.utils.delete_session(session_appmix.id) + + + def _set_objective_and_timeline(self): + # Change the objective type to 'Simulated Users'. 'Throughput' is not yet supported for UDP Stream. + self.utils.set_objective_and_timeline(self.session, + objective_type=cyperf.ObjectiveType.SIMULATED_USERS, + objective_value=1000, + test_duration=30) + +class CaptureReplayTest (object): + def __init__(self, capture_folder_path ,agent_map={}): + args, offline_token = utils.parse_cli_options() + self.utils = utils.Utils(args.controller, + username=args.user, + password=args.password, + refresh_token=offline_token, + license_server=args.license_server, + license_user=args.license_user, + license_password=args.license_password) + + + + self.capture_folder_path = capture_folder_path + self.agent_map = agent_map + self.test_duration = 60 + self.local_stats = {} + + def __del__(self): + self._release() + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, exception_traceback): + self._release() + if exception_value: + raise (exception_value) + + def _release(self): + try: + if self.session: + print('Deleting session') + self.utils.delete_session(self.session) + print('Deleted session') + self.session = None + except AttributeError: + pass + + def _set_objective_and_timeline(self): + # Change the objective type to 'Simulated Users'. 'Throughput' is not yet supported for UDP Stream. + self.utils.set_objective_and_timeline(self.session, + objective_type=cyperf.ObjectiveType.SIMULATED_USERS, + objective_value=1000, + test_duration=self.test_duration) + #soumo + def get_capture_file_paths(self): + try: + # Check if the folder exists + if not os.path.exists(self.capture_folder_path): + print(f"Error: Folder '{self.capture_folder_path}' does not exist.") + return [] + + # Get a list of all files in the folder + file_paths = [os.path.join(self.capture_folder_path, file) for file in os.listdir(self.capture_folder_path) if os.path.isfile(os.path.join(self.capture_folder_path, file))] + + return file_paths + + except Exception as e: + print(f"An error occurred: {str(e)}") + return [] + + + + + async def configure(self): + print('Configuring ...') + #read the pcap files + list_of_paths_of_pcap_files = self.get_capture_file_paths() + list_of_paths_of_pcap_files.reverse() + print(list_of_paths_of_pcap_files) + #upload all the captures from the specified folder ( in yml file ) + #to CyPerf Resource Library for Captures + while(list_of_paths_of_pcap_files): + capture_file=list_of_paths_of_pcap_files.pop() + print("uploading capture - {} ".format(capture_file)) + await self.utils.upload_the_capture_file(capture_file) + #create an application from the uploaded captures + apps_created= self.utils.create_apps_from_captures() + print('Configuration complete !!!.You may now use the custom apps created from pcaps.\nThe custom apps are available under the Resource Library in CyPerf Controller') + + def _start(self): + print('Starting test ...') + self.utils.start_test(self.session) + print('Started test ...') + + def _process_stats(self, stats): + processed_stats = self.local_stats + for stat in stats: + if stat.snapshots: + processed_stats[stat.name] = {} + for snapshot in stat.snapshots: + time_stamp = snapshot.timestamp + processed_stats[stat.name][time_stamp] = [] + d = {} + for idx, stat_name in enumerate(stat.columns): + d[stat_name] = [val[idx].actual_instance for val in snapshot.values] + processed_stats[stat.name][time_stamp] = d + return processed_stats + + def _print_run_time_stats(self, test, time_from, time_to): + stat_names = ['client-streaming-rate', 'server-streaming-rate'] + return self.print_run_time_stats(test, time_from, time_to, stat_names) + + def print_run_time_stats(self, test, time_from, time_to, stat_names): + last_monitored_time_stamp = None + for stat_name in stat_names: + stats = self.utils.collect_stats(test, + stat_name, + time_from, + time_to, + self._process_stats) + if stat_name not in stats: + continue + + stats = stats[stat_name] + last_time_stamp = max(stats) + + if stat_name in self.last_recorded_time_stamps: + last_recorded_time_stamp = self.last_recorded_time_stamps[stat_name] + else: + last_recorded_time_stamp = 0 + + if last_time_stamp != last_recorded_time_stamp: + last_stats = stats[last_time_stamp] + + print(f'\n{stat_name} at {self.utils.format_milliseconds(last_time_stamp)}\n') + lines = self.utils.format_stats_dict_as_table(last_stats) + for line in lines: + print(line) + + self.last_recorded_time_stamps[stat_name] = last_time_stamp + + if last_monitored_time_stamp: + last_monitored_time_stamp = min (max(last_time_stamp, time_from), + last_monitored_time_stamp) + else: + last_monitored_time_stamp = max(last_time_stamp, time_from) + + return last_monitored_time_stamp + + def _wait_until_stopped(self): + self.last_recorded_time_stamps = {} + self.utils.wait_for_test_stop(self.session, self._print_run_time_stats) + print('Stopped test ...') + + def run(self): + self._start() + self._wait_until_stopped() + + def collect_final_stats(self): + print('Collecting final statistics ...') + stat_names = ['client-streaming-statistics', 'server-streaming-statistics'] + session_api = cyperf.SessionsApi(self.utils.api_client) + test = session_api.get_test(session_id=self.session.id) + self.print_run_time_stats(test, 0, -1, stat_names) + print('Collected final statistics ...') + + +def start_appmixbuilder_test(): + + agents = { + 'IP Network 1': ['10.39.69.98'], + 'IP Network 2': ['10.39.69.99'] + } + with AppMixBuilderTest( capture_folder_path,name_of_existing_cyperf_configuration,csv_path,agents) as test1: + test1.configure_appmix_test() + #test1._set_objective_and_timeline() + #test.configure() + #test.run() + #test.collect_final_stats() + + + +#async def main(): +async def capture_convertor_and_custom_app_builder(): + agents = { + 'IP Network 1': ['10.39.69.98'], + 'IP Network 2': ['10.39.69.99'] + } + + with CaptureReplayTest(capture_folder_path,agents) as test: + await test.configure() + #test.configure() + #test.run() + #test.collect_final_stats() + +if __name__ == '__main__': + #asyncio.run(capture_convertor_and_custom_app_builder()) + start_appmixbuilder_test() diff --git a/samples/capture_folder/mssql-db-encrypted.pcap b/samples/capture_folder/mssql-db-encrypted.pcap new file mode 100644 index 0000000000000000000000000000000000000000..97e3a2e96ebc85df4d331e5f9516a22d34a11ccc GIT binary patch literal 3906 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 J*jN{M{R_IN(oX;Y literal 0 HcmV?d00001 diff --git a/samples/capture_folder/ssl.pcap b/samples/capture_folder/ssl.pcap new file mode 100644 index 0000000000000000000000000000000000000000..4a410c713756eb2b7b3a0b6596808ec1062f7272 GIT binary patch literal 564674 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
  • 91re!*1fyL%Ep~d$C>37X zGTUzf4MNQzr!KLEsW}cr>RX8Fv1gOUeUv(lL#0EgY2;Mq>TT@_}{{ETI+BFY{%7{?o$f?}sFg0Njk;(;8 z6EB6RUq-1kIMihbHJ+ThngOPIJtI=32u9X2*{#7>Q7Rn{bvZ(fA*Tvwz*KkQas~e# z5H+lrgLflJU5rCzLa4FiRBU@y+S=}M(bXeik1C+W9hq@S{CXrKRs9~z30+AX@ zFtRz|$QDV3QkUaU7a`PSa;khXOts}CQd1%7vk4W?+b9(-X_)ONYbioaAg3zp!PKMB z&-#oB{a+A_Z26ip&L2mqkk4nOE_3a^q0h5o#nk zb*mLjHGzJUW~c#+A!^T8jSv}>%7;T`L#TJisk@nA>VD!^AV7d%WJfb!CG`h zM5y89RJ|OSDicintj7sPzNb7>IE-efL(uQotPQ7{FxCDQ=^axq2g_S(`(xBz3xZL= z@x-qB`Qh}o3vKQFdsNP4*29EtT5c`*s z+TY+1mSJ2#ykYg0!S?^_;|@T(Bxsie?ULYK4ZN!@3+?ER8fSjO5+CLS?{@RHB3rOvM_#qbi`*ITPe-*a> zkEpf#zIQc{n;K+)?enZ~L!;zCL#0lDhd_|m6ONz?-DkNY%Y!>>Pf0qB4-3czY##)< zDUh4uJzs2bS*+^#uTqB#+z?#2q-C)a?Wl^4MXbVW>6?o1u-! zfuXU(sQq(r?5~H|KabS@R)$#Y&tFQk|1fO-XS4=8o^kSOdr+tk_A@=Eeae>S@yycj zFfh)*X5r5I<4KWqQc5&dTmuUq9)B<;8`dwZP;UF}wu0bpZ|nJCp7Iy(0OMfBSz>vB z&4(|iDK;&hcU|2(rPhWo<8q8|t6m4YG|(of?y_An?yHxXOeH+Jq%*HO+-PCaT#eh5 z!CJI_No{+3K{lL2QpxzA$%ccb1Ko8sDkWm~-c$vZNgsJ@J|QPm?|fg5vrJb+Kv6SV zjw@0wQqd#0H?ZWYh{lHbH&(XVryZ$a+0amMymW8Xeed`FNxM%Z$GqU5Q*K+i>r&{e zkMAVDNGJswX9wJ{U$dp~A8$H{{IFaWwLg>rn)TIJ7qS0SQu{klVX=Sh7R>&i^kDn* zj+RY(d-Qzu>Ss31u9~x{N5*h(kvw(gu#s%~lwt|p+#ZGCc>X~Rs7ZPpeQ z*RWnPP4x7sSm;!?r@VqfDBr7){;5sx{*RUqotu zrz9-)UqHs+KZ!VvXZjkjh8bx`8@pQ0M$J)H&NNKlu%{Kh&W* ztNnK)_Aey0zxO>X_D>Hd+W!M=e~`+*f2`5^Drb{%INOD^jfcc%(8F zPUf{w3DVSgq}Tci^yvE>TU}3;^WY(OA$N0phtvAOP5fMO%W1iOd@3KjqWo)LdXRep z8+pEa$Wp71sr8KghoL_DS?zCt*#9x9{r$INu|MY(%>DyPu>Iw7&oS6juPu~#IN~kn z;3V@vNRdCsTE^lJdj0>|bb04tT<*DtrGdP)C2s zq44Nalhq;j{lDEv3fehXvv==4+Y9fWl(}7yl&snoX6jxYJv{nG*K$?jt`NamBfVY; z|L*(R!+URkk=lIFVM5a;4D`R2ajUarFeZTh*MFt|^-t_bgzuuIrkndjEAww6xnT|L3|;^E`n{A@>t^8#rVr z6(#yJs9r2;UvS$YXQ<$SV6f?@mY4s)!#H@t22a@F3HzmE!fXUlxxtwQqtMTi-_g8Bf^Y z9TmKze)l`-ukqs9T*lBNB-qf`x%?YE;T_sdH0A{TJ=;8P4%q&75#)2D4GtmGs2`&C zH>P_+FzU5vzUS46Qk`+AEC}@;In{I_Ox5)yQlWX=y=5;3Ws6bjIUFi8LX9S;9)1W@ zcWPr)dT1(rpXj3?st%NT0f)K*q248@S{cGrjdesSG?l*Zs*rI3^xq&~SLpx6tY24F zgc?Oob)bT&O3;*eWO|-4Jv5cRZ`eOH${eNo;84vF>RxiHQxZ&-gE~A=Dm0b;lU~nb zMRYU4m@f|XAVS?oPIb|Rsgh+x>LG~w_@c7GQuMP1;84vG>K=0H`EM{)ERjfcCK&az zZOn{oLaBi`)I$hWpPcG_52mj1CQ^eSs!KgZSpiDDhC@AoPz}kc{@Ya0;;$YNi2f_dZnt$iLT zC*_X6ds6&N5j-)0C#E>=O-$a>=_bjn$)g`P8})2C`J!ZzwAFr(g$hH_^D`4)(f7YR z2%eb06Vs1;V*1k_gIY_5?r;5n_MbHZFhoDxQ< z;n3f+#cQSrbw4>Z^cGBInNOsSLezVgw2uX$)H^s-6NG9&PK{83sY{@kZzir-u#jN% znI=Xc2I>StVjyUO@T{Np7((4mPQCXDrqUM?sk{)?BF%K{B1(Q9OGFM{nKP&}28TYhj}B|qD`qS&Qw0dXrCKfL}DioY!y{3`sbzY6GE3&`Bciz#)aIA zHW>P{vr_Vid3gv7`zTW=nf(HpTf${?Xm&E3Px=bID$Z*EyNLbmN$p>-0gL@5wqy1e z5*CWQ1NmSu zWBT_SOaZcXAZz!7vv%8g5}@5Di2b43hgt0(jo9CY)c((#u-MK?t($?`KNiRS5s3XyklMdm7>oU{oW|_Gb|-9q3yqkgiSMfS za5-ae5IY>%(f<-v$<>;41 zYjxCL(2X$^3U8PEjn_&`fouu2*u0|3-OD$M+?=ALu!Z>IExV zDmh1#QcN78cJSo8N8a0~E;c>KQEHb;OLK$y>u$r2lY2R2o365%P%mT?yE*?_Z4!4~ zmMzB|i<-N1ywn$}hV)<08@FKPG0XE2k zea>ZCzN0jp3_AxF6?) z{OWT#zyuAj&-s^I0`a$IQhv3ZhuNIKw`k_449EVFi2aX~+W#XT7W>mIB--B*w*Srn zCrS?Rf)6q&U<)P~AV}Fs2u>HMzBAD5A6e$^=sxJzuKPD{8aDVPpiPS^gFD(I`0|y9>>~^xTfo(x7 zce32i)cNf1%t)=X0*m;-B0liszeF+fWsWPeG}{>CJX!9YjJv^;zc9zDyHgB1PFR+I zux7d&y|zG3!m8WAX;(_F0#9viAlsgR?VCiMUUgO1?`Qe^Uf6LNB?H@a_CrF7COg|7 zq~=rgWjtP562!OB^>Qf(*md#$-*u4@lb(&*e+b9^$%y?AliGiT1B?B46%g%z3${Pl zpdsA<%=a9hfPed8i6QJUtl((mgvWm7$>+q~GspB0l;V>vG*%nuoKK4a; zQNLv8q(=IA^UaT6rj7bNNXxTbx>bF_X16kn3r%8vpF>N_mko?O|5-XIzz!H2?kHQ( z3-s&$1cMYFs5O~Ag}3-BLndl}dK~-5BKEf;v3~`n6&Cvk>l5v73djDSg5@_!41yG1 zOp0lZVZzPV%VO^O`}$cYyYJ$W%2YU+*FGgkQ|FOh>nqTs?{jQ*Jyp(whunqS&G8*h z>jyy!52WyZg%n=0kJ@v@{tPoSqoMz-LSQWd~S3` zJ3Ke~f;eV>4QS5K8p?5Nj}0hQ118QwJ%Uhm$*E6#VX6yp%D09ZL_K(RvJCq7KyMTb z6wqFZS??(^$Vp{QdQwp{VfU2J(1wheHwyJJ_@oMvx}2L8Q|*&KNyWmvF|XqhozU&2 zy1&L295Chf`={K3d9DYeXX47{h}4;bojm@Tzx!|cBe=S(?_oGE(bXG*~a_)IxT-0`4c1f3~G z3-(RS^t&-o?8Kp3AXIH~>hmU;YFCV%DHEck2BUBA?YPB#-J>*XOtWglP&nb*C8l~>Rp*kQ`O>%1e z8<=WrLZoI9jMwgr>l{9dQupCd9TBP;IkovLOx?SMNUemZsYe4(T|udaIMfpeRh^vr zkq@S7a}udt1Y_Z4pBf(Wq0|F7R9l3)ot)ZT22;0v!>F1R5Y@5X=Lz;%58_a55b6$c z>c9z@stQ$P&qOPl%n-Fc)m9t+iGpLGBREuhgu0cSI>G@{x5N{vn+V1t8#yDqPoPw2 z()+COnjJ#jMoyh9f~m5^tz?=y5cQU%S{D5Egg+}Z32auXGeT7$qe9sQm@09Y_*pFp z#-fx8-6}#T)fR_(2B9jFQ>mF?s)#C)>JCv&&)QNmqEx(%e=Z1hGdYz$2c|OICsN-L zjMc}ks{~x1q0SY-iCE)ds)HqI#7duo_j{I(=;@LiEcme9n#z0fDN@jR9Qc3z9J5>|1nbgFFAzeD`+6bOaySG3AX=% zE9+Ufl+$P~d4G6C_d=_a{>Y|>`xC7*>&_L91m2CYADZLp?Q@3l`Q+Yot(CEfYZAJW zB6W9ezJ6@+#0~R@g_pL@ucmFa5MW%YJ#R@I>PSHrP?I%0nlQu{M6#A5$Ws)A-Q5iO zkikT^=Wnu1vOpj5cl04QF0p;7DLX>X67uU`GkrrA|I!1>~Q_mvMv`V*e*6<8(HuUW-g(!ctl zd2+9rVrk9-?>$|b-0dQ=dnjpIDiyjOaJ4R&BR=3+2u^lzvj1EsJ3mkA%nxyCs5 z&p_;NOlp5_LoD|9BfcnE%!BPu`K9Tyg@dxmSZZq*`~A1g6`zzVUu5pAju1{+o_{l2 zh@~*GdEce7SFUem5BJkQdbVfZtKMkuXAC#0#P~}SLt;YWdtE=b3UF87nmX*?uGDRR zbY1j7Amehb`T|jx|9?9eXxsvgTVPfzc-nA3akqg(hEh?YKZEMUqV@&gY2%9@eb0y3 zt9i+7l3As6>U8JLn1!^90|sumw$d}zeM(rcy1}&jZS)eTeiDB7o(sLE&zkpth}i!C zsr^?|VX?no3l{rZn8Wt>X&Nx8temXQbQzI04VOMp*_{#*I}|9rY0rx1`h~hzc3Yi~ z_a4|R+*yA-DY8xq3~mK&FQDyZ_O_S*xgQqi_*7srG{`xJ~<#c< zKeBWXX&(*kt`PQei|TJ!YZsAKZ*H^dm2K`&s(|U_^j%*Tk21Ayf*D)iM%_QwXnmEl z$vB+tLfXc|XZAZ?9Y{v1-VlRxdQvPiM|P^7lYEw zRmLZEsnxT4FF$5GyY{m3EtxLHTU?(%>2-%REuXh+F|AHhNs{G;?6ERc-`HJU#Z}&? zGgyWD+!*I|J4ExSQJNH3Jo4w|O>JXUI_q8}Z^?JURFF&Sf;IQW{=(aTaPD-+fsR%Ally#wm&E~2gT+Ap5}Q1l|ny7M)qf(*7Q9$cW}oB zckJ)EV|PAHyouWX7LNT>5&N5w+F$-27W==?BHI5mZ2y28+uBS8Vx4O(&By!}In;Z5 z2POt7ZgX3{g=y^SwAUlX+(8qCtKw~U&$b@(H**PTU_E`MprTJgdP{@XRpwUN%`zuH zT|1?_u12Lq?B1KIpfc$rZ_Ov7s;9%@(mTTUoDt@V+3x;i!e*7GsHv^;XmFuh=*8nVKbAALHEdA4(j*{V z_AAV~`DZ7+dZH6o%N5mAK@AfXY-BZ23?DhQ&k7~0Lnv39$4er?A zbH`2#xV;#)|3k>&>F}SK32HOb)ZjU4ly{o4Vf&v9B(=ZB2p0SATSK(}L)iY|Q+b~J zW>R8|S1t!j{J_1}e`uTdxd$h)3*2LzBRm&5!uODV6StaSN4P#TF%Lb$tKe_qUmk*; zG-m-}{}ngF6btQ{{TW{1xehcjf>sBMn!9wo)EBCT^k2^#w_z0p4{P9It!D4ueYW4x z#Q48Yp4+@i8oGQTWxGW<_Rm4=uTN_K-3zcI+!1>IpRvD{1Z@9V=h=ZNPOJ=X*iM2e zPPu9k7r$AFG_MnAp)FESwO8<&^gc=F?(F=irg^K`rL4=75p?omhlX8Jz|cqy(K@je zEiswD<}%evod6GkAg?DJK^3~saz~a2ch;VgbQ&KPkPFy8XqNygiwo@n=G*Z1u^C41 z;S8oZf1HVq<)xruW42AtKfX<73)QIz)c#dC_J4%f-;mV)dg)l~Kc!Bze=ux+mICWI zy|0^DwRbX`KYcZ!!ehW%GR7#$s0OAuMbFPnd_~{?^5A;yD+kZ8GL@O~jj+|<-rJqu zT+w)7*U9DHLaZ7#)g!fnH%0vnQbgv;>fJInVO9i7dcl(38!KDw(~eZIY-lJrUb?sH zzW00oq}^bO(+^yyTTHE2ef%^1reXhY=Ikoe37__B&pT_h!?5gK8-K6P7}e($j9Iq6 zH`;c0-cS`CysGFGUQux3*tn2;(FQ|bc2-IrF)t5+VIO4*C9}~6%QZVI3zA%#ynLSV zJ-K<{T+Q?b{&qVBR zL~4Hn11$D0yi2tI1Z;m$t*>&Uc~7>?#HY8V#;ohx!-7LQwL!JM=ND6^T8%IUF4c9M zL3w$NMVWJ!aWFcA^dFdE1}2?QFJu$DIsaO15_eseEytW6lKwN8KDTVf{xl3Y_Rm7> zzn|3prt`4aUjZ7QKa=_KbcXF;nbX2OH}EjWp8drhRu3B^_Ga^y_q1g^Q@Lez>~_DG zRg;L7mkZaX_=WA)Z_@|Ho?%bb580Yq&&dtm{6V!osMh}yH%K>lWUxr`T+zvnRW?^5 z!JB_RpN(WgXXrUSnH(>^Yqa^3v=^6Nk1lL%TsR0Kh;*yQcgu=(v1ER5X%cX;QwDoRoR4NWIzqA6p`9p918Ion6Q2VdIvHxSl z{syG>Kb(lg{%)Iy_HTym|I<|KgK-7_UQ!XH|Cqd`(@m0DlSe;pHtN}O@wRWof3GdD{lP*S@RV75 zzS7CUz+v>mxPF!4TgA4C)HQa|7d5r`lrx68TFk&xCU=x=-yd3O_?K$}x2iAL>{e!R zp-If|b7*P#vVoE3GWl`paz}UAg(VkccAwohS2Bj$KVe?$kjt4X)N_rg>2HIm<1a@Y z_XyFhsQtxp?0+7y|5Z}^+fQM!zcq1Dqt{*7{@^L|UwzC0&o1EEg`=^->9u{OR#nLA zLs3Hc0zTHG;MoPNjv*Z|A9i?2J8J*UIQGAY*x#Sj{!UR??7x}^i~YT+VEcoy^&Kbo za>zDaWi$CV9&^C63xmG_!(yuEt*lR-dd=nOMYz8@_itA-(v@-Mb@x0U!E3|9_^nH6 zuSl1cNa*v1FkjDhc{4gQn%?yP@Yx0Q*MVHSiyuYe7X7f?oX;-F{v37B0ygWqu4$_G zU#CP%yWMgzgZHT%Rk?}yFN?!c4t-C5-9OL?XM5}vy7-(1S|~s3uqF@0{sE-+ciD!; z{_cl~_CEsKAB+b6mkM~nt@khAdcpk^+)w}XQ9$$agd@?|{~V6}-4OdX3fhQNR!FklFj1^%G> zDc%A!D>*OJ{(d<2cSY=foz(u`H?i1X+XS<}&sy02V6EC8HX3-x*m6*)2?{kqpZ2d+ z+;Ap%N8yb9=|*wv?~d62e++B#SH@!ht}x90(b=&5cRb_d)%KuJAM9s(O#75A&*Pb; z;o(8{*FMh*H#ABPG=T1J8Y`}Wg%6KEn34_a7gi{@{dQYHaJRSh{4h`Xi+7HA{fph- zAbA9mN54j=DkyIF-r@#5FTMve_NSkNWB+rA{ewtj|DZlB_HWR|>>tAc+ke{Iqvxwv zKeK6e)tpT|GKPDLtkBhi$v$m+XhV_zZqNh*A zLZ`Al7$KRXO^TPqnNq20-!%B#%Jy2qcfb zGkG+eI;M)+A0F7M0{@vA(K<7(6&}<|A>Mu#w!hsLN=m4#pFlwwEl!yQn}0#Yl0&YG zrl&8r5$zB4`W`vWapXJ!r844Boe-)DIhA=KOud*%q~3z4O_wF^!!&4-G(9s8^&~=7 zC8x4KgsE=ViPUt0@zKq!H;?WY zgmbE1hN%wTq^D|V5O$|p+eD&@9zzAKsW-{Ty`IkY`=b1 zuQoPe!s?8OjN^see({#z%*B!1lcJ`|nVZ%++Eg$Z=87mWzI~hp(-KN)8tffU6^_VI^||M{$iIv z)R&@Gl$TH{9}ZO&p`IkCN`Hf?CIdvO0KwQ&C{uGE{Plz*fYmru6@=q< zsqzrjFU#751*Hn%P`4n|v*c9e?J!k0kx12rsGs`+v!YNc)D1anT(KFUx{y;fMquhr z;{4oLTY~X1txkg~^rem->d={$s)$g}kW;ss!PM=(#Ls#IqCV{4TXz}#tdclX1%&EM zPTjo#rmCe9srMmjRhbqIluX71_m&7qcxG*1W|(v7V$Np)Xg~59SGHioN77`rf%3yqz*&WiLm)yt5B*U4s|<1 zwI!z>PK2q;=`d>C0)nxpb9KUoLnsvr&S$OEyaZDnd`M%<2stccQwvpj&iuy4Kz7l5 z2gdUJ!}Z~K@9&5G+m*WQkFJXz2xMH&RbL?LLRI$uS)h*ol0)IqrzWdI?)!hckrcFZ z@SmCW4W@GEHO3~~JG%P>t)_!EclcOnD2=9QuJ72nQZSX9TdM515tzy?KCiyeOR2c# zie28n+RAQwF#?N$py|r9MgZ!_nR0^sOtI2|&y+=_nEm6nKxay`9`)Rh=$Wz=hpL89 z9m%QoQ!te_fk@p$F!tJ__ohk$rS8T#sr-`nfjy~)4LhGPTkW+l}AS~zElCUT}Y zke?||QSg~E*8;PDoG)~y+TzV6!m4njRjPCY*iQ^yt%sc#_ak*0&2#!#vu&iUtyoPS=V=U>8Q?50KmA|<;!!k8XH6hL{ z^EQ*CE*YiZ4NB0-9nAaqQS&}PKDQ*vazpl5nW}H>uCC%L@6#Eq!hLRx^ST|PdH%4@ zC$L)me;KnrS`h)&FClj-OPn)h7jmXpk)J7{LGYPUKzy%`-%l`(;o$gm2s#lFDpYed z>+6>%a;Er^o+%mcusc=Ca^jhC3_er9Ncdl5Oby7qg3K$(ygKN(u3jpiLsPVfpgtiQ zEObw$O)mLQh>KM|m%K*pe+K7F*@>Jf*5qeOgdBXPWXEClkFST$6t`4~gf}P^s<)Z- zO-&1-T9Q-meT1nGH(}HSS%}&?eb%87rN-k>cO%qe`YEEX)#2N36MDjy|N*vY9$VJKSDJnr)Io^sS(is7NuS! z7{}2SepL}fsqb;9MhMlIociPfObyi~QpX|cvC)-t;2bSf1-7&TCj#(+sScM&BY-Ct zuv?{DFh*_nA{Zx)-*sP-X1Q8K++B)gGA`5FIJBbmU5M1>+^m>tpZrNG7Uqq49gpaQ zZZFl9%RR?nPrbHK;^BxlxN(IpqX0Lq%bgW{fi=|fZ-;T)c|3a_R ztD8={ynh$&2|lmub?in zO&9)!M|gs}5x5(HyO9HU9y^V)pOY4%lcyvE6JP97%!J|7(a_g^XFu$ZdOu$*p|IT^QhS^ zH;JJKzq&vCSNG4%UxO@TldAO-inP{hVV?W!ewjQ8N^dV21$0E8kafJurU_$TH5|8Tp5285_5L2%NG1GHANYtS|r95&OH6+P|6? zi~UuJmEWC9Vf*VJEmZ)yOOU%l{zy+{nnWYyKN^mmtg7 zt2+0=%Ti~D>9-rV)5Yj?Ep0Ai=n)cZ=u49_*~;-@r_LJng)H`UCzv+O?y19LiZNk!O-tM=|R z1Dl0A>yIZz)=4RWsjOPQqcofhI}6@k4{j{s+~dwsI@vA$RV`)e#2Ux5yyuH8E{pvO zS;loO%-2!-GvL_&8e)GpQv27JVzK}DB`o&uJPzAmDvo;-nDq)~y_zUo6>qzHw)L35 znM+6m>**^66@3!YTN=EsGPlZZmO1(9+9}<2H7X@yKii_9FjM#H=;6^fx|XXFcZCSn z8tL^)_;=se9^QNVi`3?W4ilO-Ve7=5>IxGpvTw^Zucr~7cP-%T;#Cbkj|-lx{w%kn zTL(mzAO>U~weS3yW56~09;0<%%b+QZv&Q}bi2XfC?caPHi~W~p6YXCI+h3r$Tm7JI zov~(G$~+a%`O%@~T4P&F;#fl-QP=jmrwn$hpSWI;FR<1!pm-`FxBTF|N`AI=MX^iW z0^(LOet7*Qv@5pXYhU1U>*EGOiJ`X=CzK{zE8o3aL{XYYX6VySnR)w82fk0 z!1n)rUd6%t^#Ot-yPa%oU|SH&oh;k?cJ@3xOnU5c*ncpvRcGJ~`owvaIHFyLwIXwX$!2~N=Dmh1#QcN78cJSo8N8a0~ zE;c>KQEHb;OLK$y>u$r2lY2R2o365%P%mT?yE*?_Z4!4~mMzB|i<-N1ywn$}hV)<0 z8@FKEnTO=}JdfvD-`_fGTjz7m*}L6vU)NFpoa=S1jb8Wre&6q5 z$(?*uI}TUlhwrzXl8auajJc=TBRPJ-#nKxrFCXhxX>duEn0*A4|g} zio3&TkGxfF>PpP1HoKcM+5zIeA3g2^5gMp4OydhcCzHV0n|Wh@U&Q`biS0jl5{vzp zK|OOw&5sFHD{Oxd_ubj^@>E>*R}lAsxR3L2?T;Aufe7sviO~LsC#pc#9=OXH%KWT; zL&pC15c{7ew*MF-7W>QSV)mz2f$a|-zN8gZ6yGiC1`l69`okA^qO!;oEEjgMi>TuS zgTH^X;-~J1H_%zXtEbPQzmVvK0jTN5b}R8*#eeOS90Jh9TtFSadyg z*WyLqb*)8}bZsXD8CU9#fVVd1NwwiX%Pu#~XYz7K{N5Q@N!+hri7(4Ou(qTAqQEJ9 z)`5cOtqOZVujsm8w^tOriv8TLVvAmIK|89D*uRC0{X-D@pCht=Ii)!k``$`@ zw*U0~gb(^XtH5q;0msrSHUgPK;X|$A^=uDCmxQ#kB@{|m|p2yj$0*|P|0&Ay{UhQ<7HVvhwl)&}}Erm4RF;u@?{tFZUsXU7irR0=UHok3CbDy=FcUQ}r0vdzLvtJmtje38x z;83NB`Fv29>f+(uhe{gT1^V*9T=)u;w8*}z&rY2+m zJBa9ho4P;w)yl#E{;ke8 ziaCQTBQydyR!U!^>0MZoCFPWQnb#=lMCzLlsrXdZZ%b|5sA9iqU#-+JtIOSCnYpb% zeTfa2e)R8q)F1SpL-YR3(97+-6`w(f{Vx#PpP34a{nOtP?C%HLA9TJ?)1J32-OPMW zppN&-I#2)vl2M>XJ=gNk(P57F-!&O!y;f7q)JA3ePTr*;x3l*;N*UYhwkY{F2}+l& zP)d21?#wLBKEX&O%d>}ZZ~kqH0-?HQYTueZ$=%*tHezP! zZad4;mQzd-ksEU4He2-?Ie^&0xk2{m0K-eLF%xXeG`5)NI>~taE!dd(CmS=t7RtY8 z3#H@EN;lO0(PYk){m7Yeg!D{#Qw5(Xjf$B4`L;o4$|rf}S#OjYLx#E!p<0kqOB`Wp z^%??IA7|XPtHf2%7NtHTLp^{{%}A-0d@%L>Hv$#fZ`u{dwdMudL(LXXhH8jV%}J@X z#W3|<1A%%4XWYF~zemIm{j7=eQ(GRwRC`uZYV%o`nwv_X20_%b>)u@NLaE7Qs1XR2 zjg;EK0aMfO5~v9fwdV11JoJ(TzbLW2B14TtsLZ6)-aMF^a*jZKi!<(#iDQUEs}$JM z$WR|4R2EX|;7OQz3;K!A#T9%XaVAvGp~r3sqMtRB3^fd)a*|TV7-6dSUIKL-qP9w_ z8J|I^@D7c6BCD2Agt~^5I-LenJqXp*{K7aBY84+w24$35OosXZp|X=up-MfN>H^hw z&wU5@4@1;QAwl)D=cB)y9TEjew{nf=g4Eq0|~O)Mp5F z87Y;S3Z|aeO`xXYOlZa5mXAYQDv^H$O=PG~5$bYMDn~3#HD5)b_CZv?l;ihapj7x@ z?tH&l(Fm1^l**$AQZRGyP$V)hQ0dS)YW%+8~S<$+F_U{q5p zoQZ%)<~0FxDRD;M8+SJ6(i!c}<>J$IrBELpV0uND#h&TJlvI9>WdXFm7KWR zB<}SksJL|1;Mg0P?8oYI$9JC&ewLHcd-1?R$tdu64W?Is$Lj#4otIW`XPWSvb^nzg zuV-Yoq@(tKK*s*E$>pjyb}TL`>^s@hXodwp zzxHCz*FO10`&;W`@yq~?FYQ&TOPL&W#k@-FvJzVR2lNi9IX3Uk=;Ws`Io_rm6Cpa8 z^Wd8LLFYc!_irili+En82hO^Nm84{XR!guz6AU1g70QqUb!fd0Cz;cjJae?=cr(pG z9ooOVYgbt3K`}p^rQ(5d!}Hp|6S2QIvHc}>VzGbceS-Z1VEg}IV@<($D=^*)459*c zXrK=5U+U1Dk1Q=k?N4szTMJ_UEyVVh9>rq+q)~$XCt&-7-G1eIKCXfR?rEF>W%{`p zW2^6USD%-3m>d<9^Vv0gI)*+u@sn5n^Prfg`8z9}>|>4`P&8iiq^-_;gibu4-I~#0 ztmTL`&qJI3^z)_rE1#+z+I{bh`c%LoqrNCgn+!emV6fZI>v>|P&42oq0d^C5USNDP zZJ4C9COUR~OwZGB{XGh|Pc8>jt<_TFxARSG6l-`AhCZX znb`j$Vt+|u`^(?MV*e^a^@q73Y=4lzQF6W0=U?b2sTYp&~St^*A2{i1x()C$}GoE5SCHTtpGUoH@{|B*GY{UhBpb@)}1 zN4ZKU;iFNRMo(SDiF+9gwq>DB44m@^CO7WfR_b#7x|s`(tVcwL=ouV>RBzPudF zi{EGSo6U;{ldZsNJ22VmA1w~14*fiY+8=7Tp7-g#53&DxV*Br1h#lb*`v~?Af$d+^ z(YSeNv$DVOYoEKe>$m5b?H5(H@74(x0~JM}q6kzJrL79;0u@ES+oX&iaQZ|})i~T- z3l(nhE+S+90mS}-#P&Cc!D9cqb(sAvC}I02wpEJo#&LM1@Wv=NTtA#i&DR{RWlfW` zD*xe_DCO3*Qr6F-pW7~%zEBAgQXnA(5>g-`^;adNzE|q#*9c<|Z+I|`+8=7Xm^b$S zjM#r2vHgv7vDkl%Fb=}P47Puk`m(^sEQ z+Mk<@{d*AmixAr%{}qeey-OVbugdLzp}o&w-oRfrZvadNsAK)ZH>a$Bv-I{lcR%Jf zSp}JMpKqPl-&mzuDE{a}ML>!4u?CANIpJEzI61BoeNjOr%}6=!aJlfmrm#GE#9w;b zKDIae^Ynf9T3vbKF{mKYU3>QF@3$fCXI$nU=QLK1+Fyo@{XZf07bdp9l@=EJbL_%m z|D&F;{aK%lcPJeGwEf)M!e!D{2VIvajzlg>i7jFncxSdr*VpUowxg~#EW#GG^ z1S-`42Z7o;qdCuLoc!9*= zwY)up4wRf76QcS`-IbdAi)`p8eF#G@$JTY~`A+42<*scK2xjNTeR!JR;z`E{H3Q32!r;)5?)&J}bn0 zJ-0{kao7vV_z{gQulQnj4Tf^=N@%|A*tTWT`exoViKE&*Q`2uYJaog@H2{wW3#)Ab)wFOda0wH0q z2Zk8*`c&2NJW?fR4`--_xqY(|ZQdx@N|&#=!&cFA+T$F(i=*Sqs%9!~-U!|3J3 zkBmCSb7b>dOV)JSbIH(!UA*w)xFZ~gdpE)WMBBF8`ITds}O{`VUq!7j%i z<}4DsX!ZfM|5Y;fA3^NTLu`L1Wi0lWgI2!Hjp@GJ1lyn1DcScezdHy2yYYd5#?tJa zhgbrSi7UR@>rra6uX0n#`?zG+O7lWmH)`f#I^Bts(jsa*24|HV4JHZy@Ff~D2BX#E zf0zlm@@%DJ-YseFI&jiYz^m;RPTJ}95u2ySN5U*jukFJ4u>Jks8CXf&uV0BT z%RaESqyD16DSXy}g6FLYd(Y^3ZhvUY@O0COXtR?(_a1h*b@ntFQLPlQe7NwL!G$wp zUe4709RsP2M;+UHKSr6$bK53-3wQbykhB#%6{@LA#9t|(j@!@eoaxQILf~4ky3`X9 zCgFacyF1%W1*09SEiERzmYu2f@bHiQO}31HeIQ^T2wiZ&kujP2DxLdr(_hAVk4bjk zzGzS#9W!NhK~%>6YQ}&>>z$P4;XKn~rYb4hHrQL2F&)YfRc34`QhTv#bSPL#d7sW= zsrD$^FTdFgDxABD9cur3Wb8kN*q@u&{vQ5V?EeR$-rwZ`lKBD2_;sQZE>bMhk5f(= z2bHxo21?z?NR6uW%$}xZVcweA^^#sVWTn1b#^t59ztjvWus8#>xz~6e>`qRov1)BQ zlTBlCGPLIe$MYeN%IBcX{YUHMxw2R16l(uyGWP$1*q@)+{@z=$*x&X57W-eJg6;o% zo}}Ob+VVF&^D?&;s4uZ;+Hoq4;ur4^0uRu?(gXDB{kd7F{h`{Qd0%}85&H`e+drTk zi~SQm6YPH!wm(SvYp~y~Yl`i7?XlBvV?p88W?bGYnnAH>`qk0_pL7lGZB;JtvAE9G zq9@qgt8QjfW2~7NPo;WoQDl&X&V*LsW7fczG}V1B@x#69cDKv21vgmw6wJhAl$ueM z3$Sm@k6wAn=kXfGmiI$JJ<$X12mDu`I%^~x8+1Q*N_o1iys>c^MU67(4+2ZTK?T0n zqDs296M~E@bw_TgS(fPB9H-@4x+kaM_MQ4du6-_?#nZhKU)ADg?AF^~kcE{Bg}y2B`hB$=H7wu|F@d z{e!M!u|G886Mgl)@(#8?Ncw|nC)E!fBHVPG@u^pDY_zvaznh$Xy7{yM-N<GHbb}uFO(6Su6lb{*(A8tkk?OuE{ z(V*Q6w0r$oyVp#6D^%wXcYCdemNm?4{~5&oti<+z)PTkQ(S%74Vd=2_@u1!7aIH`| zOF0)HK3wn3{O4;@wCoe9VtFaTczcLy7e?{!iMr{9> zD_HE$rh&!&;asr&IS3{8NqBG%@B{aNi)|;p&7A_lJ%HnJ*bVCq`2|(1*G*%ud6q44 zDA`w9Mj@Q-p2v`7-FGm);`ok8*(%*5P4AQHQneCGDrEu<+~pt1dwdFG=Fn53Tngsr zMn!nv^g8v-MVnVDMe$r_=Zp|-jVqX+OaCHjIj#EB+BGTxQu;FGLH_q$fa@77h(BG=?*W7BhxixL@f;I|?mMGZ%_Nyr=p#%?( zf-+KqG8H!e;&Qnl_>c~o< zK7*)bQpq2nI|g!3F(gBMj8GX#sS-P3s@)`k3e6)m+cb1Z%MGO(k)b9bR60_s^e9ZV zt|3qxA!@*RjTp3+YP~TTY9d0VC#A~YgQ>?~5U3*%b)w3&?GE}`&B#!b5h^VyRYe7+ znnC})b3d&IEI1Q$!$mLk{y?b~WT-C?>LOCAMn6nFWKN)pLDV!sjRm79^*9-7JVISU zO4T|DQx9GsP}OiIN7igMNrh_Dkl%rmWT1Et!Mp(Y^I#iUdtU6`r?{Z^tsts6KK3rda6a>6L} zEE#GVLdB6%@n2!8vMhlb0a495X5%0j@_lt669vw~)HBS)QDA~FmIG*{#Hisea1^L5 z$D3)sUFU%P^r}}NAHWIn0r44%yw%nIARn;rKv7?$NAA+Q72*QLv4K&6Px_p{vSN7vyOM603H%VenO2$hnQYC8f`#b0972pWj$*?OXc1EpRibN+op&Oa97^Dns) z%YyhqwKHfO+yS3|G8WTccYyiip#BilAA*Db%EA zibF7brmTnV=5zLs;DpYU{+H*EFrn0IWKOCSgi1q7by9|@41~;j#Acky4T}%>jUoIzAaAcK{2Afz0J#|zlQ8uRC-GUGb^*Jqs;$7N z51`C~fC$JRTKpIJLvX{F-0i(pUeo?#p?(VkxZ!Jq8-9cA(E)~+x!{KX)N;$~iIN>R zqqTbqDm?y3W)BXg!)LCzFe(laHX9X?Zrb20lz_~1;gP`MS|p+c#FWTg81*5v?JX?b zP|pIRJ|=TgO(Q22JMl^NrU<*Km43vi5sTrI>Zcs62u2`Ah=OrYU>p=O4l1?Q!g_6y zO~y!qpy~8%#7!30618ta$yyB$<4)F}^5beU4rRZZxb^6TgZ5U__Y*$o_pH(%b*Dca z?>KJF80coUfwE2L;lYaZ{nJ@Gr%nv=bo4~Un^ql)x%Yk*C=H|9)({q2WjMIclJ=^D zbbM7%M|to+lG;&z7Je8#Q)0-RDK5yFvX%5qNf3t5l$2YT{U090nS^t_&=-bkSCHEe zR2?<%?Pm%(Q#gpvl#(;p?Pr`&R}^syK2yM+p{G1GsWzMoPEpz#D0nvhLH zfpp0~k}lDBX{n3aKaI?pav3>O9qCLe!2D|3p)i+CYYS9ieU_rRMO$)QE2cY9-F(fm-dCCve^q&i-|h zp?V=yF;eQABA9v~${fz+6eEQo>SK4oVd#5;P^ZaIZz5E2QfkQ=n0mX9KrO?WL@ZwV zIT@-Zg{i{SWT@^4bqguAk{zb{K(BpspEZgfqMp(cek(pl-L#BM1ds<)&+H|R04mwB zTjf`%I(d%z(HUnF#SeB(f$1yuF-Hz48n1cMR%bo}rmukME5GEZub+GVEp6E@@NNYc<78ZB)kZS(EFdpzplCsn&a=G9BG6%jHWw z&ZqB?n|d;|JUIS{&ozro!E#|2yNEilJ@VJu9@*~JGljKg&t!y?9`uWE72e3{~5R(*<9eg~l`t zAuKtk9vggBVAb8jY>`zorOIo>S~$Td$*5-HyNQKcC6V^JM@td?Tb*u(W7}RFj6Ic7 zb2)F!KO)L@WP!7X=LN<$(}qbpYocS<$Mif6*WaUX`{eSeyB06uTO0M z-q%>{&$<<}e=8kq{}()YJk3wK95xMa6X1TlnvQ#+j;Z7VaZ@juA8kJu*rY|9zYM27 zC3Z*nk!ri#v-8y_2Cdh)@~>!FrDmxn%#~`s3N!+Rdo{$6D4pN-gGkJ$dxiCFBcKL zs|%ts_E$3oBwFvJEDz_I7Bf{z*|x#nx{T>ihNv=QLy_8xRii_}Qp)>&w#>_(`V}_s z^g*IG?E0oB%dFNHfkba+W_^Cjf>oT1j!zdX#vSy%g!_RLy&xs~e;eNZn+#vFqey~g zfWoo=mH{&MFGlRIL1h05%7a+!KLX7Po2&Pqb%N~=D%~c_nl^Q9PQ4VPdvoPv*KG|E z&b1LUOLyB@mbQROx9oebg<0}qn-5$sDf&k$-9Sy+ZO67Plh!vYpYJH)QD;eJj1gdR zPXsk-53N+A!<@bNev{$CWj3YPQTu--WB($={+h)0r&)-_{uf^n?B4?0e+^^H`=Owo z=mGZw{;N-&H4=^ux*t2GJl$5_*tm?MMj4DO1_O=2KqF9-1~Q{!UL|%}39bDDdWX~; zn|Eh)@>7@`Z&Qx>t1_cG)n<2dMmwY)^K4`Ckp9gMb(3C$LsH}7AH*k6s< z{tPi#>@S8#?T@EWMlwGKU6&}1L@r8+En*mWXSPY#*X#l-Q;8}67<+AqVQ+SGS-pw& zxz!%RtQyvpW7R_U#Jr5+MHk8%+&9KED|IaZS@SB@rA!XGfA{YONMmIOx^77UX)KN4 zrQACkG0ZJQO7Hw{vrQHzYP=+5j7J-h@AMF2w)K&>ozWB`v0pTAAdIJ;JE z{HGrolt1=VqxOd;waxqL`wp@HPGb8Dj$pArEhlDwoEU8X#>#!%jtZ2qlNyJ9m>n5l zHUW5K0FMljK^8g_T7{2U17Ffq_qoIm_o{>I*l&9sGh@@ZJ7<3x=oFaO{#A(m6^ZRH z5{$+EZqUX7q~0IT`5Lx=OSE6+vv$eUVsOa=GwMLim$LbIo#pyHHXQVfzbuo*KV4WXexk{o`wELIrkkbaJ70-A7=QPzw}dka<1xq ztCJxE?p95rR_;#R+ny}xydA>eAANx%VSk`jMlBc5anQOr)O6#-85R6QLR$|$MQ2aE;ui#gDpXR{uF?Qnutxsw@im#T~OnDj(9_|@n+dZ**vs{jC`&9pw!`oqL<_hC; z`ZVgV`)<5qzqsKBs3iMoE6INRnlF&?Xnl9*x6OEf+{E1ceEH;-kEs30_4@Q?auzwqDe=wCFbh&w|P=Jib=UeCXH&&?@ia+{L5l|w1tN~;^B4w*|k2Jkc zs!P>MEUA$Z=9w0X%p5yf3x!gLNBxt@5-0da1h)nEXhS*<~*#0V8vDm*z z5VJp*Dr|q#BQ7B00lM6@Ra=|uEZ+AX>H=ME&l9q#`;%X-EDYe^>U^V^6FfkJE;l<< zA#R_`VqoAOrP@$lF z(DWX*|2b!3`)jmgvHv)<7i{j;mn#akf5y#;%f+lqKd_&ht&8prkkD-Np&xkDFI(U8(bl*oRo@i+ zwBPgOx?1Y==U@;i7(@z|WB;Gu-QwroD` zhS>fF53tx@A(~)+57_>hu3!);=q~+>yGubIsKJFZV_weG{T&0TjYl2Zdp|~*%X8a; zKG1)%57hZfn;=i+{h6cQoyxto$2Ud}`ZKQPuFVnq83u$rX-a?|Uf|e&+gs>wRrqTz zaW|L5gVT2u5-rzY``b%WQbJie90g^h1Z660{>A0Bxz5XGXI~~`_TQ$6Gx_LT(R6e* zO3fg1rZgaDiY)P&Vx)=PD4O78A|)*y4lg$apDCcs3nV=qUsg41nO{%6F&#!PFMedy zDV`&n-&(S+JLs~3%zu{j{2$VjpwA6deSwZNmBc^qNZZKUvH-RJdopK=Cvv7pke(^c zmhhSK+#a+4wsX*#(%QR3>?%sFCPVc=sGCWt9jjn!tR#UNi!*6a83~wpfKnUDPy-OE z5Gl3yHB5apMWB9!sFAD9)S^)8M>14@gu0%TI%oz{Lu&}sahypj-R8I^s67TbtDzQ~ zdC%(G2vv}jI>rD~1CueT{1S+2nc#T&4NC1KL%oGi*O5}E6Je@<0D;N}QA-mkmEBQl zHyNrALKPvQLJOc^>di9*>QZ$>WJqVRy|J2j4}g{G?QoV3@j{u&_X3DGrYlllx@Dic+b` zQ12jA0aB`jGE7~w8vCpY{5YsBS~h4kCrVvJh8l!Wc}c0#eK3`Ij6jvc;n#2q`Zj5x z)TLyofe4k4lq&BJQ&$k|qF@72FHKWkEJ3L&$WS|As$Cr^Rb?woWw=H7tk)oFTQdcf zF-m15Lv2T>^`umdc9=>{u!}+n4$q~Ukr@R2yMX^~$wRN;^ZpKWA=DaDs+JB+U3i`F zSzkcZsD(z`OHnEt8EPj&ttF*uPs7x2gx3{?Vu+fj?M;`DQsLhPRru>q?rkjywTYB! z@BpSx+#r0`b{w8t@S=h85tPbHhWZhqHj`3~G-2uxwyqn$odu$L)KKQ|qf{|6)Ha0L zKuX0A!_?Ax?6Yn^1K$XrUb+Zdn+hs1kt1`*Y=)_4q=@gB_+cz>dHX&_4eG(+x1WiK zvK?9A?BRKV@y)bhlFpjw*!3|zPs8>1DBM1|{HGX8N_H=c71*G{!Ai+1?(Ql$3Tn$0 zWX^%w@_nUc6vEl=c??&rOuxm>#v#%ImK_^n6T zP_#!!H0VuT@Xc$T^5*pBw0%jGzv%S3-+RuS|LRj`jf7)^?#E6kPq&pfHZG&6QO3>q z9gFR$T@=b-v&6>M`;+R24iRoT&iK@;H#XYarQc0XKizy@QEq```P3WPUClSST4q z;~hiQHsW-_mu8_c4MPY^&Z)-+Ulmw&_b^*z6-}w~8nG5mFiOsiJOjhYz;H6~fD9gx zmpGJw2juDd2_N)(R_Twr)1Qua9JgltyB?4qtzBG<+TV?g{rM344-wn{N*fmYO9W!E z|9vUg{$4V&LK$*?><-LN3%fXH6OX@azO(1$skrQ~_$Y=E?Ut`kN*7V6uNZt~oO*a} z}Cwm{}hk^XT|qJ33G&Q2Wb{g>ci< z;a5o>}j+uwT=7W>!E66`+%+rM(G zTIimbmr=avLRo|R#&~9>t_A0_u3Z$lI^vq;8c6Z6K_#ouc~WgS(6Y--^O?Nd5x;i^ zRucE?SK`aE53KE|zbJ4DpLL+%d8@+SGkTueAKEfJ-E<<_?4-}VhaGO6JxxYbD@80H zE_?D14_fgWemMiu==WbiPKw|J(-kw1RO3sc6QGKQE zO3nR6HWJyP%DNI-gU4>YEK1FnKR>Q3nuFiU;=FF9d^&CZGMxI9*d5_Vs_k;m&R3rp zv|i)NzoKQ8nx&dBSE@xj{l^Mm%`R|Ffrj@aPU6-NAAn7SmsMtWdYO zITA6-F>c#kCBY_;-FUD->DNsLfohmsI-}jWTztB&6zanROt0v&*g>WVWSaiDOq2I} z#yhC}bII7B3$gz=vHio=W3hh{G=*o*{-M0E{q?_2av7dKZ5j6XzWC%4oHtR<)@sF$auOp#H+fm}+zLZu{QuyF0{&{gm88%W~{aP71r^Z#i@m zj8oVwy}i!ekGV}2RKtL3n1l9K->n+v_o!QGl@bs{?GH^8oVVMH2eJPMvHc%aV6p#h zXz~zhe>d3vDgjdZU`YyClJb9Ek^zFfxTrM5I1 z@;1L1^AK`yDNhI4d!wbxso%7*W;yg($TNuYe03byr3SM1AbSt8_y0C~|KI1?*&llJ zqxOd;v(6j)Z$RwdOKkrH9xV1hB!SsKOcRd%N!Fi&O#q;Lk+Wrw-g@;VEVeav%v(Qd z^}EKOlRMj(R*>H3;oU}Y*XnPU41K2tgg_mosC=uCNNvRHgF!dv0(ow1_4u5aCe7V_M zl)9M=^)o^(Bc(dY!qnP9?4(M)2T^U=xtnjJR7o<_9)$XVlzOENrdALtv{Dlx>IcKc z9{wm*h77eEp;nVpJv?D*aV+7p7UJ;trL^4aSW)VBGSp89wUU(Ty$PlkoFY)aLDZmh ztK;e@RhbO67ok>>QUmH?YSu1{`kE1<&XigQ+M-l=;_ZAf&?rKEM@kKHfvKq*2vlJl zKIHKA%nw>975>cgQAZGJAt^O%JxooaCs36jYIaA-`gW8G)90g(Bh)vf)JGLC^=S`* zdH{zHfyT|w zKy`s&p><2H~NA~KMB^D@7n zcjwW~BLcqbg1FL{@HWFatsHsgvqH?*b9)pYhrN)DAJN$IiZ6E8U?}IVgy!3hZCfU- zZ&p6vQNp9nk_ z(rIav^4|Ec=n;SEZTr~X?9bB&rPKdh=`<*EuDQ?J&AY2*O#zKT<=HO`+eW>=S#YS* z#C$%eOLg(^?n5Pw?E-yz6TerHv$4W1JZgXFPBpLn*CF=*OlA$29jh%ZCcKuNsrB&ij}1`Td1>`_rU}1U_m_+r!+1qMiS~$#Z703WodWAv|M1Nz z>j$ON1y!uqO=GWlmVwe~P&yq&`{mkh_D8pD%mo}vuhuwm+Df^W7HUfT=m)DHlBD9&rIrx&Oseu4teF{O$*5ek{n? ze?4OVK4SY2G_xbIF~2WoMk`-3=D!zA>H@vo-QKvNpnH zMWW?8Q3)3*mg&bSr;LNj+8P6;Ze*lJReEMmQ-iwuhhw6YTh~fiKaYNHyIlH0rAnNt zv&o|ML8~a1dV{+A{W2RLF8{bEzM8pPSn&5=RRebQ{;xB5Kb>*oM(wXb#{NQx{ridS zUz>`>{#4NW{#@p#K_9k1zqMptchF@6nRIvlTXfmebZ#qeN9NVnFBz0A?bmX=e7;(S z!UqrP?!nk#`Y64gmCYqfKM4!f^(RWAdQ6#oiitxs9c%|^hC^uX` zoJq~s9Ij^HomTx-W9Od-WFX3BO?rPOTqsa{P6j zKD45KnKK#tZ$|9jN^JjTQ!Ms3xI(ah3T%JZO7lWmH)`f#I^Bts(jsa*24|HV4JHZy z@Ff~D2BX#Ef0zlm@@%DJ-YseFI&jiYz^m=npLF1Y5oAL_J<$X12f#wU&0qvsD0oMY zj$I$q^E6z4kHYPf%ct&Iya4a$|I=7{o5}DXH1@wv#{LqB{o9D`-$9SX{t2fs`!}+| z_6Ni6K?l}U|% z+8(6s|C!Z!#eCIJClHeNhdNj0ef5<@?EjJ2{=Lt!*k6;dJF(Fbwm%ql|I3cc0js_P z%Jg$H##Z0yu0Aj6FgYqH=d)}0bPRoR;wP{C=Rq+~^LJJ{fmPq2ASgQV3E#$=M&-7; zp-DN$hKl!hoa*Rec4*tIn7nC_@{C$!d_{L=Z|LDAPZw&0P~Op@^LpBD*cZj}(yzhd zJ5}hW-#uf9+CPGf{kI_YZy~n--~lZ5H%cVL{vTocgXtFk{d5a3+u?dpQAgwEq0P$v z#;<+u+OFT8XSQEd*}hvRSWH%ow_k5jS5POoSzLT?KN+jwH*Sx1kuSSO#{S}n{X2>6Kehmi{du7d^to5xCPmo(#p#PZ_VsA;bpEvy zBfuIhFo+Wr1c9eju$22R+QTG&r3tY=GUcsRVec6|&+QLw8J=ze)t{jH6TH!aH`>B3&e_D{FPrb|d3h=>`zt<* zVMH6e(f%iIw153;D&C*jea`+XN1?yxn?^kZ+uyF3bZ&GG8$36fjga}748!5;j>pEn zcS5NHWX_bW$eB`4e5S0>#BLOxyx5sC=>Xp-+U1^|2P+uCC!KRy7jrUkaTmMb*^lC+4WDD9KR2BGepGYRL(h zx`dFWn!@4m^`dU=YRxEhAsOl{Ld_(lR<4Apw1i1VQ!EhGCo%HGF_cO}hWZVmW|2~B zQ(-F32>T99i9yt1`&q6#D3y*3brPZGlTw>aVd__COUhh@))cgrtU;eU-QY1wg?2p5 z8v#rp)B;j!2R%$3V%p-z!f#}>fUHbUL= z)B}k6vAu6%45hM@q2?mgX;SL+6PVg)Px!1KaQG(0@KkqGl)8otwE&?`kWiu5I+$9m zN1*mY)Q4v#-#VhywPdLI2z8Q_N<9rz%fvD2H1z)0OjB^68U8(me_GHK;Ca8VSqSwT zDV5;?Ono;(pvpkhV&XQ7BXu{Oo6aw`GL@nZ(wA4kZTggy!5b9S_ z>gr*b8ZkwnR^srJ4z6qxawye`OdKo^Q_s{8$HA+IvD`|bKSrIg$Kj_dHx__pv|t(S zk2(|wOa<3tf&wKaRIj{+}{~u)RzYVc}EwTMMg0L)zunNKcb+G*lk~FUJJA2#Wo+fUQ*=TAbWIx;brc=14Vt29=S{JR)`A} z#|B0PKIsE%^uQXu@qvKG((IjwSOSlUE56z5QEIcVa#P9sxMWZ={eLT&2F1;vTi@Nu zThY4Qfo@#Y`b4zid+B@ek0LaS`KjYwLuWFt3H;8*&8ivUN@(owN5=kAi2a+1?JxKV zi~ZMp!(#t$A+Y^>J(~vc<>k|rDNbY3pk!JHluUbpl45+ol2(X zFK#|Z@8bCDb~|3ZvC-Zx{cdvl>E_c4bR*{_sA}`vl?$qTPiIzNDR($wbY{F|a(~64 z2Br3?g!QK*-86OhRgy=!Tg^4ZYQ))Eqf%^c2M>gEd@D0`{F2nMj)~TU_V6-u?GXt! zntYoi5ht%-V*L5xiL7eW{-I>-FN@f}f!O{cepu}PM?Yr&SxVUcM-C_&uX)l|XFftF zp3iR0XfW1t#G2=!O@I3N(*2cB)eh~x_eOmxV3AQ@6s1juo_cWKj)l+P6+51pZP>c& zrx~ga^2fh%{`k)dltC$RNlNCrDu>XlDCe|4Kq>JLUrJ0ZHRg%hKZcC`Wf1!}65C%w z7K{CGsV@txJt-o)G;ZNc zq0|fgp;n*nK@*%k$7$JU|Mmcu2dOR>YCI2iCnwZcwYHtfrZG7g+H->A`H)BDa~-~! z=*9fUJ~e^@Wl*5Z*FY&X7~K$&VdRvxHsz-lAL8PkCxmZ8#U4sEVNHqq$l!vj08R zq&2Hp_)f6@Eft^2`faI=8&&K#?W>hKW_7tcEHk$i{6d>_&Q};gRclDq(@Qe;r-JQ& zb_KEhrQ5LBzpIg8|9sf~jH|h8bHtpeOFriM>+N50BsB8;bY)De(`JiUGAKJIuNy6~5`^>{SC*Bkq#@yzk{-TAmT@91rR(6LG)v zaG-9(>9EvV3+uH-HW?!cf~M265jR;}OVqvzC2KW2j5}F>%8%LaQGSfwyJ}01uaxbyBg}zotIjSP=`pVJW4S2 z920t`n6g0ixifX^81S(u70Oo4JMDiVa#F1%KB?qAv3ts`HyD-qHGEPH@^th>#hX?g zin;fGRaAucO|MhWT(o(mQWVc+cFqXV*0?5B-xU0`-}B_UTI%%YFL?5JnxAwzY#QDs z!2NhN9rr>VQ^^J5rd~3#LK$*?><-LNL0AsLa^IRh$=%*tYs9WJ1osyVSG-XM*Vf)jaTd!H{4LUFVn+#pZm*agWkZV)l{pN z)9E!8KDFHPdZJ{<&1mhOf(nm6l37Li|9yoE&DRxDsQtUioGC@fnKDdzrU-t5&lHFD;cgDA^!7jP1C>84bo%f_FMW}7nOyfe*$)5rE1j?5?^Y8T+iP)i_g@S+{|_5Rrl0o0s_oy*rO?9ue?e7sQpu zgtr;aY30Z>pA}-hp4+4NIP8UF{D{VuSA4O%217Y_B{bi5Y}+zveY5iUjuIYqmSn~l z0dO_@Yp-VDZ5q5ygI8oGdX{%Whw5Kjf67{xn!kNkk9R%3hHtWv)2O6y=i`c`i6N0? z1Crg-8cA0y6kfec9QS&jm}#?8OMS7zrIHg@o5a1o1QnOA8XS8gll@p-?)dK0!OwD1 zdM_SWC>cfL9YfVN;&j26W}z_+LkLUGsmBIi6r*NgfkKU5VbV0BRtB7@I%yxe~6)FId&%}gP|(&s_erdnT8w-u(H3qmTi2t0qw^cPH*`PnLAv4q@<* zzQB>NKTs>9mW${3WLeXuuFa{JVsvk=ocs;j1Hd)s`&@HCFiNL8ky2VjZO7oOlB2;S z;UB(4L&jjVdi)PFAy=NQbj-UY&0Pmh`U!Zo-NH#by*^^|^!P}ah3S>P<+xRX36(r| z(wq8+I9`?&gn(dFI5y~h?3D6!TX|#SGKv~y+>GC`*q+)&p$s-lY;3(hseb4X;ilt^ z|6Y4BEfTh(DO_e_GWMrH?7y1W{@O#>5$VWg{u$|CoSu1e*5vvq=z~Uk&}jej zhPWUY?d)kXqFO0p`EcPgg9~TIyqu}~I|fo4k2<#ZevC4g=eAAw7Vh*ZAZe@0vj|a2 zPDy3s+eS6_S-W|6wX7+iF{nKIg<;#M_csd;RhpR32X(0~9^QSZq_JI~Pj7*d%N%M5F7SWa{nvB#vi4S|0j4oJ`(xL3yu9P$=H7ZV*gdd_BRN`V*eIGT9lm) zwm;am2?`~_i|o7cp=BHN)ZfxiFpo}A;gg;hj&(+MrL79;>U}uLoCaQGf2kK))}LLDPpm|VKEl_vtNYm4{m>8M-^z}W~%udvv^&dDQLLP%97I4#bv0#{NqY`_mHJ-%19H{lA4^_Luw$+y9H)?q0oxV3$4EWj}7)T_wRL zkllE&K&iGsicKIS?DfD9gI=GiI-WdLOcZfXTkOdn|Z@76WNcD(l3X$bBk;6Cyr+($m}Ys8`UZzp4aTEzZK ziS6&S5sUo=_G9*!=7H^BBc)7h#XY#>#o6aGvcUr)ilsK+LKMLuUNDF^`@q_c`ilam z@L2~6o`XTWU=XiPu$ZhEZ@=E6uAt5zeh}}kHZqrWS9{-_{n?=*EAzhk(j)d~Ah!RN zIxP0z;6bpzD{TM&dFC0YTL5(n8Sx1@%=erw({wKB&~#dxH5w|tJ9OU#5$>p6E@@NN zYc<78ZB)kZfC>Z93NPBDBO3IkF8JoPPI+^BbK1V2|3UVJq%Ib#2C-NH}UxbefbSb$E?-+pH`m=pYi*7~w8U9=0e z|3)(QXGH9eBeuV{5ElE(t-|atqXF9=%-;Q7D~LfP4kB?7iGNbtQGB(;X3EoW@Nmxn z+wO@UbSTv|6LEFa{@cjde-&c?1;q9bD92)d`#S{t$HMjpb63HrD*^V6`Ozyc`8;03 z*z$fTs3&^B{eb`KQ)i8Sv{6@JC{=UG(oe!db^VD_Kf+Tacr;YAEYZ0+PRq4)Pfo+_ zJN1QJ`&>ASr~igWLt7;i(>eREi6mqH)rkEmi0vPA9*g~{QZV})Y=iCZ*I=Rlb&|{Q z{AtUu$M?l2m+&0w(B3@Qwb+yUV`;cVad#N)k+-T%U5Po>W_NQ&JER`-Y-93}PQpK9 zeKy{qaQM^qb8icmNn0IsU7|P=xhN&Jh+*KJ*(P0IvkR`iW|6S#)=qci^`~RE)Q*JLqxOfE0?zyD%Y@jUlGy%XYq8kB{T#vmVX*zddL=N- zi*axMZHfY+x@PKMYM2+8JANgM&zgntTaU6KNb&qEDV}+fF*nYq!E1z|SKl=~WbD5j zu|GAj{U5!vSdm3jznSO4@*y=mo)#oK0CPxM3e0B|=j-gLZ{N$DY zJSgUA{?1A#`xvlV2dvh4!nd)eQTeaxQ$3zKERWiMjEw!4A@-*swtviNEcQRWfnfh5 zu>HxUc%ncpBdBF8PjBT}=zo-R-@yV`s~7cQhOhZcKeZ?4s@}Ic3D){t1L3vO`!olJ zkFgsUX#M2j^$#5g(?3}UReHj)KNqx)ZQjh!O2qyPiS3`jipBo)|Ihw!VEbq9Jj4=s zOkDBJUXN0neU+O^-p3`oR+<;mx=}L^gJnU@z3OH*HO88W@l>kU7DWbG=uBu8K4uMk zNmJeD5oXMLT+d)soIiokV89tvj*dY&4;He||Ftr5JGd&fqu zfXvML{FDW&I2j$EE?E34443)I#=z_k{&kDOEbVz7q4p=&>&pS#|I8Gz{nL(PvHvYz zg8gs6_CH^`zw)Wtq22f1s80nfGU|(>w8_v@5ANHs@cFx9$1}4HTX)e%>GiAxrDngz zitpbdPy!(h2x+$Gne7)b8am=h}#wrC?z|Wtd3(>g;>3g<0}qn-5$sDROR*JvzYfGI!sBqP|Fv+@*Ib z#083D1ET_;^f`ZN6XdD9KXcT(Q@Pjn_{PXVf5z3^wK-x=)FmHt{q^>*I1(Cpe!4O+ z&imWlhQZ~_Q2TEpllfVL*nfi9{%>AkvHw+0%>IUZVf#M~*fU&ZXn4TpYGYc-rK^&X z6+44XT`D6-$3N&>u8q|W6skUK(6`yUH%@odFl0zd!R*YGrgiW}35S}z*s|9la?P7) zMW}B1TwK1k&hu4H+Pd#D^#aVN`0k5Ef7tmHU_ht2_J{-9mduHqDU-x!N(mmj zQ3OGKXmd9TJ16)|0i(V^d5ncTgDB5e$AMjHhxKI~`CLGG3_mE3`RTT)|Jp$h&R+wX z_zzbS@!TVGrc@zkN-yb|qNM?!DHT-&`?ugs*`(jC*Ls6eL&;Dp5$Y#Ws`d~}Er$9V z&@*KUqUtYUj2=a)56Dof5o$Ln)gTb277#wG{c?!<((%MWsDA?a4#4j!^PQ?65NZ!8 z)kqPhW+h^u)m{*1%Ffmsdl!0PL8vigsAUNCGbt6{3sY0=2-E`*^`f@u^?sE4j12WX zLLDHb9`%K(NqPk8X^8rz8*hMT*ms7(btz2ofl7a#fkWvMK6LuQYB0^!5*qfpdwo~Uyr;D)OwTZ6iuu)v8aPc9b&4S8T>= zCyUyL)KR8dXad!@J?XKg6C~)Tz<1t>RUwPI9;&hDnQCz>R9($zwG^m@CWlsAVzrk= zeI2O+rut13R2@Oo$Mf|%{QsA4+BIXWg8Qg&W|<>3o2gdoL)BJ7t3sgeznENb0IP#6 zsu@zhVyaggplSi0OFUL7P+LxvZcN1LFpIhlsd-Gb_8?SE3MiHL5<%|Rpmh9JELOoO z3JR})F;erHYNHlZ4HIb90H|rZ&n(-5)dwu9DN=KoYD*1NbwS*dT>+jwP^V_XhCux; zidhpZstHnandFHH=+CUP4~E%3Eae_x~(+3IKQGWDJEvaWPbFNdn+W>xNlNA85j zrC5ZU(DpU=ac^~2Ns~zQDtG#x#^R5AORBnAC&R`7NB0c|5N$sKNvvm{ZoRd z=-=%Dqd(VW;pD~8l%}Y3wFq;mVNi*`arW}$7_Q60Kig&Dx&9j%SKZ82H$V5)&Cz;Z zb2$3PfL}$~YHO5F%3$V`daGeR$u^XZ{&@u1mC%`X66Azo?#u2Ni)x2dK2seGfU3n> zS{(uEaC7uc9jr#PtW^4FrFtg6QjHSSdrBpp`|2)+E7fiPI~_{a4Jl2p)JHrDtm-`Y zZi(xQcK#rDQsCC_-?WL1=+F(iZs(!X-{yHm`^n=RClCMaE1gD4m;ZWdn_+QgWUzVs zo#aUK8&(${i!X=_ifrF<%^hz3V|HVhH`iUqf$Ofrb=O(N-?d6pwMC*|&wKPQmk%{w zGPT0d|1%{e74R<`6>nKxovg+VR@bRG4Fsb##ZNx^Pn1#7-y@Y;Q-o?T`v1N9ZmtrW zKNjLRZGP5LU|g%aH}bbeu~Ad-yw#f1Hy^r}6<*m?HSPUI&HBpB=9I*;dzf`odv_xtqQ=KS- zssYGM%f6}#lYm-0u9FX{IMB}W9E)m!)GVedEr+V!7_EZupF%b17dMQtTDw41MwPn+ zRk=H`52|rH 101): + print("You must fix the percentages such that they add upto 100 ") + else: + weights = self.percentages_to_weights(percenatges) + + input_app_dict = dict(zip(applications, weights)) + + #Search a configuation by name as provided in the test-aparmeters.yml file ( example - PANW-APPMIX) + if (self.utils.search_configuration_file(name_of_existing_cyperf_configuration)): + #load the configuration and create a test session + try: + session_appmix=self.utils.create_session_by_config_name(name_of_existing_cyperf_configuration) + print("The configuration was loaded successfully !") + except Exception: + print("The configuration was not loaded successfully !") + return False + + session_id = session_appmix.id + #print(session_id) + + #check if the apps are present pre-canned in CyPerf + app_list = applications + + + + #convert this list to a dictionary where the keys are the app names read from the csv supplied by customer and values are list of + #names derived from them ( split into tokens , that enables better search ) + #For example if the customer provides a list contains application name as ms-office-base + #then we will store it in a dictionary where the key will be 'ms-office-base' and value will be [office]. + # This will enable more hits in the search against CyPerf applications ( pre-canned + custom ) + + #create the data-structure as described above + app_dict={} + for index in range(len(applications)): + app_dict.update({applications[index]:[]}) + + #The app_dict created above must have weight as the first element in the list + #for example if ms-office-base has weight 3 + #The app_dict must have a element ms-office-base as key & [3,"office"] as value + + for item in app_dict: + weight = input_app_dict[item] + app_dict[item].append(weight) + + #convert the application names mentioned in csv to common names + new_app_dict=convert_app_names_to_common_names(app_dict) + #pprint(new_app_dict) + #found + found=0 + # create a variable called target appmix which we will eventually use for appmix creatin in cyperf Traffic profile + target_app_mix_dict={} + + #search the application in CyPerf library ( Pre-canned + custom ) + #collect all the applciation from CyPerf Library into a variable + cyperf_apps=self.utils.get_apps(session_appmix) + + #convert cyPerf app-names them in lower case + for i, x in enumerate(cyperf_apps): + if isinstance(x, str): + cyperf_apps[i] = x.lower() + + for item in new_app_dict: + app_list=new_app_dict[item] + dump_app_list=[] + for ele in app_list[1:]: + indices = find_indices(cyperf_apps,ele,False) + if ( len(indices)): + temp_list = [cyperf_apps[i] for i in indices] + dump_app_list.extend(temp_list) + + #pprint(select_best_matching_app(dump_app_list,cyperf_apps)) + if((select_best_matching_app(dump_app_list,cyperf_apps))): + best=select_best_matching_app(dump_app_list,cyperf_apps)[0] + weight=new_app_dict[item][0] + target_app_mix_dict.update({best:weight}) + + #Now you need to tag the weight with this app + print(" Target appmix is = ") + pprint(target_app_mix_dict) + + #coverage percenatge + coverage_percent = (len(target_app_mix_dict.keys())/len((new_app_dict.keys())))*100 + print(f"coverage Percentage = {coverage_percent}") + + #add_applications + self.utils.add_apps_with_weights1(session_appmix,target_app_mix_dict) + #self.utils.check_if_traffic_enabled(session) + print ("done!") + + + + + +class CaptureReplayTest (object): + def __init__(self, capture_folder_path ,agent_map={}): + args, offline_token = utils.parse_cli_options() + self.utils = utils.Utils(args.controller, + username=args.user, + password=args.password, + refresh_token=offline_token, + license_server=args.license_server, + license_user=args.license_user, + license_password=args.license_password) + + + + self.capture_folder_path = capture_folder_path + self.agent_map = agent_map + self.test_duration = 60 + self.local_stats = {} + + def __del__(self): + self._release() + + def __enter__(self): + return self + + def __exit__(self, exception_type, exception_value, exception_traceback): + self._release() + if exception_value: + raise (exception_value) + + def _release(self): + try: + if self.session: + print('Deleting session') + self.utils.delete_session(self.session) + print('Deleted session') + self.session = None + except AttributeError: + pass + + def _set_objective_and_timeline(self): + # Change the objective type to 'Simulated Users'. 'Throughput' is not yet supported for UDP Stream. + self.utils.set_objective_and_timeline(self.session, + objective_type=cyperf.ObjectiveType.SIMULATED_USERS, + objective_value=1000, + test_duration=self.test_duration) + #soumo + def get_capture_file_paths(self): + try: + # Check if the folder exists + if not os.path.exists(self.capture_folder_path): + print(f"Error: Folder '{self.capture_folder_path}' does not exist.") + return [] + + # Get a list of all files in the folder + file_paths = [os.path.join(self.capture_folder_path, file) for file in os.listdir(self.capture_folder_path) if os.path.isfile(os.path.join(self.capture_folder_path, file))] + + return file_paths + + except Exception as e: + print(f"An error occurred: {str(e)}") + return [] + + + + + async def configure(self): + print('Configuring ...') + #read the pcap files + list_of_paths_of_pcap_files = self.get_capture_file_paths() + list_of_paths_of_pcap_files.reverse() + print(list_of_paths_of_pcap_files) + #upload all the captures from the specified folder ( in yml file ) + #to CyPerf Resource Library for Captures + while(list_of_paths_of_pcap_files): + capture_file=list_of_paths_of_pcap_files.pop() + print("uploading capture - {} ".format(capture_file)) + await self.utils.upload_the_capture_file(capture_file) + #create an application from the uploaded captures + apps_created= self.utils.create_apps_from_captures() + print('Configuration complete !!!.You may now use the custom apps created from pcaps.\nThe custom apps are available under the Resource Library in CyPerf Controller') + + def _start(self): + print('Starting test ...') + self.utils.start_test(self.session) + print('Started test ...') + + def _process_stats(self, stats): + processed_stats = self.local_stats + for stat in stats: + if stat.snapshots: + processed_stats[stat.name] = {} + for snapshot in stat.snapshots: + time_stamp = snapshot.timestamp + processed_stats[stat.name][time_stamp] = [] + d = {} + for idx, stat_name in enumerate(stat.columns): + d[stat_name] = [val[idx].actual_instance for val in snapshot.values] + processed_stats[stat.name][time_stamp] = d + return processed_stats + + def _print_run_time_stats(self, test, time_from, time_to): + stat_names = ['client-streaming-rate', 'server-streaming-rate'] + return self.print_run_time_stats(test, time_from, time_to, stat_names) + + def print_run_time_stats(self, test, time_from, time_to, stat_names): + last_monitored_time_stamp = None + for stat_name in stat_names: + stats = self.utils.collect_stats(test, + stat_name, + time_from, + time_to, + self._process_stats) + if stat_name not in stats: + continue + + stats = stats[stat_name] + last_time_stamp = max(stats) + + if stat_name in self.last_recorded_time_stamps: + last_recorded_time_stamp = self.last_recorded_time_stamps[stat_name] + else: + last_recorded_time_stamp = 0 + + if last_time_stamp != last_recorded_time_stamp: + last_stats = stats[last_time_stamp] + + print(f'\n{stat_name} at {self.utils.format_milliseconds(last_time_stamp)}\n') + lines = self.utils.format_stats_dict_as_table(last_stats) + for line in lines: + print(line) + + self.last_recorded_time_stamps[stat_name] = last_time_stamp + + if last_monitored_time_stamp: + last_monitored_time_stamp = min (max(last_time_stamp, time_from), + last_monitored_time_stamp) + else: + last_monitored_time_stamp = max(last_time_stamp, time_from) + + return last_monitored_time_stamp + + def _wait_until_stopped(self): + self.last_recorded_time_stamps = {} + self.utils.wait_for_test_stop(self.session, self._print_run_time_stats) + print('Stopped test ...') + + def run(self): + self._start() + self._wait_until_stopped() + + def collect_final_stats(self): + print('Collecting final statistics ...') + stat_names = ['client-streaming-statistics', 'server-streaming-statistics'] + session_api = cyperf.SessionsApi(self.utils.api_client) + test = session_api.get_test(session_id=self.session.id) + self.print_run_time_stats(test, 0, -1, stat_names) + print('Collected final statistics ...') + + +def start_test(): + + agents = { + 'IP Network 1': ['10.39.68.164'], + 'IP Network 2': ['10.39.68.184'] + } + with AppMixBuilderTest( capture_folder_path,name_of_existing_cyperf_configuration,csv_path,agents) as test1: + test1.configure_test() + #test.configure() + #test.run() + #test.collect_final_stats() + + '''with CaptureReplayTest(capture_folder_path,agents) as test: + await test.configure() + #test.configure() + #test.run() + #test.collect_final_stats()''' + + +async def main(): + + agents = { + 'IP Network 1': ['10.39.68.164'], + 'IP Network 2': ['10.39.68.184'] + } + + with CaptureReplayTest(capture_folder_path,agents) as test: + await test.configure() + #test.configure() + #test.run() + #test.collect_final_stats() + +if __name__ == '__main__': + #asyncio.run(main()) + start_test() diff --git a/samples/combined_report.csv b/samples/combined_report.csv new file mode 100644 index 0000000..8b41ec7 --- /dev/null +++ b/samples/combined_report.csv @@ -0,0 +1,87 @@ +application,weight +adobe-echosign,1 +alteryx-base,2 +apple-push-notifications,24 +apt-get,1265 +azure-log-analytics,407 +azure-openai-encrypted,1 +bacnet-write-property,3 +canva-base,1 +capwap,1 +cloudinary-base,36 +datadog,429 +dns-base,4484 +dtls,5 +facebook-base,1 +firebase-cloud-messaging,21 +github-base,62 +github-pages,1210 +google-analytics,7 +google-base,1469 +google-maps,29 +http-proxy,6 +icmp,37 +insufficient-data,59 +jira-base,4 +ldap,161 +linkedin-base,3 +microsoft-dynamics-crm,33 +microsoft-excel,4 +microsoft-intune,2 +modbus-read-holding-registers,1 +mqtt-connect,2 +mqtt-disconnect,1 +ms-ds-smb-base,16 +ms-ds-smbv3,187 +ms-office365-base,509 +ms-onedrive-base,2 +ms-onedrive-business,2 +ms-update,420 +ms-visual-studio-tfs-base,37 +ms-wmi,4 +msrpc-base,163 +mssql-db-base,66 +mssql-db-encrypted,2508 +mssql-db-unencrypted,127 +mssql-mon,52 +mysql,2 +netbios-ns,61 +niagara-fox,44 +ntp-base,178 +ocsp,440 +octopus,2 +okta,580 +open-vpn,6 +openai-api,2 +oracle,90 +oracle-eloqua,39 +outlook-web,82 +postgres,4 +quic-base,1 +rabbitmq,4 +rtcp,2 +rtp-base,2 +rtsp,6 +salesforce-base,67 +sendgrid,216 +service-now-base,36 +sharepoint-online,60 +slack-base,2 +slp,2 +smtp-base,108 +snmp-trap,3 +snmpv1-get-next-request,3 +snmpv1-get-request,2 +snmpv3-get-next-request,2 +snmpv3-get-request,3 +soap,111 +splunk,166 +sproutsocial,9 +ssl,6812 +unknown-tcp,13 +web-browsing,2566 +webex-base,1 +windows-azure-base,139 +youtube-base,1 +zendesk-base,7 +zoominfo,2 diff --git a/samples/dict_of_pan_app-id_to_cyperf_app.txt b/samples/dict_of_pan_app-id_to_cyperf_app.txt new file mode 100644 index 0000000..31f7ef7 --- /dev/null +++ b/samples/dict_of_pan_app-id_to_cyperf_app.txt @@ -0,0 +1,103 @@ +web-browsing: YYLive Microsoft Edge +google-base: Office365 Outlook Microsoft Edge +google-maps: Meraki Microsoft Edge +facebook-base: iTunes Desktop +http-video: YYLive Microsoft Edge +alipay: Ctrip Chrome +alisoft: Alibaba +google-analytics: iTunes Desktop +amazon-chime-conferencing: Amazon Chime +amazon-chime-base: Amazon Chime +appdynamics: AppDynamics +appogee: Appogee +vimeo-base: appointy Microsoft Edge +zendesk-base: appointy Microsoft Edge +appointy: appointy Microsoft Edge +amazon-aws-console: AWS Console Microsoft Edge +amazon-cloud-drive-uploading: AWS S3 Microsoft Edge +bittorrent: BitTorrent Upload +blogger-blog-posting: Blogger +Web-browsing: Blogger +youtube-base: GooglePhotos Microsoft Edge +google-hangouts-base: GoogleHangouts Microsoft Edge +google-hangouts-video: Blogger +ms-ds-smbv1: CIFS +cisco-spark-base: Cisco Spark Microsoft Edge +webex-base: Cisco Spark Microsoft Edge +cisco-spark-file-transfer: Cisco Spark Microsoft Edge +jira-base: Jira Service Desk +confluence-downloading: Confluence +dns-base: DNS Flood +http-audio: Skype Microsoft Edge +facebook-voice: Facebook Audio Microsoft Edge +facebook-uploading: FacebookLive Chrome +facebook-posting: FacebookLive Microsoft Edge +facebook-chat: FacebookLive Microsoft Edge +facebook-video: FacebookLive Microsoft Edge +ftp: FTP +gmail-base: Gmail Chrome +gmail-downloading: Gmail Chrome +gmail-posting: Gmail Chrome +gmx-mail: GMX Mail +google: Google Classroom Microsoft Edge +google-drive-web: Google Drive Microsoft Edge +google-play: GooglePhotos Microsoft Edge +google-plus-base: GoogleHangouts Microsoft Edge +google-calendar-base: Google Calendar +google-classroom: Google Classroom Chrome +google-app-engine: Google Cloud Storage +google-safebrowsing: Google Cloud Storage +google-docs-downloading: Google Drive Microsoft Edge +google-docs-posting: Google Slides Microsoft Edge +google-docs-base: Google Slides Microsoft Edge +google-docs-uploading: GoogleHangouts Microsoft Edge +google-hangouts-audio-video: GoogleHangouts Microsoft Edge +google-hangouts-chat: GoogleHangouts Microsoft Edge +google-photos-uploading: GooglePhotos Microsoft Edge +google-photos-downloading: GooglePhotos Microsoft Edge +instagram-base: Google Search +google-update: Google Search +apache-guacamole: Guacamole +hbo: HBOMax +hulu-base: Hulu Microsoft Edge +c37.118-cmd-frame-send-cfg-2: IEEE C37.118 Synchrophasor UDP +jira-posting: Jira Service Desk +qq-games: League of Legends Microsoft Edge +mqtt-disconnect: MQTT Subscriber +mail.ru-base: Mail.ru Microsoft Edge +mms-ics-base: Manufacturing Message Specification (MMS) +ocsp: Meraki Microsoft Edge +new-relic: Meraki Microsoft Edge +windows-azure-base: Microsoft Azure Chrome +modbus-read-fifo-queue: Modbus +mongodb-base: MongoDB +mssql-db-unencrypted: MS-SQL Server +portmapper: NFSv3 +hotmail: Office365 Excel Microsoft Edge +office365-consumer-access: Skype 8 Microsoft Edge +ms-office365-base: Office365 Outlook Microsoft Edge +ms-onedrive-base: Office365 Outlook Microsoft Edge +outlook-web-online: Yammer Microsoft Edge +ms-onedrive-downloading: Office365 OneNote +ms-powerpoint-online: Office365 OneNote +ms-onedrive-uploading: Office365 OneDrive Microsoft Edge +outlook-web: Office365 OneNote +ms-onenote-base: Office365 OneNote +ms-outlook-personal-uploading: Office365 Outlook Microsoft Edge +odnoklassniki-base: OK.ru Microsoft Edge +oracle: Oracle Database +pop3: POP3 +postgres: PostgreSQL +reddit-base: Reddit Microsoft Edge +reddit-posting: Reddit Microsoft Edge +salesforce-base: Salesforce Microsoft Edge +service-now-editing: Service-Now Microsoft Edge +sina-weibo-base: Sina Weibo Microsoft Edge +skype: Skype Microsoft Edge +smtp-exception: SMTP +tubitv: Tubi Microsoft Edge +weather-desktop: TWC Microsoft Edge +vkontakte-base: VKontakte Microsoft Edge +vkontakte-chat: VKontakte Microsoft Edge +yammer-editing: Yammer Microsoft Edge +zoom: Zoom Classroom Teacher diff --git a/samples/invert.py b/samples/invert.py new file mode 100644 index 0000000..623d027 --- /dev/null +++ b/samples/invert.py @@ -0,0 +1,890 @@ +''' Script to create a csv which maps the App-Id from Plao Alto to CyPerf Applications . Input to this file is a +dictionary present at https://bitbucket.it.keysight.com/projects/ISGAPPSEC/repos/appsec-automation/browse/appsec/common/PAN_regression/PanSignatures.json +The disctionary which our Engineers at keysight maintains maps CyPerf AppNames to the App-id . Here we want to do the reverse, one-to-one mapping between +PANW-APPID & CyPerf APPNames ''' + +def invert_dictionary(d): + inverted_dict = {} + for key, values in d.items(): + for value in values: + inverted_dict[value] = key + return inverted_dict + +def count_values(d): + k=[] + for item in d : + for value in d[item]: + if value not in k: + k.append(value) + return k + +def print_dict_to_file(dictionary, filename): + with open(filename, 'w') as f: + for key, value in dictionary.items(): + f.write(f"{key}: {value}\n") + +def print_dict_to_file_csv(dictionary, filename): + with open(filename, 'w') as f: + for key, value in dictionary.items(): + f.write(f"{key},{value}\n") + + +if __name__ == '__main__': + k = { + "Adobe Reader Updates Chrome": [ + "web-browsing" + ], + "Adobe Reader Updates Firefox": [ + "web-browsing" + ], + "Adobe Reader Updates Internet Explorer": [ + "web-browsing" + ], + "Adobe Reader Updates Microsoft Edge": [ + "web-browsing" + ], + "ADP Chrome": [ + "web-browsing" + ], + "ADP Firefox": [ + "web-browsing" + ], + "ADP Internet Explorer": [ + "web-browsing" + ], + "ADP Microsoft Edge": [ + "web-browsing" + ], + "Airbnb Chrome": [ + "google-base", "google-maps", "facebook-base", "http-video", "web-browsing" + ], + "Airbnb Firefox": [ + "google-base", "google-maps", "facebook-base", "http-video", "web-browsing" + ], + "Airbnb Internet Explorer": [ + "google-base", "google-maps", "facebook-base", "http-video", "web-browsing" + ], + "Airbnb Microsoft Edge": [ + "google-base", "google-maps", "facebook-base", "http-video", "web-browsing" + ], + + "Alibaba": [ + "web-browsing", "alipay", "alisoft", "google-base", "google-analytics" + ], + "Amazon Chime": [ + "web-browsing", "amazon-chime-conferencing", "amazon-chime-base" + ], + "AppDynamics": [ + "web-browsing", "appdynamics", "google-base" + ], + "Appogee" : [ + "web-browsing", "google-base", "appogee" + ], + "appointy Chrome": [ + "web-browsing", "google-base", "vimeo-base", "zendesk-base", "google-maps", "appointy" + ], + "appointy Firefox": [ + "web-browsing", "google-base", "vimeo-base", "zendesk-base", "google-maps", "appointy" + ], + "appointy Internet Explorer": [ + "web-browsing", "google-base", "vimeo-base", "zendesk-base", "google-maps", "appointy" + ], + "appointy Microsoft Edge": [ + "web-browsing", "google-base", "vimeo-base", "zendesk-base", "google-maps", "appointy" + ], + "AWS Console Chrome": [ + "web-browsing", "amazon-aws-console" + ], + "AWS Console Firefox": [ + "web-browsing", "amazon-aws-console" + ], + "AWS Console Internet Explorer": [ + "web-browsing", "amazon-aws-console" + ], + "AWS Console Microsoft Edge": [ + "web-browsing", "amazon-aws-console" + ], + "AWS S3 Chrome": [ + "amazon-cloud-drive-uploading" + ], + "AWS S3 Firefox": [ + "amazon-cloud-drive-uploading" + ], + "AWS S3 Internet Explorer": [ + "amazon-cloud-drive-uploading" + ], + "AWS S3 Microsoft Edge": [ + "amazon-cloud-drive-uploading" + ], + "Baidu Chrome": [ + "web-browsing" + ], + "Baidu Firefox": [ + "web-browsing" + ], + "Baidu Internet Explorer": [ + "web-browsing" + ], + "Baidu Maps Chrome": [ + "web-browsing" + ], + "Baidu Maps Firefox": [ + "web-browsing" + ], + "Baidu Maps Internet Explorer": [ + "web-browsing" + ], + "Baidu Maps Microsoft Edge": [ + "web-browsing" + ], + "Baidu Microsoft Edge": [ + "web-browsing" + ], + "Bilibili Chrome": [ + "http-video", "web-browsing" + ], + "Bilibili Firefox": [ + "http-video", "web-browsing" + ], + "Bilibili Internet Explorer": [ + "http-video", "web-browsing" + ], + "Bilibili Microsoft Edge": [ + "http-video", "web-browsing" + ], + "BitTorrent Download": [ + "bittorrent" + ], + "BitTorrent Upload": [ + "bittorrent" + ], + "Blogger": [ + "blogger-blog-posting", "Web-browsing", "google-base", "youtube-base", "google-analytics", "google-hangouts-base", "google-hangouts-video" + ], + "CCTV Video Mobile": [ + "http-video" + ], + "CIFS": [ + "ms-ds-smbv1" + ], + "Cisco Spark Chrome": [ + "cisco-spark-base", "webex-base", "cisco-spark-file-transfer", "web-browsing" + ], + "Cisco Spark Firefox": [ + "cisco-spark-base", "webex-base", "cisco-spark-file-transfer", "web-browsing" + ], + "Cisco Spark Internet Explorer": [ + "cisco-spark-base", "webex-base", "cisco-spark-file-transfer", "web-browsing" + ], + "Cisco Spark Microsoft Edge": [ + "cisco-spark-base", "webex-base", "cisco-spark-file-transfer", "web-browsing" + ], + "Commvault Chrome": [ + "web-browsing" + ], + "Commvault Firefox": [ + "web-browsing" + ], + "Commvault Internet Explorer": [ + "web-browsing" + ], + "Commvault Microsoft Edge": [ + "web-browsing" + ], + "Confluence": [ + "web-browsing", "jira-base", "confluence-downloading" + ], + "Crawling Wikipedia (Chinese) Chrome": [ + "web-browsing" + ], + "Crawling Wikipedia (Chinese) Firefox": [ + "web-browsing" + ], + "Crawling Wikipedia (Chinese) Internet Explorer": [ + "web-browsing" + ], + "Crawling Wikipedia (Chinese) Microsoft Edge": [ + "web-browsing" + ], + "Ctrip Chrome": [ + "web-browsing", "alipay" + ], + "Dianping": [ + "web-browsing" + ], + "DNS": [ + "dns-base" + ], + "DNS Flood": [ + "dns-base" + ], + "DocuSign Chrome": [ + "web-browsing" + ], + "DocuSign Firefox": [ + "web-browsing" + ], + "DocuSign Internet Explorer": [ + "web-browsing" + ], + "DocuSign Microsoft Edge": [ + "web-browsing" + ], + "Dreambox Chrome": [ + "http-audio", "web-browsing" + ], + "Dreambox Firefox": [ + "http-audio", "web-browsing" + ], + "Dreambox Internet Explorer": [ + "http-audio", "web-browsing" + ], + "Dreambox Microsoft Edge": [ + "http-audio", "web-browsing" + ], + "eBanking Chrome to Apache": [ + "web-browsing" + ], + "eBanking Firefox to IIS": [ + "web-browsing" + ], + "eBanking Internet Explorer to Nginx": [ + "web-browsing" + ], + "eBanking Microsoft Edge to Apache": [ + "web-browsing" + ], + "Ebay": [ + "web-browsing" + ], + "EpixNow Chrome": [ + "web-browsing", "http-video" + ], + "EpixNow Firefox": [ + "web-browsing", "http-video" + ], + "EpixNow Internet Explorer": [ + "web-browsing", "http-video" + ], + "EpixNow Microsoft Edge": [ + "web-browsing", "http-video" + ], + "eShop Chrome to Apache": [ + "web-browsing" + ], + "eShop Firefox to IIS": [ + "web-browsing" + ], + "eShop Internet Explorer to Nginx": [ + "web-browsing" + ], + "eShop Microsoft Edge to Apache": [ + "web-browsing" + ], + "Facebook Audio Chrome": [ + "facebook-voice", "facebook-base" + ], + "Facebook Audio Firefox": [ + "facebook-voice", "facebook-base" + ], + "Facebook Audio Internet Explorer": [ + "facebook-voice", "facebook-base" + ], + "Facebook Audio Microsoft Edge": [ + "facebook-voice", "facebook-base" + ], + "Facebook Chrome": [ + "facebook-base", "facebook-uploading" + ], + "Facebook Firefox": [ + "facebook-base", "facebook-uploading" + ], + "Facebook Internet Explorer": [ + "facebook-base", "facebook-uploading" + ], + "Facebook Microsoft Edge": [ + "facebook-base", "facebook-uploading" + ], + "FacebookLive Chrome": [ + "web-browsing", "facebook-base", "facebook-posting", "facebook-chat", "facebook-video", "facebook-posting", "facebook-chat", "facebook-uploading" + ], + "FacebookLive Firefox": [ + "web-browsing", "facebook-base", "facebook-posting", "facebook-chat", "facebook-video", "facebook-posting", "facebook-chat" + ], + "FacebookLive Internet Explorer": [ + "web-browsing", "facebook-base", "facebook-posting", "facebook-chat", "facebook-video", "facebook-posting", "facebook-chat" + ], + "FacebookLive Microsoft Edge": [ + "web-browsing", "facebook-base", "facebook-posting", "facebook-chat", "facebook-video", "facebook-posting", "facebook-chat" + ], + "FTP": [ + "ftp" + ], + "Gab Chrome": [ + "http-video", "web-browsing" + ], + "Gab Firefox": [ + "http-video", "web-browsing" + ], + "Gab Internet Explorer": [ + "http-video", "web-browsing" + ], + "Gab Microsoft Edge": [ + "http-video", "web-browsing" + ], + "Gaode Maps Chrome": [ + "web-browsing" + ], + "Gaode Maps Firefox": [ + "web-browsing" + ], + "Gaode Maps Internet Explorer": [ + "web-browsing" + ], + "Gaode Maps Microsoft Edge": [ + "web-browsing" + ], + "Gettr Chrome": [ + "web-browsing" + ], + "Gmail Chrome": [ + "gmail-base", "gmail-downloading", "gmail-posting" + ], + "GMX Mail": [ + "web-browsing", "gmx-mail", "google-base" + ], + "Google App Engine Chrome": [ + "google" , "google-base", "google-drive-web" , "google-play", "youtube-base" + ], + "Google Calendar": [ + "google-base", "google-plus-base", "google-maps", "google-calendar-base", "youtube-base" + ], + "Google Classroom Chrome": [ + "web-browsing","google-base", "google-drive-web", "google-play", "google-classroom" + ], + "Google Classroom Firefox": [ + "google" + ], + "Google Classroom Internet Explorer": [ + "google" + ], + "Google Classroom Microsoft Edge": [ + "google" + ], + "Google Cloud Storage": [ + "web-browsing","google-base","google-app-engine", "google-safebrowsing", "google-analytics" + ], + "Google Drive Chrome": [ + "google-drive-web", "youtube-base", "google-docs-downloading", "google-base", "google-play", "google-docs-posting", "google-docs-base", "google-docs-uploading" + ], + "Google Drive Firefox": [ + "google-drive-web", "youtube-base", "google-docs-downloading", "google-base", "google-play", "google-docs-posting", "google-docs-base", "google-docs-uploading" + ], + "Google Drive Internet Explorer": [ + "google-drive-web", "youtube-base", "google-docs-downloading", "google-base", "google-play", "google-docs-posting", "google-docs-base", "google-docs-uploading" + ], + "Google Drive Microsoft Edge": [ + "google-drive-web", "youtube-base", "google-docs-downloading", "google-base", "google-play", "google-docs-posting", "google-docs-base", "google-docs-uploading" + ], + "Google Sheets Chrome": [ + "youtube-base", "google-docs-posting", "google-base", "google-docs-base" + ], + "Google Sheets Firefox": [ + "youtube-base", "google-docs-posting", "google-base", "google-docs-base" + ], + "Google Sheets Internet Explorer": [ + "youtube-base", "google-docs-posting", "google-base", "google-docs-base" + ], + "Google Sheets Microsoft Edge": [ + "youtube-base", "google-docs-posting", "google-base", "google-docs-base" + + ], + "Google Slides Chrome": [ + "youtube-base", "google-docs-posting", "google-base", "google-docs-base" + ], + "Google Slides Firefox": [ + "youtube-base", "google-docs-posting", "google-base", "google-docs-base" + ], + "Google Slides Internet Explorer": [ + "youtube-base", "google-docs-posting", "google-base", "google-docs-base" + ], + "Google Slides Microsoft Edge": [ + "youtube-base", "google-docs-posting", "google-base", "google-docs-base" + ], + "GoogleHangouts Chrome": [ + "google-analytics", "google-hangouts-audio-video", "google-hangouts-base", "google-base", "google-play", "google-plus-base", "web-browsing", "google-hangouts-chat", "google-docs-uploading" + ], + "GoogleHangouts Firefox": [ + "google-analytics", "google-hangouts-audio-video", "google-hangouts-base", "google-base", "google-play", "google-plus-base", "web-browsing", "google-hangouts-chat", "google-docs-uploading" + ], + "GoogleHangouts Internet Explorer": [ + "google-analytics", "google-hangouts-audio-video", "google-hangouts-base", "google-base", "google-play", "google-plus-base", "web-browsing", "google-hangouts-chat", "google-docs-uploading" + ], + "GoogleHangouts Microsoft Edge": [ + "google-analytics", "google-hangouts-audio-video", "google-hangouts-base", "google-base", "google-play", "google-plus-base", "web-browsing", "google-hangouts-chat", "google-docs-uploading" + ], + "GooglePhotos Chrome": [ + "google-base", "google-photos-uploading", "google-photos-downloading", "google-play", "youtube-base" + ], + "GooglePhotos Firefox": [ + "google-base", "google-photos-uploading", "google-photos-downloading", "google-play", "youtube-base" + ], + "GooglePhotos Internet Explorer": [ + "google-base", "google-photos-uploading", "google-photos-downloading", "google-play", "youtube-base" + ], + "GooglePhotos Microsoft Edge": [ + "google-base", "google-photos-uploading", "google-photos-downloading", "google-play", "youtube-base" + ], + "Google Search": [ + "web-browsing", "google-base", "instagram-base", "google-update" + ], + "Guacamole": [ + "apache-guacamole" + ], + "HBOMax": [ + "web-browsing", "hbo" + ], + "HTTP App": [ + "web-browsing" + ], + "HTTP Excessive GET": [ + "web-browsing" + ], + "HTTP Excessive POST": [ + "web-browsing" + ], + "Hulu Chrome": [ + "hulu-base", "web-browsing" + ], + "Hulu Firefox": [ + "hulu-base", "web-browsing" + ], + "Hulu Internet Explorer": [ + "hulu-base", "web-browsing" + ], + "Hulu Microsoft Edge": [ + "hulu-base", "web-browsing" + ], + "IEEE C37.118 Synchrophasor TCP": [ + "c37.118-cmd-frame-send-cfg-2" + ], + "IEEE C37.118 Synchrophasor UDP": [ + "c37.118-cmd-frame-send-cfg-2" + ], + "Instacart Chrome": [ + "google-base", "facebook-base", "google-analytics", "web-browsing" + ], + "Instacart Firefox": [ + "google-base", "facebook-base", "google-analytics", "web-browsing" + ], + "Instacart Internet Explorer": [ + "google-base", "facebook-base", "google-analytics", "web-browsing" + ], + "Instacart Microsoft Edge": [ + "google-base", "facebook-base", "google-analytics", "web-browsing" + ], + "iTunes Desktop": [ + "web-browsing", "facebook-base", "google-analytics", "google-base" + ], + "Jingdong Chrome": [ + "web-browsing" + ], + "Jingdong Firefox": [ + "web-browsing" + ], + "Jingdong Internet Explorer": [ + "web-browsing" + ], + "Jingdong Microsoft Edge": [ + "web-browsing" + ], + "Jira Chrome": [ + "web-browsing", "jira-base", "jira-posting" + ], + "Jira Firefox": [ + "web-browsing", "jira-base", "jira-posting" + ], + "Jira Internet Explorer": [ + "web-browsing", "jira-base", "jira-posting" + ], + "Jira Microsoft Edge": [ + "web-browsing", "jira-base", "jira-posting" + ], + "Jira Service Desk": [ + "web-browsing", "jira-base", "jira-posting" + ], + "League of Legends Chrome": [ + "web-browsing", "qq-games" + ], + "League of Legends Firefox": [ + "web-browsing", "qq-games" + ], + "League of Legends Internet Explorer": [ + "web-browsing", "qq-games" + ], + "League of Legends Microsoft Edge": [ + "web-browsing", "qq-games" + ], + "LwM2M over MQTT Client": [ + "mqtt-disconnect" + ], + "LwM2M over MQTT Server": [ + "mqtt-disconnect" + ], + "Mail.ru Chrome": [ + "web-browsing", "mail.ru-base", "http-audio" + ], + "Mail.ru Firefox": [ + "web-browsing", "mail.ru-base", "http-audio" + ], + "Mail.ru Internet Explorer": [ + "web-browsing", "mail.ru-base", "http-audio" + ], + "Mail.ru Microsoft Edge": [ + "web-browsing", "mail.ru-base", "http-audio" + ], + "Mango TV Chrome": [ + "web-browsing", "http-video" + ], + "Manufacturing Message Specification (MMS) ": [ + "mms-ics-base" + ], + "Meraki Chrome": [ + "google-base", "ocsp", "google-maps", "web-browsing", "new-relic" + ], + "Meraki Firefox": [ + "google-base", "ocsp", "google-maps", "web-browsing", "new-relic" + ], + "Meraki Internet Explorer": [ + "google-base", "ocsp", "google-maps", "web-browsing", "new-relic" + ], + "Meraki Microsoft Edge": [ + "google-base", "ocsp", "google-maps", "web-browsing", "new-relic" + ], + "Mewe Chrome": [ + "web-browsing" + ], + "Mewe Firefox": [ + "web-browsing" + ], + "Mewe Internet Explorer": [ + "web-browsing" + ], + "Mewe Microsoft Edge": [ + "web-browsing" + ], + "Microsoft Azure Chrome": [ + "web-browsing", "windows-azure-base" + ], + "Modbus": [ + "modbus-read-fifo-queue" + ], + "MongoDB": [ + "mongodb-base" + ], + "MQTT Publisher":[ + "mqtt-disconnect" + ], + "MQTT Subscriber":[ + "mqtt-disconnect" + ], + "MS-SQL Server": [ + "mssql-db-unencrypted" + ], + "Netease Music Chrome": [ + "http-video", "http-audio", "web-browsing" + ], + "Netease Music Firefox": [ + "http-video", "http-audio", "web-browsing" + ], + "Netease Music Internet Explorer": [ + "http-video", "http-audio", "web-browsing" + ], + "Netease Music Microsoft Edge": [ + "http-video", "http-audio", "web-browsing" + ], + "NFSv3": [ + "portmapper" + ], + "Office 365 Outlook People Chrome": [ + "hotmail", "office365-consumer-access", "ms-office365-base", "web-browsing", "ms-onedrive-base" + ], + "Office 365 Outlook People Firefox": [ + "hotmail", "office365-consumer-access", "ms-office365-base", "web-browsing", "ms-onedrive-base" + ], + "Office 365 Outlook People Internet Explorer": [ + "hotmail", "office365-consumer-access", "ms-office365-base", "web-browsing", "ms-onedrive-base" + ], + "Office 365 Outlook People Microsoft Edge": [ + "hotmail", "office365-consumer-access", "ms-office365-base", "web-browsing", "ms-onedrive-base" + ], + "Office365 Excel Chrome": [ + "hotmail", "office365-consumer-access", "ms-office365-base", "outlook-web-online", "web-browsing", "ms-onedrive-base", "ms-onedrive-downloading", "google-base" + ], + "Office365 Excel Firefox": [ + "hotmail", "office365-consumer-access", "ms-office365-base", "outlook-web-online", "web-browsing", "ms-onedrive-base", "ms-onedrive-downloading", "google-base" + ], + "Office365 Excel Internet Explorer": [ + "hotmail", "office365-consumer-access", "ms-office365-base", "outlook-web-online", "web-browsing", "ms-onedrive-base", "ms-onedrive-downloading", "google-base" + ], + "Office365 Excel Microsoft Edge": [ + "hotmail", "office365-consumer-access", "ms-office365-base", "outlook-web-online", "web-browsing", "ms-onedrive-base", "ms-onedrive-downloading", "google-base" + ], + "Office365 OneDrive Chrome": [ + "ms-powerpoint-online", "office365-consumer-access", "ms-onedrive-uploading", "ms-onedrive-downloading", "ms-office365-base", "outlook-web", "outlook-web-online", "web-browsing", "ms-onedrive-base" + ], + "Office365 OneDrive Firefox": [ + "ms-powerpoint-online", "office365-consumer-access", "ms-onedrive-uploading", "ms-onedrive-downloading", "ms-office365-base", "outlook-web", "outlook-web-online", "web-browsing", "ms-onedrive-base" + ], + "Office365 OneDrive Internet Explorer": [ + "ms-powerpoint-online", "office365-consumer-access", "ms-onedrive-uploading", "ms-onedrive-downloading", "ms-office365-base", "outlook-web", "outlook-web-online", "web-browsing", "ms-onedrive-base" + ], + "Office365 OneDrive Microsoft Edge": [ + "ms-powerpoint-online", "office365-consumer-access", "ms-onedrive-uploading", "ms-onedrive-downloading", "ms-office365-base", "outlook-web", "outlook-web-online", "web-browsing", "ms-onedrive-base" + ], + "Office365 OneNote":[ + "web-browsing", "outlook-web-online", "ms-office365-base", "ms-onedrive-downloading", "ms-powerpoint-online", "outlook-web", "ms-onedrive-base", "office365-consumer-access", "ms-onenote-base" + ], + "Office365 Outlook Chrome": [ + "office365-consumer-access", "ms-office365-base", "http-audio", "google-base", "ms-outlook-personal-uploading", "outlook-web-online", "web-browsing", "ms-onedrive-base", "google-base" + ], + "Office365 Outlook Firefox": [ + "office365-consumer-access", "ms-office365-base", "http-audio", "google-base", "ms-outlook-personal-uploading", "outlook-web-online", "web-browsing", "ms-onedrive-base", "google-base" + ], + "Office365 Outlook Internet Explorer": [ + "office365-consumer-access", "ms-office365-base", "http-audio", "google-base", "ms-outlook-personal-uploading", "outlook-web-online", "web-browsing", "ms-onedrive-base", "google-base" + ], + "Office365 Outlook Microsoft Edge": [ + "office365-consumer-access", "ms-office365-base", "http-audio", "google-base", "ms-outlook-personal-uploading", "outlook-web-online", "web-browsing", "ms-onedrive-base", "google-base" + ], + "OK.ru Chrome": [ + "odnoklassniki-base", "web-browsing" + ], + "OK.ru Firefox": [ + "odnoklassniki-base", "web-browsing" + ], + "OK.ru Internet Explorer": [ + "odnoklassniki-base", "web-browsing" + ], + "OK.ru Microsoft Edge": [ + "odnoklassniki-base", "web-browsing" + ], + "Oracle Database": [ + "oracle" + ], + "Portal Chrome to Apache": [ + "web-browsing" + ], + "Portal Firefox to IIS": [ + "web-browsing" + ], + "Portal Internet Explorer to Nginx": [ + "web-browsing" + ], + "Portal Microsoft Edge to Apache": [ + "web-browsing" + ], + "POP3": [ + "pop3" + ], + "PostgreSQL": [ + "postgres" + ], + "Reddit Chrome": [ + "reddit-base", "reddit-posting", "web-browsing" + ], + "Reddit Firefox": [ + "reddit-base", "reddit-posting", "web-browsing" + ], + "Reddit Internet Explorer": [ + "reddit-base", "reddit-posting", "web-browsing" + ], + "Reddit Microsoft Edge": [ + "reddit-base", "reddit-posting", "web-browsing" + ], + "Salesforce Chrome": [ + "salesforce-base", "web-browsing" + ], + "Salesforce Firefox": [ + "salesforce-base", "web-browsing" + ], + "Salesforce Internet Explorer": [ + "salesforce-base", "web-browsing" + ], + "Salesforce Microsoft Edge": [ + "salesforce-base", "web-browsing" + ], + "Service-Now Chrome": [ + "service-now-editing" + ], + "Service-Now Firefox": [ + "service-now-editing" + ], + "Service-Now Internet Explorer": [ + "service-now-editing" + ], + "Service-Now Microsoft Edge": [ + "service-now-editing" + ], + "Sina Weibo Chrome": [ + "http-video", "sina-weibo-base", "web-browsing", "http-video" + ], + "Sina Weibo Firefox": [ + "http-video", "sina-weibo-base", "web-browsing", "http-video" + ], + "Sina Weibo Internet Explorer": [ + "http-video", "sina-weibo-base", "web-browsing", "http-video" + ], + "Sina Weibo Microsoft Edge": [ + "http-video", "sina-weibo-base", "web-browsing", "http-video" + ], + "Skype 8 Chrome": [ + "skype", "web-browsing", "office365-consumer-access" + ], + "Skype 8 Firefox": [ + "skype", "web-browsing", "office365-consumer-access" + ], + "Skype 8 Internet Explorer": [ + "skype", "web-browsing", "office365-consumer-access" + ], + "Skype 8 Microsoft Edge": [ + "skype", "web-browsing", "office365-consumer-access" + ], + "Skype Chrome": [ + "skype", "web-browsing", "http-audio" + ], + "Skype Firefox": [ + "skype", "web-browsing", "http-audio" + ], + "Skype Internet Explorer": [ + "skype", "web-browsing", "http-audio" + ], + "Skype Microsoft Edge": [ + "skype", "web-browsing", "http-audio" + ], + "SMTP": [ + "smtp-exception" + ], + "Socal Network Chrome to Apache": [ + "web-browsing" + ], + "Social Network Firefox to IIS": [ + "web-browsing" + ], + "Social Network Internet Explorer to Nginx": [ + "web-browsing" + ], + "Social Network Microsoft Edge to Apache": [ + "web-browsing" + ], + "Splunk Chrome": [ + "web-browsing" + ], + "Splunk Firefox": [ + "web-browsing" + ], + "Splunk Internet Explorer": [ + "web-browsing" + ], + "Splunk Microsoft Edge": [ + "web-browsing" + ], + "Tubi Chrome": [ + "tubitv" + ], + "Tubi Firefox": [ + "tubitv" + ], + "Tubi Internet Explorer": [ + "tubitv" + ], + "Tubi Microsoft Edge": [ + "tubitv" + ], + "TWC Chrome": [ + "web-browsing", "http-video", "weather-desktop" + ], + "TWC Firefox": [ + "web-browsing", "http-video", "weather-desktop" + ], + "TWC Internet Explorer": [ + "web-browsing", "http-video", "weather-desktop" + ], + "TWC Microsoft Edge": [ + "web-browsing", "http-video", "weather-desktop" + ], + "Video Platform Chrome to Apache": [ + "web-browsing" + ], + "Video Platform Firefox to IIS": [ + "web-browsing" + ], + "Video Platform Internet Explorer to Nginx": [ + "web-browsing" + ], + "Video Platform Microsoft Edge to Apache": [ + "web-browsing" + ], + "VKontakte Chrome": [ + "vkontakte-base", "vkontakte-chat", "web-browsing" + ], + "VKontakte Firefox": [ + "vkontakte-base", "vkontakte-chat", "web-browsing" + ], + "VKontakte Internet Explorer": [ + "vkontakte-base", "vkontakte-chat", "web-browsing" + ], + "VKontakte Microsoft Edge": [ + "vkontakte-base", "vkontakte-chat", "web-browsing" + ], + "Yammer Chrome": [ + "yammer-editing", "outlook-web-online", "web-browsing" + ], + "Yammer Firefox": [ + "yammer-editing", "outlook-web-online", "web-browsing" + ], + "Yammer Internet Explorer": [ + "yammer-editing", "outlook-web-online", "web-browsing" + ], + "Yammer Microsoft Edge": [ + "yammer-editing", "outlook-web-online", "web-browsing" + ], + "YYLive Chrome": [ + "web-browsing", "http-video" + ], + "YYLive Firefox": [ + "web-browsing", "http-video" + ], + "YYLive Internet Explorer": [ + "web-browsing", "http-video" + ], + "YYLive Microsoft Edge": [ + "web-browsing", "http-video" + ], + "Zoom All Hands Participant": [ + "zoom" + ], + "Zoom All Hands Presenter": [ + "zoom" + ], + "Zoom Brainstorming Participant": [ + "zoom" + ], + "Zoom Classroom Student": [ + "zoom" + ], + "Zoom Classroom Teacher": [ + "zoom" + ] +} + + j = invert_dictionary(k) + import pprint + pprint.pprint(j) + + print(len(count_values(k))) + + print_dict_to_file(j,"dict_of_pan_app-id_to_cyperf_app.txt") + print_dict_to_file_csv(j,"pan_app_id_to_cyperf_app_mappings.csv") + + diff --git a/samples/pan_app_id_to_cyperf_app_mappings.csv b/samples/pan_app_id_to_cyperf_app_mappings.csv new file mode 100644 index 0000000..40df765 --- /dev/null +++ b/samples/pan_app_id_to_cyperf_app_mappings.csv @@ -0,0 +1,188 @@ +app-id,cyperf-appname +solarwinds,SolarWinds NCM +solarwinds-agent,SolarWinds NCM +solarwinds-base,SolarWinds NCM +solarwinds-msp-anywhere,SolarWinds NCM +solarwinds-npm,SolarWinds NCM +solarwinds-rmm,SolarWinds SAM +solarwinds-sam,SolarWinds SAM +solarwinds-loggly,SolarWinds SAM +solarwinds-loggly-base,SolarWinds SAM +solarwinds-loggly-logout,SolarWinds SAM +google-base,Google App Engine Chrome +oracle,Oracle Database +azure-log-analytics,Microsoft Azure Chrome +splunk,Splunk Internet Explorer +smtp-base,SMTP +dns,DNS +rtp-base,Zoom All Hands Presenter +sharepoint-online,Microsoft SharePoint +ldap,LDAP +windows-azure-base,Microsoft Azure Chrome +okta,Okta Multifactor Authentication +ms-office365-base,Office365 Outlook Firefox +ms-update,Windows Updates +service-now-base,Service-Now Chrome +unknown-udp,UDP Stream +salesforce-base,Salesforce Chrome +github-base,Git over HTTP +github-pages,Git over HTTP +ntp-base,NTP +mysql,MySQL +sendgrid,SendGrid Chrome +mssql-db-base,MS-SQL Server +postgres,PostgreSQL +openai-api,OpenAI API +snmpv2-get-next-request,SNMPv2c +linkedin-base,LinkedIn +microsoft-excel,Office365 Excel Chrome +zoominfo,Zoom Classroom Teacher +ms-onedrive-business,Office365 OneDrive Microsoft Edge +jira-base,Jira Chrome +modbus-read-holding-registers,Modbus +outlook-web,Office365 Outlook Internet Explorer +mqtt-connect,MQTT Subscriber +mssql-mon,SQLMon +canva-base,Canva +ms-onedrive-base,Office365 OneDrive Microsoft Edge +google-analytics,Google Analytics +snmpv1-get-request,SNMPv1 +mqtt-disconnect,MQTT Subscriber +snmpv1-get-next-request,SNMPv1 +ms-ds-smb-base,SMBv2 +capwap,Capwap +facebook-base,Facebook Chrome +youtube-base,Youtube Chrome +niagara-fox,Tridium Niagara Fox +netbios-ns,NetBIOS +bacnet-simple-ack,BACnet-IP +kafka,Kafka +cassandra,Apache Cassandra +nfs,NFSv3 +http,HTTP App +radius,RADIUS +ftp,FTP +outlook,Office365 Outlook Chrome +ms-sql,MS-SQL Server +rtp,Zoom Classroom Teacher +google,Google App Engine Chrome +ms-ds-smbv2,SMBv2 +bacnet-read-property,BACnet-IP +dns-base,DNS +zendesk-base,Zendesk +bacnet-write-property,BACnet-IP +web-browsing,YYLive Microsoft Edge +google-base,Office365 Outlook Microsoft Edge +google-maps,Meraki Microsoft Edge +facebook-base,iTunes Desktop +http-video,YYLive Microsoft Edge +alipay,Ctrip Chrome +alisoft,Alibaba +google-analytics,iTunes Desktop +amazon-chime-conferencing,Amazon Chime +amazon-chime-base,Amazon Chime +appdynamics,AppDynamics +appogee,Appogee +vimeo-base,appointy Microsoft Edge +zendesk-base,appointy Microsoft Edge +appointy,appointy Microsoft Edge +amazon-aws-console,AWS Console Microsoft Edge +amazon-cloud-drive-uploading,AWS S3 Microsoft Edge +bittorrent,BitTorrent Upload +blogger-blog-posting,Blogger +Web-browsing,Blogger +youtube-base,GooglePhotos Microsoft Edge +google-hangouts-base,GoogleHangouts Microsoft Edge +google-hangouts-video,Blogger +ms-ds-smbv1,CIFS +cisco-spark-base,Cisco Spark Microsoft Edge +webex-base,Cisco Spark Microsoft Edge +cisco-spark-file-transfer,Cisco Spark Microsoft Edge +jira-base,Jira Service Desk +confluence-downloading,Confluence +dns-base,DNS Flood +http-audio,Skype Microsoft Edge +facebook-voice,Facebook Audio Microsoft Edge +facebook-uploading,FacebookLive Chrome +facebook-posting,FacebookLive Microsoft Edge +facebook-chat,FacebookLive Microsoft Edge +facebook-video,FacebookLive Microsoft Edge +ftp,FTP +gmail-base,Gmail Chrome +gmail-downloading,Gmail Chrome +gmail-posting,Gmail Chrome +gmx-mail,GMX Mail +google,Google Classroom Microsoft Edge +google-drive-web,Google Drive Microsoft Edge +google-play,GooglePhotos Microsoft Edge +google-plus-base,GoogleHangouts Microsoft Edge +google-calendar-base,Google Calendar +google-classroom,Google Classroom Chrome +google-app-engine,Google Cloud Storage +google-safebrowsing,Google Cloud Storage +google-docs-downloading,Google Drive Microsoft Edge +google-docs-posting,Google Slides Microsoft Edge +google-docs-base,Google Slides Microsoft Edge +google-docs-uploading,GoogleHangouts Microsoft Edge +google-hangouts-audio-video,GoogleHangouts Microsoft Edge +google-hangouts-chat,GoogleHangouts Microsoft Edge +google-photos-uploading,GooglePhotos Microsoft Edge +google-photos-downloading,GooglePhotos Microsoft Edge +instagram-base,Google Search +google-update,Google Search +apache-guacamole,Guacamole +hbo,HBOMax +hulu-base,Hulu Microsoft Edge +c37.118-cmd-frame-send-cfg-2,IEEE C37.118 Synchrophasor UDP +jira-posting,Jira Service Desk +qq-games,League of Legends Microsoft Edge +mqtt-disconnect,MQTT Subscriber +mail.ru-base,Mail.ru Microsoft Edge +mms-ics-base,Manufacturing Message Specification (MMS) +ocsp,Meraki Microsoft Edge +new-relic,Meraki Microsoft Edge +windows-azure-base,Microsoft Azure Chrome +modbus-read-fifo-queue,Modbus +mongodb-base,MongoDB +mssql-db-unencrypted,MS-SQL Server +portmapper,NFSv3 +hotmail,Office365 Excel Microsoft Edge +office365-consumer-access,Skype 8 Microsoft Edge +ms-office365-base,Office365 Outlook Microsoft Edge +ms-onedrive-base,Office365 Outlook Microsoft Edge +outlook-web-online,Yammer Microsoft Edge +ms-onedrive-downloading,Office365 OneNote +ms-powerpoint-online,Office365 OneNote +ms-onedrive-uploading,Office365 OneDrive Microsoft Edge +outlook-web,Office365 OneNote +ms-onenote-base,Office365 OneNote +ms-outlook-personal-uploading,Office365 Outlook Microsoft Edge +odnoklassniki-base,OK.ru Microsoft Edge +oracle,Oracle Database +pop3,POP3 +postgres,PostgreSQL +reddit-base,Reddit Microsoft Edge +reddit-posting,Reddit Microsoft Edge +salesforce-base,Salesforce Microsoft Edge +service-now-editing,Service-Now Microsoft Edge +sina-weibo-base,Sina Weibo Microsoft Edge +skype,Skype Microsoft Edge +smtp-exception,SMTP +tubitv,Tubi Microsoft Edge +weather-desktop,TWC Microsoft Edge +vkontakte-base,VKontakte Microsoft Edge +vkontakte-chat,VKontakte Microsoft Edge +yammer-editing,Yammer Microsoft Edge +zoom,Zoom Classroom Teacher +mssql-db-encrypted,CCA-mssql-db-encrypted +ssl,CCA-ssl +mssql-db-encrypted,CCA-mssql-db-encrypted +ssl,CCA-ssl +mssql-db-encrypted,CCA-mssql-db-encrypted +ssl,CCA-ssl +mssql-db-encrypted,CCA-mssql-db-encrypted +ssl,CCA-ssl +mssql-db-encrypted,CCA-mssql-db-encrypted +ssl,CCA-ssl +mssql-db-encrypted,CCA-mssql-db-encrypted +ssl,CCA-ssl diff --git a/samples/sample_attack_based_script.py b/samples/sample_attack_based_script.py deleted file mode 100644 index ace63d8..0000000 --- a/samples/sample_attack_based_script.py +++ /dev/null @@ -1,217 +0,0 @@ -import cyperf -from cyperf import * -from cyperf.utils import create_api_client_cli, TestRunner -from time import sleep -import os - -if __name__ == "__main__": - import urllib3; urllib3.disable_warnings() - - # Enter a context with an instance of the API client - with create_api_client_cli(verify_ssl=False) as api_client: - - # Get some strikes - api_application_resources_instance = cyperf.ApplicationResourcesApi(api_client) - take = 3 # int | The number of search results to return (optional) - skip = 0 # int | The number of search results to skip (optional) - search_col = 'Name' # str | A list of comma-separated columns used to search for the supplied values (optional) - search_val = 'Google Chrome' # str | The keywords used to filter the items (optional) - categories = 'Browser:Chrome' - - strikes = None - try: - print(f"Finding first {take} strikes with \'{search_val}\' in their name, from the \'Browser\' category...") - api_application_resources_response = api_application_resources_instance.get_resources_strikes(take=take, skip=skip, search_col=search_col, search_val=search_val, categories=categories) - strikes = api_application_resources_response.data - if len(strikes) != take: - raise ValueError(f"Couldn't find {take} strikes.") - - print(f"{len(strikes)} strikes found:") - for strike in strikes: - print(f" {strike.name}") - print() - - except Exception as e: - print("Exception when calling ApplicationResourcesApi->get_resources_strikes: %s\n" % e) - - - - all_files = [] - # Add the attacks - for strike in strikes: - - # Find the pre-canned empty config - api_config_instance = cyperf.ConfigurationsApi(api_client) - take = 1 # int | The number of search results to return (optional) - skip = None # int | The number of search results to skip (optional) - search_col = 'displayName' # str | A list of comma-separated columns used to search for the supplied values (optional) - search_val = 'Cyperf Empty Config' # str | The keywords used to filter the items (optional) - filter_mode = None # str | The operator applied to the supplied values (optional) - sort = None # str | A list of comma-separated field:direction pairs used to sort the items where direction must be asc or dsc (optional) - config = api_config_instance.get_configs(take=1, - skip=skip, - search_col=search_col, - search_val=search_val, - filter_mode=filter_mode, - sort=sort) - - if len(config.data) == 0: - raise ValueError("Couldn't find the specified configuration.") - - # Load a pre-canned config - api_session_instance = cyperf.SessionsApi(api_client) - application = None # str | The user-friendly name for the application that controls this session (optional) - config_name = None # str | The display name of the configuration loaded in the session (optional) - config_url = config.data[0].config_url # str | The external URL of the configuration loaded in the session (optional) - index = None # int | The session's index (optional) (readonly) - name = None # str | The user-visible name of the session (optional) - owner = None # str | The user-visible name of the session's owner (optional) (readonly) - sessions = [cyperf.Session(application=application, - config_name=config_name, - configUrl=config_url, - index=index, - name=name, - owner=owner)] - # Create a session - session = None - try: - print("Creating empty session...") - api_session_response = api_session_instance.create_sessions(sessions=sessions) - session = api_session_response[0] - print("Session created.\n") - except Exception as e: - print("Exception when calling SessionsApi->create_sessions: %s\n" % e) # Add an app profile - - # Create an attack profile - print("Creating a Attack Profile...") - attack_profile_name = "Custom Attack Profile" - session.config.config.attack_profiles.append(cyperf.AttackProfile(id="1", name="My Attack Profile", attacks=[])) - session.config.config.attack_profiles.update() - print(attack_profile_name + " created succesfully.\n") - - take = 1 # int | The number of search results to return (optional) - skip = 0 # int | The number of search results to skip (optional) - search_col = 'Name' # str | A list of comma-separated columns used to search for the supplied values (optional) - search_val = strike.name # str | The keywords used to filter the items (optional) - - attack = None - try: - print(f"Finding the attack with the same name as the strike...") - api_application_resources_response = api_application_resources_instance.get_resources_attacks(take=take, skip=skip, search_col=search_col, search_val=search_val, categories=categories) - attack = api_application_resources_response.data[0] - print("Attack found.") - - except Exception as e: - print("Exception when calling ApplicationResourcesApi->get_resources_attacks: %s\n" % e) - - session.config.config.attack_profiles[0].attacks.append(Attack(id = "1", name = strike.name, external_resource_url = attack.id)) - session.config.config.attack_profiles[0].attacks.update() - - - print(f"Attack {strike.name} added successfully.\n") - - # Set the objective to single iteration - print("Setting the Iteration Count to 1...") - session.config.config.attack_profiles[0].objectives_and_timeline.timeline_segments[0].iteration_count = 1 - session.config.config.attack_profiles[0].objectives_and_timeline.update() - print("Iteration Count setted to 1 successfully.\n") - - # Create IP Networks - client_ip_network = IPNetwork(name="IP Network 1", id="1", agentAssignments=AgentAssignments(by_tag=[]), minAgents=1) - server_ip_network = IPNetwork(name="IP Network 2", id="2", agentAssignments=AgentAssignments(by_tag=[]), minAgents=1) - - # Append the IP Networks to the Network Profile - session.config.config.network_profiles[0].ip_network_segment = [client_ip_network, server_ip_network] - session.config.config.network_profiles[0].ip_network_segment.update() - print("Client and Server network segments added successfully.\n") - - # Get available agents - api_agents_instance = cyperf.AgentsApi(api_client) - agents = api_agents_instance.get_agents(exclude_offline='true') - - if len(agents) < 2: - raise ValueError("Expected at least 2 active agents") - - # Create an agent map - agent_map = { - 'IP Network 1': [agents[0].id, agents[0].ip], - 'IP Network 2': [agents[1].id, agents[1].ip] - } - - # Assign the agents - print("Assigning agents ...") - for net_profile in session.config.config.network_profiles: - for ip_net in net_profile.ip_network_segment: - if ip_net.name in agent_map: - agent_ip = agent_map[ip_net.name][1] - print(f" Agent {agent_ip} assigned to {ip_net.name}.") - capture_settings = None # str | The capture settings of the agent that is assigned (optional) - interfaces = None # List[str] | The names of the assigned test interfaces for the agent (optional) - links = None # List[APILink] | (optional) - agent_id = agent_map[ip_net.name][0] - agentDetails = [cyperf.AgentAssignmentDetails(agent_id=agent_id, - capture_setting=capture_settings, - id=agent_id, - interfaces=interfaces, - links=links)] - - if not ip_net.agent_assignments: - by_id = None # List[AgentAssignmentDetails] | The agents statically assigned to the current test configuration (optional) - by_port = None # List[AgentAssignmentByPort] | The ports assigned to the current test configuration (optional) - by_tag = [] # List[str] | The tags according to which the agents are dynamically assigned - links = None # List[APILink] | (optional) - ip_net.agent_assignments = cyperf.AgentAssignments(by_id=by_id, - by_port=by_port, - by_tag=by_tag, - links=links) - - ip_net.agent_assignments.by_id.extend(agentDetails) - ip_net.update() - print("Assigning agents completed.\n") - - # Start traffic - print("Starting the test ...") - api_test_operation_instance = cyperf.TestOperationsApi(api_client) - api_test_operation_response = api_test_operation_instance.start_test_run_start(session_id=session.id) - api_test_operation_response.await_completion() - - # Wait for the test to be finished - print("Test running ...") - session.refresh() - while session.test.status != 'STOPPED': - sleep(5) - session.refresh() - print("Test finished successfully.\n") - - # Download the test results - print("Downloading test results ...") - api_reports_instance = cyperf.ReportsApi(api_client) - generate_csv_operation = cyperf.GenerateCSVReportsOperation() - api_test_results_response = api_reports_instance.start_result_generate_csv(result_id=session.test.test_id, generate_csv_reports_operation=generate_csv_operation) - file_path = api_test_results_response.await_completion() - - last_separator_index = file_path.rfind("\\") - directory = file_path[:last_separator_index] - file_name = file_path[last_separator_index + 1:] - - print(f"Saved as: '{file_name}' at {directory}\n") - - # Add the file path to the list - all_files.append(file_path) - - - print("Aggregating all CSV files ...") - aggregated_file_path = os.path.join(directory, "aggregated_all_strikes_attack.csv") - - with open(aggregated_file_path, 'w', encoding='ISO-8859-1') as outfile: - for i, file_path in enumerate(all_files): - with open(file_path, 'r', encoding='ISO-8859-1') as infile: - if i == 0: - outfile.write(infile.read()) - else: - next(infile) - outfile.write(infile.read()) - - print(f"Aggregated results saved as: '{aggregated_file_path}'") - - diff --git a/samples/test_parameters.yml b/samples/test_parameters.yml new file mode 100644 index 0000000..f8e89d9 --- /dev/null +++ b/samples/test_parameters.yml @@ -0,0 +1,25 @@ +#Path of the folder where the captures will be stored . These will be used to convert to custom- applications +location_of_folder_containing_captures : "/mnt/c/git-new-features/cyperf-api-wrapper/samples/capture_folder" + +#The name of the Base Configuration Template where the application profile needs to be configured . +#This template must not have any pre-configured Application profile. +name_of_existing_cyperf_configuration : "PANW-APPMIX" + +#The path of the csv file +#This file contains input in form of csv [ application name , weights/percentage ]format +csv_path : "/mnt/c/git-new-features/cyperf-api-wrapper/samples/combined_report.csv" + +#weights or percentage provided for the application if percentage is False , then direct weights are provided +percentage: False + +#When set to True the user wishes to have exact name macthes with the CyPerf Libarary +#It is recommended to keep the boolean value as False for maximum coverage +exact_match : False + +#If the coverage percentage is equal to or greater than the non-zero threshold value ; then only the test will start running . +#If the threshold value is set to zero, the script enters interactive mode (menu driven) +threshold_coverage_percentage : 0 + +#dictionary- which contains mapping between app-id and the Cyperf app names- no need to modify this +dictionary_path: "/mnt/c/git-new-features/cyperf-api-wrapper/samples/pan_app_id_to_cyperf_app_mappings.csv" +#dictionary_path: "/mnt/c/new_python_automation_panw/cyperf-api-wrapper/samples/dictionary-of-app-id_bkp.csv" diff --git a/samples/sample_attacks_load_and_run.py b/samples/test_samples/sample_attacks_load_and_run.py similarity index 100% rename from samples/sample_attacks_load_and_run.py rename to samples/test_samples/sample_attacks_load_and_run.py diff --git a/samples/sample_create_save_and_export_config.py b/samples/test_samples/sample_create_save_and_export_config.py similarity index 99% rename from samples/sample_create_save_and_export_config.py rename to samples/test_samples/sample_create_save_and_export_config.py index a0dc980..e36799c 100644 --- a/samples/sample_create_save_and_export_config.py +++ b/samples/test_samples/sample_create_save_and_export_config.py @@ -44,7 +44,7 @@ # Create a session session = None print("Creating empty session...") - api_session_response = api_session_instance.create_sessions(sessions=sessions) + api_session_response = api_session_instance.create_sessions(session=sessions) session = api_session_response[0] print("Session created.\n") @@ -188,3 +188,4 @@ file_name = file_path[last_separator_index + 1:] print(f"Exported as: '{file_name}' at {directory}\n") + diff --git a/samples/sample_load_and_run_precanned_config.py b/samples/test_samples/sample_load_and_run_precanned_config.py similarity index 99% rename from samples/sample_load_and_run_precanned_config.py rename to samples/test_samples/sample_load_and_run_precanned_config.py index 7e238a4..cf71dce 100644 --- a/samples/sample_load_and_run_precanned_config.py +++ b/samples/test_samples/sample_load_and_run_precanned_config.py @@ -44,7 +44,7 @@ # Create a session session = None print("Creating session from config called 'Not Working From Home Traffic Mix' ...") - api_session_response = api_session_instance.create_sessions(sessions=sessions) + api_session_response = api_session_instance.create_sessions(session=sessions) session = api_session_response[0] print("Session created.\n") diff --git a/samples/sample_udp_streaming_run.py b/samples/test_samples/sample_udp_streaming_run.py similarity index 99% rename from samples/sample_udp_streaming_run.py rename to samples/test_samples/sample_udp_streaming_run.py index a36caff..2c2ce64 100644 --- a/samples/sample_udp_streaming_run.py +++ b/samples/test_samples/sample_udp_streaming_run.py @@ -51,6 +51,7 @@ def _set_objective_and_timeline(self): def configure(self): print('Configuring ...') self.utils.add_app(self.session, 'UDP Stream') + #import pdb;pdb.set_trace() self.utils.disable_automatic_network(self.session) if self.agent_map: self.utils.assign_agents(self.session, self.agent_map) diff --git a/samples/utils.py b/samples/utils.py new file mode 100644 index 0000000..3d8991f --- /dev/null +++ b/samples/utils.py @@ -0,0 +1,699 @@ +import os +import socket +import time +import datetime +import warnings +from pprint import pprint +import sys +import cyperf +import json +import pyshark +import asyncio +import math +from urllib.parse import urlparse + +def extract_path(url): + parsed_url = urlparse(url) + return parsed_url.path + +def extract_last_part_of_path(url): + parsed_url = urlparse(url) + return os.path.basename(parsed_url.path) + +def turns_coversations_dict_values(input_dict): + # Use dictionary comprehension to create a new dictionary with the updated values + updated_dict = {key: math.ceil(value / 2) for key, value in input_dict.items()} + return updated_dict + +def udp_identify_client_server(pcap_file): + + + # Create a pyshark Capture object + capture = pyshark.FileCapture(pcap_file, display_filter='udp') + # Initialize an empty dictionary to store the client-server information for each TCP stream + client_server_info = {} + + # Iterate through all packets in the capture + for packet in capture: + # Extract the TCP stream index + stream_index = packet.udp.stream + + # Check if the packet is a SYN packet (i.e., the start of a new TCP connection) + #if packet.tcp.flags_syn == 'True' and packet.tcp.flags_ack == 'False': + + # If the stream index is not already in the dictionary, add it with the client-server information + if stream_index not in client_server_info: + client_server_info[stream_index] = { + 'client_ip': packet.ip.src, + 'server_ip': packet.ip.dst + } + + # Close the capture + capture.close() + + return client_server_info + +def identify_client_server(pcap_file): + # Create a pyshark Capture object + capture = pyshark.FileCapture(pcap_file, display_filter='tcp') + + # Initialize an empty dictionary to store the client-server information for each TCP stream + client_server_info = {} + + # Iterate through all packets in the capture + for packet in capture: + # Extract the TCP stream index + stream_index = packet.tcp.stream + + if ((packet.tcp.flags_syn == '1') and (packet.tcp.flags_ack == '0' )): + + # If the stream index is not already in the dictionary, add it with the client-server information + if stream_index not in client_server_info: + client_server_info[stream_index] = { + 'client_ip': packet.ip.src, + 'server_ip': packet.ip.dst + } + + # Close the capture + capture.close() + return client_server_info + +def udp_count_byte_direction_changes_sync(pcap_file): + + conversation_per_udp_stream = {} + + # Create a pyshark Capture object + capture = pyshark.FileCapture(pcap_file, display_filter='udp') + + udp_packet_count_per_stream = {} + + # Initialize a dictionary to store the byte direction change count for each TCP stream + udp_byte_direction_changes = {} + + # Initialize a dictionary to store the previous packet direction for each TCP stream + previous_packet_direction = {} + + # Initialize an empty dictionary to store the client-server information for each TCP stream + udp_client_server_info = udp_identify_client_server(pcap_file) + + # Iterate through all packets in the capture + for packet in capture: + + # Extract the TCP stream index + stream_index = packet.udp.stream + + # Initialize the byte direction change count for this stream if it doesn't exist + if stream_index not in udp_byte_direction_changes: + udp_byte_direction_changes[stream_index] = 0 + + # Initialize the UDP packet count for this stream if it doesn't exist + if stream_index not in udp_packet_count_per_stream: + udp_packet_count_per_stream[stream_index] = 0 + + # Increment the UDP packet count for this stream + udp_packet_count_per_stream[stream_index] += 1 + + # Check if the packet is in the forward direction (i.e., from client to server) + if packet.ip.src == udp_client_server_info[stream_index]['client_ip']: + current_direction = 'forward' + else: + current_direction = 'reverse' + + # Check if the previous packet direction is different from the current packet direction + if stream_index in previous_packet_direction and previous_packet_direction[stream_index] != current_direction: + udp_byte_direction_changes[stream_index] += 1 + + # Update the previous packet direction for this stream + previous_packet_direction[stream_index] = current_direction + + # Close the capture + capture.close() + + for stream in udp_packet_count_per_stream: + if ( udp_byte_direction_changes[stream] == 0 ): + conversation_per_udp_stream[ stream]= udp_packet_count_per_stream[stream] - (udp_byte_direction_changes[stream]) + if ( udp_byte_direction_changes[stream] == 1 ): + conversation_per_udp_stream[ stream]= udp_packet_count_per_stream[stream] - (udp_byte_direction_changes[stream]) + if ( udp_byte_direction_changes[stream] > 1 ): + conversation_per_udp_stream[ stream]= udp_packet_count_per_stream[stream] - (udp_byte_direction_changes[stream] - 1 ) + return conversation_per_udp_stream + +def count_byte_direction_changes_sync(pcap_file): + # Create a pyshark Capture object + capture = pyshark.FileCapture(pcap_file, display_filter='tcp && tcp.payload != ""') + + # Initialize a dictionary to store the byte direction change count for each TCP stream + byte_direction_changes = {} + + # Initialize a dictionary to store the previous packet direction for each TCP stream + previous_packet_direction = {} + + # Initialize an empty dictionary to store the client-server information for each TCP stream + client_server_info = identify_client_server(pcap_file) + + # Iterate through all packets in the capture + for packet in capture: + + # Extract the TCP stream index + stream_index = packet.tcp.stream + + # Initialize the byte direction change count for this stream if it doesn't exist + if stream_index not in byte_direction_changes: + byte_direction_changes[stream_index] = 0 + + # Check if the packet is in the forward direction (i.e., from client to server) + if packet.ip.src == client_server_info[stream_index]['client_ip']: + current_direction = 'forward' + else: + current_direction = 'reverse' + + # Check if the previous packet direction is different from the current packet direction + if stream_index in previous_packet_direction and previous_packet_direction[stream_index] != current_direction: + byte_direction_changes[stream_index] += 1 + + # Update the previous packet direction for this stream + previous_packet_direction[stream_index] = current_direction + + # Close the capture + capture.close() + + return byte_direction_changes + +async def count_udp_conversations(pcap_file): + # Run the synchronous code in a separate thread + loop = asyncio.get_running_loop() + conversation_count = await loop.run_in_executor(None, udp_count_byte_direction_changes_sync, pcap_file) + return conversation_count + +async def count_tcp_conversations(pcap_file): + # Run the synchronous code in a separate thread + loop = asyncio.get_running_loop() + conversation_count = await loop.run_in_executor(None, count_byte_direction_changes_sync, pcap_file) + return conversation_count + + + +def prepare_payload(json_string,item): + # Example JSON string + #json_string = '{"AppName":"Custom application Mar 14 2025 01:12","Actions":[{"Name":"Action 1","Captures":[{"CaptureId":"68","Flows":[{"AppFlowId":"all","Exchange":[]}]}]}]}' + + # Load the JSON string into a Python dictionary + data = json.loads(json_string) + + new_app_name = "CCA-"+ item[0].rsplit('.', 1)[0] + c_id = item[1] + + # Substitute AppName in the dictionary + data['AppName'] = new_app_name + data['Actions'][0]['Captures'][0]['CaptureId'] = c_id + + # Convert the dictionary back to a JSON string + new_json_string = json.dumps(data) + + return new_json_string + + +def format_warning_cli_issues(message, category, filename, lineno=None, line=None): + return f"{category.__name__}: {message}\n" + + +warnings.formatwarning = format_warning_cli_issues + + +class Utils: + WAP_CLIENT_ID = 'clt-wap' + + def __init__(self, controller, username="", password="", refresh_token="", license_server=None, license_user="", license_password=""): + self.controller = controller + self.host = f'https://{controller}' + self.license_server = license_server + self.license_user = license_user + self.license_password = license_password + + self.configuration = cyperf.Configuration(host=self.host, + refresh_token=refresh_token, + username=username, + password=password) + self.configuration.verify_ssl = False + self.api_client = cyperf.ApiClient(self.configuration) + self.added_license_servers = [] + + self.resource_api = cyperf.ApplicationResourcesApi(self.api_client) + + self.update_license_server() + + self.agents = {} + agents_api = cyperf.AgentsApi(self.api_client) + agents = agents_api.get_agents() + for agent in agents: + self.agents[agent.ip] = agent + + def __del__(self, time=time, datetime=datetime): + if 'time' not in sys.modules or not sys.modules['time']: + sys.modules['time'] = time + self.remove_license_server() + + def update_license_server(self): + if not self.license_server or self.license_server == self.controller: + return + license_api = cyperf.LicenseServersApi(self.api_client) + try: + response = license_api.get_license_servers() + for lServerMetaData in response: + if lServerMetaData.host_name == self.license_server: + if 'ESTABLISHED' == lServerMetaData.connection_status: + pprint(f'License server {self.license_server} is already configured') + return + license_api.delete_license_servers(str(lServerMetaData.id)) + waitTime = 5 # seconds + print(f'Waiting for {waitTime} seconds for the license server deletion to finish.') + time.sleep(5) # How can I avoid this sleep???? + break + + lServer = cyperf.LicenseServerMetadata(host_name=self.license_server, + trust_new=True, + user=self.license_user, + password=self.license_password) + print(f'Configuring new license server {self.license_server}') + newServers = license_api.create_license_servers(license_server_metadata=[lServer]) + while newServers: + for server in newServers: + s = license_api.get_license_servers_by_id( + str(server.id)) + if 'IN_PROGRESS' != s.connection_status: + newServers.remove(server) + self.added_license_servers.append(server) + if 'ESTABLISHED' == s.connection_status: + print(f'Successfully added license server {s.host_name}') + else: + raise Exception(f'Could not connect to license server {s.host_name}') + time.sleep(0.5) + except cyperf.ApiException as e: + raise (e) + + def remove_license_server(self): + license_api = cyperf.LicenseServersApi(self.api_client) + for server in self.added_license_servers: + try: + license_api.delete_license_servers(str(server.id)) + except cyperf.ApiException as e: + pprint(f'{e}') + + def load_configuration_files(self, configuration_files=[]): + config_api = cyperf.ConfigurationsApi(self.api_client) + config_ops = [] + for config_file in configuration_files: + config_ops.append(config_api.start_configs_import(config_file)) + + configs = [] + for op in config_ops: + try: + results = op.await_completion() + configs += [(elem['id'], elem['configUrl']) for elem in results] + except cyperf.ApiException as e: + raise (e) + return configs + + def load_configuration_file(self, configuration_file): + configs = self.load_configuration_files([configuration_file]) + if configs: + return configs[0] + else: + return None + + def remove_configurations(self, configurations_ids=[]): + config_api = cyperf.ConfigurationsApi(self.api_client) + for config_id in configurations_ids: + config_api.delete_configs(config_id) + + def remove_configuration(self, configurations_id): + self.remove_configurations([configurations_id]) + + def create_session_by_config_name(self,configName ): + configsApiInstance = cyperf.ConfigurationsApi(self.api_client) + appMixConfigs = configsApiInstance.get_configs(search_col='displayName', search_val=configName) + if not len(appMixConfigs): + return None + + return self.create_session(appMixConfigs[0].config_url) + + def create_session(self, config_url): + session_api = cyperf.SessionsApi(self.api_client) + session = cyperf.Session() + session.config_url = config_url + sessions = session_api.create_sessions([session]) + if len(sessions): + print( type(sessions)) + print( type(sessions[0])) + return sessions[0] + else: + return None + + def delete_session(self, session): + session_api = cyperf.SessionsApi(self.api_client) + test = session_api.get_session_test(session_id=session.id) + if test.status != 'STOPPED': + self.stop_test(session) + session_api.delete_session(session.id) + + def assign_agents(self, session, agent_map, augment=False): + # Assing agents to the indivual network segments based on the input provided + for net_profile in session.config.config.network_profiles: + for ip_net in net_profile.ip_network_segment: + if ip_net.name in agent_map: + mapped_ips = agent_map[ip_net.name] + agent_details = [cyperf.AgentAssignmentDetails(agent_id=self.agents[agent_ip].id, id = self.agents[agent_ip].id) for agent_ip in mapped_ips if agent_ip in self.agents] # why do we need to pass agent_id and id both???? + if not ip_net.agent_assignments: + ip_net.agent_assignments = cyperf.AgentAssignments(ByID=[], ByTag=[]) # Why is ByTag argument a must???? + + if augment: + ip_net.agent_assignments.by_id.extend(agent_details) + else: + ip_net.agent_assignments.by_id = agent_details + ip_net.update() + + def disable_automatic_network(self, session): + for net_profile in session.config.config.network_profiles: + for ip_net in net_profile.ip_network_segment: + ip_net.ip_ranges[0].ip_auto = False + ip_net.update() + + def add_apps(self, session, appNames): + # Retrieve the app from precanned Apps + resource_api = cyperf.ApplicationResourcesApi(self.api_client) + app_info = [] + for appName in appNames: + apps = resource_api.get_resources_apps(search_col='Name', search_val=appName) + if not len(apps): + print('Couldn\'t find any {appName} app.') + raise Exception(f'Couldn\'t find \'{appName}\' app') + + # Add the app to the App-Mix, which may be empty until now. + app_info.append(cyperf.Application(external_resource_url=apps[0].id, objective_weight=1)) + + + + if not session.config.config.traffic_profiles: + session.config.config.traffic_profiles.append(cyperf.ApplicationProfile(name="Application Profile")) + session.config.config.traffic_profiles.update() + app_profile = session.config.config.traffic_profiles[0] + app_profile.applications += app_info + app_profile.active = True + app_profile.update() + app_profile.applications.update() + + + def get_apps(self, session,): + resource_api = cyperf.ApplicationResourcesApi(self.api_client) + cyperf_apps=[] + try: + api_response = resource_api.get_resources_apps() + print("The response of ApplicationResourcesApi->get_resources_apps:\n") + for index in range(len(api_response)): + cyperf_apps.append(api_response[index].name) + #Process individual AppNames to get more common names + return cyperf_apps + except Exception as e: + print("Exception when calling ApplicationResourcesApi->get_resources_apps: %s\n" % e) + + + + def add_apps_with_weights(self, session, app_dict): + resource_api = cyperf.ApplicationResourcesApi(self.api_client) + app_info = [] + for appName in app_dict.keys(): + apps = resource_api.get_resources_apps(search_col='Name', search_val=appName) + if not len(apps): + print('Couldn\'t find any {appName} app.') + raise Exception(f'Couldn\'t find \'{appName}\' app') + + app_info.append(cyperf.Application(external_resource_url=apps[0].id, objective_weight=7)) + + if not session.config.config.traffic_profiles: + session.config.config.traffic_profiles.append(cyperf.ApplicationProfile(name="Application Profile")) + session.config.config.traffic_profiles.update() + + app_profile = session.config.config.traffic_profiles[0] + + #It is very imprtant to forcefully upadate the application Profile + app_profile.active = True + app_profile.update() + #Now update the applications . The Order of update is very important . You must always update the parent , then you must update the child + #The update in no capacity is recursive as of now . It may be in future - we do not know . + app_profile.applications.extend(app_info) + app_profile.applications.update() + + #Update the weights of the individual application + for appName in app_dict: + + apps = resource_api.get_resources_apps(search_col='Name', search_val=appName) + if not len(apps): + print('Couldn\'t find any {appName} app.') + raise Exception(f'Couldn\'t find \'{appName}\' app') + + for i in range(len(app_profile.applications)): + + if((app_profile.applications[i].name).lower() == appName.lower()): + app_profile.applications[i].objective_weight=app_dict[appName] + app_profile.applications[i].update() + break + + def get_session(self,session): + session_api = cyperf.SessionsApi(self.api_client) + return session_api.get_session_by_id(session.id) + + + def get_apps(self, session,): + resource_api = cyperf.ApplicationResourcesApi(self.api_client) + cyperf_apps=[] + try: + api_response = resource_api.get_resources_apps() + #print("The response of ApplicationResourcesApi->get_resources_apps:\n") + for index in range(len(api_response)): + cyperf_apps.append(api_response[index].name) + + return cyperf_apps + except Exception as e: + print("Exception when calling ApplicationResourcesApi->get_resources_apps: %s\n" % e) + + def add_app(self, session, appName): + self.add_apps(session, [appName]) + + #new function + async def upload_the_capture_file(self, pcap_file): + #validate the pcap first + #turns - means the numbe rof times the direction of bytes changes in a TCP stream . + turns_per_stream= await count_tcp_conversations(pcap_file) + udp_conversations_per_stream= await count_udp_conversations(pcap_file) + #update the value in the dictionary such that turns are replaced by conversations for each tcp stream . The UDP part is taken care . + conversation_per_stream=turns_coversations_dict_values(turns_per_stream) + + #Total Number of TCP coversations / exchanges - Summation across all streams + total_number_of_conversations=sum(value for value in conversation_per_stream.values()) + + #Total Number of UDP coversations / exchanges - Summation across all streams + udp_total_number_of_conversations = sum(value for value in udp_conversations_per_stream.values()) + + if( total_number_of_conversations > 0 ): + print("\nThe number of tcp conversation for the file {} is {}".format((os.path.basename(pcap_file)),total_number_of_conversations)) + if(udp_total_number_of_conversations > 0 ): + print("\nThe number of udp conversation for the file {} is {}".format((os.path.basename(pcap_file)), udp_total_number_of_conversations)) + + if ((total_number_of_conversations + udp_total_number_of_conversations) > 10000): + print(f"The pcap file - {pcap_file} has more than 10000 exchanges/ conversations and this is not suppoted by CyPerf. \n The max number of converstaions supported in 10000.\n This file will be skipped !") + return + #if (udp_total_number_of_conversations > 10000): + #print(f"The pcap file - {pcap_file} has more than 10000 exchanges/ conversations and this is not suppoted by CyPerf. \n The max number of converstaions supported in 10000.\n This file will be skipped !") + #return + print(f"\nStarting upload of capture file-{os.path.basename(pcap_file)} to CyPerf Resouce Library.") + + response =self.resource_api.start_resources_captures_upload_file( pcap_file) + + flag = True + + while flag: + res = self.resource_api.poll_resources_captures_upload_file(upload_file_id=str(response.id),_request_timeout=300) + if res.state == "SUCCESS": + print("File {} is uploaded SUCCESSFULLY!!.\n".format(os.path.basename(pcap_file))) + flag = False + else: + print("Uploading file {} is {}".format(os.path.basename(pcap_file),res.state)) + time.sleep(3) + + def create_apps_from_captures(self): + list_of_captures = self.resource_api.get_resources_captures() + user_uploaded_list_of_captures = [ (x.name,x.id) for x in list_of_captures if x.owner_id!='system'] + for item in user_uploaded_list_of_captures: + #create the json payload + json_string = '{"AppName":"Custom","Actions":[{"Name":"Super-Action","Captures":[{"CaptureId":"00","Flows":[{"AppFlowId":"all","Exchange":[]}]}]}]}' + pp=prepare_payload(json_string,item) + #import pdb;pdb.set_trace() + flag = True + create_app_operation_instance = cyperf.CreateAppOperation.from_json(pp) + response=self.resource_api.start_resources_create_app(create_app_operation_instance) + while flag: + res = self.resource_api.poll_resources_create_app(id=response.id,_request_timeout=300) + if res.state == "SUCCESS": + print("App from {} is created SUCCESSFULLY!!.\n".format(item[0])) + flag = False + else: + print("App from {} - creation is {}".format(item[0],res.state)) + time.sleep(1) + return True + + + + def set_objective_and_timeline(self, session, + objective_type=cyperf.ObjectiveType.SIMULATED_USERS, + objective_unit=cyperf.ObjectiveUnit.EMPTY, + objective_value=100, + test_duration=600): + primary_objective = session.config.config.traffic_profiles[0].objectives_and_timeline.primary_objective + primary_objective.type = objective_type + primary_objective.unit = objective_unit + primary_objective.update() # How will the customer know that update() has to be called twice (separately)???? + + for segment in primary_objective.timeline: # How will the customer know that primary_objective.timeline has to be updated instead of objectives_and_timeline.timeline_segments???? + if segment.enabled and (segment.segment_type == cyperf.SegmentType.STEADYSEGMENT or segment.segment_type == cyperf.SegmentType.NORMALSEGMENT): + segment.duration = test_duration + segment.objective_value = objective_value + segment.objective_unit = objective_unit + primary_objective.update() + #import pdb;pdb.set_trace() + + def start_test(self, session): + test_ops_api = cyperf.TestOperationsApi(self.api_client) + test_start_op = test_ops_api.start_start_traffic(session_id=session.id) + try: + test_start_op.await_completion() + except cyperf.ApiException as e: + raise (e) # The error shown in the GUI is not sent back to the API caller, why???? + + def wait_for_test_stop(self, session, test_monitor=None): + session_api = cyperf.SessionsApi(self.api_client) + monitored_at = None + wait_interval = 0.5 + while 1: + test = session_api.get_test(session_id=session.id) + if 'STOPPED' == test.status: # Why don't we have a enum here???? + break + if test_monitor: + if monitored_at: + monitor_start = monitored_at + 1 + else: + monitor_start = 0 + monitor_upto = monitor_start - 1 # Anything less than monitor_start will mean up to most latest + monitored_at = test_monitor(test, monitor_start, monitor_upto) + time.sleep(wait_interval) + + def stop_test(self, session): + test_ops_api = cyperf.TestOperationsApi(self.api_client) + test_stop_op = test_ops_api.start_stop_traffic(session_id=session.id) + try: + test_stop_op.await_completion() + except cyperf.ApiException as e: + raise (e) + + def collect_stats(self, test, stats_name, time_from, time_to, stats_processor=None): + stats_api = cyperf.StatisticsApi(self.api_client) + stats = stats_api.get_stats(test.test_id) + stats = [stat for stat in stats if stats_name in stat.name] + if time_from: + if time_to > time_from: + stats = [stats_api.get_stats_by_id(test.test_id, stat.name, var_from=time_from, to=time_to) for stat in stats] + else: + stats = [stats_api.get_stats_by_id(test.test_id, stat.name, var_from=time_from) for stat in stats] + else: + stats = [stats_api.get_stats_by_id(test.test_id, stat.name) for stat in stats] + if stats_processor: + stats = stats_processor(stats) + + return stats + + def format_milliseconds(self, milliseconds): + seconds = int(milliseconds / 1000) % 60 + minutes = int(milliseconds / (1000 * 60)) % 60 + hours = int(milliseconds / (1000 * 60 * 60)) % 24 + + return f'{hours:02d}H:{minutes:02d}M:{seconds:02d}S' + + def is_valid_ipv4(ip): + try: + socket.inet_aton(ip) + except Exception: + return False + return True + + def is_valid_ipv6(ip): + try: + socket.inet_pton(socket.AF_INET6, ip) + except Exception: + return False + return True + + def format_stats_dict_as_table(self, stats_dict={}): + if not stats_dict: + return + + stat_names = stats_dict.keys() + col_widths = [max(len(str(val)) + 2 for val in val_list + [stat_name]) for stat_name, val_list in stats_dict.items()] + header = '|'.join([f'{name:^{col_width}}' for name, col_width in zip(stat_names, col_widths)]) + line_delim = '+'.join(['-' * col_width for col_width in col_widths]) + + lines = ['|'.join([f'{val:^{col_width}}' for val, col_width in zip(item, col_widths)]) for item in zip(*stats_dict.values())] + return [line_delim, header, line_delim] + lines + [line_delim] + + def search_configuration_file(self, name): + try: + flag=0 + api_instance = cyperf.ConfigurationsApi(self.api_client) + api_response = api_instance.get_configs() + while(api_response): + dn=api_response.pop().to_dict()['displayName'] + if (dn == name): + print (f"The configuration was found and it will be loaded now ") + flag =1 + return True + if (flag==0): + print (f"The configuration was not found and it will be not be loaded.Provide an existing configuration name ") + return False + except Exception as e: + print("Exception when calling ConfigurationsApi->get_configs: %s\n" % e) + + +def parse_cli_options(extra_options=[]): + import argparse + + parser = argparse.ArgumentParser(description='A simple UDP test') + parser.add_argument('--controller', help='The IP address or the hostname of the CyPerf controller', required=True) + parser.add_argument('--user', help='The username for accessing the controller, needs a password too') + parser.add_argument('--password', help='The password for accessing the controller, needs a username too') + parser.add_argument('--license-server', help='The IP address or the hostname of the license server, default is the controller') + parser.add_argument('--license-user', help='The username for accessing the license server, needed if controller is not the license server') + parser.add_argument('--license-password', help='The password for accessing the license server, needed if controller is not the license server') + for option, help, required in extra_options: + parser.add_argument(option, help=help, required=required) + args = parser.parse_args() + + if not args.license_server or args.license_server == args.controller: + args.license_server = args.controller + args.license_user = None + args.license_password = None + else: + if not args.license_user or not args.license_password: + parser.error('--license-user and --license-password are mandatory if a different --license-server is provided') + + if args.user and args.password: + offline_token = None + else: + if args.user or args.password: + warnings.warn('Only one of --user and --password is provided, looking for offline token') + + try: + offline_token = os.environ['CYPERF_OFFLINE_TOKEN'] + except KeyError as e: + parser.error(f'Couldn\'t find environment variable {e}') + + return args, offline_token + + \ No newline at end of file