From 24ba6fac9d3ce62863c30b6a49bd1108e2f0eb01 Mon Sep 17 00:00:00 2001 From: shadeofblue Date: Thu, 22 Aug 2024 17:37:55 +0200 Subject: [PATCH 01/31] fix python 3.9 tests --- .github/workflows/tests_unit.yml | 11 +- poetry.lock | 177 ++++++++++++++++--------------- pyproject.toml | 2 +- 3 files changed, 97 insertions(+), 93 deletions(-) diff --git a/.github/workflows/tests_unit.yml b/.github/workflows/tests_unit.yml index f182051..9820965 100644 --- a/.github/workflows/tests_unit.yml +++ b/.github/workflows/tests_unit.yml @@ -23,11 +23,14 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install poetry - uses: Gr1N/setup-poetry@v9 + - name: create the virtualenv + run: python -m venv ./.venv + + - name: install poetry + run: .venv/bin/pip install poetry - name: Install dependencies - run: poetry install --no-ansi + run: .venv/bin/poetry install --no-ansi - name: Run tests - run: poetry run poe tests_unit + run: .venv/bin/poe tests_unit diff --git a/poetry.lock b/poetry.lock index 9ca3cb3..4810471 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,13 +2,13 @@ [[package]] name = "astroid" -version = "3.2.2" +version = "3.2.4" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.8.0" files = [ - {file = "astroid-3.2.2-py3-none-any.whl", hash = "sha256:e8a0083b4bb28fcffb6207a3bfc9e5d0a68be951dd7e336d5dcf639c682388c0"}, - {file = "astroid-3.2.2.tar.gz", hash = "sha256:8ead48e31b92b2e217b6c9733a21afafe479d52d6e164dd25fb1a770c7c3cf94"}, + {file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"}, + {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, ] [package.dependencies] @@ -16,52 +16,52 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "attrs" -version = "23.2.0" +version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "black" -version = "24.4.2" +version = "24.8.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, - {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, - {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, - {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, - {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, - {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, - {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, - {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, - {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, - {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, - {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, - {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, - {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, - {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, - {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, - {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, - {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, - {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, - {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, - {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, - {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, - {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, ] [package.dependencies] @@ -81,13 +81,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -231,13 +231,13 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -459,44 +459,44 @@ files = [ [[package]] name = "mypy" -version = "1.10.1" +version = "1.11.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, - {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, - {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, - {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, - {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, - {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, - {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, - {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, - {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, - {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, - {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, - {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, - {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, - {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, - {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, - {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, - {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, - {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, - {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, - {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, - {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, + {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, + {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, + {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, + {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, + {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, + {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, + {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, + {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, + {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, + {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, + {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, + {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, + {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, + {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, + {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, + {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, + {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.1.0" +typing-extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] @@ -560,13 +560,13 @@ files = [ [[package]] name = "peewee-migrate" -version = "1.12.2" +version = "1.13.0" description = "Support for migrations in Peewee ORM" optional = false -python-versions = ">=3.8,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "peewee_migrate-1.12.2-py3-none-any.whl", hash = "sha256:2930bf83ef802cdb5fb123116c5eb87cbf3756cb27674f674923be6bb27dabee"}, - {file = "peewee_migrate-1.12.2.tar.gz", hash = "sha256:c8187c97b756909ea57e77cce06ae66395219e86764ef0b286a7bc72ff7405ad"}, + {file = "peewee_migrate-1.13.0-py3-none-any.whl", hash = "sha256:66597f5b8549a8ff456915db60e8382daf7839eef79352027e7cf54feec56860"}, + {file = "peewee_migrate-1.13.0.tar.gz", hash = "sha256:1ab67f72a0936006155e1b310c18a32f79e4dff3917cfeb10112ca92518721e5"}, ] [package.dependencies] @@ -624,17 +624,17 @@ poetry-plugin = ["poetry (>=1.0,<2.0)"] [[package]] name = "pylint" -version = "3.2.5" +version = "3.2.6" description = "python code static checker" optional = false python-versions = ">=3.8.0" files = [ - {file = "pylint-3.2.5-py3-none-any.whl", hash = "sha256:32cd6c042b5004b8e857d727708720c54a676d1e22917cf1a2df9b4d4868abd6"}, - {file = "pylint-3.2.5.tar.gz", hash = "sha256:e9b7171e242dcc6ebd0aaa7540481d1a72860748a0a7816b8fe6cf6c80a6fe7e"}, + {file = "pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f"}, + {file = "pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"}, ] [package.dependencies] -astroid = ">=3.2.2,<=3.3.0-dev0" +astroid = ">=3.2.4,<=3.3.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -646,6 +646,7 @@ mccabe = ">=0.6,<0.8" platformdirs = ">=2.2.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] @@ -653,13 +654,13 @@ testutils = ["gitpython (>3)"] [[package]] name = "pytest" -version = "8.2.2" +version = "8.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, ] [package.dependencies] @@ -667,7 +668,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.5,<2.0" +pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] @@ -761,13 +762,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.5" +version = "0.13.2" description = "Style preserving TOML library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, - {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] [[package]] @@ -828,5 +829,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.10" -content-hash = "1f384658dbc65f6e964526a839137b9bfbab63bad09ca1540c6c1b21f001d9b6" +python-versions = "^3.9" +content-hash = "c5e8ae97155539b024e504ca359b2158f089411b5e064a3c8d7f777620dd950a" diff --git a/pyproject.toml b/pyproject.toml index a5703ac..650429c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.dependencies] -python = "^3.10" +python = "^3.9" httptools = ">=0.6.1" python-dotenv = ">=1.0.1" requests = ">=2.31.0" From 0bb88b1ea7f28c21531c1d85641f45c60c66fda9 Mon Sep 17 00:00:00 2001 From: lukasz-glen <> Date: Fri, 23 Aug 2024 11:19:16 +0200 Subject: [PATCH 02/31] feat: admin panel optional params --- admin/admin/admin.html | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/admin/admin/admin.html b/admin/admin/admin.html index 5f1ff24..f5b51e6 100644 --- a/admin/admin/admin.html +++ b/admin/admin/admin.html @@ -177,14 +177,29 @@ Free Bytes - - + + + From ac8ac2ba1feec9d3adcc726773d5be336c73cb5a Mon Sep 17 00:00:00 2001 From: lukasz-glen <> Date: Wed, 28 Aug 2024 09:22:56 +0200 Subject: [PATCH 03/31] feat: nodes sync test --- admin/admin/admin.html | 9 +- .../endpoint_pool/endpoint_connection_pool.py | 21 ++- .../rpc/node/endpoint_pool/pool_manager.py | 127 +++++++++++++++++- 3 files changed, 144 insertions(+), 13 deletions(-) diff --git a/admin/admin/admin.html b/admin/admin/admin.html index 5f1ff24..87df4da 100644 --- a/admin/admin/admin.html +++ b/admin/admin/admin.html @@ -574,10 +574,11 @@ #endpointHTML(name, endpoint, viewIds) { const deltaMillis = Helpers.millisSincePythonTimestamp(endpoint.started_at_timestamp); - let cardColor = "bg-c-blue"; - - if (name.toLowerCase().includes("infura")) { - cardColor = "bg-c-pink"; + let cardColor = "bg-c-pink"; + if (endpoint.status == "ACTIVE") { + cardColor = "bg-c-blue"; + } else if (endpoint.status == "OUT_OF_SYNC") { + cardColor = "bg-c-yellow"; } return `
diff --git a/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py b/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py index e4491f9..24f6e46 100644 --- a/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py +++ b/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py @@ -124,6 +124,7 @@ def __str__(self): class PoolStatus(str, enum.Enum): ACTIVE = "ACTIVE" DISABLED = "DISABLED" + OUT_OF_SYNC = "OUT_OF_SYNC" CLOSING = "CLOSING" CLOSED = "CLOSED" @@ -171,9 +172,12 @@ def __update_status(self, status: str): self.status = status self.__logger.debug("Changed %s status to %s", str(self), status) - def get(self) -> EndpointConnectionHandler: + def get(self, out_of_sync: bool = False) -> EndpointConnectionHandler: self.__lock.acquire() - if not self.is_active(): + if not out_of_sync and not self.is_active(): + self.__lock.release() + raise Exception("the pool is disabled") # TODO better exception + if out_of_sync and not self.is_out_of_sync() and not self.is_active(): self.__lock.release() raise Exception("the pool is disabled") # TODO better exception if self.connections.empty(): @@ -217,6 +221,9 @@ def put(self, connection: EndpointConnection) -> None: def is_active(self): return self.status == self.PoolStatus.ACTIVE + def is_out_of_sync(self): + return self.status == self.PoolStatus.OUT_OF_SYNC + def disable(self): with self.__lock: if self.status == self.PoolStatus.CLOSED or self.status == self.PoolStatus.CLOSING: @@ -227,6 +234,16 @@ def disable(self): self.stats = PoolStats() self.__logger.info("Pool has been disabled") + def out_of_sync(self): + with self.__lock: + if self.status == self.PoolStatus.CLOSED or self.status == self.PoolStatus.CLOSING: + raise Exception("Tried to set out of sync after close") # TODO better exception + self.__update_status(self.PoolStatus.OUT_OF_SYNC) + while not self.connections.empty(): + self.connection_close_queue.put(self.__get_connection()) + self.stats = PoolStats() + self.__logger.info("Pool has been set out of sync") + def activate(self): with self.__lock: if self.status == self.PoolStatus.CLOSED or self.status == self.PoolStatus.CLOSING: diff --git a/web3pi_proxy/core/rpc/node/endpoint_pool/pool_manager.py b/web3pi_proxy/core/rpc/node/endpoint_pool/pool_manager.py index a5914cc..c06b9b7 100644 --- a/web3pi_proxy/core/rpc/node/endpoint_pool/pool_manager.py +++ b/web3pi_proxy/core/rpc/node/endpoint_pool/pool_manager.py @@ -1,8 +1,10 @@ from __future__ import annotations +import json import time from threading import RLock, Thread from typing import List, Tuple +from httptools import HttpResponseParser from web3pi_proxy.core.rpc.node.endpoint_pool.endpoint_connection_pool import ( EndpointConnectionPool, @@ -64,10 +66,6 @@ def __is_broken(self, pool: EndpointConnectionPool) -> bool: and failure_rate >= self.__FAILURE_RATE_THRESHOLD ) - def __is_outdated(self, _pool: EndpointConnectionPool) -> bool: - # TODO: Implement - return False - def __suspend_pool(self, pool) -> None: if not pool.is_active(): return @@ -92,14 +90,114 @@ def check_connections(self, pools: List[EndpointConnectionPool]): daemon=True, ).start() - if self.__is_outdated(pool): - self.__logger.warning(f"Endpoint `{pool.endpoint.name}`: falling behind") - self.__logger.debug(f"Endpoint `{pool.endpoint.name}`: okay.") +class HttpResponseParserListenerSyncController: + body: bytes = b'' + block_number: int = 0 + block_timestamp: int = 0 + __logger = get_logger("SyncController") + + def __init__(self, __pool_name: str): + super().__init__() + self.__pool_name = __pool_name + + def on_body(self, body: bytes): + self.body = self.body + body + + def on_message_complete(self): + try: + block_data = json.loads(self.body) + result = block_data.get('result') + if not result: + error = block_data.get('error') + if error: + self.__logger.error(f"{self.__pool_name}: Sync test failed: {error}") + else: + self.__logger.error(f"{self.__pool_name}: Sync test failed: reason unknown") + return + self.block_number = int(result['number'], 16) + self.block_timestamp = int(result['timestamp'], 16) + except Exception as error: + self.__logger.error("%s: %s", error.__class__, error) + self.__logger.error(f"{self.__pool_name}: Failed to failed to parse sync test response") + + +# TODO: Tune parameters +class SyncController: + __SUSPENSION_TIMEOUT_SECONDS = 60 + __MAX_DELAY_SECONDS = 60 + + __logger = get_logger("SyncController") + + def __test_pool(self, pool: EndpointConnectionPool) -> bool | None: + endpoint_connection_handler: EndpointConnectionHandler | None = None + try: + endpoint_connection_handler = pool.get(out_of_sync=True) + req = RPCRequest() + req.headers = b'User-Agent: curl/8.0.1\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: 82\r\n' + req.content = b'{"method":"eth_getBlockByNumber","params":["latest",false],"id":1,"jsonrpc":"2.0"}' + endpoint_connection_handler.send(req) + + response_listener = HttpResponseParserListenerSyncController(pool.endpoint.name) + response_parser = HttpResponseParser(response_listener) + + def response_handler(res: bytes): + nonlocal response_parser + response_parser.feed_data(res) + + endpoint_connection_handler.receive(response_handler) + + current_timestamp = int(time.time()) + block_timestamp = response_listener.block_timestamp + if block_timestamp == 0: + return None # no logs, failure already logged by the listener + if block_timestamp + self.__MAX_DELAY_SECONDS > current_timestamp: + return True + else: + self.__logger.warning(f"{pool.endpoint.name}: The node out of sync") + return False + except Exception as error: + self.__logger.error("%s: %s", error.__class__, error) + self.__logger.error(f"{pool.endpoint.name}: Failed to run sync test") + return None + finally: + if endpoint_connection_handler: + endpoint_connection_handler.release() # safe: does not throw an exception + + def __suspend_pool(self, pool) -> None: + if not pool.is_active(): + return + pool.out_of_sync() + while True: + time.sleep(self.__SUSPENSION_TIMEOUT_SECONDS) + test_result = self.__test_pool(pool) + if test_result is not None and test_result: + pool.activate() + break + else: + continue + + def check_pools(self, pools: List[EndpointConnectionPool]): + for pool in pools: + test_result = self.__test_pool(pool) + if test_result is not None and not test_result: + self.__logger.warning( + f"Endpoint `{pool.endpoint.name}`: sync test: out of sync." + ) + Thread( + target=self.__suspend_pool, + args=[pool], + daemon=True, + ).start() + + self.__logger.debug(f"Endpoint `{pool.endpoint.name}`: sync test okay.") + + class EndpointConnectionPoolManager: __DAMAGE_CONTROLLER_TIMEOUT_SECONDS = 10 # 600 + __SYNC_CONTROLLER_TIMEOUT_SECONDS = 60 __logger = get_logger(f"EndpointConnectionPoolManager") def __init__( @@ -109,6 +207,7 @@ def __init__( ): self.load_balancer = load_balancer self.damage_controller = DamageController() + self.sync_controller = SyncController() self.pools: dict[str, EndpointConnectionPool] = {} self.__lock = RLock() @@ -123,6 +222,12 @@ def __init__( ) self.damage_controller_thread.start() + self.sync_controller_thread = Thread( + target=self.__sync_control, + daemon=True, + ) + self.sync_controller_thread.start() + @property def endpoints(self) -> List[RPCEndpoint]: with self.__lock: @@ -143,6 +248,14 @@ def __damage_control(self): self.damage_controller.check_connections(active_pools) time.sleep(self.__DAMAGE_CONTROLLER_TIMEOUT_SECONDS) + def __sync_control(self): + while True: + self.__logger.debug("Running sync check on endpoint connections") + with self.__lock: + active_pools = self.__get_active_pools() + self.sync_controller.check_pools(active_pools) + time.sleep(self.__SYNC_CONTROLLER_TIMEOUT_SECONDS) + def add_pool( self, name: str, conn_descr: EndpointConnectionDescriptor ) -> RPCEndpoint: From ec84f7117d3165b25d1e7754402e1285c29bed67 Mon Sep 17 00:00:00 2001 From: lukasz-glen <> Date: Fri, 30 Aug 2024 15:41:04 +0200 Subject: [PATCH 04/31] feat: admin panel colors --- admin/admin/admin.html | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/admin/admin/admin.html b/admin/admin/admin.html index f5b51e6..35dc528 100644 --- a/admin/admin/admin.html +++ b/admin/admin/admin.html @@ -144,27 +144,21 @@

- +
-
- +
From 8b0e427f6a584ef62f2a502231b8debcd515abd9 Mon Sep 17 00:00:00 2001 From: shadeofblue Date: Mon, 2 Sep 2024 16:40:50 +0200 Subject: [PATCH 05/31] fix typing issues on python 3.9 --- web3pi_proxy/core/interfaces/rpcrequest.py | 5 +++-- .../connection/endpoint_connection_handler.py | 17 ++++++++--------- web3pi_proxy/core/rpc/request/rpcrequest.py | 19 ++++++++++--------- web3pi_proxy/core/upnp/ipgetter.py | 3 ++- web3pi_proxy/core/utilhttp/errors.py | 11 ++++++----- web3pi_proxy/db/models/billing.py | 2 +- web3pi_proxy/interfaces/billing.py | 4 ++-- web3pi_proxy/interfaces/permissions.py | 4 ++-- web3pi_proxy/service/admin/serviceadmin.py | 6 +++--- .../service/billing/billingservice.py | 10 +++++++--- .../service/endpoints/endpoint_manager.py | 10 +++++----- web3pi_proxy/service/http/adminserver.py | 4 ++-- web3pi_proxy/state/wrappers.py | 4 ++-- 13 files changed, 53 insertions(+), 46 deletions(-) diff --git a/web3pi_proxy/core/interfaces/rpcrequest.py b/web3pi_proxy/core/interfaces/rpcrequest.py index e52665d..984bc6b 100644 --- a/web3pi_proxy/core/interfaces/rpcrequest.py +++ b/web3pi_proxy/core/interfaces/rpcrequest.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import Optional, Union from web3pi_proxy.core.rpc.request.rpcrequest import RPCRequest from web3pi_proxy.core.rpc.response.rpcresponse import RPCResponse @@ -9,10 +10,10 @@ class RequestReaderMiddleware(ABC): MAX_LINE_LEN = 65536 MAX_NUM_HEADERS = 100 - ReturnType = [RPCRequest | None, RPCResponse | None] + ReturnType = [Optional[RPCRequest], Optional[RPCResponse]] @staticmethod - def failure(err_res: RPCResponse | bytes, req: RPCRequest) -> ReturnType: + def failure(err_res: Union[RPCResponse, bytes], req: RPCRequest) -> ReturnType: if isinstance(err_res, bytes): err_res = RPCResponse(bytearray(err_res), req) diff --git a/web3pi_proxy/core/rpc/node/rpcendpoint/connection/endpoint_connection_handler.py b/web3pi_proxy/core/rpc/node/rpcendpoint/connection/endpoint_connection_handler.py index 9e0db2e..61f93ed 100644 --- a/web3pi_proxy/core/rpc/node/rpcendpoint/connection/endpoint_connection_handler.py +++ b/web3pi_proxy/core/rpc/node/rpcendpoint/connection/endpoint_connection_handler.py @@ -29,6 +29,14 @@ class BrokenFreshConnectionError(BrokenConnectionError): class ReconnectError(BrokenConnectionError): message = "Error while attempting reconnect" +def _acquired_connection(func: Callable) -> Callable: + def decorator(instance: "EndpointConnectionHandler", *args, **kwargs): + if instance.connection is None: + raise ConnectionReleasedError + return func(instance, *args, **kwargs) + + return decorator + class EndpointConnectionHandler(ConnectionHandler): def __init__( @@ -45,15 +53,6 @@ def __init__( self.__logger = get_logger(f"EndpointConnectionHandler.{id(self)}") self.__logger.debug(f"Created handler for connection {connection}") - @staticmethod - def _acquired_connection(func: Callable) -> Callable: - def decorator(instance: "EndpointConnectionHandler", *args, **kwargs): - if instance.connection is None: - raise ConnectionReleasedError - return func(instance, *args, **kwargs) - - return decorator - @_acquired_connection def send(self, req: RPCRequest) -> bytearray: try: diff --git a/web3pi_proxy/core/rpc/request/rpcrequest.py b/web3pi_proxy/core/rpc/request/rpcrequest.py index ff4db22..58dc890 100644 --- a/web3pi_proxy/core/rpc/request/rpcrequest.py +++ b/web3pi_proxy/core/rpc/request/rpcrequest.py @@ -1,21 +1,22 @@ from dataclasses import dataclass +from typing import Optional, Union @dataclass class RPCRequest: - user_api_key: str | None = None - url_path: bytearray | None = None - headers: bytearray | None = None + user_api_key: Optional[str] = None + url_path: Optional[bytearray] = None + headers: Optional[bytearray] = None content_len: int = -1 - content: bytearray | None = None + content: Optional[bytearray] = None method: str = "" - id: int | str | None = None + id: Optional[Union[int, str]] = None priority: int = 0 - constant_pool: str | None = None - last_queried_bytes: bytearray | None = None + constant_pool: Optional[str] = None + last_queried_bytes: Optional[bytearray] = None keep_alive: bool = True - http_method: bytearray | None = None - cors_origin: bytes | None = None + http_method: Optional[bytearray] = None + cors_origin: Optional[bytes] = None def as_bytearray( self, request_line_1: bytearray, url_context: bytearray, request_line_2: bytearray, host_header: bytearray diff --git a/web3pi_proxy/core/upnp/ipgetter.py b/web3pi_proxy/core/upnp/ipgetter.py index f802dae..1b571b3 100644 --- a/web3pi_proxy/core/upnp/ipgetter.py +++ b/web3pi_proxy/core/upnp/ipgetter.py @@ -1,8 +1,9 @@ from functools import lru_cache +from typing import Optional import whatismyip @lru_cache(maxsize=None) -def my_public_ip() -> str | None: +def my_public_ip() -> Optional[str]: return whatismyip.whatismyip() diff --git a/web3pi_proxy/core/utilhttp/errors.py b/web3pi_proxy/core/utilhttp/errors.py index 885c9a3..458677f 100644 --- a/web3pi_proxy/core/utilhttp/errors.py +++ b/web3pi_proxy/core/utilhttp/errors.py @@ -1,3 +1,4 @@ +from typing import Union, Optional from email.utils import formatdate @@ -65,12 +66,12 @@ def current_datetime(cls) -> str: return formatdate(timeval=None, localtime=False, usegmt=True) @classmethod - def web3_json(cls, code: int, message: str, _id: int | str) -> str: + def web3_json(cls, code: int, message: str, _id: Union[int, str]) -> str: return cls.WEB3_JSON_TEMPLATE.format(_id, code, message) @classmethod def bad_request_web3( - cls, code_web3: int, message: str, _id: int | str = None + cls, code_web3: int, message: str, _id: Optional[Union[int, str]] = None ) -> bytes: _err_msg = "OK" _now = cls.current_datetime() @@ -82,18 +83,18 @@ def bad_request_web3( return cls.to_bytes(err_msg) @classmethod - def forbidden_payment_required(cls, _id: int | str = None) -> bytes: + def forbidden_payment_required(cls, _id: Optional[Union[int, str]] = None) -> bytes: web3_err = -(cls.PROXY_ERROR_BASE_CODE + cls.PE_PAYMENT_REQUIRED) return cls.bad_request_web3( web3_err, "You exceeded the limit of calls. Check your plan", _id ) @classmethod - def parse_error(cls, _id: int | str = None) -> bytes: + def parse_error(cls, _id: Optional[Union[int, str]] = None) -> bytes: return cls.bad_request_web3(-32700, "Invalid JSON format", _id) @classmethod - def connection_error(cls, _id: int | str = None) -> bytes: + def connection_error(cls, _id: Optional[Union[int, str]] = None) -> bytes: return cls.bad_request_web3(-32603, "Could not reach server", _id) @classmethod diff --git a/web3pi_proxy/db/models/billing.py b/web3pi_proxy/db/models/billing.py index 7cbdfc8..677c258 100644 --- a/web3pi_proxy/db/models/billing.py +++ b/web3pi_proxy/db/models/billing.py @@ -13,7 +13,7 @@ class BillingPlan(BaseModel): glm_call_price: float = FloatField(default=0.0) glm_byte_price: float = FloatField(default=0.0) user_priority: int = IntegerField(default=0) - constant_pool: str | None = CharField(null=True) + constant_pool: Optional[str] = CharField(null=True) class Meta: indexes = ( diff --git a/web3pi_proxy/interfaces/billing.py b/web3pi_proxy/interfaces/billing.py index 84daf29..92f03cf 100644 --- a/web3pi_proxy/interfaces/billing.py +++ b/web3pi_proxy/interfaces/billing.py @@ -1,4 +1,4 @@ -from typing import Protocol +from typing import Optional, Protocol class BillingPlanProtocol(Protocol): @@ -7,4 +7,4 @@ class BillingPlanProtocol(Protocol): glm_call_price: float glm_byte_price: float user_priority: int - constant_pool: str | None + constant_pool: Optional[str] diff --git a/web3pi_proxy/interfaces/permissions.py b/web3pi_proxy/interfaces/permissions.py index 9677e84..7a272b9 100644 --- a/web3pi_proxy/interfaces/permissions.py +++ b/web3pi_proxy/interfaces/permissions.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod - +from typing import Optional class ClientPermissions(ABC): @@ -18,5 +18,5 @@ def is_allowed(self, user_api_key: str, method: str) -> bool: def get_call_priority(self, user_api_key: str, method: str) -> int: pass - def get_user_constant_pool(self, user_api_key: str) -> str | None: + def get_user_constant_pool(self, user_api_key: str) -> Optional[str]: pass diff --git a/web3pi_proxy/service/admin/serviceadmin.py b/web3pi_proxy/service/admin/serviceadmin.py index a14fd9b..0a8b4d8 100644 --- a/web3pi_proxy/service/admin/serviceadmin.py +++ b/web3pi_proxy/service/admin/serviceadmin.py @@ -17,7 +17,7 @@ class RPCServiceAdmin: CONSOLE_CONTENTS = "console_contents" USERS_API_KEYS = "users_api_keys" - ReturnType = Dict[str, Any] | None + ReturnType = Optional[Dict[str, Any]] billing_service: BasicBillingService activity_ledger: SimpleActivityLedger @@ -135,7 +135,7 @@ def register_user( return self.query_list_registered_users() def register_user_flat( - self, user_api_key: str, free_calls: int, free_bytes: int, priority: int, constant_pool: str | None + self, user_api_key: str, free_calls: int, free_bytes: int, priority: int, constant_pool: Optional[str] ) -> ReturnType: if constant_pool == "": constant_pool = None @@ -159,7 +159,7 @@ def update_user_plan( return self.query_user_plan(user_api_key) def update_user_plan_flat( - self, user_api_key: str, free_calls: int, free_bytes: int, priority: int, constant_pool: str | None + self, user_api_key: str, free_calls: int, free_bytes: int, priority: int, constant_pool: Optional[str] ) -> ReturnType: if constant_pool == "": constant_pool = None diff --git a/web3pi_proxy/service/billing/billingservice.py b/web3pi_proxy/service/billing/billingservice.py index 7fde48e..85c4371 100644 --- a/web3pi_proxy/service/billing/billingservice.py +++ b/web3pi_proxy/service/billing/billingservice.py @@ -1,4 +1,4 @@ -from typing import Dict, Generic, List, Type, TypeVar +from typing import Dict, Generic, List, Optional, Type, TypeVar, Union from web3pi_proxy.interfaces.billing import BillingPlanProtocol @@ -64,13 +64,17 @@ def get_call_priority(self, user_api_key: str, method: str) -> int: return self.user_plans[user_api_key].user_priority - def get_user_constant_pool(self, user_api_key: str) -> str | None: + def get_user_constant_pool(self, user_api_key: str) -> Optional[str]: assert self.is_registered(user_api_key) return self.user_plans[user_api_key].constant_pool def create_plan( - self, free_calls: int | str, free_bytes: int | str, priority: int | str, constant_pool: str | None + self, + free_calls: Union[int, str], + free_bytes: Union[int, str], + priority: Union[int, str], + constant_pool: Optional[str], ) -> BPP: # FIXME: naive type handling return self.billing_plan_type( diff --git a/web3pi_proxy/service/endpoints/endpoint_manager.py b/web3pi_proxy/service/endpoints/endpoint_manager.py index e5c967e..e1b2907 100644 --- a/web3pi_proxy/service/endpoints/endpoint_manager.py +++ b/web3pi_proxy/service/endpoints/endpoint_manager.py @@ -1,5 +1,5 @@ import json -from pathlib import Path +from typing import Optional, Union from dotenv import dotenv_values, find_dotenv, set_key @@ -25,7 +25,7 @@ def __write_conf_to_file(self, config: dict) -> None: for key, value in config.items(): set_key(dotenv_path=env_file, key_to_set=key, value_to_set=value) - def __save_endpoint_conf(self, name: str, url: str | None): + def __save_endpoint_conf(self, name: str, url: Optional[str]): env = dotenv_values(".env") endpoints_config = json.loads(env["ETH_ENDPOINTS"]) @@ -57,7 +57,7 @@ def get_endpoints(self) -> dict: nodes_data[endpoint.get_name()] = endpoint_entry return nodes_data - def add_endpoint(self, name: str, url: str) -> RPCEndpoint | dict: + def add_endpoint(self, name: str, url: str) -> Union[RPCEndpoint, dict]: descriptor = EndpointConnectionDescriptor.from_url(url) try: endpoint = self.endpoint_pool_manager.add_pool(name, descriptor) @@ -66,7 +66,7 @@ def add_endpoint(self, name: str, url: str) -> RPCEndpoint | dict: self.__save_endpoint_conf(name, url) return endpoint - def remove_endpoint(self, name: str) -> RPCEndpoint | dict: + def remove_endpoint(self, name: str) -> Union[RPCEndpoint, dict]: try: endpoint = self.endpoint_pool_manager.remove_pool(name) except PoolDoesNotExistError as error: @@ -74,7 +74,7 @@ def remove_endpoint(self, name: str) -> RPCEndpoint | dict: self.__save_endpoint_conf(name, None) return endpoint - def update_endpoint(self, name: str, url: str) -> RPCEndpoint | dict: + def update_endpoint(self, name: str, url: str) -> Union[RPCEndpoint, dict]: descriptor = EndpointConnectionDescriptor.from_url(url) try: self.endpoint_pool_manager.remove_pool(name) diff --git a/web3pi_proxy/service/http/adminserver.py b/web3pi_proxy/service/http/adminserver.py index 732714a..0fa042c 100644 --- a/web3pi_proxy/service/http/adminserver.py +++ b/web3pi_proxy/service/http/adminserver.py @@ -6,8 +6,8 @@ import socketserver import string import threading -import traceback from http.server import BaseHTTPRequestHandler +from typing import Optional, Union from web3pi_proxy.config.conf import Config from web3pi_proxy.service.admin.serviceadmin import RPCServiceAdmin @@ -108,7 +108,7 @@ def do_GET(self): self.wfile.write(response.encode("UTF-8")) - def log_request(self, code: int | str = ..., size: int | str = ...) -> None: + def log_request(self, code: Union[int, str] = ..., size: Union[int, str] = ...) -> None: pass def do_POST(self): diff --git a/web3pi_proxy/state/wrappers.py b/web3pi_proxy/state/wrappers.py index 69e67b4..c53445c 100644 --- a/web3pi_proxy/state/wrappers.py +++ b/web3pi_proxy/state/wrappers.py @@ -1,4 +1,4 @@ -from typing import Callable +from typing import Callable, Optional from web3pi_proxy.interfaces.permissions import CallPermissions, ClientPermissions from web3pi_proxy.service.billing.billingservice import BasicBillingService @@ -30,7 +30,7 @@ def is_allowed(self, user_api_key: str, method: str) -> bool: def get_call_priority(self, user_api_key: str, method: str) -> int: return self.billing_service.get_call_priority(user_api_key, method) - def get_user_constant_pool(self, user_api_key: str) -> str | None: + def get_user_constant_pool(self, user_api_key: str) -> Optional[str]: return self.billing_service.get_user_constant_pool(user_api_key) From 03b0552befeb1657700b65798e4c8a50e31253da Mon Sep 17 00:00:00 2001 From: lukasz-glen <> Date: Wed, 4 Sep 2024 22:16:25 +0200 Subject: [PATCH 06/31] v0.4 --- README.md | 6 +++--- pyproject.toml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7ef8c35..27357c6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Web3Pi Reverse Proxy +# RPC Reverse Proxy A reverse proxy for Geth intended for use within Web3Pi ecosystem. -Web3Pi Reverse Proxy comes out-of-the-box with several features: +RPC Reverse Proxy comes out-of-the-box with several features: - Multiple geth nodes - you can hide multiple Geth nodes under single instance of reverse proxy - JSON-RPC parser - our custom parser validates JSON-RPC requests before they reach the nodes @@ -20,7 +20,7 @@ Simply install `web3pi-proxy` package using your Python package manager, using * pip install web3pi-proxy ``` -Web3Pi Reverse Proxy expects you to provide **ETH_ENDPOINTS** environment variable to your system. +RPC Reverse Proxy expects you to provide **ETH_ENDPOINTS** environment variable to your system. It should be a list of endpoint descriptors for JSON-RPC over HTTP communication with Geth. diff --git a/pyproject.toml b/pyproject.toml index 650429c..6784e3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [tool.poetry] name = "web3pi-proxy" -version = "0.3" -description = "A Web3 Pi Node Manager - proxy to Web3 Pi Ethereum nodes" +version = "0.4" +description = "RPC Reverse Proxy - proxy to Web3 Pi Ethereum nodes" authors = [] license = "GNU GENERAL PUBLIC LICENSE Version 3" keywords = ["web3pi", "web3", "rpi", "raspberry", "geth", "ethereum", "proxy"] classifiers = [ "Development Status :: 3 - Alpha", - "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.9", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: POSIX :: Linux", ] From 06bb87cff12d24fee55f724282d2e1fcd73b9620 Mon Sep 17 00:00:00 2001 From: lukasz-glen <> Date: Wed, 11 Sep 2024 21:50:00 +0200 Subject: [PATCH 07/31] feat: endpoints management in admin panel --- admin/admin/admin.html | 161 +++++++++++++++--- web3pi_proxy/config/conf.py | 6 +- web3pi_proxy/db/migrations/004_auto.py | 55 ++++++ web3pi_proxy/db/models/__init__.py | 2 + web3pi_proxy/db/models/endpoint.py | 14 ++ web3pi_proxy/service/admin/serviceadmin.py | 6 +- .../service/endpoints/endpoint_manager.py | 29 +++- .../service/providers/serviceprovider.py | 11 +- 8 files changed, 251 insertions(+), 33 deletions(-) create mode 100644 web3pi_proxy/db/migrations/004_auto.py create mode 100644 web3pi_proxy/db/models/endpoint.py diff --git a/admin/admin/admin.html b/admin/admin/admin.html index 35dc528..bdff607 100644 --- a/admin/admin/admin.html +++ b/admin/admin/admin.html @@ -201,34 +201,40 @@
-
+
Crude dashboard
-
- - -
-
Processed proxy queries0
-
-
-
Last processed request
-
- Proxy response -
-
-
-
- - -
-
Active endpoints 0
-
-
- -
+
Processed proxy queries0
+
+
+
Last processed request
+
+ Proxy response +
+
+
+
+ Endpoints +
+
Active endpoints 0
+
+ + + +
@@ -265,9 +271,17 @@ remove_user: 'remove_user', update_user_plan: 'update_user_plan', + endpoint_name: 'endpoint_name', + endpoint_url: 'endpoint_url', + + add_endpoint: 'add_endpoint', + remove_endpoint: 'remove_endpoint', + update_endpoint: 'update_endpoint', + proxy_admin_panel: 'proxy_admin_panel', rpc_methods: "rpc_methods", crude_dashboard: 'crude_dashboard', + refresh_endpoints: 'refresh_endpoints', query_list_endpoints: 'query_list_endpoints', query_endpoint_stats: 'query_endpoint_stats', @@ -284,6 +298,10 @@ removeUser: "remove_user", updateUserPlan: "update_user_plan", + addEndpoint: "add_endpoint", + removeEndpoint: "remove_endpoint", + updateEndpoint: "update_endpoint", + queryProxyStats: "query_proxy_stats", queryListEndpoints: "query_list_endpoints", queryEndpointStats: "query_endpoint_stats", @@ -414,6 +432,23 @@ return userdata.free_calls >= 0 && userdata.free_bytes >= 0; } + static isValidUrl(string) { + try { + new URL(string); + return true; + } catch (err) { + return false; + } + } + + static validate_endpoint_data(endpointdata) { + if (!this.is_valid_key(endpointdata.name) || endpointdata.url.length === 0) { + return false; + } + + return this.isValidUrl(endpointdata.url); + } + static FieldReader = class { constructor(eltId) { this.elt = Helpers.getElt(eltId) @@ -495,6 +530,28 @@ } } + class EndpointInput { + constructor() { + this.endpoint_name = new Helpers.FieldReader(conf.tag.endpoint_name); + this.endpoint_url = new Helpers.FieldReader(conf.tag.endpoint_url); + } + + endpointName() { + return this.endpoint_name.read().trim(); + } + + endpointUrl() { + return this.endpoint_url.read().trim(); + } + + manageEndpointData() { + return { + name: this.endpointName(), + url: this.endpointUrl() + } + } + } + class Display { constructor() { @@ -615,6 +672,10 @@
Endpoint: Endpoint: Endpoint: Endpoint: Endpoint: Endpoint: Endpoint: Endpoint: Endpoint: Endpoint: "} ] + ETH_ENDPOINTS_STORE: bool = True CACHE_ENABLED: bool = False CACHE_EXPIRY_MS: int = 300000 @@ -122,6 +123,9 @@ def __init__(self): # Cast env var value to expected type if field == "ETH_ENDPOINTS": value = json.loads(env_value) + self.ETH_ENDPOINTS_STORE = False + elif field == "ETH_ENDPOINTS_STORE": + raise Exception("ETH_ENDPOINTS_STORE is auto field") elif field == "MODE": try: value = ProxyMode(env_value.upper()) diff --git a/web3pi_proxy/db/migrations/004_auto.py b/web3pi_proxy/db/migrations/004_auto.py new file mode 100644 index 0000000..7b6fc80 --- /dev/null +++ b/web3pi_proxy/db/migrations/004_auto.py @@ -0,0 +1,55 @@ +"""Peewee migrations -- 004_auto.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + @migrator.create_model + class Endpoint(pw.Model): + id = pw.AutoField() + name = pw.CharField(max_length=255) + config = pw.TextField() + + class Meta: + table_name = "endpoint" + indexes = [(('name',), True)] + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model('endpoint') + diff --git a/web3pi_proxy/db/models/__init__.py b/web3pi_proxy/db/models/__init__.py index 9ce9e63..5afe283 100644 --- a/web3pi_proxy/db/models/__init__.py +++ b/web3pi_proxy/db/models/__init__.py @@ -1,9 +1,11 @@ from .activity import CallStats from .billing import BillingPlan from .user import User +from .endpoint import Endpoint __all__ = ( "CallStats", "BillingPlan", "User", + "Endpoint", ) diff --git a/web3pi_proxy/db/models/endpoint.py b/web3pi_proxy/db/models/endpoint.py new file mode 100644 index 0000000..1d27c83 --- /dev/null +++ b/web3pi_proxy/db/models/endpoint.py @@ -0,0 +1,14 @@ +from peewee import CharField, TextField + +from .__main__ import BaseModel + + +class Endpoint(BaseModel): + name: str = CharField() + + config: str = TextField() + + class Meta: + indexes = ( + (("name", ), True), + ) diff --git a/web3pi_proxy/service/admin/serviceadmin.py b/web3pi_proxy/service/admin/serviceadmin.py index 0a8b4d8..c91a1d1 100644 --- a/web3pi_proxy/service/admin/serviceadmin.py +++ b/web3pi_proxy/service/admin/serviceadmin.py @@ -208,21 +208,21 @@ def get_endpoints(self) -> ReturnType: def add_endpoint(self, name: str, url: str) -> ReturnType: res = self.endpoint_manager.add_endpoint(name, url) - if type(res) is dict: + if type(res) is dict: # TODO not too good error handling return res self.register_endpoint_stats(res.get_name(), res.get_connection_stats()) return {"message": f"Added and saved configuration for endpoint '{name}'"} def remove_endpoint(self, name: str) -> ReturnType: res = self.endpoint_manager.remove_endpoint(name) - if type(res) is dict: + if type(res) is dict: # TODO not too good error handling return res self.remove_endpoint_stats(res.get_name()) return {"message": f"Removed endpoint '{name}'"} def update_endpoint(self, name: str, url: str) -> ReturnType: res = self.endpoint_manager.update_endpoint(name, url) - if type(res) is dict: + if type(res) is dict: # TODO not too good error handling return res self.update_endpoint_stats(res.get_name(), res.get_connection_stats()) return {"message": f"Updated endpoint '{name}' with address {url}"} diff --git a/web3pi_proxy/service/endpoints/endpoint_manager.py b/web3pi_proxy/service/endpoints/endpoint_manager.py index e1b2907..d42c363 100644 --- a/web3pi_proxy/service/endpoints/endpoint_manager.py +++ b/web3pi_proxy/service/endpoints/endpoint_manager.py @@ -2,7 +2,9 @@ from typing import Optional, Union from dotenv import dotenv_values, find_dotenv, set_key +from peewee import PeeweeException +from web3pi_proxy.config import Config from web3pi_proxy.core.rpc.node.endpoint_pool.pool_manager import ( EndpointConnectionPoolManager, EndpointConnectionPool, @@ -13,6 +15,7 @@ EndpointConnectionDescriptor, ) from web3pi_proxy.core.rpc.node.rpcendpoint.endpointimpl import RPCEndpoint +from web3pi_proxy.db.models import Endpoint class EndpointManagerService: @@ -58,28 +61,48 @@ def get_endpoints(self) -> dict: return nodes_data def add_endpoint(self, name: str, url: str) -> Union[RPCEndpoint, dict]: + if not Config.ETH_ENDPOINTS_STORE: + return {"error": "the endpoint cannot be stored"} descriptor = EndpointConnectionDescriptor.from_url(url) try: endpoint = self.endpoint_pool_manager.add_pool(name, descriptor) except PoolAlreadyExistsError as error: return {"error": error.message} - self.__save_endpoint_conf(name, url) + config = json.dumps({"name": name, "url": url}) + try: + Endpoint.create(name=name, config=config) + except PeeweeException as error: + self.endpoint_pool_manager.remove_pool(name) # TODO use db tx instead? + return {"error": str(error)} return endpoint def remove_endpoint(self, name: str) -> Union[RPCEndpoint, dict]: + if not Config.ETH_ENDPOINTS_STORE: + return {"error": "the endpoint cannot be stored"} try: endpoint = self.endpoint_pool_manager.remove_pool(name) except PoolDoesNotExistError as error: return {"error": error.message} - self.__save_endpoint_conf(name, None) + try: + Endpoint.delete().where(Endpoint.name == name).execute() + except PeeweeException as error: + return {"error": "(db inconsistent): " + str(error)} # TODO handle inconsistency return endpoint def update_endpoint(self, name: str, url: str) -> Union[RPCEndpoint, dict]: + if not Config.ETH_ENDPOINTS_STORE: + return {"error": "the endpoint cannot be stored"} descriptor = EndpointConnectionDescriptor.from_url(url) try: self.endpoint_pool_manager.remove_pool(name) except PoolDoesNotExistError as error: return {"error": error.message} endpoint = self.endpoint_pool_manager.add_pool(name, descriptor) - self.__save_endpoint_conf(name, url) + try: + endpoint_db = Endpoint.get(Endpoint.name == name) + config = json.dumps({"name": name, "url": url}) + endpoint_db.config = config + endpoint_db.save() + except PeeweeException as error: + return {"error": "(db inconsistent): " + str(error)} # TODO handle inconsistency return endpoint diff --git a/web3pi_proxy/service/providers/serviceprovider.py b/web3pi_proxy/service/providers/serviceprovider.py index 81ffed7..a80f010 100644 --- a/web3pi_proxy/service/providers/serviceprovider.py +++ b/web3pi_proxy/service/providers/serviceprovider.py @@ -1,3 +1,4 @@ +import json from typing import List from web3pi_proxy.config.conf import Config @@ -13,6 +14,7 @@ from web3pi_proxy.core.rpc.request.middleware.requestmiddlewaredescr import ( RequestMiddlewareDescr, ) +from web3pi_proxy.db.models import Endpoint from web3pi_proxy.service.factories.requestmiddlewarefactory import ( RPCRequestMiddlewareFactory, ) @@ -95,8 +97,15 @@ def create_web3_rpc_proxy( def create_default_web3_rpc_proxy( cls, ssm: SampleStateManager, proxy_listen_address, proxy_listen_port, num_proxy_workers: int ) -> Web3RPCProxy: + if Config.ETH_ENDPOINTS_STORE: + eth_endpoints = [] + uuu = Endpoint.select(Endpoint.config) + for eth_endpoint_data in Endpoint.select(Endpoint.config): + eth_endpoints.append(json.loads(eth_endpoint_data.config)) + else: + eth_endpoints = Config.ETH_ENDPOINTS # Create default components - connection_pool = cls.create_default_connection_pool(Config.ETH_ENDPOINTS, Config.LOADBALANCER) + connection_pool = cls.create_default_connection_pool(eth_endpoints, Config.LOADBALANCER) return cls.create_web3_rpc_proxy( ssm, connection_pool, proxy_listen_address, proxy_listen_port, num_proxy_workers From b2a68c8ea51f6f9c4f4e10ecfd34999cb6d96d83 Mon Sep 17 00:00:00 2001 From: lukasz-glen <> Date: Thu, 12 Sep 2024 11:44:32 +0200 Subject: [PATCH 08/31] fix: admin panel refresh after endpoint change --- admin/admin/admin.html | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/admin/admin/admin.html b/admin/admin/admin.html index bdff607..ee0e57d 100644 --- a/admin/admin/admin.html +++ b/admin/admin/admin.html @@ -788,9 +788,10 @@
Endpoint: Endpoint: Endpoint: Endpoint: { + add_endpoint_function().then(this.endpointHandler.handle_refresh_endpoints()); + } + Helpers.addClickEventListener(conf.tag.add_endpoint, add_endpoint_handler); + + const remove_endpoint_function = this.caller.getRemoveEndpointQueryHandler(conf.query.removeEndpoint, "Invalid name - endpoint not removed"); + const remove_endpoint_handler = () => { + remove_endpoint_function().then(this.endpointHandler.handle_refresh_endpoints()); + } + Helpers.addClickEventListener(conf.tag.remove_endpoint, remove_endpoint_handler); + + const update_endpoint_function = this.caller.getManageEndpointQueryHandler(conf.query.updateEndpoint, "Invalid input data - endpoint won't be updated"); + const update_endpoint_handler = () => { + update_endpoint_function().then(this.endpointHandler.handle_refresh_endpoints()); + } + Helpers.addClickEventListener(conf.tag.update_endpoint, update_endpoint_handler); Helpers.addDOMContentLoadedListener(this.endpointHandler.handle_init_endpoints); } From 35be50f49721558ebc8efc3270072fcd62f63989 Mon Sep 17 00:00:00 2001 From: shadeofblue Date: Wed, 25 Sep 2024 14:07:47 +0200 Subject: [PATCH 09/31] wip - enable web3pi proxy on oses other than linux --- web3pi_proxy/core/proxy.py | 40 +++++++++++++++++++---------- web3pi_proxy/core/sockets/poller.py | 37 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 web3pi_proxy/core/sockets/poller.py diff --git a/web3pi_proxy/core/proxy.py b/web3pi_proxy/core/proxy.py index 20c191d..9bff9d1 100644 --- a/web3pi_proxy/core/proxy.py +++ b/web3pi_proxy/core/proxy.py @@ -8,6 +8,11 @@ from web3pi_proxy.config.conf import Config from web3pi_proxy.core.inbound.server import InboundServer from web3pi_proxy.core.interfaces.rpcrequest import RequestReaderMiddleware +from web3pi_proxy.core.sockets.poller import ( + get_poller, + Poller, + POLLIN, +) from web3pi_proxy.core.rpc.node.client_socket_pool import ClientSocketPool from web3pi_proxy.core.rpc.node.endpoint_pool.pool_manager import ( EndpointConnectionPoolManager, @@ -86,7 +91,7 @@ def __close_client_socket(self, cs: ClientSocket) -> None: def __close_client_connection( self, cs: ClientSocket, - client_poller: select.epoll, + client_poller: Poller, active_client_connections: ClientSocketPool, ): active_client_connections.del_cs_in_use(cs.socket.fileno()) @@ -97,14 +102,17 @@ def __manage_client_connection( self, keep_alive: bool, cs: ClientSocket, - client_poller: select.epoll, + client_poller: Poller, active_client_connections: ClientSocketPool, ) -> None: if keep_alive: active_client_connections.set_cs_pending(cs.socket.fileno()) - client_poller.modify( - cs.socket, select.EPOLLIN | select.EPOLLONESHOT - ) # TODO hangup? errors? + try: + client_poller.modify( + cs.socket, POLLIN | select.EPOLLONESHOT + ) # TODO hangup? errors? + except AttributeError: + pass else: self.__close_client_connection(cs, client_poller, active_client_connections) @@ -130,7 +138,7 @@ def response_handler(res: bytes): def handle_client( self, cs: ClientSocket, - client_poller: select.epoll, + client_poller: Poller, active_client_connections: ClientSocketPool, ) -> None: endpoint_connection_handler = None @@ -138,9 +146,10 @@ def handle_client( req, err = self.request_reader.read_request(cs, RPCRequest()) if req is None and err is None: - self.__manage_client_connection( + (self. + __manage_client_connection( False, cs, client_poller, active_client_connections - ) + )) return if err is not None: @@ -240,17 +249,17 @@ def __print_post_init_info(cls, proxy_listen_address, proxy_listen_port: int) -> ) ) - def closing_cs(self, client_poller: select.epoll, queue_cs_for_close: queue.Queue): + def closing_cs(self, client_poller: Poller, queue_cs_for_close: queue.Queue): while True: cs = queue_cs_for_close.get() client_poller.unregister(cs.socket.fileno()) self.__close_client_socket(cs) def main_loop(self) -> None: - client_poller = select.epoll() + client_poller = get_poller() srv_socket = self.inbound_srv.server_s # TODO async? client_poller.register( - srv_socket.socket, select.EPOLLIN + srv_socket.socket, POLLIN ) # TODO EPOLLHUP? EPOLLERR? EPOLLRDHUP? # TODO Implement Keep-Alive http header active_client_connections = ClientSocketPool() # TODO close stale connections @@ -276,9 +285,12 @@ def main_loop(self) -> None: if fd == srv_socket.socket.fileno(): cs = srv_socket.accept_awaiting_connection() # TODO connection hang up? errors? active_client_connections.add_cs_pending(cs) - client_poller.register( - cs.socket, select.EPOLLIN | select.EPOLLONESHOT - ) # TODO hangup? errors? + try: + client_poller.register( + cs.socket, POLLIN | select.EPOLLONESHOT + ) # TODO hangup? errors? + except AttributeError: + client_poller.register(cs.socket, POLLIN) else: cs = active_client_connections.get_cs_and_set_in_use( fd diff --git a/web3pi_proxy/core/sockets/poller.py b/web3pi_proxy/core/sockets/poller.py new file mode 100644 index 0000000..9c5b9af --- /dev/null +++ b/web3pi_proxy/core/sockets/poller.py @@ -0,0 +1,37 @@ +import select +from typing import Protocol, List, Tuple, TypeAlias + +POLLIN = 0x0001 +POLLPRI = 0x0002 +POLLOUT = 0x0004 +POLLERR = 0x0008 +POLLHUP = 0x0010 +POLLNVAL = 0x0020 + + +class HasFileno(Protocol): + def fileno(self) -> int: ... + + +FileDescriptorLike: TypeAlias = int | HasFileno # stable + + +class Poller(Protocol): + def register(self, fd: FileDescriptorLike, eventmask: int) -> None: + ... + + def unregister(self, fd: FileDescriptorLike) -> None: + ... + + def modify(self, fd: FileDescriptorLike, eventmask: int) -> None: + ... + + def poll(self, timeout: int = -1) -> List[Tuple[int, int]]: + ... + + +def get_poller() -> Poller: + try: + return select.epoll() + except AttributeError: + return select.poll() From b743a72aa8f284cc6f47a449283b066f534d1eb9 Mon Sep 17 00:00:00 2001 From: shadeofblue Date: Wed, 25 Sep 2024 14:33:51 +0200 Subject: [PATCH 10/31] ... fixes --- web3pi_proxy/core/proxy.py | 36 +++++++++++-------- .../core/rpc/node/client_socket_pool.py | 8 +++-- .../defaultmiddlewares/requestreader.py | 1 + web3pi_proxy/core/sockets/basesocket.py | 6 +++- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/web3pi_proxy/core/proxy.py b/web3pi_proxy/core/proxy.py index 9bff9d1..04806b8 100644 --- a/web3pi_proxy/core/proxy.py +++ b/web3pi_proxy/core/proxy.py @@ -2,6 +2,7 @@ import select import threading import traceback +from collections import defaultdict from concurrent.futures import ThreadPoolExecutor from typing import Callable @@ -94,8 +95,8 @@ def __close_client_connection( client_poller: Poller, active_client_connections: ClientSocketPool, ): - active_client_connections.del_cs_in_use(cs.socket.fileno()) - client_poller.unregister(cs.socket.fileno()) + active_client_connections.del_cs_in_use(cs.fd) + client_poller.unregister(cs.fd) self.__close_client_socket(cs) def __manage_client_connection( @@ -106,13 +107,11 @@ def __manage_client_connection( active_client_connections: ClientSocketPool, ) -> None: if keep_alive: - active_client_connections.set_cs_pending(cs.socket.fileno()) - try: + active_client_connections.set_cs_pending(cs.fd) + if getattr(select, "EPOLLONESHOT", None): client_poller.modify( cs.socket, POLLIN | select.EPOLLONESHOT ) # TODO hangup? errors? - except AttributeError: - pass else: self.__close_client_connection(cs, client_poller, active_client_connections) @@ -125,6 +124,8 @@ def __create_response_handler( add_cors = req.cors_origin is not None # TODO CORS support here is very crude, needs improvement def response_handler(res: bytes): + if cs.socket.fileno() < 0: + return nonlocal add_cors if add_cors: add_cors = False @@ -146,10 +147,9 @@ def handle_client( req, err = self.request_reader.read_request(cs, RPCRequest()) if req is None and err is None: - (self. - __manage_client_connection( + self.__manage_client_connection( False, cs, client_poller, active_client_connections - )) + ) return if err is not None: @@ -252,7 +252,7 @@ def __print_post_init_info(cls, proxy_listen_address, proxy_listen_port: int) -> def closing_cs(self, client_poller: Poller, queue_cs_for_close: queue.Queue): while True: cs = queue_cs_for_close.get() - client_poller.unregister(cs.socket.fileno()) + client_poller.unregister(cs.fd) self.__close_client_socket(cs) def main_loop(self) -> None: @@ -264,6 +264,8 @@ def main_loop(self) -> None: # TODO Implement Keep-Alive http header active_client_connections = ClientSocketPool() # TODO close stale connections + fd_lock = defaultdict(threading.Lock) + queue_cs_for_close = queue.Queue() t = threading.Thread( target=self.closing_cs, @@ -281,7 +283,7 @@ def main_loop(self) -> None: pending_cs_size = pending_cs_size - 1 events = client_poller.poll(Config.BLOCKING_ACCEPT_TIMEOUT) - for fd, _ in events: + for fd, ev in events: if fd == srv_socket.socket.fileno(): cs = srv_socket.accept_awaiting_connection() # TODO connection hang up? errors? active_client_connections.add_cs_pending(cs) @@ -292,9 +294,15 @@ def main_loop(self) -> None: except AttributeError: client_poller.register(cs.socket, POLLIN) else: - cs = active_client_connections.get_cs_and_set_in_use( - fd - ) # TODO what if does not exist + with fd_lock[fd]: + try: + if active_client_connections.is_in_use(fd): + break + except KeyError: + break + cs = active_client_connections.get_cs_and_set_in_use( + fd + ) # TODO connection hang up? executor.submit( self.handle_client, diff --git a/web3pi_proxy/core/rpc/node/client_socket_pool.py b/web3pi_proxy/core/rpc/node/client_socket_pool.py index 45a31b2..a222c12 100644 --- a/web3pi_proxy/core/rpc/node/client_socket_pool.py +++ b/web3pi_proxy/core/rpc/node/client_socket_pool.py @@ -35,7 +35,7 @@ def add_cs_pending(self, cs: ClientSocket): # assert all_client_connections.get(cs.socket.fileno()) is None with self.lock: entry = ClientSocketPoolEntry(cs) - self.all_client_connections[cs.socket.fileno()] = entry + self.all_client_connections[cs.fd] = entry if self.head is None: # and self.tail is None self.tail = entry else: @@ -43,6 +43,10 @@ def add_cs_pending(self, cs: ClientSocket): self.head = entry self.size = self.size + 1 + def is_in_use(self, fd: int) -> bool: + with self.lock: + return self.all_client_connections[fd].in_use + def get_cs_and_set_in_use(self, fd: int) -> ClientSocket: """Searches for a client socket, it must be in pending status, is changed to in_use status and returned""" # assert all_client_connections.get(fd) is not None @@ -92,7 +96,7 @@ def pop_cs_pending_from_tail(self) -> ClientSocket: tail_entry = self.tail self.tail = tail_entry.prev self.size = self.size - 1 - del self.all_client_connections[tail_entry.cs.socket.fileno()] + del self.all_client_connections[tail_entry.cs.fd] return tail_entry.cs def get_size(self) -> int: diff --git a/web3pi_proxy/core/rpc/request/middleware/defaultmiddlewares/requestreader.py b/web3pi_proxy/core/rpc/request/middleware/defaultmiddlewares/requestreader.py index 5f385e9..d38525a 100644 --- a/web3pi_proxy/core/rpc/request/middleware/defaultmiddlewares/requestreader.py +++ b/web3pi_proxy/core/rpc/request/middleware/defaultmiddlewares/requestreader.py @@ -70,6 +70,7 @@ def read_request( if not cs.is_ready_read( timeout=0.1 ): # TODO total timeout for request reading, TODO parametrization + self.__logger.warning("client socket read timeout") req.keep_alive = False # just in case return None, None diff --git a/web3pi_proxy/core/sockets/basesocket.py b/web3pi_proxy/core/sockets/basesocket.py index 91c1c35..b0fd105 100644 --- a/web3pi_proxy/core/sockets/basesocket.py +++ b/web3pi_proxy/core/sockets/basesocket.py @@ -15,10 +15,14 @@ class BaseSocket: HOST_IP_MAPPING = {} def __init__(self, _socket: socket.socket) -> None: + self.fd = _socket.fileno() self.socket = _socket def send_all(self, data): - return self.socket.sendall(data) + try: + return self.socket.sendall(data) + except BrokenPipeError: + self.__logger.error("Broken pipe while trying to write to a socket.") def recv(self, buf_size=Config.DEFAULT_RECV_BUF_SIZE): return self.socket.recv(buf_size) From 9cf6c26a1f38b7dc1d135440aff88d0c56fd309c Mon Sep 17 00:00:00 2001 From: lukasz-glen <29129196+lukasz-glen@users.noreply.github.com> Date: Mon, 7 Oct 2024 10:52:01 +0200 Subject: [PATCH 11/31] Wallet integration docs --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 7ef8c35..a5d0014 100644 --- a/README.md +++ b/README.md @@ -92,3 +92,22 @@ Remove endpoint at runtime by providing its **name**. For example, in order to r ``` **IMPORTANT:** Resulting changes are saved in local `.env` file for reuse. + +## Wallet integration + +Any client can connet RPC Reverse Proxy. +A web client, scripting tools, backend servers etc. +It is the same as with other Ethereum RPC providers: you need to use a user's access URL containing API key. +The proxy support CORS what enables usage within web browsers. + +Users may wish to integrate with wallets, e.g. Metamask. +Below is the example of proper configuration. +It is convenient to create a duplicate of Mainnet network configuration. +The only data that is specific is RPC URL that is access URL and points the proxy. +See the example below. + +![Admin Panel](./admin/docs/screenshot_admin_example.jpg) + +In case of problems check, the best place to start the investigation is to check the network traffic, +for instance with a web browser's dev tools/developers tools (Ctrl+Shift+C). + From 45c2ab2ca4ba4ea493ebc819b26e13f3a83e4824 Mon Sep 17 00:00:00 2001 From: lukasz-glen <29129196+lukasz-glen@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:13:14 +0200 Subject: [PATCH 12/31] Add files via upload --- admin/docs/metamask.png | Bin 0 -> 130153 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 admin/docs/metamask.png diff --git a/admin/docs/metamask.png b/admin/docs/metamask.png new file mode 100644 index 0000000000000000000000000000000000000000..cbcee3eb841ba929be9e31e0252252deedb08097 GIT binary patch literal 130153 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

k|3OYcj1= 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

#s0qAItl&MC_!VJ{T{! zT4ykl&fvDmRGQhwqx03%t*e=*N?n&>j2d*JU41`s(d3mRhyOBPU!Hi}oR#3Z1x+n?3&u53{lUM8*Dm$yok6lya zMwFv^ZVbie>SWcO=*4X>hDY4hopMPNA(kW@Oop=)7U%-_H&^Gd3ep^v1sNBCB6?Dx(z2Y8~HUq-;lbjG{CXl#8ConO^ZL>De&}!l>(cR;+m`LtDmD; zOfk;?fDqsE931ROvLni%*GN8YKthvNmz`4iJ$1ZvSts8czT6pIZ;czZr8k`j>)bvQ z&*(5s9b;fZ$A9RX`f{3fCWX7|M7LB}bVR+6arH#MfrnW`{>vOb$d!G?2sf$2SeW^= zKrKdDh@_XY{rz+8K1M;HAiV=~-GDmM|U6&uhRBWNF90#Gm(HW6sVm z_sTu?F*+=XZrF-(B#^?i!-{;z`eNdNy{%LOH+iJC_Vyd?6qvWXHawjZEOZg znz(CUj_W$AJbo%)j_lz5Cw(UcOq0o4s(ykB)^7MC(~(e~V**;{y@3*9W{usR`bT!vfuSh)XMNQe!{&dcP)E5h|AP{h)Qj8Ii>D;YAH z`J(k|t+qdT1sUZl1i3hx+BbXo@_K8ax?!2JSU*i$&CVe07{&%4ViB9jFS%WOTiSFg z_QWU!oh>4hM1|WMskaF@T?@KnJ>6cuj+PJ{T})DYf%3YGJ;p#K{M;rzo-0SSR8J80 zH(Mlfh!u{&k!pA=4Z{+Wn=>|#wi=@&%Yqp-uG{wbvJZ;N`9#ak{X1#LezJE3+PeY} zY-`Oa8MhRydsg!efmbliQvVX86ce%~BX9$Im$h2hAE@PGXpC!+xUNr+>Rt5b?g@jU zU+3V@ALp0m`@yZQ zuG0kp9yF+3WOjn?j!d7=N6<0aid2US{&@6{LM`h~;XQl$x_>%?rJ@=?ac+p~yka7| zK3RT;%4#Lxb#<6mv_>&{60{n^ptj!G9dc?_N0y1TKQq0iyf(4H{uGqGkE(pDq!WXW z?r`jD313DN7GCel_o${+3W{^l#!O2C5jgEWZ+f8;t%S+;(fT=Ls=eryHhl~Hxz!hP zdO3g{7_Lk#wxmF{Ae(Kt010WL5nfj2s=v8Z7th7O>=6iSb`Y${+L%sw&ESD zSMrqG%mSd51xg`q^KxurRH(Bo!#3FsV&#<*k_^)&m@h*&5k&>`@Dp z>~j&^x>VY4-ON?f({oCtz$qFjRkRA5)3{Wwf~ZeAtC#&U@AYUIzS_s?j= zg|dkjs&mC$kdSNT!xa>ZmC#s^_yXosdypHgc3TDVavrx-auv)U22vTqY$V@1a3~=I zx);F;>dB!9ia2Qp4@coVks3_a#eiBa$xs<}ZRAY;Q94Us(8zKxT%P_HAi`SnbjyGO0fKkhN%0RGmZr z@XKIv;8?}^? zXvz_p_8=efXI$F3s!dVa*ORD!WKod4Z+7u#w~XBIjJ^&7Q&glB4QYy39pc_nw(j@h z7b-h7Wvvo7l-2~=9JCfSoD96%r!GN z|C(Hmtjw55ZGly`^2)}V2gh_{3v+F@XSJi?qmzsc3L(f z97=jilR2-$bouBF1PUq{p%xp^cgSKK9gy>P&&kc`9>#lS2Jkw-%qPOfCcTg<07NUD4Oq zBuB!o{HOO1mRSt4GzT7vyFjMKs+dqQX|(wOD=^IvU6Ci(0OGmy0gsXaAYoD$W81`2 z)hE$Q1Tk?|ti?*vmsb=k1g|c%{T3N7!yOz&JkTV5HCw=OFYUv>j8jGfzxvR-UQjff zHEwPn2)^bBJMSi(%(uqyu096Yu<>fOntv#j<*pX2gXXmE+&=~DW&hPq9k>xR6JS=8 zQSUoSpCc62>gy(_l`kU8EmOgK=RyEBETCM@J(|~*bpF*gkJYQ!z~sonD#rCoQSs&1 z*HYb3+a4~2{6)`t9>9K!o=lrd0Dp%sNBgk*~IJ(Q+-J?7OF`C~Qs-388+89l5nrxy+z6D`<9mf#FEg_jk~7K_Whj>jiUl_mDM^YTa}ud>qSl5 zP3fe=sx=YdiqYVghr&c`y`lT&IGjYSBWh|p;;Nd;6hmJ3?7HTAXnR(6=atJn03O~Z z87)k?sG*shlKR0U<0sfKzCPsj?P$z=khr^=w_%`G;jv=O5T_yHBp?w1%2d1ceKjG_ zY&fxPRq-D@w=I3Xpf_3j{S`|prT%4*#K^CubeJ^tv8oB`AvaOCtDH+PpR{B%0dv@x zPo#3w(bbY^t>eqBmPKAjr!?|hxf6Aq-}PuwUE39;siXZgI8XiqO&vayIGxLRO`(pq zo?l=;xytHpr2vfrJ}C#J-*BL0p&De4QTo)|Rj)pVFiFc!p1XFJ4XW#@eR4LSb9rn{ zsdDWZkkxj>pQ%Yu`ZV_3?2n6EaVsVDIafM+>Q}eIj2sT9uX23AOqm_(a7s-YbW0i| z_UD7PTE+*red115R{ExfgWm0YbQw%m-=k1{w{Gs6qYWLh!S&`UE3W*v-c9BOkjnb6 z#UasP5(HkJmcE6t_>OP;X9tZ$joT+fc5D+mbj{?*ncWiybDFCgPudzuW}B~^~S zrM**drC%*?d5vUKd`AZbmX*c8R$BWzN#eL_Rr!P>g>rDTh8<#pQP4==Ri87Z9R8er ztd0g_FXm%Fui`rDm58%!E0VgjD1wd-qY}$-Ns(%g0exBSx8*JjakDY0dfD@hB+H?k z1i#&}FYE!0`%D|3EJTH~gKSI`k0l%daQ0_pQc{xDn3HOOWaU!*-oS8(=dW({H6=w8 zKG6sx%jeh`|46G#8tS~LYio;{i>By}4OaI~O1H9L+>|}&AcW9t3^$omB^Gb7_};^+ zJz|I<#Zj*Ur&mnOJFp_77!n$*u9MwC@wqkPo2{+v-!V9d#F(UeVMo?4)z4@&_u!>D z+-dYD5Jy#y$IwZ1{ppN9@)+ZaAjaEyDb2}*m~w5t+#oo z)ukjRk6gIjwU=OBF7(Md--?&d))fx6R*!gg{9P+VI_!5jg|biQc?;*~P$J2|BrPNS zLYElBy^LJ~vT*ZqT{Qq*`?NNg^ST^&3Q*!K+uiS&bfox2o~+FZIA7-dv>oJRY24y!)eQ6RoLSSro8-Qzda21{pnctfjxONkKZHNXA82`0y9&%tK%_0>Kb=OjN~ z3H|W*l|w#)*sV#Bw%z|-e6p74s5}L|B^G$H$ki%+3LXBdr1RkigwLK^a}FXRU2^y{ zfq|}{@?M{UB&7zIq}^uJWwUQM#XD5c`lr8jbavM>HjVP45EXnV>W}Bee6u&za!xQG zAe@@l=j`S}IS|<;lesLD$XkQ#TCyCN!G7c6j?$$24x6o{ryff$=A62A3XdUhTT~)n znb7Arlr9$@C-BcVx=JuHrW34xdI)n4#bB6ks@Vp5QQIq1Op9koucoEf>Y7?jTw)RR zx+~fW@6=)*>9%}HiR$li44DLYfV9x6WiPQ5-@yypPcsh!TO6_zs8wrV< zlzPX_M@VBc7vQeR!WEKMQ997UR81{H(vG07PWu_+DSM(BSG_uN3N`lh9J6Shjkm2A z>Sso^BaEez^xlkuFDH=l?8Xnq`jU3~pqX1xJ_hQOXXH8)=H zIXHUWz1E4X^wFh7h-P0?lgH!Up*j&o=$Mp944*lrlwSu7`g531cKozBb+B3Qy6m!ctV zQbnD++eYY{x7on}u^ww;He1bM%8cfFHiKG%ppEgEpIhTQYM5ty#fv(&I}Lw?4RaVx z7V&={9CUvj854+GJYR}l6P`#rw=>PD{jflL`)~25S z>~dVg-)6O(IApc_J^pw}5uMFrwJ@@1JF zKTZrmM6QZ8#+uPWrzT5Pt=y7Gi9pFk{;dh2 zET=ERJ~2R+Daa9nrA&ma`ghq!Nl9Z$?L`4qO=;A0E;H%@#XSXjtkg?Ac8C{*2O%3* zb3tQfMDcwOb8Yp`OyA!(6_vgT`;CB@8A)%8`b~>t+5%uZ z7gw5^38@~AdXrU!@c~4`lF@;B-fI9Ww-+=SJL*vA))ei*Ok5>(}fJWQ}l1 zxU{f<<6Lp(?-qJV-15D1Vx@mu68rqcR~60CbTk3Zh-s)44_0uG z*?~F#4b+Fe&d5ui=W)WAKMS?^gK3yj(o8PM$(g@kBTx)UZSriyEk57K6z?(HK7I50 zn+JRwHy7Ksg(t3ju7$$Y9#+})TTIQ+(Mp*Bw3Pa0b~X|cxo&6YTH_C+6oU~m18d>r ztPL7{GQLFEm>`A%ESWS!Bj#jshi`gPnqq;HKe)~@QqM~yVn#R50}KO|NYMs4aQw>- z$20z?8bHg;pJ{wfZ(2adsjFHUX_x_dtJ5(dG`CiS6>WHGL4^!sAbDq}yU-TmRsPW# zJ>S(gieBhov%b?)**-z*l>jAUjJi&2YD|KD9~tF7GcHr<>gyM5V{|IwXmLK1?53;C zu~8BI@=)A6B;48Q!q)1s4MhUpXrhCc%3pw69Tk9pxKO?DTzxtxQ7kYWj|GWtaCsyV zi484~5DEZDbYz^W*ubUwZge9eJV``RbcnN@yG~EQcOr~hOKqP&r z?-wLRRJc)4RxtCjH9)d`(jq7ot^1i5o3KTmm(zx$6t3soe5HgWL+qz6)6D&BarxA@ zClM-UStj#6zu~$X&xq&Fy$PSr5j|5vRGeR$mU5AIan^}RhcGNVuH4T{ol#dMIsMn^ zmW8#mjV_hXP{z4Y5hH@rcehFowld?cY!78+}?^6 z#&qAz>gu#;`q`F8+3$4$GJ1x3S<}emfOC7XVeNEa19*J@>hTn1<<;fyc`sjlaIqs$ zK`;@aEWy)dlggLq>{BfDRP{y@DA?~FUeR5y{`9ZyWxskE0(n4|?4giFU0;=G739XKF_UXeRE7zD znr2OKmu2FlfI$s>x-V0UNIX!O9#dWe)D*ck{HhNw;5HIH zCrr6%AZ{`1O3fK>x$$J@X8aJZ8bV<{7$TMM_2&i-F8Bwvzw}G6RW5)!3f8LuHbtN~ zJzZNy2fzVqe;>Dd&T4WiW6^KY{pbcV+=U7XQz`Kh!#RobmFXpOc(Ht!Hx zj24Bpksz%H>Ah#*1Iky*Nr?)nNTIvG1GN5&;pYSv3+AGoQq+_wcX!F}-r6tTJ1yR| z;4Iubz&;Lpx47_x1(=G5X90@r?yzTVW7_5KnowLp_(0N2Np!&8ffdLk7<~1M68?%} zz>F{b+L$)3-`feWd6Wvvm%-gu*jirY1|I6B`k z@@GDK21s}#|7)Cu0-h>XF2uKP02jDc#m_>#UALgz<%*jU(fEgPzFVgd&vHq{ixazK zflu`!<(ax7u%12*JB$ixkt!9%Z{Nu4)%lsejMajy?{7wb=n-PACX$yu^^LX+kGX`> zYEG=`-ykFIv(;<&3ZQcd&a7k;HG}?TPX$e4SW?W_@?#5BNo%h49cr>2!DRyf8iQdykYIvOJVZ^mV1z0v*uB8XH`9)$4q=Px*@Vl&sXV_B~tI8 z{85pu7dIGPY))7N{o}LpG^A0wHIJsdDW#yRtPaIt>vf93NK*t(XP(^E>U~L3-C*@qT2dYH9g=O0Rx_*(>hy( zc5CtHtR_n_U5c@f=#!1E2=K=k6D~a!-X2TiWE}W+>l=^WuL}20%H$5Y z_JZVkCx>3AYe?%JV@}W4WVf0X`cA69U<|BYERX%RgxO%+(2Vj^g4hau$^MVQ}nz#21Yll>* z2PCcX-bQB~o>$F6SBeNmUY2CMeOISrEh8V9YilCmtB$ASny#VKsC;hL%k`G=t#JYo zIu}|FDg|0tMs?4Zy}GlV<;0n`9=p5x3Ogd`ZK$vOa=zaJxvw!5;#Xf@*g1Zw>8&k`gfpQlXa3zskPhxqttJt-Rj)E@tG7{Vd1 zvvMnH*+++&J|O_J?>oX*V-DY`|L#6b6`L>4yo5`kFE1UmRlSQ8M>e0e?cWF#y-T1r zj3x|oz~!O*>Ib@&j;Mu5c;*xF-D-%-e%Z6hv%7Ec`?CVC3;Wqe`0dke_tW7WG2e{a zF16_#Hrqn}6AwGH^TdjE8YA&L3_Dy*j1M^62RKd}TwO=&K*d)W;!jlX_$owG6XFyA zP=Epc{s{8Vbuu{>?_luM50@kliwviurlx0PG-bv~OiaAKcGWA@RxXrlwL3|J0M3R9 z6LR%ELp^rw#fk%}7;km4)7dLoeR*uVIXJ$jUB8BD_gcfW9S@hxsoXu2vQGTbQW_ar z@sR}+=2VCdU2SUzJBwiZ@VK9~;BNC#dyLg?98vMWB{w%8)Ml?J7`5_cvNioRHsEIj z<1VAeAiZz0y@h#xOIq>92Nf+2IDVj{fn}emrgrL!*%e?qqZPnKlsi^bYrX8dHukMT z&GtW?j20>)0K*I(i-Kd!j{V`%ft_SfzPnwN;1o0b1xW;ZbZzwK~< zM*mMzDI@!@CK?)2DQ%)|z%N6;M08cbm>52^o<{7%ys>JXb zfDC}Im(PYP)hj76_GtE4YfW3A=wbP6*t?U=`?SXklwlZd$WUKEi3D)F_Bm|}%KH}u z^=WAs7JvKLv9!fgt5;T3rn9*GWa$l|hJ|@92bu+%gt`5&4&HLI%0oR~;QT~i;N$f& zLado`HAUeQHtnXFo|-%3vj~ zHi0Yb_pg!KZPvQU%}X+~;YpcQCvssC*8l!O@Xvr->&lh(O#B{;oIfeSPp>yc z@@*%hNB&^Fn>1{J8uMdmrCXJ0OgGI7{mZd1yu4BmG?^c2AX}Jfvw1Qk>4QUOUEf)l z`6n%8W2{`C55WIJBBhF{ekf9N$g89i94%?_e1s!RDC(be2e`nrf4!$}?bqu$rwxG6 zWgMl3S{`I0=(m4VsOmq&9Ls$#6|02nbS?@eU^}^^2=GZVCgft@i1bCkjV=eq5)#pit_Mfsr*i2Bj4LXsV0?9mDmmKV~Qd#n~b%HU*#cB?8D z@JAzHyw5ZbFnDtOQTI#dz7RDnWjr@`eeweYt-tQu3w{)5PQ0`BJT)V3$6)5(T3QJ$SW@Ws9hXf zG!vNi7tAFv_TPz!n4nS&3PL4%ba4MoDDzSp%~A?0klA+AH$ZZYTsKcP-rS~hv5OzpLch`t#h9(EJfa-?jD*lUQaInCa;~ zm!@$Mi;I(!lVf07*fkoOm~3xvV-lb(lzO@G@oAT(&;ZoribOdkaR2{Rv`mw-W8_Bl z#6P<76GzhTS@2DOib3zcOtv5@7RhSpeO2)`Sj{9{tg~RpGqjX5X7w0IQWi{O?(Z4s z?8fnxgIVK;1uWZ6ZGT$jYO%*dq{(U7_h2z$pc%1B9h+3+`f?e~HiwD8w#!cFAaL4grE=S#bP;#-%DK95+fBEz%GRl(c)HMn%Cdl zponPeu0{&Iw0+$erlXb~9TI{iMq>gx?Nt6R)a0cAKus8!FOA+7LPD=t3gO3w7R18B zfT$&hGA0S=^bI_I+(UC|Wr(df`PN|QxprR6``bX1%KNG`akYWXBw|&>3O71*O#ps6e8<*l3)!cpfad?^(6PF7;LYxGX5YDvut&t(U zAJR%BmTER`N|ueFIB;c7E@+RN9hp z^ta=E{H<>z}FDe-&_m9ldj<#n`hKtsKeJV(5`?>HPXJQr-@iVy5Q&7IPbT8xP}ilKub zuV`yh#fz8m$^GTkn}5^;G7R8ALMsi{Ne!zhIvfZxgf>*(DpJNgH!4d;#7I^(&3!O} zVNV(No31Ry@(ulT(7y@fdC^TWBJsI^m0+fTPbzhsWJ5zkc6K%j3Q9B)Kd<}gCl=Lw zr(Jp1YZRbN68I5q0(^v)5L%PLQmUpEvm?-kPQ_Wl#nFDbxsBl{)markmHt?w(;8rE zZ7WI-jyY9ELjzHnnQdDT7k#Q2ib&}`QC2mkqUa_!HQCuQ+fI`1PyV#;QQ0}Q60di$ zKea6eN1^iI%IK^ISTvbh8VUEZZw$?CdB^5P*AtB8rdeJhVe zZP>MuM~7gY+sP>M4|W1Vn9taL;O&ozz;{perP7wK`! zoU0<;`R7l=z9!GSqlm=+;_NHK;%K^UNeCnai9sM}0>RxKg1fti;O=e#f&>lj?(QxV zAh_$`?l!m#Fms!{-?{gENB-RUHO$lWbaz!(?OJ=UwH#_aa6iWkqK1SF{=lM&)09_* zeS59?V&I1~vVS*|DJSocpLTdVYfuwC_YW!EoMLh_ zbGL6!+?$%8%XZI`Wd;{f4$CAsw9VI{qMlUd#1)Wjx0)t{$EYXxL@X6QEOjh^luYk1 z_^yyUhjxY2!-Q*WFELCF20Iwukyjvq*kBl<;rI?JRMRw9eWl1>T_cKW zgABewB^+*#MjUV52MAjNaZ=IC2^8@f&HKIqs1{m!tV?|D~_x?|G`0!F*Z=XgT})}1?}h4 z(-xeM=0#$i0Q_JijhkJ+^I7&{VUxoS#)ILHN2e8QeBHA{-Rh|#t4f2TK=X;koi)xw zX9DdZNA&y7nndrc&;7bD(@g&}EPt1*mM5xm5@P~B|VYh9uS27vfQQJ#CHG}SXADoZgBa8V%v z9uS~;U`nh2XvjnR*e@qF#YR9rSWCQABc6fn&wok)0W)TxzNHC&$p{HS;1Drcn~_L2 zDIgyxm{nVd?EO0L#p}G^7Em9NqS)FY&?_SK24`A@88g*b=xHy}b2Qxb<9+E^Jn#jD z3TL?rvt-_5iqArnB&>725=&z|GNh^Eb>8c|O5s22$@}mZtl7#N2c-mgRK0b!X0VN3 zB>HUShl*g0D8h$elifYD?g?JSfSzrf@v3~WqS%Yl;=UioN3W;o))i`>#LUfikwP_E zy}2fqK{92qhcBUL<_eOUzi=De@dw-MbXMrYwSux^Xx4$qH|S+=z|S{s2@O*A?-qtQ zO}}o4O8Z!78prw}dsmRsvIeMKWP@J*P*eYvo-B!p;ChFH)|?kWs^ChRZ)|KzD!f)^ zV9SA4CmdlHHZ1+K4DIpbfN- zRY@3MX1>Pt2oQ(>7>2(Y{uB9y_>uh5#wB3ZzhfXulXy;`Fq6dZ(1X1p8Xw2HlU{(( ziVQGfW=7H}l)JX@v1YxZDdK<8gcT|gBTjjDKI5}2I;>1-?AnF&zQMQ zB8a*97|R+3X<`6+K`-Dj2GSMEZKxX2bd>w_-_svsdf!F$amOT#M3C<7qvW%vWQz65 zzTo{o#i7#ivKKL@*#`;;$;3M=AoZ&<0>0u?D+ohiF7Sus}7WtyQq!Usl$xG9|Hoi~gdnt8gB#2PlF% zLg59Jl&+7jlR5ZW>fbp>tD2OZ)Y{Z>$iEr=?Mw%>Kf+aprWa{b+1Z=guB64uJ z{z&GZ&rQTPfs)7sNYpzrUj(XHg|W$!=^vKenDmKe9i7O ze_>-hR0)9i1Mp9PQmERRGMB&V4mp8#S!{_o3s?BrHu30!x``EToOD)aApqnbJFMce zMkw@!^EV;)A_2N&Rl+&tOV8erQba_3a^!fTyQW#RkWt-Ch%MB@hoDJ^58_Q-y8N@i zy8}S=zgtA&YOORgBH^uF6{pPg>)RpT{xhO%Gt#?OLF+4Q3` zGKhc}rW#M4S}aRs3{b|(zF#3$+Cv_42ZCh<)7>cc6u0FbE(;Q+Bxz$W!26<)eo)~B z0G(%a{Tp=Nii_X-39v89)=#|RKv;eXI2DbIjCe225P-^b#bTZ0-^d8Rdb0)2QWYPV z9DTT-+$-Jhkf6j4-3(uOCLZHusTql6Vs6ZEvIfZIu&KTeji)KQRxAw6OsWf2*U0P! zroG3O*$=!}Az`((gjv2`ErDL_Rs-M|awimXE!Irpcc)I9dMT0(i79WGWU= zMX(-`-r+!Fm=EjGVE z>#QF+VQp^IYWQBHf(sXNaoz`_`YF0;rHmcndhL4Wi9U|vV{`&f!o^snv@e_5Wn$~z6D&uDZ2PtMDsw8VuYkec5 z&TsD5`^GoB^EofI-*_DHE!Q-tEe><@gi{>>;bsNl5{)<#ICAl8_>Gw;i0!>4?gE|T zVRVJ3Vq~gYxy?uMdc3QL+V=L5dgErC3wg)pY z(s7gD88e`GU3J57H8ifJnfZayhT)r2pz!{-H_Cv#LK_r-%;FbfO6>k(7y%6MSb^3G zY-TI^CI;sJ%>JO5(l_s^tM&A6lTxC&qtoCJc023EYACT*&!eNFP#laNfh@E;MOt%U z(mjSU;L>KnT`PAdmXKBgjkhij7)=L>ln!HWY+dBx;5 zLw}ZrwdK`6PR12pjYJ4L#lEK-8YpCNbZlTR_dJkq+r$@L3xb!!Gkl<3E-Nw%B3|{qa!EU?s@3vUg135u^;YtSPxH&sPFO-JYUbmf&CaY5&5f6bI+DHVe5z?4 z9;7-vwuk1va*&AkhHtwEdK6=lk`<<?*|>KdN{E0Ve00v%Jd;Exvn$tpGoFy8Xv69SGDB5f%2Q(TSMLz2q4b3iE|}Wr-qj zL@<{>HZKGlp)Ozx7wlJjBVKTX=PI3c7;zsP{+w=1g&$5ulJMz0d|8+#n(LcQjD6r~ zuFMw9JyMCBX*OZcIf-d7U21k8XRzKm_U_*q-MYOv^oz#)X}Msb>@q%Xghx*rFCI5> z-`6$B&gVeDIosfiH=;-*Xkw$qSZE0bGr!`c@154z*$&roCl--rbU8g|=#G=z?fJ)j zsTKivwghRiaId4D@eML{mF1!5;bBP(dtiQe_Bd|er-~rUX_w>C4eed1IdRy+PVY)S zCl73E@`?V@9a^fl^gN_RLcsEfBwvSI#?iIaP&rbUUw6tGEM~FP5=t;#uSG9ctCOf< z3iJ1a!sbonynibL?7lfe7v31iki7p1b%Fd4rCV<8Wt(EFypt;;HM+MC{{)Ve zDVf{w*Zc&Q|4^GxhF4b`rk8J=;((r#fym0FFTvUW%fj-@r~FzTHc=Z=enf;XP5>QP zmvH!i@M?w}c}e!IO!dJfxD}GlgF3ojyVn!_mE+fw69M-I}|?zZQLUSDUiuj*(#!QUNcS3c2<+_0Q+25pJAxO^ia96tspR} zhLOZ%{|+x;fdR;;HVBiyzXB^KkOl+v?*F#m&jAStyf0bu^L4L)Wa6hslkvyKXZjaH zfBg7A*be~T{(HXVAFGHpQ(sj2mB736rwCt||1m1~pO1Vz?SFAVc$y$m#`@2@fPjGR zOCh2@Hu*k)0x%r-*9wV%zyhE(`nmkqdzO0t_3OtI0_|T`3adha`Zy@Pk5*ksopwid z2(3u}N(cZlKXQpU|K_7*gQ-dv{W^yh@n2iXI3(@&NPN<`;i5%~CF4FUxDS!zIHG`g z0e~Hdiok!{P$0Bcyp5i0<D0iAejiQty))*& z^`EWiBjwwOyh!>}`~X%njmmW4^YDMg7MXLwfIl1&mm}qWd-l+87URPwdh;=88j$MCbL_s+5?PLf$ zIy9Mf+w@T-nr6bg`OC!qE{GYTD4fM9A1mk^Lecu9Q86ojtVn(Z{kPT`*JH0Q6E>;> z2&wLbDN99s&g&LvAxX14E) z&n4aRN0pv{BkmcSarBgiGMGbm!wT)vdyVWtcATZ5k-72yjvvS_6_4ZL_w!eO;g4`p zB^&;C$$r6IE!S3HAh}%z)SxfswOC_MDY@lASkow2Q1Ye=24Tyfja`_P2YBei8dYoi zoqDdg)|>7NoYu_*jjqLA=Hy z2x>61<365zRpuWa>ym3gG~f9cTBCIua)3q&(`5C&#qf1>91C*AYC@m5biHrA{DVE& z!_5e_iRuvaD%57j>vpRYa?P_o9pi1Np~fzq!r{@KM?)Fn=IVBTYjqAvKfQHXkxRK6 zp2~Y}y*2RjgVX1v1~$5M-qRAd$@XMMx3&6nz+3u9!lF8}^?C?%z_e}v*UqdT6 z8>;Tv?(ZIwNF>Od-4psq&h|^?u<|oF9HGJ$I(qevacepQCSx5Pi0y597Z9t~x_<4{ zS^#-|A`3X0{JtwIvO2eQqtyGsjE=V75>}Lie$V39c+e}C&gXtM1tTcIhD7GMU+%7d z`9xSMtK@nl96R?50@JR$FJL6>O!07D#F}y?Yb8GZqsRZCv&(!Lbo-Es#vpR`lNFRc z;DUd3wC_~H4reOEW_CXy3L;eV(z19-Pt;icBJJX!IL^W+d2xZl3=Y8?-7#y__u_J3 zqSKKm;zEq!13A-1E|mFXqG;D6X|fq@4lH*7nOO^rPhH?%s%q=QIVbK0*(rqry}34{ zgCbK#1?yCfhci89F-J=xOdQ<(gB7zAHJ|igtbM-iWaOC@2AT0C#D#`?f)1PcClYa$ zTayubhoj>oWdx8+FS92dX(yl0(w~|ybzEelHT~|9RKo2&C4@&NQPDmDJ+qZP5Fb{ zwzt_<64sp4Y#0Q&-l?vt_WH#K=JUaJ(V~Kp?t#Q*L=VoF$2x8wr`q1OUMz2keY@60 z;mFTGH2*jP4nmcd*kLWwpK6znYiFcz_5$gg_a4gWBvHjh=_OoYT&g;KN=%Z&!&~=6XWmR6XJslXJsVW| z;AKB)rLekb(|cokAx7+^DjA|3=k0ts)PxF}Mqhbwq!BG17^eFuJ4hk>$#;fc76E-x zXdx*?N$Gsbu;tim-e7ceX-LeWn8`*j*uW9zwK0l{VrF-jG%uI)!Ay=vL%D#gWk{7r zP`PQVSi@H}+38*(N-Dbb(2@rRIb%CCJ$x#lkVMxmAbZ4&Uf0fM>z>z-uOz}Mg*m&X zLW`OmG}2{k!&U9js-y5twQfIz>j112ax3DgFs!PB0e#$r~c8BiUS*=xaBeOAvb`34Z2R922owM zaYi9kKsD8?tU4@|+{?1k5xPzl`=rt)?5cL{whA>`tdkoorxfrSkWEx5%3W*ADTt26 z!~)m2lGuhTlEqBx2-8vm{_+*f1b}RGYeMVu+R%7q>k@0<>UmVtl|B)p>f}4n_MJe9 zZv*wuTnc(RnzFkcrJqu+q^#TEcAlbj#PE(xV1o|WZs4|{5YBNpuD_z*_E|S5RA5dM z^iM;>3*i}##831+IDwD_Dj3vP@O}V~dF3*2#R!A}vR1g}DSRaJzX%UkoIXj3N(=U> z>UZ_ZrgN0rCo(u3r*b}|6|XBD(U#E|h#oz6VI6JvK1x!4>vikwYXugt(iry9K&YkR zfWwLzdHq{1UL1`%4C%eeYmThmJw$_d;H{7MXXW2)+vEuOyI&oFX&w94eV9u!HrMY5 zpO5}*_T(;T5xG=C8R9U!njz3b!ad;gzAV@CU0G?K1Vx8e3}HI z>2i;UR_)+d-IHjzgsuVhM{p8+Z?7Hhvi%F!**@$qnlhE ztKWYR=ZQ>|=S;9)p`jnJW%`&VjDwapr@2H_vBfO!&E;*y!6Ruxh|NA%8~uh>WJ-&v zcoQDCB;WWch{|@~pLIFa>ikt!Ca+lFukkTmkgNYwQcM5Y@+v-D@5+3^dtmKpc(RL6 z*Ts_#0{6>9z{Todkk#c99KH_-R8EKBwuoO^UVf>-2(^)V3-;dJSvKBn{!={ZD6_nx zS9@@IfJmu8X7(~t24%1HraYJ;$BUYSkJPj;egI0-U8iw$R~9UYrwuuX`<#An=GstM zsp}>9lQnk(@e9@e~9F49C}l!a2G7zFMS&@KCj;&y+Y^1kMqQ0G(YB}Ad;K5ZK) z(7WR31~jwl{=nQ~D^)oQv0T ziBeGck{`DhbaKmY;AphH_UzpHQ(DXQ@jTsTj*YX(bo0FmoKK^6KV?~8hs$Mu>Yzl( zj&cVZWAE)motSumT*A^vSn2;FD1D(>(nhrZpkUg@oC!m zP?c~}z3ass-BPenidnHL$^kl_s=x8FZBR5KF`0z&_1662u-(RqBq?<*G<4{Iet z_m^8tEbKPtYOqInm7%Y8w-@ElNf8hPxwIJw3-8QP*)-g2S4`9Qu#D$OGTwFAxdja!YK&a*_l##GuNm4iiG#X)WzO z$=J-cBR{zt=qiYei|(Kbd>n8d%jvwt#D%+FrcqccES{}goUj~fzJ|C1kqhLHbaQk$ z5X)YwiPt4NyFo3U4OKYi*K@zWpJxKivc4lbR`m2c;K-^z#pTqqNeIk^xUau3$Kxu)k$os^0;xjH12IxOJG;#k`RALh&U+Mn{7so*AxQB_Y(I6HyVEzqGX1)`sg`=~|H zb|MEcWWYW6eEmf;K`02i5riCe9pt*kX!$x|lnhsF{>ZZN4a6h^eSuMQei!okKManG zxqPzI7D*Dgj%>1I?79UyqfpiW>{lx+gbf#W-s#3@R0~B5rFsQ7)0W>vewPM%vbWXn zhFTv20y@~Doi*E67@12PGKdL&(Zi)f)08eFD^f+3`>b|~VkP@2nXdL1Uyb_~SK43H zm?-RNLq3IGr_M3c3xgHR{QG1{-j5LN?iicoa}%#6uKd}W^r*F~((lFRjy>iy3@%|g zn%gKS`j)`ARF;w6F1aX>eN2~)ldPqI%iO0d#7u2LZCzI%|M~5IC^tKLu_!Oj`BXQa z|A?}_JZ?|kZ`YMsyY&YbE(JOnV|9y)@Q<1}?SsqjJDbE_s;n20C>$&K+%?yPGKQNU z^0|&j8l)cFMSN@tySu%6VvYYYX>0aqAx@8rA9^QUJ>LrdBXBWzH)>wu&LV>SDz*1c z4p;feUbDF>4+|VDnvbWUuBDY!brC7QC^notyFBq-pODAQu#)9iBR@p>e^ac!V;2~5 z-yPaZD@{UcS@!oYQjAnM4{UTW#b1eZ_lPAUJ!1{&?Ty$B9KLnZu1xzB-gAl9=rNv|MCxjImv6WQXNSLU&GtO87GBNsahJLnLXc4yOm1&+nUr zi;Ln&6J)v*$!3C4J{P@Op-$(2(iR&#)fe>20^OAOlmWqpD11J40K3>}=XBD;p=RWG z_Bz*?$el|@OQMaTCUbN<+>y4e+Uscg9+%&g{Y?Pg+3h{VL&>J>!N9?_=`NU)-_tP7 z&b;fM^Bkmc175>CUe#FfR#fQ_u0Q53C+Gckoz1!8)8N<}XFqzn;=?SiMxJM|x{}xS zA-^pBtlVzkIYJ@!T;F)Vv=8xjv%72MVsp=6#^c|47Cp%U6*x;d78!dfT^5@(AteQP zoTqlm2K?Dm4c<=r)=###_Y|>SQ#c|++!mB#Rw>>lrEI-F1t^+930iGI z>>-mT)M{v*qp%Z+$jZp=*tp(jli z$CdAq(2a~@{|Y%ip0D-xx1M$68K(LDGjED_LkjlHNq#9vZa2~8o6is7G#6)xihtHS z4+lJn8dsz1QYXENt5;d^JsC@dE5+z3^~+Hnw}~-W$$mWPRY5H((;tY3%bZG}&cxHx z3uXJG;`OJt>@))+PeSIr7daveo=1>T#P#=H@!R8GYT%?K7U^U5sz&@-Ho(F8H7&_1 zG5jwP?2lIqfn_@(X-_S8>neP+XSwF~u=5y_&&Y*>4f1K+u|Ix(PXS}kWhSO1>AWeAeED}h)__tIhn~}Bdz#6F1czWCAZbX<~c3$dsl$u;c>H9$M2XH((H@IxE)vW7SWhMNAeX>Fj)G4v# z?tMO(%$ovRtTyY4z*2hNTvL)JQaF)qfHWVPubT=aif{$;SzsDjnl8!pus_4kvArR> zttPPiplRIos4{5ezEDfMw}IiX=F|R~w3X2%G2s~Z8M&(Znx4t%iITVeT>OA898Sd( zp0`rG&`&c(&PYnnLX% zsw|8DiKbec$yVZj*R|e;h?=6BHEI{PNfBj_3=fnqj@RkYTN_!3)DhGgzN%6IXce=y z1_wiQfrfb8sQQxop(VYsW)hcd!Ag0gxvCEzSOX}Fh^4qKF3c$~0F|Ln98K`WJpW$T zG=8FE#T{x%wFHfv_R6beAqs6gi+nrqTnhE|LOHJ%yVYG4=!&vfC{#7YZ(8?NPkq%I zCRsu2jfJFAO!a{WFFKrd-x@z8!daE&I;!Ma`M9l3tKLS3zb=hUYhO3bnr`gr6g>Zl zjK1~A(Q+9cZk9YnM~A>aTnYQ8?Pd*x8+wM_%i7~i_Xy1zqg(Yzb3J{`&=I||**foK z`2<`UjD4MZIAC+c*ig&DX!>;!0Hy(6oN<}h=^43+&AC;}HM(V?2Vg4f5KLG~$frNO zL>!luR*``yl~<^s@0WC;S?P(hHybuy{W|;CMrv>v^w;C!MFCIP^YJf3}$F>W8 z@dkQ@!&PlHQIDhrF;SyX!7{^ErQwd%XcMfCCVTiT=c7CDx`;J8l^f4{Hh$duLM(lL zgUNXDp+-)o9CooG;;;V>nm8T)NtCQ7H>^PJjEaitGdkqCEO^}iWMk0#j$NYgPn=(F zjg%-_vP1ivPT{0~U}S9z$ozDz7ik`3e0XXEO1D}s8^jg3#AoW2P=IKi?XNkT) zDT5n8=vK35uQ9%lJECEdiM;&Y}f)e*S_INoW$;H{_%NO}C z7yW1O_J_p=6LXX6(ysffR8Ho0D-gk_Ga|gg`R4hFQC}OyCQfY}C&5dB*kTT{m?ql} z?Ta77#u*&$IJ{%;0JGtwAwY19YJ zCW34#wHE$oKEil~RT%R(+C1$tZ~y+>{c$=W;iZSQtl3hF{`isVufy|GN<@!KJTCX+ za_%HLgR69Yrt;$kT^$j9Wb|ehl932g)s}LX7AuXkf=dTLsp2a~_+onz`(@{>DthV? z?xi~R8l%aeBR%WO>Du1ZJtzK(17%S8&i5R1+GOlzPNBWTx}Ib8Njk{MDw$K7b{iL+ z=K+Ja>a`-|An*4T0biBwRCD>IS0Ev*TqM|ht|{S_$hSEQaRV9q33kTz+lFQR+3zjEvtKEb$?hShj=Sos)Vii435U$_uzn`?? zHf|gaZ5qa7NpfY)xtPR|&GB)eDJQCHcj?k(J#f98dwam^;7;vt4_EFfE-oHY# z-Uh_*%{yr)bfCf*CAc4v>45QBd@@V!mnKSEB91DYt%MCK zdP37s(N!95ZW*-ie@v}b0^nZT)vnq>^vo3r6-P`0k)3U6rQ`ix;a;eT}Oz*}L z$Yr5(QM>7P#V_yqX_wu#LK_{7LU$eZrBuB`dX2m{yt=c%9*OO_4J{0`(jt%B+fjLIp!?DQ^&yx3MN&c~OO4>G2CYSZ zb4bu)gVIFIW!b!z?W;)4gcx@0PZ5r^?wh?xx5gJo!NM|fatAGu**vxas+t)2R=HiU zxH8Y@USbnX%2tSN>qIfiG}#R57u7Hic_d1+4`|5#qB;66HMOHz|Qy z>ae9PM3!q14U(H!9ii=ct<^vc@;B#Gk7*U4)Hz?*K#`13d+DCsr1^Co)k_gd%)-9T z(ahHY*_av#2+D7K_#}NnXoMb);srS)9uGS(L+K7Uk;HC19U@U3zlX6O$$earXt7Cb z@suG36@kj+h?Jd)`)!Uy4$~$@5(76WtJsAX{py6Z2^~C1va~Vo&gJ?(er1n`mK^^6 zT2WW@?3x{c-m7M1DZBm8olk=6wVk{G1+H0Pl{eQ*BuxZbI@2U7+Lj=lmRX)uCt`usGGn^J)e{2 zNK1-rAEal=yW{whB-!ommep)FbGi@xRrJ|)&gMeWAjD)v|1_P|-}As-4)rx=%TB{+ zBWtJ!`=$BB&}7AE$lbV&bEM_=WNwvSmvK&n*Jp^L`kAp*yNH*>HJt9~cw<#fzDSvs zO(`kXwAQAe|HG&f(QwZ2eLy$nV}W--?n~v@q%nS`^F8%4Thk1NzVd|$DOvSw;AzZO9Y*x zo~_PM{k`vX4q8+gYSpIjP)aKo7&Z><_!#DWAvIZWh%V$?GPRIIGBIm`{Z^}j+eg1$ zud1bQ?L#DNaL;4c=%j}EfuaCTz;a&Ho6Kz^cuZG;*Y4Ejv;XQin(q36cAvGq1@58& z;lrR3_h znph=otRAMydKX9AFRz|U5r{}}=7TM-piip(l>#x+90m=~<|Ul^wN@0uxkmLCCsiVS zt7LeB=rguIIP6PjHUd{asMev5PEPk>wy!8q;#44l!PtMTriHs7g1uanm-eM>?vFxy zk7c~u0o`&~aOC&|k#+qKt*OD;?qj&lRZHaG5+{TcY(EF^%eSH3HO7PQrgjxbEG?$w zWgTrRiCFEQx%(MsA;rG)L7A@IJE3GYkrs;F^V;8BArNZ2s*DPEBkG(bAit`j#_9Cc z?V78~++oQ-+BJ=hN$g9Acjk7uRa7?6T8M`H_BX>?q)+WOoN?%elnYsPyjCcAWE( zY+CcQ`&(L{uj}z$h=43L&{pZ6DHIbElkh{9e7@7fKBfKso&*(3^kiLVrOgem(DPY$ z=Hlm<+$;_-)PmauHpncO=s{;Gr`p?ClxK2H)t0B-k#lbaQ?H<7_vrMc(2L?;noH+x zP(E=AtvwvF;&5&E_h%4buY(n_7)@Y)Z=t1F0q?(rMg_c#5lKtrYh3v*5Yo$^bA^=0 zqseP#;@nU-*rGKyHzLGFcL{C$295Z5JKSMfpVo}0OEg(qUL#hM)%zn2Z1X`UMj$KJ zo%>u*$#?jD8~hs5bjR_}kNF@kW;ZNmXXh-`1D2rP+gHN}tmd=70#<$=@K>$r_9`Wi zaYkd9W24n_9kwfbQ3Y=eZJ6ZQxJ)Y(F`-BqqcFTDGG*F&a-}^Ro83Ad*r@zBPw(H?>{%SsUlmOa zKShAO9P4-$iv{(bott-ebBC=y`Kwo;Cz6^HpGtXSU8+n;Pr~Z2=1N2QG~or(`dK}p z#BEZ4zlPsiwU`jnF&rcWdES4DMO1n&Gj^A)ub^(co}S6Kw``Vd`|=EOoI!owE{2i` zJ?P2xR)T3<>v-&?D zs2Q}T>&$EJ7{2xAu-S`anS;Cflk-!vS&QYL@>?>9q>(#-GF!dHU15CPK{!v+1?``H zMM9c`<-)D7!SR73pb}*o5YPu6hP+)heFMxOjO&i)WfGw#?jh)U^3@+CmF7v4~yI6VlP{}yT4?c;?RSUfbCo84+C{WB;pA-(6di-;A}csJQn`Sj)8X;&GXfQ(5W<{aSS?OBX5)<>XZcWvL)TYq+UJxB{)aVGwY*tKZJnxm?eTJ8g!GodtkIsL(U&(MhlAEt}3dt7WLQ zTH>u}baj`V*V4xttb}B037Ao1Qu~6Z{HU`GQS_spzC&d;N_iKjdLxav!IJLbnAO!~ zCbY90mhkJ{6NJp-fBHpP$gzN~O~=1sborWC;rqt@ACs2ehNA`&;V@%`I;Xn448)aO zdxu6r<(U)nNp*bNZAMAOc$Q?91vXW=5u_J*^=3b!F$0b#48A^$dcc<4;STlUoe%Cd zllNYy?fxw@6is)8J+=F_>o;5(a-l)8oaY)}vQ{NvF_gwwFV`Sa!hn_J&Ct{N*Poj|RF znRp7KaT=`#I|EZzvx6=L%OLYPi;Z#x(d!+jvvRE(Z*bEn4H-mZeE8xyIS$K4zC*;} zZhVv5&FsUcCco0!!_GRKQw>{`o&%AjM%TwxPNx?7V4G(`;i(1u;tEidDg# zU&jaC6=Gr&qT++%D$_EePa2t6G4Q;XkTR+KwJsj}F|%3U7EM}4a)95_c+LS+5Ijcj z#?9Y7_8lOS7Dlg*{iXQ;$8G&XTLwNM44?zGJpZ&)jKF9Uj62tjucTuRaf11A6M0kqz zc+_utpr^m@%af5}X7$-qjy)YH6^Z{_4fo|17<*qtGA(})8fKvlwD3G0jzED1oVEB1 zO_XY8T+ug+f9mF*4Q`$F7fd7mr>zRs_hPhOyYw%@?+4_B!bS4pZ`H7fHzv=6S+?R- zapJPFY&O1H6hGk~KsX8V3<;4q#5E^s%6r3%E&Tx2IV*cLzBq zi;2JGDBo^XzEf^at}jYUDEtGB0~9PWcgBeMFeN!xqrD+iJ0wWB`J(e}^Kt}>2Gj$NNDe_JI~kX% zMAMnaz@MmSe?nkIyuTQm*FlJgo4ee2N+%;N&*?n;Tn1j?I}X`%fAJfRR128cz+|Uu zR6^KHj(N_^octy*BR25SovP8k#Hq}G-*y$~+4^?a7|f>f}Lhxlb&o(}(3{)psO~9pu=SH<4;3gNadI z9(AXVnW9LkZG^eoYde`DdE)JSsIG4=_F{(GRti7C9jz3>A9wQLTkvXgmLh{8%;yB! zPK+&Ujop*Tj)egtLB=?ZGqsEjc4bGuZ{DUJ&sy~Savp9qpT{Xg^^c^Ty+AuZX4W72 z?tQech3ZeA>OE94*C&)AYUqX5NQ8;o@36<|sSS7L3p9&-fOyVaTNxsPHJ(gIrAHYR zR*1a^0@8|(4OIDvY(Nj6N>boCKSD8EcC4(U{g02(x#2+=m|m~V({U&Nfj(d++>1c6 zd2?3`kMc=JB>d#CBkdgf0a1`t_DGCAWnv1GKr}b-j0t5cb7|D zRwX|?t7E?nUs1M#Ffv0~oY>eI>x-9+cMoABO_3FgQazN7!Zf6X=FP}l$@#}=1*5wv zVnG9J)lLp|%nDsNP&Vsw)+PM#sh^uI*y%5E@0mU1b2N?Jp*>gLh&^bwBdrb_1nh8H z^?E$U>ZH){q9&~c-=(xXq7|`_e$KyBP8`bPloR^8K)BThx>80BhM^9znK79T)JCWp zS$IGhligAz@W8SMX1(!}tV?*{8s%E(>-$2vDopGQD0Ia;bl#%T@6|0b-#P=EnpJ8svH!jK)3lRwhJ2CJFXQF(($TrOGF3i>*lxbv~S4%6#Uw#IcS<;BIUl&$M@nTqr-eLh>iJo8XS1^PEuKjOdczm=MyrnNS*GA z^B?v9@o(l>7gosMGK_;WaP#x8(f!2{IQhr;^M)1g6M>yF5rs+)c zySb0sI9;`;yKlnlaw@F(Kg8-t-br&>k88b9S#cQJq+?aIv0A*OP!R2yI?^tsAj$yI zvWRDL>^sR9C%1*hRc8gS2DYQz#~hSQKB`ZQSWw(jk)LgKPf5g1R7qI@tsQ4*zLS0Q;mjLq%(1`Lf{7@}t>$gWkATMugq!SC_{ys|DvY?--N}LWE3&zqf?kMkC}k zDRU3|upr6bR z@Ab?FCI@HkbH6M)oVu1*PSPAL&K);qq4628-05#aem;w=j4!*g9uh~k8L`=9lNhWf z=I=V856G@L{lS_K$6k0$LKvl*d^XoHhWL}kcca-q!&wDKXi^>^jeAmH9jU4VRwwh4 zXi?H<2$&xm-+5eGu%vwVe7jJ;8%nY>j2YZcPk*)YM*D;`?feCMg;K4_G4#6l7HVrm83^+F)93Aqwir*p#PxoQ zXZX#zR0QeK8EglWYt+#y3m{E5qa{o;Gpv%aJt%!qcBdGiaR&j9lu|^eMgqIm) z!gV498MsPp0mHlf=0Gy!UpWXjl*9+i`x-%At~dq8qe#!`SKMHI@Y!`jR#paui#T+0 z!2$aQ1i?;h%p)ucth4UhGG;|xcR~C5%G@4Y!PvCKUp;Zr-Ja2lQ@NY?cAR+KfR=>) zH*?+oh6!rl&)Bphq}bRyd7on&%cXxsCC1(YO!^hf`Ew=^4nn58-{dd7vz3SELV=Nv zmjH!LyF_8bSST=Dy~%xer;x&)4Z-IbkQ88~=IGOwJ40=-i+M(R!=dOgkxldI*_+yk6L<0}-vI za!q{B4yMo6Zw@~b)Z|7^4v+Bm8><(1#2b51f#C!92GfbgESqZ)*to*;yPO9{NF}NC zo5_>L9DkiO3(kgxldGC<GY zD~5N!Z`WR29Wfls|ElgH>3DE-x6TM$mPxKR)dVSk`-7I_*uDb1p5K(pNO=VX*ZT`T z7qh2w8GP@4ZmOLXQ(+0|?wEJ}Gtm6waPSRnMlW??hQWc?g|v(^vMCAFB-JYe`!L|;$KfkT}4Vg-W3 zX?N{k;Tf3TRK%8k0i8P3Mb@-D|Eg>e z&Xu+|mTMqH{XVYSSi>1_R{D92SG)UK*=v#P(FFQ-Yz1|vyGv&^Sn21Vo*yI|>ZkHU zV+czL_)q0=Cg&w1KS_8rS@fS4`oYGk6B{bd!w9S#Kito9TaeQ?#Yn7D!p?hWT5*zp z?j@!ij60Cn0RMS}=4JtG7buXkl|_VoZYRo2G5}b!&t8E-_-M)HD|E=G{|xdeqOs~= zN(!eQ4f8AGhb{9ID##fq%y@J0H(22n1{cD;!jF4rOUy9Ay!eTYEAm3sC3apkcNY_5 zf-_g&54Gy+aK4K@suV3TBBM9>k+QrgEGK3Q4RItfonB540*WvqvhYBMisk0Fw)U;u zX~dyE*K`4QH-F|{Fisa94SWx3AX?_;%8}YBk|JkicGzeB4PdIZ=dkhUS}klsqj!!4 zx?W!GTReiFLDOu$8`9^-8}*h+%LA!J3*Ww|p;D#0o*!;#kKaipkmM%Ho(rVW_K~0s zTmN|iPXX7XR|>ylm`qQx)@^C_NhAm-zP;zG&D04w=Gg2jvjAH!b6i9cgo~qcc(GIN zpZR+1{m@L?GlB3lx`2JWNoXZUCjCwPvI>Y0GC$j@D!n=1rk9qLB}09AY+cU9HE$@e zHh+mH7C}gY3KGSyZ3=bS!J>a=s3Cyw?Qq?%_B!v2zBIB9uPPB$@?iVtykB&&rL?tK=*P)-6}i@B4M;` zU5V8L>e_+I!SCzzfk1IEN*a-)$=F4bC(pIU4OPsS8PHr`oP^;DkMs3l6P_*SOtHnw zF894{)W$j}TTDN3DKTq{c%&$z1xl=;HXaKm>*GuM`$R83f&2H_iK-{Pj^n(v37k)IU{A!y=?dyQ#gP{S!Y zI?7PpeuNB``eVL2tszKk2Kyfn*_0CTS%Xr22*8%eQI!jH zVLwbfpOS^;U5I`NV4VyFOoXn{`bf%V;iM#wQz=iCx1U`B#oMQ}c!$NvTJe}*FDll_qpFgt*v)8@a?0&@I$H8hv1$2ldnL}7i zN8!c$pb2b;t9RY8OUm_A0~v;`p16bL@}mfDk4SCq$wmH2{*Z%O)s0xl+2dZp21NW3 zKNSmmRpaA$2B{$gRel{PH>L60M|i|Dk}%uNB-iaRYLg7zyLddG@Z9zbiEhBoMXb&d zp4kZanoy2;AFV%yODCp9CzFE9T|?gQ>ISrwAL7GA&Z!irlHxjByplUq+ig<=8{is4tfMns#<%Rq1ov`23DQ}t z%Wz=MDs3Y$y-5SRS)fQH^fg$o6q(HBTLnmCPq+^kG3LF{E8HHfEky+vtfsEiYor*{ zLzT687;Qa{yyRsWc7HpV8IuYpL5HDscu0Ti!MRN%`8Y1T<$i_VaJD^l=0Ne4bhe3xS6uk#1S|Zav$>yf!95iiBi7RiyNABOD8I-V1B#q8;s$*=>j{JANRVpAlrUth>59 z_iy7JnThkhW~!H10o5R7=&tnqkex7PwbfN~N0k1Fiy`HFTdJ@tC{(xKV%=9-FT1$8 zT{+%wSe0%OsDQS(@c6~kDec_7A>yJ&87;4m($GN6(XE2Bvefr0D?+i^5`*r=18brC z&FPjlU1KG}@rrr!MpXqNZ^Zi196{~Zufjd_1oPM2b@6%#qHL$knN8`{Cndb5v)6|f zWP#oTv4vW7sZ~Y_czj{z%dR^h@)h#~l!fzi98{SHry3{ALkks9EEGf`A%I!z#H6Mk z&(%U|^v{)=O|`W+)2TP!T`g=Ps-~wS%Hs1$V-804H?ibADK7l5NLe)Bc{glX^OZ6yErZ)@IDle#+{-mC8^z&7w=B$84o${DRik#}%qqe$00mb##mo!`*yvF2q>z z=)ASZ?#?(|D2%MV-qM!wr`So6@<^sXBGP*BtW6nCsNQa z69}yPBOi@j=;}WqAUF-?h^VM^&52NQSbf#60U}HDK}tTMROHLC$_z?t@(q3|U2%Tz z_i}U37rhOv6#UT;juY zNr9O1S$h+w04X)}6w9mM77?ToN?~*YvX9`EdJ5 z+8CCvhyg-~$4C^W_6W1}lqp_fb@U20L3z8oH z2+QttlAyxqQw|m+!5;mg+L(4sk+EXK+8`Hw&1i<3@-f3LKLUO7a>~K!uYO2WT<@K?mmd|dHj88kj#2h){{ucHXt}>%*@{IeA}R@op5k~30ZxFKB*MtLT)YR{0hbv4 z=bR+0K|2{1>$iaym`~+EP58|FlQXvG=0pkB?d=U4?WHSh{*>E&8`hYvZrYBHkT(h- zV1f2QGEOUDG_Bvn|NT2A{pI-6dC)Mene z2m@hfcro6vp_;+HlaAIdALBqqtqdM-nx%tsM(Y4kyb`P^7 z-d`Zll0MMVV@)wlQ#(+X;}Z``jGz}C;F&Xp9=}aakCvHWn>6-pu=s-+4A>aLyq5B= zq)1jy@`gr_j z<&JyuN^^YKM03lT%L)cMj|c%=L%l?MnH^r+;Y4Y z*I$7`Q$Sgp+n4m$>8nKL-{b!WBKbQ&-uf?w2fWK%{%eH}ycBHu3$6h#zuN)Yz2D)w zY_-Ya&-_Uk^nUim{N-j$(Ag3HImsD3@WD()c|}y19^N-_GKxtElrVPOK-_0ak_-fX zg0ItqI17Cp#U1DxTK_9?ryQ=Pm(OCS{A2*5R8~ZW4?pDfLsc)ai`-rWCuZskLJK7EjUXY-Z@% zU0W9-)sN}jlBiyUYB1uc0I2rrvrZ$&)&EX|in8|WeR&EAGe4g@3N|H`8_o67XJ6+q z!n%E@d=D&mffp8$1ETb0Zugr>|CZBsJNP!bD6FTOo*c@tOCrWq^4L3{{z+0@f%`<& zZxIqOsA~fB>PPoE1*h4U&*RXrf<=YNPP#J592UhctY+J}6&g98lzQy-pb3hbRh}p= zG)8c_jykCUJOdrFPZXk&aXnlG%bNB`xV0*(NW8l%y*;^&U#dH40{B|4SQ5H7kx z(+OnO12+EaUcA*VotwT5x_$w?yd6gXtA9;qv;#)w?qD;bP0RUoO_m=i-812GMKx%7 zeTr5Uv}j8zBqE#DpA7=d*D-pRJuTg)dPXB`dB1L!%IjrV%#PnDg=6y1n1}PpuL*HI zZ_LD?=qop0{m^cb8LM^N-2fx;W_cj5=%*nxJl!yHQaZR4%Q*1@--h4LF00veSg7P zn7f3h48!Ncq^9!|S znVhm0B@bXJ`oKfarzU?a2@hHH0eS>A-b~!ED>bavQeT3RI3q530L}XruF)?cDSd?Y zh?*!pDn3|ay4XNO98$V_bR}X$YHWLmL+YEs)?^YUTS!5H&Dro=3x|$u1xsOMipRA_ z^n9X7h<_XgzH_G@qydil5)U_LW}t7L>_&yY=uYtVm-Ab1|ouFHuK2?M)C$sBY%7?cKiLtB!fOV+_qz(@rDS; z>ftXhjxVw}yw<-62=wEt5gG#?+kh+KS zV^hWPE-@GHeb*Phd56)q7-qaYLW+8(pV`)xOy-U4fcXrIB&?#o2IIQ71fQScnQ z6*pn|iqrD)2%cIw_6~KsCs?&e?fn3WwKmvt&VA!B?#Q^d;d5)#$+mlXbsp8-^d=$) zY8lUncC*LOp?BXs(_7>+EwrECH$zxAd)rfzK;2j;3vJdC$h4~ozb%DoKE0U17t}gt zPFOglG=CcnOCmT4dB%=BxVzf7PIGYGsr^LSi=5_pg_yJP%SrVDO^oEyLOPwb*ZX|g z5zqNpx5;p{v8P}a-6148Z+WTdGI`qO@OQi_f?YByao9sNiBw08)nfIDmXoCpr^W0r z-&o8C*;j-`1@q5!pPi|)i}eT=3p!3Pu_04;N@2~>@}z`ElX_@gp^!baigS2+tU(aM zbUeC%Z|$5`c`8wIu3}?z_iKKzKDR0ZsGQ9r$v)&6lM$h#^5qx{ z@wVPmEE1cywIvecWdP(`F*yP4zjvNzDO0kmG<(A7YB=|t=evCQywvGME ziTw~Y<<^3+l>SJ%89Fmh(e68Uvg&!bUXQrdR*5BEzxuhbwLK0(8KyH7+4FYSD)Ryb zJw^)$8o>DrF<+m0G(J~SB8|TXmzIvTmXJoKm!>o^F;K_{5a8w1>~D)7%J$ypgT@Oz z2}s#RF^LF!I7&|}`bhRND-AX0hMH@ig_Nb#5*~H!6VuxnDBhpJ!gWT1(WPw9iz;5| zJ3gMY@rFTW2=ol@kJ2|c!GOxs*z^ZLInMm~@f)()F1w*D9B39->HIeGNBOI|^evI< zyo)I=o(E#FVL~-WH6BClqHTOK3(8?x26KkWr`)l~V|qrPzgmNq?HGrOBYdyAr&<7G z=eCzCVQwE2KLL+361vdLivrAFH)F}U%~5M zZ=t9OU&0o#5E?*G5Z{>r!h_&U0EW$DVUXkkjG`rrXsD5QI1ycNmAdprk#-dML<^Ox zOtWjCw_0h+VosLA&7~5aacL{zQ;m`+FB=mqhm$?qV59?f-$D}<8w;_7pVT{7ZKm^+ z`QRcPoCC$`$r=}?jH&(P;o3P4*|fGo7j4gYsu=P%K5r2C2wj_IeHRw?utCQVJLOQJ zn)zkjvl@ab0>r(u8;BgG4;eY0Fod`Bf|rwwz=>{WAeq&vREYV458uX?;}kVoa+v1) z4pS50N))5KvLmdN&B-5WpRdEmEjT%uzGietKRTU6((UXVsVAr95D1Sg3<$ozxOi$< znm~Q*r{q}63!!3#ht|5T&BU5mOAaxl_Nn+fIH%}awA7@oz|ZeZduk+(D&B@9sZ!%y zm8}5t$rx#*SU2>Ifr(A(qzEba9Bt=@3B4UgLqd2!=^EjR3Wv!yALD9 zkjNt_BoyA=(_pCF9cOk#lWSrDM6a1*v^-ybJ@5NdZX+v_@l?ZpBh)&lKHR~3`B==} zbYfEB%5rXT>y1r!H-GE-TEot`yVwd+INip_SEyH6Mx7x+j=zi$1~N&SBIGl7>{D(@Wty=wve=Ii^8hG#gF8!HTv75dp+F! zRjtM#oDq>#f#2yWTKWcnh4sQ@w!qxC#&8j=SQ6tt`zWWrUVcvuB9&;TxZX^O?QpO9q}!4 zUwm|jb2D>wCe?5ozxJgBu-!eNj+|faxdnQHW~=#R6ZanQquTFZz+!4*zL%OiVscY- z*K)iU|6b6+-}JnmQ>dM!dRgQi(1J<$JM#VSRUX^p;g~|YE8`;bUB^WPyu}G&wdcX= z;qlNwx%&>O&8-XG-Nn|qLjLr8(Y_<4_x!7zW|aIcFJS?=7bH=5wtMRv7GnnEczOL5 z)rV}R-_U65h{Qiagn<8TaTweMb(_$q+(iS4N8J^4#M}R)5#Tjc{(vqewVy{3sTlgS z6$(%ZN?V3C<~wV@S!Pd}nja6g48OtJGu{nK)C6mmo^~h2W~2@DadK-OUTs2a0MS`Y zpxL2$$&+FrN74r#9=YDhiLH;J?f)?^-U!XHpW^i%}`RhLC^F!3rUQ6gyyFpO3ARc=?05_%e!s z;l?XV>^mIbB9-#%9t}c;5Kf5B7U@_X;i{T zlqtw@!!^GxQ2wuy`0E%ilvLR_nLqyTwcPig-fy!2$=+M>?`zPq#joR4j590nGGc`Q zKEVt!_dl6i5E>A1uTI&(tOqU2=(|aCwoqd>63ifq@vbTepO(bMAsp~OQ2Izh{)eRp z2au8nMz+NIegpX=kcSD@>1S4i+Qr6G1k$~*&s<`*{mBcz)8u@8O(wk>4gnt?-#-Rd zz_;?Emoi*2g|H!lE!OC{`DAZYBmIG;fEj`FT6ugCi`!{9&$P2oI83XG&j_3F^nABMN z8WVXWQN)Zzt#9;?d}Jv-RcY|4Pzy~rm|Y1Y=@wnBEAnzYd~jNNJ~s)M z5!qcqds(;zea+&1KE$fF*1hs*h(UqZ{a^BG)Y z7oABJC9{&9K?dG0zxm?&t09z`|5PWw(~$o5C|A_gRaN{QI)o=C!~J9Q=O+|Dk)MJ5 zr0tC#@HPjLBN*cg@QQv?W`u9=>=;sbG&`@aZIlr14NO52qYM^YiU>%>iR?aEd?K{M zJNQ!g;nBrKd(5^E)f{Q%+vea29u^Y)ap|qw*7x>H8;U1O9+%@R??*Ac&uU_L(hXDb z56EL-_W86S9dGit=6au=MjkYN=0Q*){k0?pp;YhSRS{E0m1KXjbrVN)~m z5fg{g5{IsRb~Otg(CD&jNb2WEOw~!h&RwnrXaP(bG*Nyj(LWPMX%FZ#A4#c#f*U`u zP&4`JZED5A#o-k3emp_4cuTm}Vf(eX0h2B#@fRgA68|;pxRSyd2nUb! zh>qXM@cBEYiuVMJo`;-SCX z{x^LmNxLW&xc<8&YO|)bnFC7ron{n-Ic z3wxzIIP1Jhjl*b^q4XLK9ZzWaS$Q{l1&o#Khk3q?_jPakR zr+z{WAlP5~HPe|lkO*(XEh zBW_XHm~nq1Q#J?3*$aDK@_W%i@`rpVd{?xe(j?S!*fG5xA=A~)<6=a7FX!j>Rps4# z4mDqYvS6?-i;W%^J#23Kf2I}%+Uh)?^iv{6_9t^c#bE9m9ABI!A@d47y0jNq1Uhy; znuMlsa?cUEkl-XRE_~@i+zISK?r6-lQPQgoi39?enA?tF`QvPlBsX6*Yogg|lw$oc zF+Qx7L7PWZF+CmTUt>dK6%wQsH%d;YiMvy#6I)+3;HPYB=Yk&97d&C!6oOxad?0v| zLv3wKsgQeeqRoJ%qPvwPI5FKuAm{DzUQP7J5@ul{u(#)Pv)&G`+bT#4j#J3Zh?K=? zdscN}&o?kG7axIXYBisaRbskF{2wVNR46o-2UoEW`9E?S%C|mb|ptw$1mPmfrhi9E%;L*n|0}8??X{Zka zp-05+M{j@pAS5LCb)(mO?ZfMTkzPU6v7UEbi*~yf*xKAQe=>q|H5)sGCC0)!AtxnE zZNowcLGvon2s}*?`^kjT1tqhxa*5rXzwC&j$FxU@>hXVLOp`BRc{)ymz6F8DY;r{h zT;0y3KMLFpS2=4-PdVS*i@&URx#;VkB9SJH9(*Z6_$ng%Yw&*0z|hUBre5ZDl4g36 zJoH8YOclEB;&8Z->m#?+UWT894k|f9o-BJPOJ5uO8GZfbe%h+@pJby*j8L9L^whAz znKWDW_1zh~H+bC1@_9@NW4zHBpuL!PX}pgCQWIwF+ZQ?R&Y%q3)6OoXe~@$^CEGFb zFMk@ytwLtt{TIWCLY^@@{Cy7cNFs4@hw3InnNM0JQz(nK!v*TS?A&c?-Hbp$H za&o-zgR2zSe3{RYMQW;FT`%U7kvVR1BK8pmXg-iqhsP~}<=erdYl4k)@kx6bZ^BMkPpVAreT}f>8!~Qmg?Vc;VTQgqK)B=TWx2l|3Zn=6h zRalR4@VlMEQ5eBGz?$cEmpsF`m29BeUfy4Df2mL4j=H}a@9Eka89?1cH9}9$lT&hc zAQ`E)KHL*2SRbgeUH*N?nkwXMIyDNAE1YPpjKEAkdb&#loH|8TV#u7PXW2b$#GqmsgN7YwEg{#Z6|Lxna(p}hL(Oq@Bw2EK;)UmQj4W6B~OCvu#Qi))3EJvx3#_pix$bfbaG zKiYnzH)cK|u%ec*9q=dprcj;RNLK7yqw`7SOLGK+n8rD*kZ7>6N_U9mlR;6Df)Of? zXnfR9syZhp!Hg!<{H=Xc#oA6qpZBmm`@SEfAijJ2iO(Qr#DTknKcjDG2(4dS>jN38 zS(cYJD+@det|;d1<|lL_qo4DEk)*lk#Z__SeZyTpC8Nan8*d@gB15ysMl-6e-9#90 z-W2wx^0ry}-eQf+qo+kF$+Upna8Yq?h>r&*PM}ZU>7^)#%5Jyve!KI} zev23NwAP81<_o{5hG4op#%8V%;#j=JaE(C zafYv8tpHY3gJ|@m6+MFg9g9hVkd<7bldqYUG5Fxi@-KoIRo?!)8yr+-YfT{`q1MN! z#Kb&lMp5~283bmfsU3x&said(9FB+C6uKpRG13Fp{&QT5RtIH^9=5ox;# z@`mR8c>k%T03C_!M}viXgT-8v=TebcjBTN%dT{v?Q%PA& zJtr3(E3-!F-lG1rUw*6SRKl8U4t8x~i|wI+J%{Z1WNeRaqQH?f!ott4%3HgFUa{Xh z>&dcdSfJEHjjLvY=9OPE?gb6?d!Hw_&}yi`AU@1fy~W;27cF?mDT_M8v20xsl*r=V zc;&nHe9s|3>Ztj#(HnQBaAmT)1kCYKpSGco^=iM)Wv!QR`^f>m^D23CmAi-`F#6K) zq!y9UzQcl$#^0??td2ylUFn@{n3Bz8XAP~U!au5yQ|(R8IjU)Wr*O#4t9b z?eGmxdd0AZcBR}z_rKw9r7QaJqKbiT>Yp zm@=UE4IMojHAd;{CMua?QhK>8BA%4PtUuxZbhpuql>=AzU!q3`V6h@RHNRZD3~)of zg^kE~rRIeE+Q%oYAp+>(S!Q4LpQiv$2rI(yV9Ok~)ZFr7Z|fxBgnrO~JI;Ar>K&zZ zHpvx$$v&GeYNw2XAVV4xErb%T@fBc-o4Zlu8v)!D9pSa#g}?Ddv(^Wm_qyBMDi+P6|Feg!QCt>4!W1T)=y-@zQS5F8&vuC_(5RisA^A4O)Pv!>HVQ^_y zP0K-SR(4J35JwH8T zO(h{Zg9T3Ti8lVUnHp&8tHJWI{qdDlbwfj8ETjFQi~dMZ)ti!cZz#0DM}Zu4$ZdH)W121I&jPMEP)6n-O0gwWJ+2aJ zH+}xh#vAo@4cPA+_jubsJ(J7rO;tMP(5vZ;yn#*EK)e0~!%C&A04aHItJKeT43Zv6h$yTBx{drel6r<7+2}OVyAyV(18S zt-wuzNw%mnNKGAUQL5#WzUf;VYsL#oVs-?EicXSP@iOZ>xnZhBXQ+RB?F_6h^fUfI zEx^M@pQ1OI4-3mkN`JZJGzdB6@QppJt@_l3DnmPC-9L5Jop?4gzs>mEQsi@E5)O`$ zdma%ePC79rDxRKhqii#K`KtDc*LKGG^tI7Y8O3UJ*>!vB(C-5bejgX^$7;$#K<%Zv zwvIJP`r?_`nfcy#G*Jz3eI&zlUdWnWntvDcf?_y)!tk2BYlrgHt&`_Z~{xn zQ#C7ThwD||q7~B}E^ikVOd$q8EOA(KUH0VGC5V=KNM>u+zsy=aFhXj#T{5g>#^|&%iry)1GZ5@~dczQzX1Z-aarnZp-^v%wH_0Ya)D1XNbzh%zDnPM?mfkGNxQGs7Y z{zJmO&V9uvX3ko#>6#*HOSvHc6Vu!3CYRpk8jr2kc<;1^bNlRA0veSdd4`?ZVGo0s z{ti6l5Q$>Y2Cz*%63q{^=AvZ=JRD7D4%CnMW8A9N_$67*CTCYZKT}J_=%y_-TJN>P zzj>ZJg2LP>L2!6i>C9PeHFj&~+bs$Fn%5a^Mo-Hq<&O{0 z?4xVvi3?7X`QcK=b<>aEcKho%@4+s+LnlMdYg;q8SEt$ZM_7#5+r%EVHWQ3q0YCD5yX($_6py;Lh-ZFjR{ybSW)6C&GQ zK8>wa1N0r~Hhc5NoZ7I;b(^fk+(Un-EXlU-4Xj0QZa(&6B%qcx%PdwJb&u{CXjZSC z=(=wrVzF4Wx^#AQYb8(A9$u$&nDqW&lA&K0d=)Nv6!@k01^ACCl2-jjr28%*iP0Yltnt7zr71B5Tg$B$pSi%Bo}Ek0=lu~I9$%Xp+8X#q?EXPFXDX<( zy?!_UhTfgE3Z)=e`ytXwX=-wQvPOJ~J1iF&#r2&iuIuHb)Uy5Yuus`a>4;3(a`H_I z%OW0ps|y?5OAlXnZrHJUy)OmZ;iRdaA@zxVtfsL)bsW|LhLhdCh>3 zASEfz>0K`FT^QlQXTGcH)Sd$2X^*}z(?ZkW!N7v$maDM6S)0-RUBX#03QSoUXldN7 z9F@cK+TU#?dkXubIuy5;PF1W$3I&(rv%AaVKTUDq+!@E9bGfh6Sj+25>U_!-!kgPD zk`O` zF6n(GS+Y{`Apt3eicFi*PKRV@F=DujmcscsVM*+6CGiPqD2FAMNDq=EyCY zF$ahzGkr-pMA-FeJ$_o{zf8Jf$xs~QoQ#s0WFKEU%=oczD{(|zW)5YK%4l5VuRg{J z;}4+cC0^8b{=j(Qq~bob%_zn7j^(zyJQfRKXskYatHdEvui0GvcDKW@)2Qa$UCbTb zGKmCsfy?%2&PhtunMtOt-EPn*=YU6?UH)bvMwB3}iK6y=bH-FHxE7BVGu7~FiMb@W zoyuWsm_BRu#Txs|ixm&tWMp4&8PZL9i(4YgBAWjEfr`}Iv-+AnBs z_l^8Fbs}SGkvt*@Ypi{dt&a#O9Xub+;cezeCBkQR(!lx4mB|f8Kk;!{tu<{n(WZ8u~2wfoFAJO?scyK(1jOt+Eh9JpNU^uXIJ z$}BO>C&`XN&h%6)XLC?65OcAm4Q=Ig_->Oyd=CMUL7CmKH7g&c`&Mv38_e(=f@d$4+LPIH3+UG%~>eK;p z<5ddfByj`n9^v+tq@e-|yHs_~weFwbKDfxKMGEBTyw8h=;?QhvjC9PRQZWr7kt8sP zTw@whLO}}=FCrl>bBQH+8}>Dx?r?sXtuq{p);W&I<;&!51${i7Ia-a?g@VM(49gM@ zhQK&hxT5J+u+0zHTesbi1X{v8s&0-E27_1{tBP#}b9eK|nNZE4ijPVnMP;POH3L+2 z8=+%`?W^3PPSN=W8dE}OzEmpV0I%{Kvg>E$2JWFbrg1*^qEX97R0#PVqrL4ON{aKl ze;Sfy56wm00w0lPSR^s{0Z9jCJ~~V#sz)bU-6RByvI?dF$-W>7QkZ*k#Spy=ChB_; z*$@MNp1Z7>39OZ;xJ51-zD&UYEj*8B`Cq}u3deGo~OeK!upN;Wu`U5gWq)Dpr-wquKCG?rR9Wpcgw5m>6*Sy4vQ;c^CszsdaNDKl|8*6$3Ovf;!t zUMYA0YU=!WLQ>%IYW9S~Hz#H)#(@9b@k*oNO1s6NmV^RZI0AN4R^+L_n0JJ%3WCxb z!6G2_81JyPkYcGufGCt5@9=bZC)oF<8sQIzwU`}AoRotxl1`qAjV;MX!|?6+gLV}| zT)AAq!Gzf&Hffj~yRPRt{7K@PTmGLt6Ee!gy^~F5nOC~`dRq;-RY1S+EoG0bQ0LiS zp~J6ukIl9-arZDwe3mYW*I=UB;1nlZi~(nDyb^(4nks#t-Bev1QR&unW$_T}7HX=P z_^BVo$CPBcJ!-JEC68V=FfP~XhGms#)WGs`xrVq-Gi2|qP%O2|I zEZf(CS03061Mt=BJXS%J1LNF~UQ}FcD_z5}bO5kG^!F!1&|qlOLJGzuB&K#5G9z9& zOxP347C8&(gh~|U2)88nE@?_Nv{dm@=tvJ~p`o3&ig4>C>g67b>JkhMRFa8bRjhzgJg6d!tCk8rc zeWDbAw6AlYu3uq&4+gse`MWuC#M=(oM--^cK?A0w{N8|IiK4+JVJ0R?4y9Q?T-^FY z(dMd!Y8>BiN3H#Du}TwNrWmy(nmSCMdV)z`T5vjVtk>kZK!3El0HqdtI0$8*i2C?? zOiFeJg$4F&B~zx_k3c5$Sys{3FY#p3rpxcyk*!!N?pHX(}USkZ+dWB#8d9w#{!;}5D;z9Jww@zjRnI#Q9%m zK1ctc!Pt(DN7Tz=aU{%bZg}%?_`J=Ne}{ zQTT8RZoHg<$+5loQ`osL?eh)iBB?6&YMVwAE5fv!8pM0fmh+Wc`%UhrsD+qpC)YbX zl4m4uZ_0IBPS*1%QBX-|3w)tT>F~IY6j80qv>p_*Z@gXx!`(;uz+p3mTZX^r@cq#% z?iL2;G!i7UU<`=xmm2k#KwER^A{U7GX(MdyPa!hf{MrrM2A7Zqc>CzOMBfK*r?!4o zY@I^_*5HzPU;ikB0)?noVJb@37UvzlaO1au4(!NF%uZ3>+`UydXGI2OqBHgv7w1K0 z#U=+EI#Yf<%#PPL=jWC3eTKn4Z^W@=mvF6C^!xjt5y_rephnK*x~N5VuZ_^gV3H%! z*qfuSI-5*_IYsgD?zXQ>3W#dpaN~Xx$r8wz^0n5rKU#*~9*=XC-hLrMM5kvaqQn2V zVJKFXZqR#mo4cCC;Q%UgknS6uS{l&~H#^>38Sm>3A=Nu*A)pwq4HDi0m$~FEd7Sy7 zSe3OO<1x9diNh|My6v~o9vPQ;^xBn~3=M#-Z-b5thM*%>u?!a6#>d}yY1+&p5Z3Mx zM3TS?@LK;WZfK?t2*X>}zCF00E@6BwaDjKx-OedVdC|eLobWmu$2^n+jBGAr3tY7B z$7!E$_kBGy02l8q_mP%B*BczZY^k#0nl121Ta@A%Q~y^+La(VW-;ilJaoz$FWZrGB zt6NtvVYv&I;M1zxzdU{P0#|@KV)mQr4kkurBK1KlbtYpJHxzZ6PB!Kx<@#%-;$ymH z+OY|5syu+&EOV~XW$#HK!wSpP`NGZCC)5y;53!SMVWk$}Yc0Dkj^gk%qHPD8-=Q0j zkgDPvG>AoR+Jxs2p%4m3gdll02BQkBechL`ld21Ac-{{8o~kc*i262r@#Whjo4UV@ z&v16T=`?fLPb$|~Etfe*&RP#brEc=j<)0CP77`}CdIEyeJPzIKahuE~_EOQr(`f?i zKlq99&4N<-O7(={Tkk*v*<;o%r5sK#yTP7I>^5^YyX|i+`s!=-?TJ7ODa+Q^H%*>e ztk#RR#{n{=1RYOq0ID`^mG`pvA&U?i%_r4GBI=cno6sp&|5{cRFlBFDF5 zAy381!dPJwE{;8vS08%O#gLrdp8xp+ooyw*nYMc4e3|}RB#nGrmxuX+0DqIAgkB|U z{O2(*d4YmzZAGBfN$PLEPca`ZpMYd6K{|GV^7|%$w`wNM=ZT&;tpjD*_;}no(X>hp zO~KHTu0m&YmO;=Yc!j?#PpB9awj^$ygp98)gPTp*RXA4vb7Cg{bAQthTid98DUT`XWN9*!~#+$vL;&!-H z!1Cs0HadKk&nmZ{vX(LbTwQ(VM|Qkl$naK`H#U%lcj9FoEg>u9*#q5d`+yWhCa_-Z zH-Vo*fj#tSIN}kg=fd`$TmsAF7N?O>I&4>-OA~h)P+0n>NJLxG7iXqY!?~icZAcc% zC_4?Kpmm-r_u|yH)=@Ht+;EKbjoTGH8euij{%ydQSE1){Tm>t;lXVMoJizTCOe> z3f?cg5HuApIm;8c5{UzHmymzk!N`AIX+>i9H*wAv7>CZ-kfAqAjaAW4)z?-WO`M)# zxyEPnG*~6p)kT`DKwU46dg==p>^ayV`J3rfv~PFyi}8nhHXmc$;>`?%(Tv>TVqSAG zM~Tb-?xd8>t;JiEoXB82J)eLdUiIbc2+toX=JVQ%ZYD}_DAOuvZ#n&*)+E#C>=^5u zn=`Y|UQFt2!*R>q=IqTtFNNc*(`58<>v+5uaoOCxKJmPNGlSUcp5XmuT&4T4W{y*t zS$wVQcjq*2kQb?(yjxk)s(PsLyz4@KXPA9=vfE7goWs6r8sUh`GOn1T4=KEX7B@?$IB}2V20;C&)#U9krE{MX-N^OP=4^6)J zaLh@MlW>vf7JJk>~A+};$=Jrgdjine1(fi@)kuVMv*d=rciM)s9t+B zS7Rcs2{XxjXATxi6#{x+By`>4MTGU_bQUU*urB-@5FIsluO-^b*?HSbDyZXr^bz22 z^BS%#rC4-l)KMBRN16lipwY7X}NLIXNBGPF}&zghQCa4EjRwt%F3u2t|u*xYPHe z++P_NC4D~3|DzrVCH#l9Dxc{MU_ZTrTp=Fizt*vn(E;eAcbt`#CYHa@649|1QG%l8 zdH6s_AwmepKOtwzeD$~S8h{B6&a$yH(E$y^Vdnrc^pO=oI$UZ@;q&>5(l7NzF$%CK z(0W!U*j#Q;ESl=Oib z>h<^|`}p_e|1%G@u=>!xLK;HQY@PEgtQ75XI(n;;%rT*6 z@$)eQreIT#@xs$HNdk+)jjJ@wfkdYg5JVN+X_|46l$C*hLVv^e1X0L8-r@Wo@~<6g zq2>RTA@7Nkq`}3d%_6L%eRG-dxsa8YiC#i0C8wf5N){-rKxU9+YvZ5v3vu=QCMPS% zw|Dipa{ih64nkf5JHVE8S|$wPS>0V(QuF^~?yaKY=-PGB2(G~$0txQ!9w4|i(r9pZ z_XG_d+&x%h!QF$qySr;6jh*KE{-9?Wo{qgyLAHuxGBjfZqcXTLSf`y~htR z-aZm*7ky&CjqhKi((xge(&jP)XTWsA%SV&1Re=o8-x+p~{iVf|bfRg;%wF5eZ`9ts zN;;W8u@B96{Gxc6U>4z}jU^(QJUbb}-S*+X?X#!T!YETn91?@<^sf47yU~&^xs_@P zm|Y(W)*DG|FSnC*BDYofT`e+7iF@>T@#Fvtxhm#;*Eivx$M}VnIps%x z|CL}eIsbj#Q$4Fd?@H0)7em&hKC>wv<9mLJ^5co}nor%EZEd^ox??N+bO$>)<#IBa z+0_TcHJ-fi*U%Sb<^J7Hdi*|yfbyQ|VJ+Ms@K5ATPJ z?JBM`1Gq@3F>Thasr7mZT7NQUkxNwcwVmF>mdlom0vDHWus!eo_g62(f|)GHZ|I|n zudAgN*9#u>Zo@8E95i;Rh{2F=nEW;|MwUrq93AQJXRs-MH?lk+4e>l%g_pjUm5w7l z=DF6|-8a5psZ~@F9YuF7z^~aWKJdtm)r!S#&KO9DXHgWQ=ls+hQSnOB{NDclzmc?! z-Z2r_-)6=K_Rch4F0v#Vq&G|P$@_j1__Av+#ZOq)(Pl`PSSV!>7Q?S?9;UKeAN@&? zWy8NFwk@wqEU3D}a^G}%sn^*vElcN2IAKlZcs^KBYLe2{mu=$NIT;|)<|b`$r(B;+ z_-Ws-gPIv=o)qde>%P7`3^@ zYiQADk9h^5!IPOsp2Vx&rbk-uB#~)xp9Ist0lv?zY}I%37`&uWvF7zGVYt2j4aDSk zR#oH_SD2|Vy$)x+*0d+~{iXK7&Iv_ed4w~}kELrY`V7XRCJpKU2%gbCE4Gc)bvECu zz~egg5xG+!Jxe^-N7$HX3S}fZA?|$g_Bl(O@fxLbrJ_j*^_j2UR0_7T($J*~dinNr z$6FD}@0B(cKbB?WNv6oS^aHoe<>W%6sWl{y^nINE=Q_u~i3n6DGp8=_3VO{xJF{tD zqEuH1wFdho&y7w=R%%WALY=tB`I}z&LV#ZEZK_lYX!!H)i0xAMU|e-pQCth(NDb6)dq_` zB%7WHVe#Yl=&ZVJihG_P_~02v@t??g*h0)zPL%a&ML101$f)S5F3lsSm5v+C?+wBZR`P3w!1_lA=fCd$8(yf(1b znW6N{?;v@)I2_Uj<`do1$kooQC`!J%IUAP6K>s}AAkwwO{+}4K_iS`GjB6j*?j`%h zQr3YvZVFkg?uWw%8CFRQ0(e%?C|-JYfy>?eq=voul=s@rz_c{2M$Qv%vl?z(_X|tS ziE`iLR1z&M6T!wO3aLu;FS~Kt6&`4eOZ=>NXYa~=IFC6jbeNg>os6&^ps_8AN5)Bh z@t7^`t~YU}OQCUaT#i*-oV6t@j>p5#q|4NWr&PEtf*SJ5OFockPg76Gz=OQ5l*y)8$TW~vg>)UxD8(}r`*-*=$7;8hw zu~aMA&8?-Fz1W@j$A0(_9h!Q(XC=QBV|tDRqaJB4^)4^x^a$$;5;&X*8|V4pwzWHu zcNINMv#uNhi4G35y=qc8no>Ob%bHn}eNpka@z~{2rYapB+{%DME!l`r>pSSC0vWog zD2m_iOX1O`kMa%;E(CewDq(w9I~XM7!{hQOzWCyxs8M)YQH50 z#Ua7hYUUBspphcpA-I|C4+I7nDTk+-F8#iK2)zzhrDd5UxG`EZN92Gj)3PH2^10KY zPrt5@x{1g%S5JH*S%6~G8aY3--xI+#ehd<0cdanX5s(|$>=M30wd8jE_&`YkXl-hX z!cWaCK?zRIv|?1P!XxgTm^0cG`*E&q`iR!uvNitdMi4ppnH6OKoj_ALl(fJu~+k;ZE#c_KT#` zMLfSYu3Rds#(i?-ty&!eC09;YWZ>%^x^wymx0tZtC_O(A6lIxVJt16+#SF&6|FMPn zxRV)=u2G$yYL2pCW^i!B_$&Jrg%L{`XIPS7sHs?C)x3BQ*@8Sq0P;UqW}vg7|#MmMUGqmLcoz>v1pO+akM~XxhUa}rSSFj+JltA z86?j4`DI61hO0c~Bv2 z>(A#|kcpZCo9o0?|sJ8MIo8Qk}oQVMousr7W*D>B$e zJM}5*JwdZ;bNLN$uKtwgYST%~Qf9jOdl^xMB+d!wE_~Xu{>(7V)#Iuhxgrm1a&ogP zlT@$X_P&muEby`sUv^C?9Kp|<`}``YKTym~9S-`dJJ#L~?fCK|mZ1F~j6eX8qLPP- zsk3WbYrz5~X;XicD(ECS?kjCFocJ*H@k8=7q%VGewia36+cTmzpEn#>7fd^lhWhFD zvzu;JffeFg$@ItF>zFGyF%?*tph2$EnbL=!dV0H;3T~IIorV7b!P>rmVtx5o4SK|% zd}WhdSnlg+r4Nrv@R)_3KcvY{)RwQ*X{^-vGz)R*)fA-hY6U~(n!+xd!&_qnhW=uz z^+hX=3-*bcSl&A#dlizIQ% z@c_f64w@oj^6i;m$U;d7uWER^18}-B#mb+ES>5+L4ENz~B^I8Z_ef0c7VFe_{y~Sq z3Q=lonzWyf<5Q=UawA*WuIep{Taf7En@d`X!JdgIhDSKIbj++zzStQLd{GbN68LeH zGF#G6Pj4D?JGgfEVC887fH-P!EmKJ%Yx57v!pgG5Ag;I@eqVafhwBx0j5xP#8%T+! ztxi-Rb)qyfDjf3j8$O(=Z<7qQ1exmTonVxfGR~LG3i#OA#LjOl<{YVydrNHBl^Uq8 zv-T3obDWpsWTbp5CoPhG-0sM1|?4i@i0G||b?{AOw>mq;J*sx_tLxOV3B zbv|WA6>j{tVU$*UBi0f#o+x&>6lfT>L|x$zVc|g#>Z+xuPVA-{N$4r}2`vD1tzB4*hEk<5mN{i}{qyRO=IK+B4z;)|I(Mt63z&0) zb^Qf^xZjMC#hC8Ocxr2`P>zuFVjV*Jtg-K}bPjI-1KGx@&F;yqa~@xvpO_R2dE)+z z_fuu&-51(y&o71!*K<7ds#Yk>dt<5`6jT=_WsxSpwBK~-F%OR2!%v8eyJbfELZyTVeDki%9v2V)g{c9$GN;%F`%Go%s$uh?BCvT_QV_ zjNCiSd>=L6(MRa&i*+ik$VgziQuGyU`;R>mK*d$T)>J*JxXok3OYR9@vhZ_0<^F-? zk-1e&+%vJj^I=$u21js3+M-Iu_=I}wE-ReP!ddBjA}0g|2cG* z`VGi`Fj7UJ^7#6p)J8FmXvs3_iq(+|0v@6g-9eVcIrh$k(D2zAipRCpx^O1@ZT|e( zMr6eFHyv^C*TC6TxjD>RKiYS&y4d;<_x-a|gD0~#Qp*3tA%N)Kn-mk5r{vHwJ$Ni( zCj?U$P+C|yU4xwmr2XP)$T?_&EmhkKCfr60c!Vi0pF)!31rWuTT1;&;v`HGiAbhu@ zqnF7M4g$VgC_Spwuci#wO1@vPjAidwJ9Kbfjr9uU;)|#yDRrLNHY?eV7z}PBQaO~sWPFL{Dc*D6^$N*QSr_pf2!g8kB?BX=*kcDDgaIK%A;=Z{va5QRkB zUd!t>mfbfH+jPWHVkh!o=1!bQF<3$PvLG{KPb^(SrsF{}vqb9ZQx!z0FC`_0XG0I}Lm zHk~+52nbz`0wm*TNhwOSe8pz7-&o-tisA@R3$#R?Apwa3q7$CMZz%oM|82SKI(i7S ziG0SC#kqCQpE8FMJ;Ze*yC?3h0_5Jjp&MeZlfi)5(S2vP-IGik838xL`LWO+w5$0I zvP)h3(4;-bI#*Px|1#mW_2nDn4>IdMF(Twz&4Na3!n`IxuoQlp^DZt@e7v*bf84qt zJnACYq;os-o(hfLwjDqD!Sz6*HwZ z`EnDxyS?7nA85X>E3gs}Ittf_DuuXGf8_z*fW80{io}#_W8-|^E`CjWzXAOc4kQIg zD^j4}IeP2+8b$(D8$j&+dB9Ac_*83JB4MGYbts}!4$8HZNRU+keW-yXBkQ*rhkleQ z#(e!p<&o#jx%ZE+tYjz-1l~b1S<1?1vbB&-{2?QP69&RjV*-}{nwOhM$!4H?yB7$8 zo1p~(0qw>Fk8tN>uPdgK1CN5Wcl@zNz?RuV9zW%GI(Ri2kUF5TC~QD_Cs3W(nNKTt z3mavT34kVNs3No+KF=aNqbd<2ii98gqjW|qY_%8ErKU~Qq zI@c*py>WsHVd0_U&X+$x9vHb2&<43R0)0P`nqno#F)R>P~{K$@R>;+^fm73>TZW8w4P6K0ig9B>lAVa`}an7*2 zA;Dm|an2#dXK5XOTafT*P+4f(M@(gR-1`D^Ev=WE3b2bsKntwxo>m%Z`oX}1R95#a zFV7mgSwqa`(r2@!{-EZj8&WB{ph5H1>C=mLaKq4%Z{~i(VB`oDUhyG!Cg>`Ex~8~q)&`s!T9dyDNbAz z7p+q{A25aQXt%Yre6{GH0Yeb)k{+Vwjik6dHZZK0V5dx4R%YcUWwIfo+K-5FBcvh6 zY)fIgSpM{^W;ACWuhNnN`@e?imaIKj-l6UX)j?9MG5ycN@s>a=>!@3>&g~JPIw|iSDw0fvl4j8uU{KWi~HFJAc{>fwC@};PAh1 zDrX0CT&s@`!Qc{ywv0L+KCq@^g22`n=oC(6h?kvtu#EpG$Tn*C6yp1~IGWPZM{7Ya z%X~>byvu=&z%ER`m3Vefa`HyX;r)#dZ=WiY0!*u18?`doZdtx)&OQ&(i_u49_t+a{ zdAp{1t<9k#+c}jn*g{4=j8^J2&T(07oZdD-sMa(8zb~JTHGfcStFD5$;N|i!qkLeYef8S}wJ8x+O+j{Z?;n1hqa=m zjh(V~;7Rp#Rv##Gkn?a~-e%J2Q_`PVz}=H0#8yZ7;Ud!{=QG8Xmp8k#Xs-i$j8w`r zrj}><0Rv>`$~bvpRLDCjltde0IkO-up-MP;J*Pkh4hrVP4ju+At%9GjCfqetOUA66 z3gUVGg9$4>vAA91nadN7{){)f9)q6{B@*ew5->WWKONd371R3#LadQpgw@p^ym51o zY9EvS%}gs|6GxS_ysyZ;f9bS@ESy|^`yvqStbJQ~+gM>VCds9RH+fq-{&%7wiFT%M z={GH--Rh5@1n8=DD-V;;Uu32g6|h*(f=z8SmpvZ3`&Q&vo%dvqd}6xCde(M~^i~Vx z&r4yGFcpJ$mS5uQj;%GXrVd!uu7(l*SqPL&o#XjSj2#*LL3!<<>iYr=Tl9ye(j&kY z?MBWWR#vbn+@u+M@`Nx@3i}mIND^ksgyazuu6$i3HTn;By?gl zt0#L&Lvu0@Z<||uWTONTNt8!yL*nl8m3D~1DcyJ>Kq$2G4~8Elw6W$ zNq`84X`g?Vvb6HG1w<+wVyFI?Scu_TNm*XuHlkWnKvtPs8=w09HbbYxQuN;$mI4XX?L%bhqRI=G5f2>5Lys(*k?kn8l5E_jZX{*?iD}JOF|~9{pnp17uVoar#Xwfp-Iex zXpcbhKz})^IB2Ig7_tq34%%$G|B=}ZnojOipama?qRRKPDM>74Qb`pc*1Oh{51FWX z8GZeVq>rOj(1DJD-8G{5^u)nk^$2y0HDSR=A&d;B{iUi#@|tV~odu;oDECYlk{I6L z1pT{~u`AM}=}4$Deh6!VYk&K19hAUXH{z|N)%k_5(cj^K@st5$)hY0x5IW-H#==8dL}dzVo5N=`Cpbg<^UP! z7A@`!S>FL2fCSQceNPV|mrYKJrhQlN{jWl#V-M;7-6~b2mbIw7bE;zb58z(V_D7iL zOxSCU0%R(TBq_uG1S68Fq6P!f2Mie_0=Qngj@a?>Fk-BV-^JnpRNbME!cqS#;e!U$ z%^-zHpX}wdZvl~jaC9(g^skp?+?YOGnCEKF$F}HWmQ7pzq-Y0JI-D z|6TtTA;d?HLoJa+U+s?o7FyPeF#35tZd=gji}+gS<_=O0;TY!k9d6}>8;xs=&SdjR zaE2cyF$Ao=mb(9)kO!im3kH|u*_HIA`q5{m!+?1HEtR2%pjJcB@^OnMa%O; z#Qb6bnnGd|iiC3LhtGj6J`cP>Ar`58?(VZ}>dc-fNxM{ma6S<-8-to!lIjXz9Y&7u zmvou)EwQy;d)d1@g&CJiXRd-c$~T}KMGR%8C|Brd*zPCw`Y61-_M>mOpqM7eVk~^R zT9jZ9j(5LUW0dv6DK&dE5#1XRZEuC{&C^@8|7;VABo!b}DwAyZC~%RI-PrxbpjVF^ zwM-G3=Vu6w05{dSYS7$COy&V6~ugnhIp-hKdY#pW@Jc7$h#PeitAbE^tiz!e-O_=w}K z*OlZ9s*JR_cYX7H@H$*M>?6$?ru!%6UO5Y6zy|AD$IA+|vL8`Yxpk>8H!4>K7MAWe zJ5aFr%m?8+N5S+p(4ju6*P*+=BTb}?|rV@fBvKG0*tWu~K9ygF} z0|KdR(;OB=B^tFv(%l|is6zD%owcuKewY>qTVQ3dmfV`-x3Qb|&D>>Ki;JH(MxW`k z0=HWzd~0IJazH852TP-{vQa2QVp4VjjI6-F&X&vLfYryK%8^#cSmv}=W**I#)wF6a zvK`**luG+w6%@X3vU^+B4 zGSFWcVP#1o;u911qNGT^07)g#Q1Kr+(c0icw3&~fKH>B{&?_kmU{h7KOpau9*j#g< zq|M09#@G0JU8x{4UM?{)RpL*L%Z~0{5rj$-Wumb8*6?mHDhT#x>7Lu#ZYn3)@@SR2 zhFGfdRpnGKXRUGEP8PYNu-+QG)#G1-xEB(3)2{uVKGMH&k&e0Z@M)&<5VPY5RwL>t zpu>hw9mnow?rOj;X>_dVvfh$g$Dh@yQOSN9C<2QG&phwF-rhrA>P=|6Vohu8xAEUqjA@U3%h#wHpesJ`* zO)8Dujr6{Tj~wA$pF>nI9}r4|r?U}p68LmI8qUFAVn;4OA zX81d=#p*Z5V{d5EBb?y&CwW>fRdx5nv@~XROAcFIS*glgs8ny!WbRVO3oVVO)ND@j zX8L@o@4brkqh%VaBkA!1v4|bJ+v&XMhz$(J#yey;bSba;MYIU!&V#5{QxuE2;gW*L zA8>+4MjcPt<6kpn$2PVg^;fKNUdMiW)opg4h^{;1tDbsMEf;uaT;^oFpJsj^ZC9=k za$Xi0l=cMbKXzm#{U^E6_ieC&C5k07JY$4EV_V!gtD2H{v`Myd-PW7C>Mj*n*)ytB z-x$7@9}~j`REyRSyA#16uJXc8{0$5IG)hT*6k_=pYC4|vr&xZuF`p>S=Dij|f_KXCtS*&Y_dk{fuM z{$TwkFEal7I<>d=6W967(ru?Qftb#(57$g8ysM<$RoxLL^nK1nBT&gQkR#9Q*!B0p}&Lw=)~OA zWM$FISj~kl$@ENw^@`4k+%!vYTV$G*JhxPy{{{@>QY8Mua8K{_GBhmnbaXQM>{E~M zqHnqF|Akl*2WLXp)&OZDE&Z2Hd^{4avZ;-qIeTr(NMlrcKiZ*L!2sGKOC{JGOR7L*wb5G_F8@A&8~l1K@3h0?a* z4vwvt5sV(dLUx=~ja8kTs&=|^r$}k?kf;7s8~YA59C)K%EK>SZ%DKf?Yr9(GQZjo& zSme{-`Ro~{RsvUWDX#?doMl=a(W<+sF{Ns4_Bk?1EYtU4ceo2E+_0=&rxs=-MD4-*;yL3^oIhN5FU+|$jqfn=>4%G%9LwLRoLyh=hTasm^Iw1O< zSyJo`U3ryOKGHY*UQ?yCMGMqoyI-Smr(OwM+#jEH-wBgzP{gYg;Ri_+PHyCezc%kN zohZ++V4umV>jdRf+$pIT#G)4LfpEw zpk|xOtnNVa<^ns99afJunviuLCyn-ER^O38D!v}Hia-{6?Sj09`9rVC@fEE4K<=#e zwj|EQI*$GIepG#LNidkH2_rSiL>~EmJTUkA98XbS*zfsJm!f$huyIS!rD!d3ojRP4 z+r+{?FF*XDpF8yH+G$FYP|04oUz)wYUyTn^#5(-*yLKV}wdetR8Skh&W#`i)p3@L) z1Gz++Na_t)d-Gp+E8`BD8KRui-!zY^ED5TKG#v?-EP>|iFPT<)<8E6La;q`*2Ni~m z4!oF|np8a-Ru@te#mR7Sro|)YHJd2qX6rU(_ZqsGumL$5$RF~*hixEBuwCDt@UKbG zUYngCHBu5~@kFh#jOS}&XHOIzTH^xmD$nw;V;r1zmh&{%NSsou+Ju@edJ3GJtQ@r` z`#-VI3D}D&52DJpw7Peuz&z+(71mTTI@c!hTujWWQRf~h=m<~O2AXc>Du?y^))+2Y z0M{sywTO1xJzck@GCky-Tp}n&MmMbvEpv9xuaD^WNX?$A-6TKBQ-B9!@1NCGUC}G< zr)cssuJTT>S_(`0NOGbVxTXrNvvA(Dcb-K-)e=gqEckrTz=)iL3tuG~?DMIq25b_W zvNH{u2T5#uJ)QHZJ?1O{zx%t1R@Ei041~EGt2vRRSUm5o!N!0iyi-(nOyUdL>zRaB zsk7fl9OpGsF+rxYj+f@bC;d*yyS9ApR}$-e=fB*TE6CQ^oHk;(ubX`M8{+eu84I=+ z@)CUS4$7A7;H0Pvsr$SU=9?V`ntbLAjj^8}R<3p#1s^{%Q1Y{1Sa~z$l$VW&NjZWF zV%~n@zP@N3-yfSOZXtUPgyfy%H8S{!Gg3TWtaOa&Q(#?wOLuZx0UvJ2|N5RUU#yrA9yqB_~^!ojkdjY>`5jyeiKfzYjqbtqRtnvCZ zlnTDS8`H&JWSBcfLdxdEvTLN`Qgu}WWXC_&`&yfvPkURl8+3Yb^f;nCNZQo}o9=ky zowvR<=9Z~ARvmwP!3wc$iQdX_7gytUIbtzaQbZRdlt_HO*&N+^3Hlv|fFwUvcdqN*a^&Y}s?yLV%3@tie&uEv+KIPG+DPoY3S0qxgR#p}xNBW3NB0V-1n-PVIFp z9%3N$r~ogPBnI-Pd;2bl_67}{T}WgLAvR5QS8R-{ypgZD+~I2{2T=4H>Sv}yD?9d8 zBLhW5Vm%k?^dUeitF5n*=l?z|FR6bJXNnW-JRP=8AT~HQx{-H~I;cF4{NW!_U${J@ zn#EF5TqukRVVXn*lOI*b8Z2}xv3>^gZ}td4(E?S~1Q109y}Fmzcy#DBmM1_-ozgT2 z2`)5OXW2NfF+b1`;7z*N+iav{6h2R=jK1VGIOFy68m1BUzxjjze3i}ex-Uhe3MkL% z&7G1VJk0q3&Co+x{Kxm|jqksrw*(nABlZO8P{}-8otx0i>|8dh9N2XT&Fc}q>%$bP zzA5^+BZ3W`l~TI$s^ZTgib@#rfgfHw{!pXb_1~|ya<8BNkFrbtJ=p*H&Hn%Wz}=3G z-j^0&5t@Euw$Lwz))GDyEP|YUa zqSp48HzGLyrsIGzm64z$Ohm%i*wn<>R7<9As@p3uHSYtE^hwpM244%mOibb5h@hR< zw0X5(P~0*u!#0^sGx1M!dVF-jiS!gm9``Z_>iqMn_T)u;#6bwv42v5ZQowg~-Kq<* z*wL>vq^pa2K@S8l6cc(u$4WL`?nnxMw6|8q2bdpQJW}Er4DXc^LjP_y1b$XM#iY0r zc8q=3%b{M;C^6!}IAw@0Kcb%c6h^TXbM-+v7LJiq`M5ZLAZN;a`)}53zzaGA8;cB! zhzu(!J~q7M$g%{dlrf7a_A58X#P2xOuU&!b+?_ycr}r*&LH>K*9`{@fgf?#lF;%Mn zGLY!Hc+&d3{S2I_AQkqxyxH3OOsYEeHTo&(@lD`WY?r>E-@U-O(H9*p(t^kvh8LrC zVNGG*QLC-d)m@Wk&4i!JcwSUYJOow;dltP6UTX*f;LdV!S}>&ckLtsF5<0quI1_T{ z^6PPU(9$HY<1(lcz}DjqeOQTLEnUQu5LA-gkv{&gJz&0!X-f(~^*R8oDByon-! zW$ocQ6yJc7pE((I!`-DKbwv|#lGTlPT*jtq1it9k4>+!}Fosl>N@B%Y18qMx!gQe< z5pnLyTyydgI}R#J3zO=$>~)p6Rjk3Zd0yQNY(2b<&P$1^B?04K<`sCKo~qhamH8Yd zS0j^1hL(nS&5}jb9&8)^U`u3xEMCgvk+vyYLzQ|Vw=c~PqK4XiJ;`dv{j%FZnffiq z<6t-axG79AeL=K_2d*xg8SA^%ZX)OdVel(`!4zi zGud*p7dMa8+(eHSaz6FIoY`AD3zy~uei;K8;e#!W<<6xgiHhTrK|@>8D-A17P^bqb zby}>NHT6(nKLr1rr3V`KR3%kR=9l!PI3`Aiy+V>x{<&Z3%Ip=NDc*vz&gH0zmRt6-LH^WGy z)uo7_9Zs>538#gFE%;H=H8<<-KwLrt>YL@CiO~%TT~k>n%TM}ZIkc47hxKXgw+xH@ zl`GAAHQeFNjVH_S2M03!5G#prrBh5GZz%Z7DALH6%qW z#*{uBWy_ZMtk1gHj}~7Szc%cban;Fg)d}8*O%8);85E;GCwmUwnXD1bbV<~|fv-+X zCY9+$mQ*f~I;YOqPu*1rj)Xy$fGLTqsYdxd3Ru`F-#12t4X(%P`i#`jj3Shng1?1I z42RsMw04I%Fk=Ul#oh0)0c*Xnw z8ZIc_YCaxx<&+>wW|%$l*X76ks3M328360MYl4il=9H4`gMzrpaf}NK_CjUm>6oCc zjv-9!&*a!e|YbXlxuT?iu4lCCo)60iGR8W`8LBgx}frrBK23lbTw^H z$waDKhL{IA;O`?(c9f+(!LqSB(n94u6txkpbA$BHC#k7Ja4vJY*aXop4s1)V!5mk@ zz*DasIysjsond)xSykX8qQFJti*~tFwfbmk7(ThIyU)?7MnHisp(a~@qhUKMmd=)~ zjz_HZ#=`gGy}oF7cjiE~`jXrLvP@j>C+V;t{UJWMFny=<(%bsez^Wq`E1kT4O2*{7 zp=v2R%uqrX%hZ92O1dwEEk?)h{4zbS7i@?Wb(%f541RUAM0p$rp<2z6T(o;WomPL5 zujdO}bw$V|akdy6ln+sWthG6AIebNvdNka6JWAHb@ywU@YcrHiG`Vnq_1ah;M}6+mWS zaCb3XMPkOh((pg5^(yqwK_7aB7TO(#c8#8&x!>8gMRd<#DlpFDm@a{SAl9Duhz;(A z5-xu~(Z1C|IB$1-v5EQ-((F;W8X5cNMZtU8Zf#}-L0-d~VeW76f}3l_ir4dG6U7-4rCQi zrW1PHw%v!tBV5yksdCqPH8B4pVIr&eeKbkLRBe$vI;-z}KviOm| zZ|>%=?rLh1ug>P`OTWjZUb@dPq0*O~pNy;uHUbgFgL@cqizO>ZXz4Ak4})dgq3JKR zZe;fI>F8t~jXH3Ft+2*4RUr-IAeS?>+99plSxYOMqrXi(bS~K{7)M35Wj3{C+A%p| zD-=J&HoyHuLV(6Uks|_c_4>&_z72)=A>TcuMz1=#c{sX-+P4dg3GU5SMnc#p$J4sJ z=f6lUgmiOFW|jA2vAX?^@&%!mdxKcO+h;^#uFsy^U+yl7u;#9c)S0fyOYcQ}TVK8T zQmLRypYOYcxneR2nFdJL_ok<7u{D%@@%~GmY(Ac2m83?~>;wBJ#-3u}e4isl7aHEy zcddk1Fj@;9O_-)l%JKfqW@w~n!uEIjWc;%gyn4G2&qYk0!)!SIOQv^ts{FJ}k1gzU zg_#w1v=gmY81VZtXe7%RExwA0Tgm86X~MSwNzR1N4rl6$&$>i^>=)`jnW=83}9KmGW; zBb+;VXeFjB6vb;la-0D?;x0uZKGX7q%w_D!TGo3hnFRH)sj^QUiWhzrtlPtEe_771 z)t>d`AJHeMcM_iu+B6#jRaWEjNjMm+y?84Z9$n*Dxmlw@50dU@Jp}nl%sb|D_olC^ zK@~5&B-J*)v64DA^d8Uh1QK=A{NqOZ= zP+ok0_?X5B+oS<&VHPMwh7~%TQa#b+X}rg{Yj;Ygn%PQD3Kkl$2iSo`OHhw-?K%`zRzN3>*zDC$6+blbw?rOh?ic zJr@#nh1*PKx+?r{^cl3@+G-%VE4JCW~g8W^q~};+do_JbDK&JCBF~-oz)?#NH6^n*DIw zzqR6hDukzpg}-DHu$MZPhx>oK>`6Pd6P~Deor^+EbpMbhE|}%n7Bgq73h`mZeW@Xv zm{ad&E|c_0q*FHaDU>7F*fTcUoAjGB_fv44wE0oxV28BylmR4jx%y)_Q_x+c2)}@6 zN4cgkF3M%#_cw3W$aIA4=*)I)aJBYoKE=t*kE7w?WenjXwC>RtkAG+W+<#atgu5o) z-u;$DMc>h{_1uwJ!XnqRE%%sCx77X554|rH9g|LgF+bYhC*KLI0^S7#yqkl!yR!g4 zl%0_++)2g$1P*?@7NqiKpUvMl-e=YTEq%!}Ah=C$iZB1+8)ywNxN?r}>L=kJo!YA% zo2tDbxIb8yH|Q)WxMuOdC#kxh{$xt2ZguM_dC0EbH9no3zcxCeyb|~YKLx)UI~IvH zKd)a-F^S*#$WnEs;jh(H(_}a?ih-aa3XBqF8z>W6%4k)xP!6&Q2G=r z=r#dUCoIZuvHV4;eon#rAQz3@rfaf8EnjlJwx6r^0~!7EQi-kbZZmgWoRI3@d2^$4`IdMM1-rp~ zppT11+(9O1C(14WM0O`tF$Ua;ta5k@3$~E@)v6M`B$ET-hC<}2S}4O(i{}gN_uWYO zScdmx1xf*;dwC3P)f{$NO*BLNB6whak~~G287Jrya<_2MFXn^NQb;F%Fngb`J7l=N ziF#TGs`9B`G>hkvS~T>x6D|$5`?*;GB_&PV=;aYIQ>0!LciWFj8~jfAro1B7FD)|{ zrX;{UUlRbThk-eeqkEt7$f6a{)SJYzq^yL^n}r4e)JRfC(TgcF8&;TT4P7&nq>EJe z?Aa0wupMdV%noZD9br=@791ReFZWVwp?viaV0pqIvEK;JV)N*l2KCi2w`nzf(=jnL z;77;br>(kVA#P~ZL6<*5D1@h9@pYVn^P zdOx|MIB$d@q3)*r)hOP3B|Y!^=^)1>gZCLktmfYWvM6oKxKlH&uhYcZvucH`p=4pp z3dA>W>^}T+6NS3-P`p@z2!BVr#=)J+<;2nfcOLZ~jw@pw))osxwQ^;ykm)q33Q}!h zza+an7|(vFzV>OC*qu#^1<)vqKC6Ef)f_o5U|Tl`#qc$?FT;_MK-}t?vGz|3lnQ*)Q&tylbi*0CB}5?5 z2UDgKxqJ^wYcoZCyWI^_8AqFAM*1tFq0O?Gl{+`ZFQgGXNk&s%zx6YgmaB$Wbmb%l z5T!X?rM{kJ^#5I%36t2rba4r!d{-bvq2_ty3P!kh6qODt);@EiV$f;zjxAZAT+nvW zhTNUNqt*;XhyLUt^wFq4T3=O<9AropmPK_B;tsOjIvgAbYfjg0EwB2xu^n{H<|#!x z5jQ++2wok6T|mE@_WBxnMP|%i4UM=>d3`ypUf+8f`sx>mWEBp`9JHhIg$&6ueI>R2 zZQP@FB}+#dJQzMU84i%}NbwXZ)2SfSMN~D%Hu1O_33s@_+uR(>3V)(M4iPNwwL?Eq zG1?9wL=db%Zzn!0pz%4CO9Az_W{Av*2#@v>KRB#pT`;U+v>sbNe~lGxYIy1IjHDO8 zee`;A)jFJEfAHh651FYmk{XiCP$|KndOky|@^xonxOP|R-SSWO3Vjf(ml8BhwES{D z%otfTeZ@L=D)@EGLyO-T!&Fcxg}saG%>Ap#xLwE?C1v4nop9k+ZtI!H@o~heu5$-zK*dY4 z{O1R$`5PY4)co5MjvD^ha&bwB^k{G}F;7zL!?v`Z<>SUWCz1cHPDxlm(@DsUgS#W7 zBOhsv+=qm7y2F)#Yc{K?#pNk8d3!j$<#?OjuVTePdV|~ZH-DPYb>^yT)lr{o)>!{Q zqu+hUsE-?+)M|EUY#lSO>arf0_TGPUIV5_Jrmgbg>ej3vrVJhx9IGot7-Pk5|I@AY zQ#sdW^>lWUpDIEg(;2!|hL8JIZ+B>MG^tskz92DfmBm@8Z1nH27OXYqJEm_F%no8u zwDk771pQvAP-d~xuH+Mo*+G~Gf4M*z7727TEI_yD=Bp?u*Rm$AFiQa4_#HAPMPh|N z^4I(%TMLSDJ5P*q_@CwQX-1yn_Re_)axp-{^4)x?z>OeJR3JvlKgwak^~XWdp7 zslhKoTq}O^sRs;`c zCy$`XZTEdEJoQ!+cg2he@>k!%;{jx%Kk{U|HE>yu71Gq!mB1O86Ov`*d$}?RI{CaJ z=`*c{55+LLDsW4e2ozn?2wAENQ+kbhtM+-lF1E`Dhn4~oN0Y0K2kM-1Z?F>OPc9kI zs*XLQkBi3G{H(UMQwd29aE6fvfn{(AZo4+Kuff4iiYh8%ni zUVzca8l&8A2fH99X8s^8?ty#vMg&VD9xHA9JRHd!87ux&0n3hvMAk;LzAq!(e(SCg+F$U^L5;ZD9WR zHOa~8b?}~{P+#HeL0Zb8aJz8)wMC^!FE@Khbs_fCZxxKC@!?t_6m$)jm!M7nJ74o1T^SFPG-UU!>z* zEc(hS0*q~Agcc>TGTBPTUtY?3#}$8PjmD?Z_?OsO8kre^>3G9>chi8W+{ zTj>rtB$n7ZpMjo#QjJrwxDfgx@OWJoZrpXsBWq1^24tYYK$MAa_KF|W zBErW{PD_D`a73H}p=uF%tlViLC=9U*}1_45_4^4suhXf5C z+}#@p3CuFcltggN1 zUVE)E<{WFz{(k9|Fr32XL!By{n6-m?w3i4LmUjTt*9}}C?~w9Fs*wM#o+6Jikk1<7 z`TX`aNeK+A+xFpiqZFvNdv{@l_rI$r-TUwbu)P}~z}a^fBNRZY3O`ozBy4#A2u*>65~~C-y9CDNLnGCc8fcDj5G7r4x8C^x0bIh4 zogT6ESknsIcIvLwNDANz(1Svy>ldX;oM$B)0Xc5ZjmQD>C0{=#qOhc1ek^VIY&9Ug z^+geE!SMn7Q;Vg)i$qBrg7y?9EgppzsY$!7!n#gdsdq21xrw6R*}vV&X>IiAMl%%1mL3-{(Oy#`S(P9 z&Gjv9yU%7^_8ilXoGw<6g!oZc9`N;fic+qk=<8~#5mkLB^G#JK_{*efJpAesDoL*K zbDI7KzJKe-rend@y6@;g+)MD34!Qzz@m#1<#&P=ewN)$q4(7#IN4bSV& zf~g^T+G5&NH=7Abuv(f(kZVV5cvyIt^l(ZSaSSY{9jX!GA;HYb$-+k3$HN{M9*!H{ z&q6`+L>^yNH$o9c9R8#*s3_#Ia5E{ z0KY7?NpXOh1CqUB)rj-8Sn!T7l*qGYV*1vOHeB|$v|R}pW~{idg^!^$rTxB%)hYj4d-!p?@=EM{~EusQZHh7XaSpe7S^lhud|n=CjN8Bws|!LL8yR|oy( z%j;K{m+ca#RAQtar$N(Y{tL1Gl>SpFd@)i&F!R(1)IXO{UQGFPU*lMtnlX5H-zB^0 zM%3rfYCJ#AiJRZe0Bb_S6N1f;kJ7_*ltVgHX(EI(4gY=BLRHD*=icNdgyrW+RpI%s z)z7)5)5(-p-W+T<)r{WCt#<|B2@{1l^2^Iw7qPvWmmY_Do!hJ)^L!b$0}aQgtcR=D zdMB7udXYzvp{`eLoKlNVk3L*JP<$?Kq_hCWtuYyk4{C8ljz+Q76 znj;q;HefC0G1#5;`)v0p#|QrznzwLbdVOcABy}-aVY+F@X0*yShJkw4WoiR+!CDtk zqIZ~``pV+62p-F7FvX&A7F;5fu(>}pwZm6}XCyczbn_RL7=W;}R*t5;=s+9WQQ~1d zFD8GTP;TPYsoYo&)`mYk#?_v`+N=u?qv7Vm{6j%H0d}{b z;T{cSJ_}&wy~{mItljAp71&N`-hG><<*^>O1jeg+n~ra6{e)v;2(!F}3$ZE4KuHDl zY;ENUxhTSx`?F}%<#Dfwh)roafB2FrS)?1M zkT1_^Ma&CB0e3Dq&_sHMPU!ud-b~HT;c^LQkRJCiEPVvyaZ7j|0>~>q!Ga4usqb@mgH zx@*l0ZBdW*{{)wG*$s|>JIo3N(3#cZM`t%9qr~XgwzSjjf2M*!5!Ln5s3Ye&mxBt} z_{)f;?QL~G+WSNTc?5fT&1;z7%*&;nYloJ9!dR4)l*TIDZ2Re`jST;m^gs2O;O~wR z{jSO~e%Q{*3meMEYddx4g4I^d@=kR?}uc% zIUYv1&wzxBHZy5N2Fhh5*@zh?!m+3EBH#%(X zoi_{OlLQ1ofi$Xp?uI|WoQ}p@rv*I&Rn_^M{JZJ34`ummh1}PDRK&$y`?}zhz(fI_7Y5aAq-ZeV>$5}sDTi`z!`l1EXz&3ro+%% zN=o9VDfW6F(sS}&JM5?6{ehGf20hFsGtdL6>64k3<(tj-v&rND=+o-a9fZKYdRDUOuP;BH`JqJM86 z4A#9o-$FPT1}VQZPxN?eb~**Ne3Y5se7&`57hgW?N-a`Y*X5u4<$?dCj3wVZ#s8ga zn;3d%>6KpkaJ6T@^4x)~Q{FVpLPnio=3@tK~?$=2y9tNLgJHvxU#}z^#zD5v$+ZR=yb|bgfl;TN5@3+-6nYP zP?`!qn7)@E(W-Gf=Q1=3*~oO?K16Qs9DG~`G2c7)4ZLM3W2xl%4%gTtyQAVEJrfSA z3*A|nAyl{0xBV9mI1fi&I0)FF{}59k*E8<)a6G`qKv&(HX!w?W?~~%R81^aQK0Yjbe&-o5ht6_5C3?7>Q3v| zObc|Hul|7e#&q5&jMbWv-TEIwAaP4%et-EEdt3Pw82{h!pS!2IWo07|nB)6!3YF*! zpNDm-_G=S(`f}9otwnU-OnaY8F>iL+JCX-dj`}ZAOEPxrJEaL-o9qxE7A5+jd zV7q<0f{0W|6>X!LwpW|@U&iSK(fHwuB&gUT2VHottW0C`GB^^Mn}4u5u2z>iPEcb# zH8Qx7hbz&n{Wi6*n#9hHTlw=R9+zNUu({3z`A-vB@SmIe9({}D{w6p(GR)LIsISU! z=aD@*^>x)fu>^znGZbxzA+?xx*%=coS;&}-FWb`1ZDmRxfY$;}|&j{O?9PEnM z{WnUbk$P<2!5>$lUh`d*d*qFzc$!5_HLC{&`NcA`m2n#48xvS`g1XfUe>(BK6O%74%_4#&5LO~_-RF!@8xss-et$? zU0?R5>!WdHhL`p@|Cph}+I|xkLK3kT5v?3nadhaf{35{DJi^nGtd7&hct3?dxjFj) zJ-zqRZo1OCCyJ84{Axpze>4#86FM;Y%cmpW>i3F+02PNSysu_zyticlcV#$V#~92& z_B4a|iB!1*VS*tW6BAqPc$IowNpnG#+^aJCit3Vrg8J6{XGP~uN`?*n!N&NmY|0^F zoD(BlQa$5g&_HQb)}rnRsBvAr+AEmWf#M$5pN#`&R_#ZOqy|PK>mvQ?I2Bo2@{SF= z7s%YT6xt#e=B8=vd&sZ;0pNlU3%8x+(_I_}>C;w*`+s^9yqjjg{Th)r1bB-cOAcno71OB?y=a+tFI(1q zuvqGnUsz^dfahk}A*a%5!lZJJ*qCuE~G-h67bqEU|~Tnj-KCR z=}K3&>y4Rb0hv#3&LYQ0hv&kei=Kl`11zf3r^se+)w#1HuJYM?=AU=nj34(nVJ_>p zrYYTM|dWTjt_=cqq}%vicT9Ov|jIn#l;3k?Fpp}>@1F9#2OQx@vyM5RPgYd-hJi`a!N7u;aw8I z!5tn-(7a8erA_-3VSG5^QMqotX*mz;HR#Rf&E4PXq&@aeXK4)fcpb$*Z3U6^zo|`3 zn+XvD9sX(#^L8N1EvZs>`{EYErug1L+;^a^xZO__CI1G~=;*RP zdPu{q6Yy{tGR1NBcgWO@rJS_bEz#vz2p?%1>-%~IZ}z{ z?wEadN){v?&vaR4$s&^4wvhemtcA-c3Q5 zs8%~_8}~s1XVORqaoZbU2jJ;I$$i~W28YKzirZSONtL#dWlU|%eZkZCg4evaRMQtF zbUY>IYmawrcX7?ObB)5Q*8V9=9jS}<74U6;^-@-d@^PlORep-+!^nbc)w%*ea_Bk&r48bn!qZiY{+%8bk>DJ@XMauW>KcbO5W<_ z9tHk|#YRAeT_hjhkPlXCnpkVge!KCzhT+4Q|E2>e6#S)~i%q6V|0Mkq&q0g6JQHpA7(yvjK8iX|1w^q!xyG7{zd5SX z(ckwI7^RH73>zG^E+RszRBh0B2)#P#A`U?!nr5awF6-sajE}Vz8L>`+Cj$B}y`SA7wCac_k#Obpw*0t}L!g8yW4f_O*AzOpFDvu<3X-9Di)qyHsb zK>mE4U<4OW4_RAN_1EF^U_U8RTjgNo~tZ-pTZv>a8)(yCMUbV0N3uI$R*E z)s(_$$MmRbw?(9=TZ?gEK>2Bf-UQQ(8U_)6w{zTLg}NXJ9jjthnX$Z*Tyykc08((P zilHi~dXz^iJ;5kN{xTcf;iK1NZaUJ~W$ZQIU)Y_`6HyfMc9*BJPFlxiK)GTI9a`m4 z+ah~j$U#UeuOZ7(J`p6%Y<5VYpPhkczVX6vAq4{UFqc0p1nYac4(ZsCOO|~Z>sI04 z883`OsQ6HEfZ4Se{Fe5MC@nLJb&lSQI*UAh=J+Blq7}q42&`0}=S>}=WENQzCKTjq zw?*s_Yfp!1xGjeC`AVU9ioxa=lW8@@2`XN5@}8H!3E=FDU|q>X#0gkkye zsu0VNy%Cwy(r!Mhu1VA`=wNldvS`o8)29Ot_NAy1uo>w_94FHbKU6P#;V)(22lOEAa#GEby>z5j+K%(mLnaJ5ta;7!a=~w>}?M zxAQ8)b;W62RqxD0RT?p+XYdLqm#s+dhYAY&s11fAzP&c*HaWH9Egw69&t`cvkRR2* zsl5|oKjr+Jb=}vsp|eqPwF6zz7(s-q(?Q}jY6^X^X7lC@8X%i?8D$95xAue2WT7#My!=CGe8!c@h z9$&qkcHGB4^QV`~lv3s7u@G`1HZ(y?Ts66%<@F}D`KAwk2H7w7KiwVHq*$Ar$Ngun&M0{4(1{7#kzGe4LhR}cJ~vArOorluDx+vz^NKu*x=FM-W&UwVBR*H= z#H{)1up7QLUF3h%oGH#m+=(4y&^ZWU)!MYs|l)U4{zUvxCG^mK9viEh~3prVBWmXd!x6=PNuloXMEe;1iXlpPL zb0rS|4GNNRN76!p+Lbvftk?`8c>34;7t2BVA`~=Me#$YPjpg7Zz6Lopw!Gs(r4vdD zQ}K*Ox0%Bu74W3*Vrh4c&av-s3V&`^A4f?tCfH;Jo$noyF{r)3QYSUQ#lt%R!F#%2 zQ{a{u?;)#$O*2B-X8rjkE_=>6H;%fzW3r#6iAA2lPB^Cm3mbF%;dkYehGFgExKQ%| zj;Mut-Qsc|lKf@~1xFVF&Z>p|Ht>HZ=JNBpK;DmtMpx79wGw)Gt{Hra>qM-4G-iDN zHYYpb5Shabo7k@kto45ER+gNrV84fR<8)*U_+I#Q3zM2jmi|>~jBFz(PIZ_3|Gwti& zxLZLlJOeSOmPD5=HqS4j)EUU=RK)QK0A2YyVW&u6B% zooK(F-s!V{@>lu4yd}Egvdn^N?*hrPC5fr?lOEjTA-ZEjxA6?C#VxD6@*dMz&ias$ zkKaDx2>`(J{}6#VEq(r<2*h$*z38n?mh_)(;t#laG$DTF{oG4C Date: Mon, 7 Oct 2024 11:18:56 +0200 Subject: [PATCH 13/31] Add files via upload --- admin/docs/metamask-1.png | Bin 0 -> 52482 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 admin/docs/metamask-1.png diff --git a/admin/docs/metamask-1.png b/admin/docs/metamask-1.png new file mode 100644 index 0000000000000000000000000000000000000000..4b684cd64a7df7fc5487a6556d5f4af4978a778e GIT binary patch literal 52482 zcmb@NQ+y;(`0iud*x26K&c>LHosDhVcCxXpjlHq8v8|15PI6{{|8vgGxjh&2>FKVT zp6aUVs^|IETaf@oDP#l!1TZi#WEp7*WiT)ZZZI(LDL4qwl{4Xp{`hBEN)13qG3ZUP4OCrw5|RMkWOJO}y*mK0&w zw4RfrJCy_+h8RXr2R7|bWdvJGxe5_PEC$#7CM?<#z{ch#8BbtaN5@3_#pmV)1_UMA zD+Ml_(!8;ayScmhQI5TvTi7q<3Tc>N^{UCy(XU)wrSm5eXb^!Sq-(5Ldj^nTl$FKB zAp^SrT9}^#FWO09x9x*Fk#Dx5Ftf_li&~2)zp;cF<>B- z1GB&mphN)O2#qJ~LI1xm5vBRuB5ZO9%&ai z5v@U2qFe!>g_~~72hiGZ5q3pb;j|@z?TV73MQ9)6M4c3b#^j~oX4u+nd>9C?kUMniAs-|-V*JoyWkM3YmiEV6b7Kk`H;QV57BM z{W*Te(8BF_=BJc&9g#mX%lxVCcvJ|{2>E>=3sJ1({ObgwZiic}gJ3ivx1EW(Ik&4o z=w^p|>j!q}E~G+zQW8f09b-OKtMxK-{~cHx-AEi!bX*+uJJa3YdM78R8vQO=j}Hci z<-)?k{8;JdPt3aTl@-m6SVKGxWyh!F;*t`*Zl8>60mL?=nK@`Fk2OYI7X9x3*90~H5=l%G}KYw}=dv?Ma z=H{xqJns%k{NKN8Y0+UJ0D-{Im!rI)Xk1K3%7^K^uDdffBHY5e$ghGL?zIt&v&;9@ z0pmYzR;K;7DaJam7k?NISv@}j-LG(E$e>iz|M>bFsUA?kDj#iY`CgA5NxU7!z?bb! zxGKuGI2{hM%ls~@;;qZ9q_5UTz&9gO9V?qV)AlC+M1_Wi1`uN8s)Je^Zn+Umu(7q( z{ncJe zN;@C^3Czjkb4959M$1*DUL6#4Z&LgEa(6^V7$SxG1vo^REG&Cg2tNYu|tz{jGMtDPM)kKZTI-J)yv1pHkSpfKS3evoEB3KgfO zq7po^Cz7co^zxUkKO-~qwJg{B>2jCMVVRMUaeU(8+fxrj!@>$IY4X^P+3qDxqHKwW zY+D(qABC{{MnwhO21W!G)!!@AViCue#oJ)F+HE$pv``BuU3a2FgW-OOg59;W7NJ5q z>To_%Y=hR<*9Z9ZB9nQj^ycS-iu4q62Mr!i64247-Rb}7cQlpb$f77AArVh3crA*l zD(Jxj7TGrpm5gYkRKEY)^(-|Z0j%d$`r_ewl*RA4K*^ylx_LIom!ZE(ev$R9Mjfw_ zp^#-$fxcqIh7Oux`Lf&jL6>!MDq8QW@z!lfTLioNZBXz=3-cS509#F%i4j?(FkbDY z4cQCuX|eTcqb;ut;ofzv#bc?W!_hoGkL%{tj%vnoWiz>BGHAFxvXUiq#1xw={g+xFjLFIVo%Gq{$dqgR(OG5k~+o+gzO$~9~CcAeWqhRVoUDDlP|VNb0vMqa}H))4o{DfSaHv9-T}@fpcq zX@=jK(Ts8q>@DHS_2t@pDP%P){)YPSNY0Q8UF>$=p zR}>Ss|MOdw{~5D@`;52{N^NfDh$T#1ngO1{0m4tSj!8iIC$&=HM=$Hj@F{R{3US2kY| zDQLUaG+(Cl_WWF@$+&{>*JL;bKZnm1;sqWFNw?jJN>{uMw@%H^PY6%OA%2Isw|)mj zF;@Uz5sI1APhy3}1RjA)fSQbq%t4|pz~PFt)d4QS%ERjQ{sa;~Ob#}j$Mg2@Xd(qH zG69r3j?>n?CSYQ~S4mzzBxj(W)Kpm+Lszg3f)q7@n-<2w&EY1itJ2|(6f(holxc(U zy{0cz(B&9SllS_o7}j5K`K`0%+V!JqiA=0(TqUgehJFuY`5=QKJVV& zzfiwXv_hRj3`Q$cQm`$)>e`lr?*#RRLJYvwz;8&hga*p&5nAHEe^ZhKuwh+1s-Vh7W}J?@;JBjF;~ zVE?oo&HWG}lObLktiI}koF4;LDFEsQ|Tr1^>aGsC~Zx><1Rqxg01bbz<;Mo`btU!2^6~a0^7dk^afk1AE~k4Rx#804 zAc1XH2(Rl>EBnZ^L3?rx4J|Xej+SHd9$%QTG^y~D8(%Sn!s(Tko{IbaATjHD|J>5! zRQtSB%-;pi-k4gUCZnV;y8J60ZN(csC4HyOU*Z#me$FSZhO->X6Kyw^mf{q_3kYOy z5XIPUM`CKE&BqW)DS1rbL20YpEng8z`aZ@cqYw0tS2i7_vMkgA%qMgBadXYf6AOj| zhj~=Ne@ObgU9{%YUek(IgIkJx%fAgZLqA4dsJ!mX;de)&hn^v{aQDCNgRSHC=r1@7 zl_FSM1Dpgy5-TQrSz*W0^&sdg3=KOf|6X)mB>oK&-fS`xIy@i>J6Ez?2A5IO;b@A; zqZ5}&8_KeOU_i8rfQXbH?crhB02UTjJQkA4g~v-@pSUT449dzg;fr*3ax$bgbTjsX z7r?V`w&(Sfq0=oGDWFa40_@w~ypnLJL!vYkr$&z-kaQrw2n|II<|k|U+R`sA$Oo2l zG+A8LOn|58A`JY_Yp+b-x8TF?psqy*zOeTE5GW|lrlO+P2&Ra}fuZPdz0+3*%Ol$6 zH4ugzd=?JHOQ0W>_>9Y?l;oCyW|88#hGc0Xz)?Ou_41)4_Zf?N!0G zR}FZWW&Z=St=M62e>iraDm}G^UK&eRFhOtyEs1kWsN8e6W;YBad%x53ZU-XhkW&XD zE4P*z(+dXW3!Iw3!pBY+!52jYSqNRV6$I@((%&kj@_}q&G+{%G7eTh-uu3b zRP8W}2%!=Sz!COa2DOOFjylZGMb~D7DZ}=I(ZreK-NL)S1E03TF`%iIMn~a(!lNJ$ zp(3E#KrmsnVUcp>t0|Bq3~%;NLK{Mggo||j$a4_fh1x}P04G}qK#&o7G(yVjuLuk1 z*7>h|$wh&)!R*xwvqro??c@qGa1bX8cMu5Ny`I~a6P;XXDudW?V1b{;6q&%yqy*H# zeHJC~KF*toeSeu7iYQE3j;tVea`nr>C{m_vdk9Ok(DOzm=CZoY+xA_d4Yt8iD`CxS zefCxy+9c5+8iW+3zkyO%Od2wyQwzE1H&XuVgoMd$v6>BTEP4KU;5{vBEbsA!wkbXJ z?`GF$-LCPc_9IiY?2?7cHp?<~?zVGnjeXA+xy2pb5CKBj#0+bF7H&O6*ebr`ATn|* zYV~C4?pwPQd$W!h?G&uq`CBn!=E;ez2id3lXl*wu4sS-E>rhwVbkH%HN z`d%Q|sIclUhX%=p;fbUYN1f1ya29izn0Yevi10Qz3_XBJ|JSg*DZ2i*Jv@I8b*SSCD+h8=9$g*T8r$r1GCItPuZ z$!~*w>`2QkH$H|s7GQzcC<@>+Kic*^czO?<$XL&NZon-!m;~TR2LLL84nc>3k*I;O z8elywvAx&>Z)7)Lg0LgPTS3D@02n557dp}fSiiA!=LWGb*qW3BZULC6IE)o6ZNv{I zbVsH8XVLZ$<;5mJ2z3Dt+k3@}<|3Ud8uQI|SBA2{ME8e;qZRwt^+lU5<*nsym!mi! zKK5xV%=6WD3k@rk)}*_`VRS9E+$)S1^>#Po>9M=t#L}-@f|``FybOWTC}BH?J_5byUCF&4MgO(V1I1Q8{r+9^Ng%DIIV z?Oh#TpyK5gPFA+E0*34Pj>pq+4Szk|l8~OVUAEHn6jLG_jC*4jmd<96dtG?A#t}2o zYD2$VMMHOY>Ctq!`O%?~B(vGx;DAOU0ah_HnW2`ueJ7{6;p|Ow33<<2h@4Lz_$7`F z@Z~h1EY#tI6Nb^n!z_O*(CrIHRe=^l%G*-4j{SBwa;$0FR02#C0S8_IMz|~#LHqTJ z_yuIvKutbzNDDn4IAAQ7*BJ)X)$K&NdXK?Rf)H?XO;*@d$HAI_1yLc$1v~w@1wh2N zcBcabtTXp5&$9e{us8_(00kc(pL{`-6hwi?y5y!Njty|K1matm>e-(^e>R#=_oEj$ zg=D@y-|Pbrdqv4;00ttEBp3lTy(c2;0@T!!v{+(qSXN3agiXzQdU~xks}N;K=1?P{ z)L$czk7+q9emIx!8CF$k)CT$W5~nih4Da9M|JBsi78CcoJDjXDe7fB1GYX)G->4(A zfnJ7nLnH$ji>ZquB_$`b7<40X5`fVlYJe$X%2H%gWJ8}b(bB?kh1@%k&-@8@Lh%gq z5o_;qJ;O19M*&}kVoT1=MLFps0Goiv5Z?_)>TUV471$!0noowB5%P-52KI=y!CB65 zbB4P7MG@x_{t1y77QnVbW&%&IVDZ2$h?6%RKKQKugD*f7EDZ$$RFS!1f&;|8J|US+ zt5paZU?B+oyBmGveOCk{BOQ0nh}rh}wcl1)i~c-CK;J;<1uy$>SPT*(3S1Y*!n^PA$FS)4@Hq%* z3~uLjsO$6rL@k!!T`lVs_dJz$#~p3m8!3KHM0e zS-%r5MPuIm0br8GtS9TCPnKSIzD&j6t<53zTzQ!&sr)=a65azeggG*7Y-Wm;fV~Ok z!>yc^ghdauK9c*c@&y$S`4Xh4sD9m+P{{Z#2@ZW~0!vdz$oVO;Wj?!N1uoB#0 z^d1NTk)eUG(@i0%xtXA`>`a>Ieh2Nw?ENB^m%=~PQ=3*wEkXo zKeX}eiokumA$TlamB6{q>g8|4T!{FRzJ=(m_RkmGy9?U%Vf@zJr1j{x1cuaNMtn zfJT=kWBKbasxUY3g*-(JM7p>{jAMbPXBQfk$_mq%u&7d?rwJhwS6x-0cMy2T4(Lt; z<$?>@yA^Hx((F+UV;K*PqB03)SDW~H%r$6DK5uA*S!u$xPyvG$#A*&UG7=w;U z5Exx+rkM5d&C4rHyE^Rty47RMo|BWa2lz4aM&ct;EKa&6^lJ6Sxv*|qJE9bANZ+ho zpo+hir(9z9Tbi}sAwUX|^0ZYsHJvQ~S^?1(qRejVIUT^75fk(0&)Kp)eM+UhnSWMd zVg_2C!|omxqO)@Zwj8Ag+m|uOEAu>mnfY3Yj{&6~i)OrH^9IeQqQb)|gXg8u#Cc&L zl+q(*9W3ow>}4shrDdsEh3Xmyej8cHH7S}sv#m@?K2E|IHb#YR&5TS?0Wc6Cb0Ew5 zUMqIjNBD(%&OXb6-hq3)k!E1(D1N@o!JZ-q62fSIZz@|*M@y^S_o!y2&BL6Ardplf z>3(IVPKaWgK*wMkY>dl(^KiR;J)FcJVXw(~Jyj-os|Y|VMt0--`dn`uhJv4+lH&XL z{!+c}z85Ixr^9uZwYqbCdW?&YjhuwDLg6y^M$$WFKAAy?vBXbH$w)@#xOmzl-v9ax z>ypAjBiw4Y{jxue9TF}9!5xA|47E3T;999VAGXU5{w717T*Vo-HH%79lEyIPoI|u% zB1WCun$qRUGD5{RawD=Eustq(^pu{i#^QUv|1t%hv$u!Ya<#J9<-HS$NmIMsx!Uer zR=wWJy}&J>d$0f9arOq2hIIAXP~2X>hk%5{|G=s(HdaBw)m6>ab>ArWO5tN?8oNpe zXhMH!o0^`!5}=r~mGkxEa8l=J${_vu63Ur<{(+EN6f0O!*vCRRYp7p^tHi&j8>0Ki z#L~siKkW-g94gXjqi-C##AQR*ED8CDxoe4N!J(Z2QEg~J+Y70#Zd}1cg*ai&H*tl6 zm6z`TrPxIy__zZNnMm#+`Mc!x?# zI0%p#Tl5VLuM~W63w3pM1vWPOc;@Y&c;)kOeJYm z;{&6%M+7+R(^vK{$2mua$R@us<>!)=Gl*DIjN)nq(VxI*q@)t1Ic$#6R8;0c1O-*B z_xJ0MTa%d>e1qxV9yb_vUO57u^u^}(BnBkXypH`L_GXhWyJZI5ueZ*6Un2jW|BdJw zw;B)l0461=Y*`K={=;ThA5GJ=uEZH z_sb3Lg534YNEkCAF3YGP9IS#x(CN!guXw3Us!p|m>k4?z_n#EX&YBg02V0%8grC7<(T zWwfAEWJAqxP_XpAkiVMF!Pw9}6hR)XFjD78c6l%vE^jgHfU%(81c1o9J56^PfVMX$ zHCO1Oar*jN5(5LHK$}cJiHeD-x}kxMlhgH`A2TBV2s|!G5{M6X&9nOOGIww!w#9KV z7}=34R|pm;GQ;6pBmwTi2h6~LK2Ao*fWG$Z945N;(53typ)sAoessCfF6V)i&Xup6 zNlig*L3(K7`pWlF%BpA3EfTHY;_3Ba3RZ=uel`A8%KP zxXeFOQsD1n_dwd~qr(r_Y`skSIG)0w(`v`>c}v+=Wo@-&zv2MGP?>9)u4f-0WBD*V zJWSNp<=SD_U%x+c;Ka+b^R!ZL?6H$<(G<2$v$_?K6^AHZ1Q}>C?IYy7cHAn7P7Dd^ zd8#VCGa=iExD<`-2vJdTabG%6Kq)I&?(XjV{2eUa?^dto^94Csd3pZ8*H|T?T(AAZ z$%flo>t%b9%uVmdZhpi5#cOM}44bQNAc*?W(bA4iOuX&KA9VG51#+or2vdPA%Kql!H{~909mr1&tpaQ?`hKQYi~_1DImV9tsmU;`r)Q;6NG5G zUC)-b{cGDDcFBngiz59v7L!24uC%yVLQ--L-vt*pP0;%h)Q;$m@kZ9x?B_*)99W*x^CMBeg9^DZUX>;zpaK_zOL()TMNt?tg9Dv!qpK|aap;JSi zvvRm028uL!&Pd;vf#U`Z`S65jDD9W9>P7|;Mo}W%KvY!OkB*K{j*lguz^0L-U4(G` zklQU^IOXDy!H~wv8uQ>pMjmuVcNFI1=WeyH%{?@WN80>VSp(nWh!}I1OD1YoNCa&1+ZONf<*dwOt%hldu;-_q;t64+~(Q;<`!J-&k6!?0+DJ zpx&0O0Z7P9MMuZqY)|gN8DQ3j54W{GJ4GSl$9EJ)*j_Xiv9_6b6nt#qVs(V)Y<}?d z3bom8Cg~T&;EFo$3xCJRyqu;~pcdpaIwZyfgZs5n2GWO%Emb?Qhp+J6RX=!EUTK|T;iOE8> zvxs_}qDDi5vq-PJd`zG3$J}CV&-1;aMDYh)e53d0Zq)erUXpU8s+DEII(&JNJoUQ& z@$qq@_zg#-4BdyH5JD)%Ld2v|;5XN^%dT09@ZI`m*N1JU59hlnNOuZjBTN;(w_TES z;h&lyF>&Zxx!~W()w=-Ifk4?e^C6NApphr+pX=WkTD5%31?~^Wgz)8U^m9X+V6;~( zhC=c!*9V)MsKzK){PunFdyds9F*QBG*OIu01dT=(9H*#=hZ|ia*gIEQX$CyjAE=P8 zkJ?>!$%%>IZFG%%j2g77g*+d{{6r0X#Ubsov9Z&F)bXQZu5*5-XjBr*>oBG<;32^@ zEk3`8t8kE^)dnoD@Ghj<&_V5e4vx(VvGKKZcNa9swvwxgDMwW`s#tlMXB7v-?jr%* zH#Q__?)J8`6N1qCOce4hNS6sBw5)N} z2BfF<3TCZ9%Zj2aY>m$EKp#!{!-p*Q*C*tC#Ahlh(=M-5>iIp#_8&-&D>lZ)i4yei z*H7rzFD`tW3xjx2%H?k%2fLNzb<=09mzK@HR~fY!507^tJq4Km$u_n@jLpgdB{7K} zdHrcT;dk83!qme_`NVJ$(oAw3IoWZBQObst65g}{YS5!(k)p*k)qj%5Fdw~@3B3x0*BxJ`8~Nk))&i4 znQI-DWswr)zF92a{-xAGkQuZ+vk%wn-UBBriPHOjGpzg=oWG^&-sYQ$sd7xCuGm88o-UUbiO zwp>I3y5f5Yfw(??t!N3@3>g3v^{}d105i;3i9A=q7?S<_@<@+$`Iw5Ll518RgRTJ{%^)I5h)ibanOl^^9t?sOCf(p z6)Ak^R8>`vrcy>?iVpXHV2k{-@AOCBwThD*t@=o&1EB(IPdF(z#oEw}9P9d@grs$S zKsLozyJU_7b?0ZLPRSH6?Cg>V{bCrpT(NW^+B6gb zJQj9;ut*bUf^4(tOckvJgXlNtSu;`k|B1(duE6@w^5PaN+w)k(mi{WF=kBTNER-#p zMJoV3^{nLAA1VC{GVUz4Yju7TmjiU6oDw_EPKE>hih5$Lq2}lxZj#^B9xft|=WY>F zv{jV#9T~`fH=GLV%lxYoCcIy=mTsSX7u9s^snwcY_i0yBD8!vdibCjjJS&OK!hWoG zZ+~Dm5shrW?Qyf4ef&OPBv#W+r@>jq`q?~E+1}Dk#>7t3ySzTVK0mn%?#KV)QBrKa zxX6u0mnp7pkdc)AGgq&84uh<^to!SUqAcsbm=qWpE336zYzFxF+ynob5Rrak54>_& z%POPg%MNk$cBVJ)%{*bhMf=e%StdzvEa!7Au?oD))n*$u~a9wVyI z#N6N9Ty2^Ez3O1DYCgDvda>zJlblmmM0(Wfww1chXE~r$UV3r0%iUs7EXmd4_wUUI zC#*fRz8wGxbakNO3%fa7p>*qQy*OD$s+u$Yqzv`vaf@hG>aCL@33oj`o;_0eI(AF0 z6i>LeJh9$$bzHGackqhTEZA$t#_s21h02b(zxLa!{|+i$vVv^BU{ssm`-LYX`Kwz0 zCgHN(MjLTPC0Qc9Q*w=Nhv&=O;vS!=%qcK>`EsxrcPY(O54i{3k5iyXwRQcp@SMN3 z#-NS3T1-3-9ZvjUUa8b+8eLTVXSQXqWmHsij~^kDwAk{+-fPWVy;`>O*jUGtxa^}3(NFxnt!%%!@LD<}6!b zvd5dXMtRlt{a4PJ`Xyf-!zcn5G;L}s4yD3Hikv%Bwm?jqyzh^7aoQaXbq&m1SXEWE z=$Md@Blra;_{|j|4o1eh-~*s{BtpBGww}+Ge(_IglcZ+Ok{8zRtQDT7&oRBP_+3r1 z$f4gTk}U)=#3F*$q(dTf)AraQjv3`Ix7CeRe;a-U9IbSA8PC?ZKQAB4 zayL|im)gAr-u@&il7rpCWj?Ilw;?j_viD-)WAVS>wY$1vK^F4FNo8Z_Nrb(u`T#%E~T!hwb%;z01!Lo*kG z#kqdQa-A|TVSD<}?1T-Y$iHxtot<5RdPJHWo1V_!aJII#6uC+PfDp5mEg}K=NyaZs zvCn)da}S|O74kIPN-W$`)G45+UeDO2KVY1RcLU7~VPR$ABiULZ61xG*0|PnNc*OR9 zUX?+^HZ?mBBcIyQZ26!vRoY02KvkuDk2hJo5$29rva9j(Li<^ampsC@z_de$CY!(@-oHbbuqa&yrGs4@H3#-tZgJ(km`StqhW~u|1ww{p#0MchO`T7f~;y% znAUcGIU=wLAa$Q&<;ic-7XUOLIz->D+}I4~v`wq>A4)Q5n3#MI461rwyd)hR$7YR3 zdUU>S|M~MXLL5{^aj|GaDGaM_p+pVk+BuoP-QdJuQ1>YmUnJimX#bl3#j8-g^I@}n z1;H12YLyaY(bs9=EH3BaGzncq+S8~wB3?B>*~Of<1BDotciZ4>6^ zZe_&)EmGt{ED*ED9obq0V?c}Kk{u=Z`?^|3;FU8$2Vt#Hs4}O)Lb0$Y(Jrb2PE1h+jPYN6M{_#$}5e{j!0}NBcaNeWspN z2A?gzuJ1FiA(C1ui$7`ZqGqgVmu<8^&1MoEEi(c?j;<;O>$_iIkWne7)0t!48{21t(|ase0LN=~OD zXgKI2`JCT|Q||C(xnif5u_TL^lqD{m4}I=r462S}-hF8LXIQ8u+TJpOs1rc8h8bekAsc3=dKhDB;crdoOjlg2qP1 zQgOumJ`3rmr>A3AJ?=lQh{?Y>yBxE#z07Ps%|%t`Y-p;+*jb%fTzvIBl?sMMIlDBJ zFm60C!3dR|P2zDwX%VN9$LBBp>;(v8WDFUq{5rS6C@R8kSUVnMG;+UyG1ap)`_B1K zW=;=WrdKBMQSDkz^lJ6CqY;buJJ`}WJF|D;%Z`tYG11dQz$TAjAwa*59u^>ErvYD0 z*P1D+mhFt)as-;ZkMQ?>xPIy9RB*AHe)oDn34RP}mwJj*55i{`{$1z1nK(+%ZS5CM zY?vyaq&M0D`3FJ6o@d4NZ-<*MeeQ}w1pmzwtgwv=MryI!*u9c+iBfuswx!dlzh{nm zipTC%(Gxr>=&d4-qgGAMWfh6L^@|8SD7r$GbY;a9Tp1mkDo=yXx$gV|+zcU!>q5+ z0vSzxgM_$;PNC|r+~xJmYk??L>N4eu*EE(|f_pXl^_`DyDjG_^+v^LmM)ml6?yRS& zw#Y10Eo{)zsYz90yNpc9Vfc2>YKKK%#fq}<-zQ`XwuRqe?2$On4~?A0B- z!Jo~R-NWte9L2Odj)%ek)*e4rqk(gGii6TG>%GtgnWn5bh_7Fds(HB1mWNEu)2?EY zk_I2P84WJ1U!Af%KntqjLQh-s{9pajSI9UEEhI_yy9u2%&4L7tZB30zT9J)MuPm?P zT#*k9DVfh6A7V)MO(@x1XV>Gy0GjA`f^&_7ZUX0BwyRy<{9T|Wx$pIPwHnoU7ZUbp zpUI^7%m76>G6N)IH(qUb-|QRQ)&u{0tU$x2BV|RbZ5MmED>2>nrAhL4tCM^E;k=8< zsbMK$ZdmCAD=qH+J6el-s6uQI8UFCyy)-x~QQytkS!wBKAk=y2(nU+^c-fu*Xo z&Tjw8;6{%!6I~dy|LlIBj}qq7c4AX*;Ct}4e)!#!C_#oc{r3c82m@Z&r4-s5UaPdw z(XEMfvpOzEiKL62zYt|#8~hZ6(kx`<=E(JMgCQwZ zS+hu@(m!dCaO%h=y4-{xuj|M<POH5O%q=}v*q0$SCX-aC5)RbA zJ)dl1r>RgYJ;@Z0r4W*rkUx~l=B{cf4{H%m5&S)@BlnVty8@?Ts{|t#h^tOwBbFZK6VxoGKZMfn)Z9OSKG@FvC#kyLrU#^;7-}g@c zR+fqZVpoOG=dpMpRrsUhnQJ|TgvtzmXDhJJoYiig5L$4OAtY8v(k2)NlOjiC`FR~` zM}CH-o$AY4=E9K~p4uihlX88xRD*9GuYkQ}`qMs6;OLX{xRBG&NU$CzBjnIwGP^hz z@i&TO8dox+emmaGj8*ed`(zN)l&a*x9MMIO<*0~ZdN|F`TZzDCVBw+#I@gQ27WrPk z(iV{IL7}j}us*GR)MrBDMuPZ-yDx~k$LqXz@-I2;_KbU2_}9_mzw2ewQtI;ATto6I z4M*~)A*@}^i(0Uk>Ye-&-0#ILUoEHm*pXFd%N?&gG;c2rc#M{SOj6)l*(B~oKl$6T ztksr)jboKUx%ZG#EXfLjLh1LZU=J^3XiBDA89@iTRIgXO>pXatZY)*XVQ4^x_`t&( zkUWauk};~__@P$Vr^vG4MQfsb|0j-!i8ti)`^+&sNQPM?d+0a}zY-80_XaNoC+;xr z=)%>K1^%b{KKM=8EDs!DUQSi<(+UHEkb&#!Fm5B zqxnHX{s)-edt4fi^nnZW(`#tne{xa&;ya35}0+5R-2dRIS8K* zEu>0K-w@FoQVln;;q$K(@f|u}bANx3p%CnY=q8#y-0$fu1WIlvg28<4Fz(7AdFmTH zkVf<~=eIlK#QHx#ivxq`->oCeME+mY^-tn{)kQKWsmr2qr%D@#C;11#)-Yizm75*I zy9By6UHplJ(;Hn(YNxzf-B%X2^Sb?<-X9u0AL1!_+ik72s+M&9aME4wmVmat4b1Tpe+%ud z+vp#KZyn}8@jV%$@3wNE_=O*4md?IO7HfxZk+>*tuRGTU)t29XVcl##-D}A*P1RL% z!MC0s=dIN*0a0O*Z)&b`oG1`Co%vTKnY%aL&E}8IhI_W&wI-WF+TSy}-^5N2ip)ot zo=umy8@=8%>uiE_i=UDwZdtJLTz_>9T|Jz%ZvV*^D6X<&sC>ESZ9Gy!h>DmH^xy@i zv#Oy*S+U^<*)c7 z@u)iQF9M(FSLtgjhcttuSaZiKSSwL~Od886El7Xhjb#?aOjcD)#kPA0k4#OGnGA+J zNg@+$v8+T<#WR#w|A`u_7Onkfp!QOX@&)}sohx|Qzt-UC6aAMswZZ1SZQ~pPq2HE( zLjb2vnUM_lv@?HLan5>{>VQjYBLdNylC*jrzn_rJAb<;BuG8`M9QYoWtB3*)Bv8I` zK|1?JCliCI<5sVJweFjrWW)8b*{aZd;QjQak~%BS-#wd7y~TQ&tU0oyI>P~wM?9J< z)&viTC)tAz+LGg(aE{3Wy&&|JgkWZ`kUM*+aV8 zUl?-5)5MUG9*y~P4jMO))+sXOF!h?uVU1?(PQge+?1(g^G&mbRc2dLtN9-<;I z=#{1=4I&01=KubX>;v(~H?l~p))@;`8gHF)eh{m^2@6uKM5aiUPLybNE?2#Wqu6o) zpxaO6yMKpMg+a6D1G&$L$IwDX*ZIqNj7>4{%)7T%N-UY{>rWBOg)x@YOEIxXkBWcZ zbPe}*Z&27Tqr+j-Gp1Om`=Rz&fE9}VfUVgtFe`YXbn^8nX=)KkyFzuTTqa$eqM9yJ zanfJ6q{*s6HG9ueguB>9y1JnK)1w00M6sG?nblRqK_aZj*SeT&BIGE6O=% zoi|`*DyzLhqa_?B{P7I)kaH*cuN+}#2UPe25)rH6c)(Uy|L|0WGDEA6u zJ^!gcTUrtozje)c?RA&^NIq{T{+s}T&E6XJ<7$;kcG{TLkM$MZ1Oq6so&IB#zSA#l zd>E0y^^Xb2oC;3Xkm2NZe|nh zv?j-E$a<;k`9db{eFyF1q#KhtS^XB<#rl6~zh{89uHQ-jK~oTY|7TL~p!+3tOZR*^ zJp(m;V-je0rTlkKGJ#UZBdUt0z1#dCyRew@>tC{kS!d9E-lpeI(B9n7?;@?0YUzyXD%C~mL)~pyH;*~63pS2;enh8-2~+_KFNio8QyPe2hMcEJCjgbhRi8va|^} z*;`FqWtdk-x6j8BNmdBw|Gd>^X1dIwj{Sst2dUUm2Eu6AqM~G-hAI^ z8(sp>MKMYJo(9Ikm$6HWr-H^!C`8N3AXRFGLiM?BNL2YxBev4|8-zLl?uK+VtC=++N9R@n z_-|t3SW5DDuh7)>c5BiuW(J|w%wLPYUtOE2({dt5u`|+PT`#~m`ASnIHX;|An?0Av z!0nzf(Y)^(f+&<1mm{-*e3};1dQs?j8Lu!rL*E81#;^)<${0GhN`r{@tV#UOqT2n- zp84^CZ>|iou~W2Zh6G9Wts-7(R#6MZ9c60$)pFDyzWCT<@Se@u&Er*aXpL;VxQ+YE zgT{}d#MG5{X?tF%NT$7ktfm{A5njkL-_XNgUSwtGD3YR{b+u!IxQkouodze_pmP{& zIUnIJEdn>&<^B#%&mMnFW+@{`eb+d_;29lKUA|IF@)9nI`m+#`!dRuhDNu>1Y|K6} zM%iG`{kh@Y8lCc|_cAA`Flpe`MXAj#b-AE6g^#-?b9pt@iH0vnwhzjU=snvbZA$+L zA$G%8x3CcLExaw|09*67#|g_cg+K{Xeme~s3mV50)ZP$eEb)0{4Y{65dHV+i!ZO>; z#QjG#;qujBqNa_dry~9$IS0>*w7w>F-zek$PA-|=#^h}jeE}c0^Y!^(yX5TjeUdL2 zIBU?!r!bNDT6{3jPfes6YI{Y!<+>W?402K|3}kdE>(wh0+vPLk2U%#o7c|}4r zSyd;yAI;U;x_43AwgUWvWd^mBvRuq%kq6EH?;D{dJ$*XBM=eA)6_w&yF*Q1Nu!iOQ#} zE`b!*xyFx$C%rya&S5$?W(}fV9X(HTg^9Abs>9MsF(<1kHlwQ}6ksu~swziVj8|3I z*3?kAHaE`5PB&EI>|imy^ZO5FWNLAVQwOv=w^e2e%EkC?rmn6RCH3iwivBNxrRzXdi2DAA@vdQKLH1mvoyJ z*_-D8)>`JxlZR0Br~N@$qn{M>E2*=9q+El`W9fwtf)oK7COSUqmQMYOp(ZD$SV-yu zp<1iiUPuO15DQRPZh$1!1uxdCgXD~zpBNjn{-QT=G z@Y0-Yl`@^^n7!)KX$mazr|nyMCg{lAPV7GI7oFwVYD8&CX?d~TpYa126=c--t*Pa{${KTg4Mg?R@T;& z$I)C!#up+eqi28=()KG?h<_pvG`2Mv*i3PAZ(LeP4!8mC8Gw>i*A2|V`{EYJ%c@!` zs<;_A=7uNzN491(5;Qat6X*KZw=vGv3PMXVm>R*@H*OvnDW}FggYoty51fKu)m5l- zBaTZHI^6vCIs@hCX^V>?C8|c0!Q+}*y2>oEs_I9<-#JIZDk~jqWMVbx%gSsvmNePt zIup!J_YXHL?Q9wIlp)Fvt;&%pngHzGM1WRGisKBIDg`ROQ$!X>>4$Ejh8!0Lddg%6 zd)47ou9yvpuqBb7xhvlOKB9TqXh!|{;tiou<@`*KMDa`}ue)Vm*^1d{<`MR6yrk77 zdOa{x*u%f)W3xXfCP7_$ZTw3j77l0^7pDsrzC0*p0BH9O!srJIRnk!QAjc$0`v!E>Vl0bH`Qb~M+OKJBVgk@p75}{G z#+N!VFG`}NtW;1}S&4;*uce);3c?yVAh**Xq=$_f+p0dWbO1{lm?8hn3+cd~#HzLD zcTp!t)i<^RDyiuFF+eC4hoJy+IFbpqS1iwW0^VtKEf=xTz)+|F6RZB@%`PtjXtAme z#H$s3!<@=fVab7nRuPMnIRL(a;o;;VT%%0_0>zIwco8F&+#H)fHHosrV!F1|05Mb` z5K3}%Ij)>pvT|f(P*bzGVY0RsA+naSWv;Qvc6($dRF%i&^w@NOhEd^Bh@+aNqDdsE zW6?Z1|D`_}g_YW8$ayhM^m)RzY?-LGANhz)ua-|j>0#f9AO{*1t zM;2nqdMVQ{yS7JViX(MV+&)qj+j=u;vRt?qG7bKgraSqJGu#(cIdYjLrTE8WV3B4_ zC$WTT{82gj1O3hFHk#?!qsLVKNK{l5`p<()Y{A3jgLr1UjP^tX|EhCUzRE``F? zm$ThW{pb0fI7rOUzC8D8U%6VbDjrCtK%Dfk`tMP7>?#pL{E!+Wj>8xyeNd`=$gL+3 zM!tBqUBkT>VQWs*O7&2t22fIwH@yni8tIF951mgYQd!jtI%-uVx<%5PKpQ=15Tp6_ z*rZzP=EjIbagHWFBR)W?mFGeixV2l$bTMpNIksK-?Yp3#!;dVVS)HB7o;Ce&S$_9t zf5*8P;x%6o0wx3KPzi;-YlNZZr#0V>0F@7-`!Dv;DB+`+{$E3@;qi5cTaW2i>OV6VwBgl9ra1iV^TT4Q2yp zcNgk`poJ+|^L=4I*z~IvhrcU8DHYdfFK$nj)HDD<74<`X}) z2T*0wawDZ-bOWx&aMISkp6D4sGR#y8f6i zvAlj}f%#868!RA(LD}Em-(r7(5kJr^oh%DJTq`e6D?YMv_clvT&`@W?|1|A6l{wHq z>gee5MKF4DazE^idmrwC`hs-u9aI6kkN(pEI7u`^o%I0G!$vzY9I?n3Qwb%jf4KOk zIn&3;w$hG=u$;u3`P@%>fEFuGu4z<)XY(X9mYjZW6mi=u3+%)XptYs}P~zcAoahiElg)IGaHMszqx5`f@$`QVodyp7-6( zr2g3RtxV-yl70yNp6&3X_iPNIMwx}YPFGVyxn@hA>)PoKQoN7Q%joIa+|dc*Lf@;r zYbLGYlY&^X5ec&t)z4 z0UtdF=D-Vf^Vx4VGtuw&uO=ZJ&F%=&ln1Tn#Zt_@jeKdO!d^PX=w@bSNi)*Yl#9?f zN&Cf~R@$kuF#c++xN@0Zo#^^8$kTZ)TYDi)BPxHokef-nq{_an!Cj#($MS*2N@8?W zVD_1!Y)tcV%s~UKV8ypI;Zng}FuO5S%J}m{)ITR*jVnyRGwRzzi0*mvSLB{Sl}4c^ z%>vUsY^qg;zr)0l{a74h*<@`ik7$LdmTSYOSS)T7qpiM}-byv8Db;W}bM}3)tM~ih$yLU0x4qg}pbzisX$bn{ zcsQNw2ydtHxwMG2wNTIH?p81{aVBDFEV&Sob7t|XmqjuWhpZt zAUsc0k;G>E7s&(mjfp$oC%?)%YGBVGSA_3t!BtF0QA_h0lU|#VuWEgi3nhlYAE}tV z0E+`i@AtD-9=73VJp289Qsc3?_QITG*V?jI=bqH|IwC8bpKq~rpGX?C$~tuAw{I{U zz6UKQGZ@eIkTwuROOY*d?6*SCAJa?Us0$T_9RPiNcGjw1vHBs+-)TscEE&awrjxhZ zky8JT6Lm2*@QF1=b#!wb$TjZvHwpGX0INkoPB`Qw+Fzo;bqv_?l;b@R?XLmLNb&c^ z++<4pJaM!ifBiq)5uq+{hZ15n5cXfHMa0=Z=&dY|=!=T}g(zrqGCHtrT|DASy!W?ban!}B?Aqq>*yTtuN-x} zcF~X}L-^I;QO7q!ME)jC$P9o#vZ#nft{mvL)SA$&o{%%?kwASr02Hd*5jP&7M| zqC%8Y{9b!;zKIc#lNG6++uN^ti1{J4xX9|T2SYeXTSdbRXwqTGQ+?^7y>|e;mj!Y6 zJ?XlKM6*?^QsiT3zc5wv*8jnvIPSUV=C+^gIwOyuxLq zOfSbNB;_JNNxFioGeijwhO7Klo>rw&IzhtgQc#yqCQ>dARyNs7ygA)b%vHwwY5}QJUld`CC6Qt3>NM2s8wIr`FPo1%>q>Q;F zK`Rx>Uap|3rFSwsO@o>yLe50Tu}a6*#84+S38bP9Wr<{@=$l%YB!UtK**HdI!dZ%N zG?J2LtWwCE7|E$~ztPh%((e$Y{ZK=Zv1oKgxsljG5#{FNDF0{2HvD;i+{?EuJ;PPo z_#m-^twsgj0q=mIi3alQcV!GFw-H`$00M~+z|lxngwj-4D8>X*WUD^;$)Ez@zWH9nmbSv%>}KfiGe8l=8ih&LE4 zma2Puxa-a9+&lc+QCEInU9%Hw3((rhRIs{BkM=mMl%Z6+r~)--#qK(P%~kHOh7F0& z?Fos82Wa6Jt6F*rL@aRhH8&qnU;1lG=PEw&_U^I09Lnj^>p5vF2ms zfyv3C@{tm@r_4mOvQGT*C35{c$YkTolf6BFkMxB{zq_0`Iu7azrtpk0Qbc?E7=K+& zL`c>-yWF|fdrLOvjpOrA_H}EZ4>}XuQ@wx$$Z)@vDM7c}|#P4U==c?($nrkht>RT)Xm#;m2m^S$Q9Oh0L%O zm$5$PP<;Y`m8T|`#eySAm-ng_X(o#~s_04DLfqmmJwrwl0WCcAc~vz!dvitD2C(bq zGwc#uPQ%gfZf3g6O*JQ_k9;2*3ma4EH;z$D$21=@y67gQNEjn!vpw$BVW*T!O}+VE z*?^kG>hv>_?!DXW55>ORd_;BPIfP$;jGG{h8F=`o;a5`~^H5)x?~P!vE&9i9Q3CJ9 zzQ0J-97dSRo9q#6Nb%VaxdG4N^*_Axp;gsaS4fx6TZ|;>f@9tO)O2HXjHCSnNkui} zm|Ho=*9kQ(v+D9#fURo>%HY3oOwn>9vNB_!LhagCaH)ctl8>IDlDbb=s`&2tJ$BFn z6<749&zl!c`DzXp?h3lPY6?sZ4}xXF`MO_NzCUjzH6qVt^hqCXX}^V!Vv7AyX7~_!SU(`1$@s|>PYM-T#!I5j;=PSVkVcXlmq$2l*gvTFLKz4xI z?01>C5(AIz+_(%*2X@prI*TuT{>}v(`wToJv6upqUos;%+LC0WdF%J~AN=Vd4OQ!_ zFa1vr2Y5sMYwZ+CugN&*KcuU=o}wL2ygXdbz6?a%Z$y_u5M6DJt-J!Uk#XPB($O8* zDral%pTQnHq(gB*B3bq`l0KJ9KG;joi>D}z*)DULTPalGHC9gpB!=pF&sriUhjTpW zoXeq#fQuvLIIFXIQGRq#L7ms4fJ%IX>=-ehxZ|O+*KYJ#y}nr}Z*yJxbY7N>(UT|M z(5`Ru@@Rw4#i1Ak@w41q7J)$bS@fB2xU{VB&qHQ?3`hF4hGOKmJ9lIv(MQBo7vt8C zf4>)5p>F}_(}{*z^yC^`MOq6Bf8GvHd}`%VVWHO35rP}*pPn5Yp6nc+Oje{;8fF|; z2-2aCFk_xR38!GisVJ03UF(#m#K6~1LHJAWPuUbtrlK%B{5$evBF?T39H@62kb<9< zoI08P@TAsC7@J4)2|{7OcJ`-=C(m)*c0`|`++m=qM*wLx?lqW+Bj0e!Yrze)kxunFypzPg_w{9r-QS+1utQ% z!|l%{`Sl+S&!Mp+#hjI9mxGb%%c^Ms-*T;H(>@C-8E5OR$%wF~JM@vxx|6!ov4@0b z27Q(V=Yw;f(pbI>o4C*JN88ydz`a*Nvs2eA5xo0#7|hbk^;lAc<1=_v!ggW93@?!^ zKKH@qfT&Xe-@X0UQ{d{voj-)fPMFPP@hN=rnyEN(g<-sw|GmmsOyks2z;ofPUR@D4-m62hw6-ngjE1L?%`>x+Ok#sR=5XE-#0R(25-mC3dqMO5 z`)4`$A!TOSbolwUI2tudto5#WP5H+{GTmB$^&^cKAJGatCOT^6qv}}x0*oxRiutsu zY;4661Xjfrg0l2?uVq?+YtU|;-{r}DF_?-%}+uL2v4iy?@ zZw3^sa%O~Io)toEjKO`(l%pwk;IgiyK$};K#|QHGF`DZxR$YCDkFj*B1Sqb*^w&a) z2rXqk>6Q(JwP{g4-)ZLmm6Od*WZht!gQY`bQ=AcjvfD?*80KT@!IuoK;fv6?ok| zoB*W+u89pEzn2FDe{1qMnv5u1@%F4BXD7SDD)e%J>_4 z8`*JtZ@3{EIMPFuyxh6KNS20?>Tw$b76xU}Yi)O(tWwvhjDxU%@7?VHdC z=#5X4F5UF4eBbud}QsX%nr&$43Q`2 zNGGz<(Jgs!@$o^;KYe{g+(TU*Rt>&AKbo(tr>HfU>u@v`CiA(a?kx#!R-27~@;Ksn zrT+!T6y(c>U%w6}G(E{|<0nz%?^ zkK1!sml|-jO>c`xM^EUg>%O!$&-+5W za?T*CP#8pnkW?;A1$3GpF5PdiC&@SSD9+}6BH1@}&MiabtV zgUVOw_F>+&^!; zpE+FKe-RaO}b1KMt&m@OKPIGOEo%lDYf~Oa9yFuXXRBpUv?m`!Acs$cUwB z#eXah`1`t8ISQcn|NHA}DX?Ts6l`=Q)VQAg(-8jUFF(Njua=S~N&QW`tLn<6YP8P%-5$qN?euX9*niAb^)aAdhWj?(8&r-WE? zGwT0Cg=G95?o#R1<&9IHTlQEAn<+_iiTPM{4X@iOe>n&Jm(%A1w;K|MtD5>|4J~5BpMP({DY$=A zS6T_Mm60#@81&!cX9YFYa$zA{P2Jt#{V?e>NkGq}DJ&|ZV`i8$uW5u0Qr5B>oA^84 zy~;~Ul0jEKs9$|t|FWUvpmF#7I9Ip*T$HdK_@ptW*O&#T!N{?zZrJ)gOpZcvVAy;6 zf#-@%z{`RwvgUE~0JDD6@tXl_?RIa_RMIJR*Gr7rPL`k#JsTfgeVnE^iORGtzcV}J zD~aF$qQ8(qMeHK1F>Q zdFiq_&Uj35?e~yr!YENl3$2;IxFNyK!)Kv9FkiB|T>ixGmnQrtSSb84)aI5LBubwM z2^~x~F<$71x))Y0o)NS@Gy+84W-At3V1UKqGvk6UMxMR+ot!GktPoT_u>Y|!qUlgq zxXtFcu@7l%EspeorFa)Vleew0X)B$r{ULGPPn$tuly){ma69&ybHMVOWWFs`JB}Np z=UC1DAtB}WN3MenTz$;Ok!(6Smiz_d^ksCdKDONpMy8S9k&zFg9G|1x9-@10B)_NT z0kxUU)UkGErPry8qV-rj7^YwE!=5NYTxZU{);`vY@ZLf}r!HrWx)q)?Rs4P*iWx6G ze$G~{@d&uNudcK|$|4!s?NC#Nr)Rj9JQT2>&0enDwFsVYGd~H@Y;X9MX4|h~nLszzCyy#OMBTid!B1n0ET%z|rPN;vSzX_%Q4PMedVmJ0v zBBhAVignU{^F2X$22#k#je_mJL<-wP6dn%!xj=5O&y82+aLnDBGA4*?D57MhVxXW9 zD<@rC{yn`t+>$VT-HfQ^CX^F9EsaNgOD!l5E=;07z>0(GlD(?Nn5>GAS8*Kt8$7jZ zk`EOOsi?rJp&*o#pnL;^XbD_S2Tm$9!H*T*nfi{@qC#Rkc{58la&C&|?|>K1Mq{HbS2iGaVON zs4A_Jn5MV<$1+ujHTzV8iZXAz|3(0NE}^#oR}kIS|1fnit}486Ui>~nL7s_`86>SY zX3YGCrs_D>@r}6IrKUs;u|z&nSLej0q*R{qr1Uo*`P^u$fcq`+B7+DYh55WVDTBJr zphVH$J9RZ{#Id!^QhiKCZ8b6HPqz-FoSd8-bWBp61k0_!{`0WxSrp1*=C;$afG0Sv zvW&fHh!nK)m5~EX0dX4T*(4;wLPWGwrmc^u2}JzPkw!*V)<&DB1{>J??9*zA)K@y3 zR=f6Q7bB{RB_=~{DMI?1I4Tiqz*48HNz$JRPu>seW*F3bX?aAG6TAXc9B$)Z@1jLZ*bhSlKdc{&)`7TCzX;XDC z*t=HZDiu);rnj*B{p#Df0Au&m)`D(0DS%9p^=bIm#3gF`Db>Dh%U=BcJ2`YKM`^YoP!>(UW| zqN2m%;(aF@iGUo@-zjW)3e^SgOw16y_<1Y)DYb00)Ps7LMo??&=-gp@lvCt$XR0(A z8+D*%4Own5w1HbZ9$Paby+K>!tQ2=~wmTg`k=C-m?QKv0?YZH(|5tWLbCQ=kbL&R` z!}P?5)=`pH?)y?!Bf@Z#81I*IhS%4Ftcb4QYdj)@*RDxWgs-Mk>+qrs_C?>C)f{n) zKSM+me8|sF1Uhb}BSkX|cCtjvd2Dw`)2b4!I%0KSG;9QYjt&z^>zAk9E~|0hF7`H5 zO=CFfcB1tYnVxEAm)E4*1B&+J;kSyA$;X~+kGx(>F%vr|{uDZ&iri1Ox0h&DKYIKO z^1l1lNCRaP?PbNgvJZ%!nf~-Qr@(s@^hwduiZAdowlHag&ZB{!Q?Jh&A&}B1u-=bi zGDE;k*lFEM^xC>+W8A=F6HcZEyNq-P4&tGNq^S30NWu=d_;5P!;h2Kv^h4QAEO z^7@RzB38NH`qGi1Dp$u~a}c1blMFRY_{;s_AJ;9c-IoB>_(6I#4k_KnXa_^nOgP>;9;NOAQ)+5(C6jdn3?_xLx63yNbr-#ibT_LRMq@bmVwN$+ zvbySe+ToEQ7RF)bqVAf@gqq_+IKKFvFG|Uu6m(_vs$GUDw1OTL|C*WRn@-cQB4d_? zkABdO%8pG*4eS3cRW(<`pEtfhESN#>SUpHU(ugORS;kY_P}7oMhDwRF>_wtMIu%T2 zNlwdbJXnz(VD3aMoxq$f3r5meBiUBW!55)cdKVxejrA2+VIKygsfY=O#H{oV^sSJ< z$ADdeM(JHd4DGMwbn*Aw8+Mo&;Hw|zf#L^((~@B@nW>n88%E=G(`_&CI4|dz8}H9f zGcp9=(KB)WVoB&LW-uS$j3|-@(M^p$yR2Y@WG-UHx2>kFWLs&Nuv>%gtmann zkP#i(NI+@v#yLRB_XBHT5XtrPUv>1SFPC(`@(w-oS>;Q6NQkfkZoRkijM0=iyMS8W zfI+G1JEyO)Jw`HH=04`uKhu()=l=TEfzj;*`x8A3rSqa`%>y*$8vxX&*Wcb0HI(DW z5{@Kwj%dS$h0Y2@phWOK1v5(lRe|xAnEhO?Ig=hBZ9j#Keng9q6Lel$Hr4a(hIZ=!TdH8HSfcgj4@^R+b5|UXe$FmNmQiP)FM^Gp-49zus$6qWKcasoL^jEBa-s+QJ6 zWLiB!bf#EHGo7zdwfx;sS50xx!v%^ZOmG{gmt>xkvB?Dz(nF7?u7P3(JjM zNv^$f?HeC_zlQT`5-3*ZvtUfW^_KsKeHd5h0_6ZfNnT$BLtSLzSx}eW{kH)(|A|}W zDuD!pBpS-+GB!0ewPyV3;BdOaV7*w7^=lh_Vk9<04P#kkIB>|gBCt-UQF+^%k)4>8 zmGh!A8#(r2#WR?|@jw(=3KMmqw}bzAn-5;(%u4EZO3(vkf7^UqatGO85?rOl*syz! zY)g)H5ljvmNcC=OAo}iOh!`pPf;JRRvB+;(OI=~00&RBR#wYPa zz5+nazIT}npTp!awdFjZB8&PX%2}7M(e-Rlsa%xVq<;%3p=QL2aCP}tpkCU~oBapE zumg8VbyP`(e4gRq(I?=|V~c;z@bU6_nyb)?&hNmtmm!BhzjK1Vdopb#KXd&;t4W$@ z?>%FuZwfxsA$HMdW3-WB@>mSp;qX>sgC^|DCTAOaTSld>MhZ!;h$1a)f|Zp2OWtND zAB!Y@_%7}j7)lX~N+eXO40#aD{57UQuND%OMMVw`qah=>Bc^gT3)G|@J0S=qbb zZSm>l*Z?cHJxY(nTE&CmadehgsW>W#CFQX{VXuv%O|bQG=$v6LRTuP2Hv6wIB3hWS zl?`IMJWhP>n=RHIIr>}5T!}IY63V0JZq{XG(BkLNw}8{G)z2nGPX@cuaz4pi*7t$u z)@)&&n3o6E++uRh7ZbY$w=&!T}j`Q=Fa(l+h0#8Go_*Ui9@)@kZ5XfVD zqln}SrnWHR&LsNS@PF4!p^*LiwYsfdi!?^oKl_Leo#SB%U%|?o=l!4uE2jW*vy~u3 zsDzg29Eo+z+?Ipxnw&UF*2Z z=r9|R4EI9B$6#lEQ6Gf4w@A_K5=f~E1mG+Q%^4~fpwojVjn1Xq&{+s7a&U&T7-9v9 z7=C~FcXC0abI`eXabCA4g1e1{tRdH_4@nJQ%7QYNgPGc7)Acjb^2$dwWux5yYzA$p zs$+XAqzlE4IcPOX=8iDYCJd6Grb$BgZ4m_gfDXRJvQ=#tTJVz!v&BL-{h;J;I7cKR z^fsF#b?JZ*9tkq+A=PepPDY9$R2T8JSoI}w3a6nOk!uUC=mB_j4V9F1r9V^rrR-cn z#Z8&}dbV+Ks}p^Z_vF`=twKM)lsHJFUp|+}F68!2sQk}_+s+v!_41vco7jIfp5YNl z4tK6=-XQ|qcB{`@(IT6tmsOMbq}JRKMQx0bKX5*FQZ!)GUCF+J0ZvK>DDp;DT~bm~ z)`!Ym)>hK;r>UN-<>N|VNeDqF`uv3&z695UmKF@5z>9fgpR(mv{G?Mm+k}k8f{ctk z(E*b zFy_aC;1vllCBH7PeWvD)%|}rQ^4P@h_hWu~0`dYh*V?Fq*XNeLtEweaZ7t32nk+Q)iPgxK8XCq;)3@H5n@kKuLlS5tN+*9DaF+5m zL1aJEkrpPL&IROIO7~lwSLJc@#k7{N>Ta)hf)5k%O&l9R?iR+5uE?hp^w{`mn%Ww6 zHWpBU~MkM zpon*a@iBDq*vS?05hQ{4iI5wzIz0TE^B2VwBn)@K`(Jsq2GJ0q;a3G7;tCi8m*ZB}Khslp@E_()CyzSF4HH(v!mU&XFYm$e$(s1Sd1|7`EESyh@K|CgT`fwz9?73`)KddWZRYxVUkpf{D8<{ zd2rosc)c*y`(Q||=9VApCr@iz({X`H%}L%O5(01Yrk6x($nu8>;=a~Q5IQ=X=;^CI zaUZ4~L>sXRocfJ9o>-u!ee9Q?HqSG%FO<{!5o2P+PX>wmwd!14^?gJ2nSOuiR3wrL z1NOaUW%m{XOCC4NfF)Sx#{MgQ{tP|RWdtH=Se_DHD(Ta*=_{R+3Fm!J+O4RoB4&irj-rke*t;KPjM-OYEG4)A%J+h|Kl0b8`h%z7 zrBAu1J;Fzf%)~!P!Y`(WuWAfdYLszxkKGGTy*l}oY*%_kPR47@FVtRqn&J_?qJIr<`sf5`e_ey3rKu^Ypqy*6k1|j~AFKT*4e+mQ3dxIyZpK8IZ2SFwrG}>i z*55+;iswR3o6KRu)pgYeLZ*{YT4k5V{p5a4a1-G7mc^t!M0R~`2z4px`;oO(=TyS3 zs;q1r5O3GzkFqs4vJ3eoK0&7(pFN*anQ;pb@cT=x5Cp}hEh})qAoO>jDRu98M1CrC zieLlNb#n2+&C5qK%cqfVVV1j2?oG$U#70cK1|em5U0^Zwf^k{sByxn9|06tax=0OA zYcMv}KG4K0%j<-4X|+6{G%UmJhm;~=EU2KkG^jlK3f%R?-eY>EfY+TD_ppmf&YdD| zdPBmcp6DEHYT(1Z(?uG7O z8UIhp)%*i}4=%!bG!#q^E%HKst1kauK*u-#@l=+G(c~s6BLw^pBN1Ak`GY?BLMcoC zg5~gdfwHCeCkX%Ndz1@Ke-S8d5$bv^-|CY8xG8d#+J0zqF^+y^)|ID(H^vv@R{fVt z!>Z=K3YU|9r`GFFh?_|blq_P>jTqbyQ_3=vIk4C`ZZc`qImN$t252>}PaHr8^h3pl|t z*3(@hr82rT#>e}u{lG~BS7ilBFEmL(j={A%m{h=JL(*8)$=Hcq0)Vat+_oJEI&Ye0 z(1yZ^Z@j$TXS4*SHQ$ni4bj&-I((+dL}8(r6oH}EuJsD=jkb+0HH-XLQgHZEXkD$B zTt$Q>Br)f;@HS+=+dZmm%&+*rY^W6+55OTUw6JjlNmkkn7<@w?@$`M%+8@6fq@TJvuqk_-9H_Nh`?o zwiBYEJg#GEs{<#dZ%Hmzq01cdN5j-Rf^Xaz>hv;MZ^US1S0_ScU8Srv(3<7U%Kn~y zMD@w?=Cs(I%Q6klI^t$MetfV3Xbp^z+a|+5Zm)A~O;;}cV#aZBpa1)6sH0`!Pzv26 z`?X=5;&{ujX~r=DNg@|9{d1f5OxW*@vliULQ9#?z;)c3BwpLVm3XQMdpsD0zmbODBs|_ZKW)khRA%F^2IsQ`g&tfhRD9o-*xiMfv}_TZN_({1cKs|z2}ol&9l1w zy&smap<+#!pHvfs30vQWGe3e=&QkbKvo;VpNHRcDnWHX&)nK?kYFcb#VMV~xxNnu? z@(_?j0NhwxNtXy~!No?$vlpRXnEDO)!*#g8<+_9ZReQ22cgoBx;M%a}|={vNeyPWO-ck#=XKUU+Fjd zSQVF)S)NTzH19%1ohv>A4iLi1sx7YbzL@e7AnTj%1^bSTqVSRfvo0S0$L z#Uq`PwhurHbF*AT%-y{t;}fB@@peTGdMrMfjiBVm_Zfj2acXD35v~wFwN+f$t@(Z6 zGoIWK@l|^IXsuVn%1st)d}L?V{R^haaNsT}H$IwF=xlbz_Th}?@%D1r|N|s=j_z)hG%msiWvD01qC2@a?*?s4!I+)t;0Q8cK^avFgY|QOr4#7 z|A-3qOva}3omFgpyg|t`sFDyV*`E>EeqPLyvwn7Yxy*QN@LAYeorY=U$}bbDwFh#^QN!N@0e2Du4&lKxVsfcR1oc)2X#= zl`HSiVR!%9KAHpszq~MAoS+Ab?f0YYde?tV#rIn?qTtvH2Bahv#IH*E!;J#tleqGu zg?;mmwnf4dw}REwv3rSFi2G%5+eOcr1Z8yn^Eta<908K0|7;VxQ9a*1RU? z=*70!1m}G>akB*M$@;b@FyV+-Twg~gURL^AuJ+%N&Qk>NmPt3AbqirKcTER6oTy)C zpQrbTjHPkhF_^suI8q}QcnO(}K9e@2Xsk#@H@oi}+}4WWwDx}cPD@_@UumSd@BznD zZoAu(jnl=1mhId*Bv|<-L~R!#cKEdr@4vqq9dUmiY&Tw+-c_pRQ7zL}5?2|_4zo~S z+qKk0ntjfg`T9Q*r_hGwR`{{x2woVDjSOuq%-)-WhgqrY%=~Wd6R03c#~L0INhEYx zT6qqSdXDiqR9!6l2Z2QBFdN!!$YRK{N&iO{NoSC}8StBR^_Gjr?-cIwc(kZr9r0$r zPq~k~SojH)f)9NS%k@W}e@o<#y_yn$d*oDPsvPjTb7wRs)lY>=31utU;Ml9mVI6;W%CUsy4c%AJ4eAcmn|%x~mw9Bnt!bdo)vYlY{gQ-cDZpGE7uao`N%@9WMKU zbij~nQoO~1|IJNtkMS?}E} zNnzA?7p$nT4;n3de;59e0fnrcbLU5N_?s&RQHl6tb^GoO2HnWw7n4`Z_Y6!YG6Hac zFPeWLNz4*=;?aGLbVhK*RT$h>3_6>dE0#8 zbUj*}QLb}bqvIGWJGHgt?zMLGlw!Ww&`Q3tfO_OUv4@S;<1LmC&2)L58OwUyeuAQK zX(I($u$vYML`oOEWd1qD#NPA{=cw9KQj>q>RuvsK!(N^LO&m{LdofZwPLGC7_ZTu~q15v8Z)53Pcf7M!WZD zESF^tPIRSugoFv#KNAIS$-) z;?CMmm%RE+gt|u%BQ}?L`?W#2Wd~WlGZefCs@tO(p|x)%)S}w&R4p%aIPB1lb~i~y zW%=#PvQ=;wb{Ly0Z_JWZ30$`aha&l-(+Gy&cA`(HaU?;;A+P}rTfTe(2GdJ8d>ShS zOv^luM18GNG7hn|5O)RlgJ$G6^pH0~y|0t%pzAkdpMeh2@*&2+n;>19??a0hQh}Y1 z5#{+*M9&3@W>h1QY=kuxRyXeh)aZyBcE6|^^oRx&f}@1H2|qj*;X>g;`ZwHTBWd+e z_kQnhAFJc!i2(=%3IYcNk8Y+WC)=lRo_!-l#F#sy`%|d-(#x1reK1X6e1{t_LT(c?-C@BxC2knK`z54AIcjH4dcB@M5I!U0m^r zcnfj!HxcR>0SZ@%DrFw7_6-}U|e7H9~&V zWa7x~iT3&K?8@Y(xirck{i)NwJR+<{M8tL~B zX@mVkYZ`Y1Q{;v1@U!kb+^XeBV z4wIMNW^-V{%=6Vz`4+C1S$I{cdw)kE+xJ@U>?xy>|@|Z z=2z?In`C8VWRH}^{oPe(dInPX_&HWXv`y>)Q)wvBxEwW057?g39HXCe*kI{+G^i8BA0>O#B zxhx}rBDt{!PloB+FS@v9k&r+{ zWt=cb1yscjU^I*V`Ab-Jg<(J*bp6;bNgfbuV*C(>JYzO232Jm8qz8$^0!bF}>ipSt ztr%FgBNikE6V&VcnSuyr12qCjOm=eGBB)xW@p>0L?K+(!(f=E^CeV%)G8g0x2|{uZ zRQkx$tVGS4yw;sa8Yyqz`o47}sRY0YTh*PFmWMld!^x&U3<6;`LZTRk!s&r(atDYoZ(Vmsl8Rke{Xr?ag!(T%g!(?31hbe;^hR z#F}n-Pi+dnP_W1k<@6}EzBjjCK#UfseR{Z$zXUhSFqAl@B;Ll0L)X6 zDe=`RWkvwQ3GhP(W8hG^QHCV6_<^QN)i-3_~9HrB=7kct-L-p#?T+z0-+;<)tqNMBAOyz4(yjrBw5 zXwG#efUMUCC%L(XNZ81suNiF4L-po4+#I64YNg~X4eh2)eyiFZQ8-(h+ZPXT-E~!2 zWjhN?ni`9>7USUIcOvC?p?3mL&#caJ*WC{Rkh{960l9VE?!Qh9Ptmw|W}?hDkIl+6 z$lywfZWTguNx(j+F1y#xy$R+YnVv=ZsQt$~9wkc$bQ^9mbx)lHal($bH_+G5@Hxz` zC#qb?S!Aex{5+8>h_4^DB>gv99C9m{&~@D9bb>!bAh_rN%rGLTrr9=8zuJ|G+<>SY zf?Y75TRf(heCMPyyI1awqVm1=imi{{bTYb8&Nozl>48`UfB-Xk(gbegCCtp(S4`hgKWxPs$s#3~T8^bLah7-o_FaE4BlxX_gwm-X7dY zqe!6>ekPJ0S8;=3`unN%&vL=dAvvt@N@CielPQLoMA>&?21j#P9vl!VDo7#Ci8icI zv%}zOq*_pwt^u;?V~3dUNV7-4&&4B0smjcb7WxkwIZ#H(3X$IMn3)HYJIg)>PM#_8 zd3qw?%V`62-OojtOo`A!0)*`Ix^%kh@ge~fmMRYHX`_W3aR_OVeFjR-cglA`?yelq1yj?oAIR*|NL*AbJ)7ARu|7E^@3D^ zfj2Yl+xycjVb7a2uS;dK{!@O>pkp3q{q0!&Zdj!ITksWk3WuBP!6Y5MRcE5!AwGgh z8!OjWYNwtO8IHHx-g`0LnWRa5oyS!=B|i-7H+^XkN%x*D7jQl6Hzt)*Eo0>wkHcJX zFR|DDHg~8#?9)S(Te#r9Bi0zcMqavtk zW9qw}JZG(aU4z@(OH<4#xM{C*vWZf5GePQ=477B9GQd~6{2+UEst05wi^EBNN7ZH9 zw)4H!c5j!xgcfBrGIf(~+2fic8}kG7*Ol_!aTQ;hq}Fju@lW`N+uI+bA%aZM{BuE! z6&gUH-gpb7g}gS=Ql!#id;{UxE}YvH8K?O!4m+-ItDLcG;m7e2B+)W@w`>2{<+JXy z-NSGOK2wDW*IT!ucuFMN9|xofGCt_f{oX*YN>PFtTFJtn>rYwNqyPHXYyO?Nd}1nd z%gmOl-VPZa@xf?`91Swe12GMPg7VoL8dCN|t(V$%&TgLY06@8q#nmfL?w1fv$d1&} ztoQd)bnBf~ow1jGU}EN4iUFT_DzWJKJs(y=FXqPF`i=&$G&5`#ZOwOO+JAQA>ch7GzW^xVQ9sAht7zml!D$aB`qs_vX zpn#=EemmVbe)aCg?xRc?RHh9QhjH@u7Ny=qDk?fFDkZ4X7x9!Cy4+^FI=BKM-rOFq z4-8foUVl2>2Denu#GI_Q)S)E+@-KLt5B5)psbeusRm>nSNrg_M%j_~y6AgD)>nw6%q;U#p{EC;N$87u9#fvOWjOWuYF!qG2Hu zRdMw3vGNLXY9Y)QX5{dS{1{4}bwhf#n~Ej!7WQU|pYjUvy8iP+5FzaNaC*`IEmsEj zqE+ZfhCdpTI35qpUuHu z)gow>^Wn9KLZZ;LOP%_#3uB!F=_U@^C)#B_<`#M44y5oex2OV%qj0Wc`FcGu> z@ym#xAI_z86<$$(a(p)1s(+0U6g?bT$Sc627jS5c;GW88M8f00;Bw;O|Esbz)H;SdwMOOQl+co_K+JXDjgk>OpU2fqj76PI52BYfpnQzs*di- zE1;I8rjAWWh{z)OPZ6J;8Hovjj|eP#P2OtRiPS$$4t-yDaUs)1-{88lNCsC&PX;Cp z@hUl}sG4$fQ9m)oN!CBdfbYEo;x^%H>!kgkLelKgK0yjTI<@djhUP*@M)^5Ulvf4B zVNBHbu=(okq@`yok8+2cT@<%hB&=3X&ao*AJsKMbu?7)=cEchg zHS1h-EHpi=t*yr=V$ZO~nwmtaRUkx>pK78++c#aq7-C3)1B`qPW=Si&Jbn#w}S=gWp)2&@9Mu=gq+A!mHz>8_iaRfk=i6VhBFEd%(;uRh@zz2p;_nOt+mCyVEs{>@etc;@lBtDdf&4HtEC zkd&M_$!JR0+RS`@_J@@l5p248p^#yqF1&U;s;UY0UP1K!{D%k5H#K8C16lH<)?$NRAcKgu6XCcktXLIuBl7LpesL8oI6#HW@8zRiP==BxyD z3wk8e)2%IEH98J+d}CJ3R#Y~llsFaX{-C*Epo%iQSt{AQXMuZTV#6D&Kl9OOyq@0> z31QTEwEQjK{}6#4bGB2YP;stUkTLYj{sJG8%^RS6hy-UfM~Qx5A%C)@H7SY-_1Ed5 zqM{(D5s-ckm8wdq#x|#ts>+}y5UE0oAwOsMPj704l`>@*pP#k}T5q3jv*)&SDrAw@ z{&6l)z68lhSv!(ZHEC}q)g_((>dL%8#9oU@@OJan5iMx;$BSh_{E$HQ#ltbK65+!D z>hoLY;rvUh+3VTzPRyDRK=pk2E@GESyZ-ST7h2%AHsnvPjT$|OLPc{8f$%8s5g<=6 z_t)4rv#k%m+z!#{9nNrcdqK@aC>YjLTzcD2Rd3@kp zBV}9qN-005nwnEvhUBhg(d7xWnvPni|StT|uJVfq>;d-(( zVVfd%xExjtifm%kxjmbYk)T{{#k=T@2aDevZ4FV67kN6_do;u6S6=;nf~xHsHH>81 zH`tzd4wa^euEO`O#<1Uu=U$Mf4-IzT{twZ7#c9;%t5mI?u@t(fWbnPZUIl zI&pIpXwLpfB>n>HHe^FaiFCuf3bfIw!U$LwkLn@cL5Rx$Wr#GzR?R-Z6cx?7xa>ML z4g{AlRw+0i9_z$rrz@yy=q74uDvoi~=}(%;Ai*N6jkwV*{_4?jRXcncjVp^W`WoIkWxVy8BO?t7Eu|hV z29~n@+4aM~OloQm3vT;c0FmITt*foIj=mARu_$;f1?416S_+7z!{u@O`@RlnYpg1R znRv?IdagVhBBzbD;HHktJV;;gm`F!uOSRU9iOu4a%3tBqJ=bkNiH2nIuE5jv!ol^% z$rZC~xG-5qgkQ|`)&_)@;KW*Xcry7uQhl5g=*XLw#P1X8BDs8cAwo6@_A+wl4S0v4 zzH&0mt%6`z10aNhbr%UMP@u6&KHZsSoruc*Ub6sbi#5P&5Pp9tKK&$#5R^IgLm^xB zCvywn&yPC0EuFIO-*ExY-tnpxBuYdbnu@gB7(5BZ;1rw4U<58ey?8%UV znEZb(Jq8~hYIX&fnNA$A(aAVM9DU+_`G4m-ZNgk(K$5xs#`gVF+5fkUe6s{e^3Pp2 z27hKgmz@r~2KxOCH8Rtq9#Z7AT4>h4iVm&+4AH-o987P~;+u~8CYJO!{zn?O$1Qok ztb)qvYCTXiSU>!7HRCpU@Cey@vEHY<*qh3BpuVttWWburMxaqdMQr3Y!9KQkn|DvO zQoH44AG4-4$gn&-rE+C;=$Tx1!M#bc#`-F{kq!1#79v)FsA*%dsjAd{e}5Ict&^H& zB_G<}-`9NU(swAs(7Q(bK4@&gAl^mP$Kh|T%p#T$)KB!c8oQTJRr;KjUcC%E&BiXx z_UIncKpP}{V;9EChx%{c#kcS{42q+-C2jhO#hS;n)RoKkzN0$!)UA9sbAnHd{FJP5 z*tX7xgl?YT98W{cEwCq8P5t2n<>)-qLfP&^7Tx1SPJcondF#zDrv5Efn?WO#s{+LV zd}rB<9~X6xwQUx--C%VhmDjHcq4R&oX63I66;DLUMwX%o9;#aZmzpId>%mp%jwny4 zT_RhaKBV~>1`@iUcw)ht89W_+FmZ$cJwit4B|;NVwd;-+m_b=if3tG)t)G zDAdg#C2+pa7&e_MOW#f2|DY(HWK%p9z*&I}z&AHGnJOD^_7Pufx(qLl$~o@`zxlkK zWYVGC-Db9UdM)c+h`|WHwhF0a)toQ8LjoJvT@SF*il3*O`kCIIa+pkCH7h3x)L*)% zKywUPZx7E=Y8P3a&FRI3$s0C3mjB#SoC*}FD*v1JuotDqfSbnbC!Tb!ITa)to$`+*Agf=B{T}0E-6HMt{{L6^KHAGOLwm|z9cjqoa0zk zdw-|0{imn(ft=|t%o?1X8n&tj$;Uksya|Y#)ut0XzTAz7ZrP%F6wHIAih;+{mdCm7Osc~7R;<^KHM&EII<-`%yo|QGx#Z6AXonW3&ZeTM5nJa00 zVc*h<5{Pb-y$Ery%*>2s7F8UdWD+16ba7g*DGfF@p~Z7kVnI`mu8sAL{DENH>>S^F zjwDf*y87HL+U|Ok{ck!K-FILH%{)l)v)e^fn)BM$3P*PO)L{DU+9;?-Ci(exxfnev zIEgz!yKFb+bm+zU=;6@A{<6|=Z}`~%mN0X3;8y%sZ$W*w0{mjZ1Dpv;m!n>kyh=>|?ve>&?FAbWZ=AuoDBe%KEnSrzt# zNiix+b-N*08Q*jKx58ArmQU?m8ZNV~em{cXS1OizT{Yz^krkG zb-0%ePqFHIsd95x6lTis3K;@U+MfGKSa*e>T-=CGyAi395As0reZ&KLzw-*Q){YN0Fjn76HBv zEvRn1!&5sd0zhLkB|j~y)H61)`=l2ezVJIw_~wJHNB~{pVUc^uX`b z-?Y9l-!Kp_w@9i;W6EM~4sqVErLeO@zFf>Yb?$^sIeDg@1;XVrj@eSyUqJT5z~YxP z0a=CCg}k`LkuG$-|>1m zY5TP@bQ0*EMe{Rgi`Y{X7VH>{I;rywLR|@izAIyaDY$W6?Ny50xMd2%Wydjwiusa;5fI*t=6!q zMn@&%IpB~lBR{@uSg9)PCiOsfsU|q`6^$;AFYI)$eAou z6schdw3i*e76>DZ6R$awilt|{ua8fA_r*jn{T_5zKi|+hW3Y(jeK@mK-APoV?8!pA zCNhdjvHkSM3MyR(DEd{Sd!9|+`^U2YHV+TX%JgUwE@LOrq4;QU_}{e-uLhv&NgjEP zw$w1xUd-h?33GHyE6UIb#cO3~P-;k}=)OJ!15Y-Hy)Mc=7!rc9 z4)2Gs8>oEhGV8q}L-u=a^(sqzSf+|EbB^MD?r+;?_N^;l_Dl$))X#SiX6}!(Z@N;F zF^JUBYmd*oMQ=b>Tz9ka%pfb-dyk!C**xEow@pLKRiT=Fu*j<3lfU$0J8RbKSrhRw zk`C+L@Wb+a_Wj|}10Pn4Jm+N-imq8I*cO}m7 zbQweM3&Fl&Hyto;Ti`{hI$cRIW3Ub`V?TB?ggB+V(l)k9;U!@8DrSsX_Yx%H3W&7d;zcyXcT7gq&!x&^0D%@MY z#k*-USN&kz4w%0O^gKow4`o?$C>Jyubssi((xl5R5I5efzn)KXFJbX0XsMOV$FUW~ zHK^0AnZVR%o@+2y^!UoJkqznmY?~&phol#1ej`(Fk%}J4X4`pFQFF z3cUTXry--lir7Z5c3q4bzr(oL{-8CYPdBqL+ww660DUjHddpvB{C3fclX4(A`WSW3k$yv8$L1b02iRcdb9dcUxxiQC!Cjk z!HvAtRS{&SS?z4z7P|)nsR<>w5?N}K1SM;_WhJ*1CSVhj_k@YMW|o=;=6@3XXf_;} zvm<@*b-yL7D`ph=mAiHqTu&@-1bN3*4b}!PGcNBe8lWnNkXF|=DC8rAMU!@W^ zpRvHATo;(@Lz16s-8i-n3lDqyuv z5?Y;L!U84&ZtwGC&6#3!0q;@^OYT2)N;*wM9b5gHK$)|nJ1+4_rq2I9O29kJq(k^! zG|z)g4SZ~G*WA`0NgWtv4~(QPELCK-boip=MjhGj>mMq-KyZ;2DqN2p$~qY8fy0Fb z<1u3jr8snN-=eK$Nw;dVqZ%ddDF}TXTC6Y{!BgbgGoz86Z1;i-)mb1Uo%R7kOb*$F zG%r_$QR0Q_?@377MDS2Q*1r@1LVu1oHO4P2FXc+Gjm|(%h^%mj5Ts&B(m(-p_G1KB z+s(zSHPZY|CYyiNAI6b!E7+7G6Y@l-OAv=!`JR;T9@bJ~GJx!zWC27hQ z(`NF{tA9#GP4IBYnr}Zq^&JS!XPd=0A3M+ ze@mAq20sqvPaQj%^Y3g}%zN;@T|Zc=G7`$H>Z_lHTK3k$=W+D<-h46Kq*PgZG(4Pu?U{{8#Js8ZBDn1 zYz++!Notxn_N?_@3?q`L($E{oI_#c`E63It8yXm#lZs@m@vvkZuZ4Fi3|9&at46cA z*$hs~Iq>CWU9Pj~Zd_D%Z{-+;Ba@P}wxxKc}RR zCiWV}x3dUKc})RgOjKH~#CoiZmiICVu%C$Zi_Hf{8I;+ubbvHRM_U61Va1~!y{vAJ zqRZvo{pg5S4ljM9=U4dkZfnQ>ZI3k7f!8S6sOu!+wj;jV&3^BpPXdR4YFx#d!)6e| z$ESmp>fRLGrjv5=^cwrtRhruo6D0uAOEOJ6&OrvZ#fhyHBfHF&~bazDhm!F zN@FoJ+FW!ru)HdMOm2UBxpwk7id)F2mOXJjG~2vZ>(4LD;HM~Tt12q~6=-X|wR?b9 z=WQv5@9p{r-9*l|MP+u%E#pVVfnIe3+lfUGY_}GHe~C)=QnSg4E!dXpeu|ES_w5Rd zbTqG1t(<4N>g}PidHS}P{gaX_-mgP>soWV~qyWJ*%7s6fB!|047YvI-_wUQ%H=j*i zd4Blv`w01~YepeWxY)}b2=wxjk;Eq9Yr|ftSQaEKAQtbWH5fga)d2rU zW$_U+2>#ks-?hV-Lblu<3HTDWM((GClT%_XPorWH_ubPfYzox^5B;1cTTViWe6;U0 zGcSrRi7!u5CGNBDvit5wS8ob8ubAggPOx`}Fi?Dk@>z-}-{-4M;XV=d1it@@#ZOoC zBZe4iU5G1yB)>BPmIek2s?$zL`rQwMT*nXpu1bOHA128yt1Qh#W6aXaU?}@m04pm~ zLl{4$NC$z2Po7YcGzqzrW)KUWmT~h>Lz}(^;h0Jm6zZF4 zq|ybYcG9~YZ!^DNN!28De+^ny9K1|>2Y4t`-l5bfuk-M>^3@6B^Szc2iRFalvS6Bb zQTtOwc97Z0DdNGpBauHi$I;bAz=&LSbDjvj9`<*=*Rk{ORDZrjbJka~eneX=;vY?U zwGzvq!N~ycFqa1pDZZ7RG2{sHUM;NotDaohCxv_M7jybmEP*wIqAJ)VpP zEwke@)DMF7^nLNF1{KE5G|RhF1t%eZ$ce16Z1m-hFYrDJjf&U`_v>0@|4!M*|3s^_ z8#oj5y|sL-gCmyfi~Ji>++x^cdZ;Zob+gK*93Sx$P-YkIfiiB`$bmbKp2h^L?u>QR zWDGkKEp1^WKK*1}hSP5QA)g35IRn#&3&SSHJ3#x^y92WG$8y?7`0**t2 zVr^7aPWd1hkXo+0Vc*h;LXg-)&14*di^zRYi2)tM=%+ z@-jh!q|0jI^e@)lfjAN6V+y*0wCM28{P^jW*3&RNr=5*CYSq?G-h-`t3e&DjR1s@$ z4ujVTlNXQ3llSI^Og)r}L*+6_CPFLz4pQesy=P1rg43T38!+`ou|Ie<8oln(?Na3M z`FV(wro0j}rGtLtn_B25oi1vowhYJ5hrG5jUY?+U8z*#7_h!Om&b(^9F8m)7U|pM! zqvX#6iz2X)>ny0P@-^68=R6SwbeOAI$V7BA)76DMTeu(G7YPKtPm-E;-5x$LdV2GD z{d)Hm@7$TgLHm=)3pJZiBdWKas}qh{A4%EaFAhbiv3qgXG&zpHKkYV=6Z>WBh$(pl zH_X{NR%dX3gO6#(t2W;GCm=W)2WU5DHUk@! zdWs7LMfR?MDw04BwQ`h}8LZt}sjd3UD9nujp2@AZUB->Zt|chx_Z8A^0%^Aca`rNmalbqo=Q*w9Kp&*f-c0rr*LD1*n z@gkH^j24X|1!AA>`;gM7XIKt;{jqs6v$dD_9*keWKcnon@q@)P)dx@N*^!LjYVsS8 zrKM4AK)bPyhJGiw182fPAc)nAE6JY-Ot9f$GHwDimtnoL%I?@imrym_51!VmO2u!t zl;Ad(Yhe47dAVEoE?G;2b83w&rS0~!l`sFnK73KSG;lUqtKgTV(p~AadHq-9;kC$l zumZ-mNwB~c;6(3>QasgV`jBzp;|81|66TUXJ;o1`_R)~mG{J;+r>}h|PGl!Ig6|%A z>$GGr$%V9Tm<>(7SgBT&*9l8jAi3g>Z>#XTlXk!_UL z>ibLD;?Gy3?$yzkte)H5HX_c)oO}&u2WQm=(jx8X&$pFOTEto#O3yq6AaRGZ=aj(Z z5}P45rx*|b4nds?aV=aSnogYerT08I5qhlT4|ZQ1ozsai#xrv|MR}& zK}RHPv6U#nh&!6CyX>`hEpVU-js~U`8=*W{9JlXtXpn)mYK?i2v{0=*1o3wB7hylM z#{kMGi+1;}RAeN$+?Mz*xU+++3(-(ys$TltOf4r3y}Rg)?&)|D9m@4a&Ndu7_3M1o zCXebS<>Z^*qmHFOuOf=Y6$E4jB|(*JTjSw+h)ze}x}YjhBHrYG7xiZD8>A*f5IPE) zL$2J~B2ZWSJQGRSN1*UgY+5g;bRj`^Jkb+bk_Mf0{i?E(-0TUD--U?r9RTieam}s; z?iV9ye1 zO4Jvdd8aw3#mi{j8MCp#7=~ExSI4AG$DHcoq5eI2EGG@=432-UQ;zKC97ZK%?&J^? z{M{HECyXgVIxFpRs<1p14a`q-g>st6ZjN*`@0h^9r)gRiufzeQvg0{eVU+%?`LyhE_y?nvT~hxOV-_8e|$e=jHL+ z>!89rAsHn!eM@;!)XdJHzo zP?btqr}VThL!#hD%yJZihc*T6_*B3>Wz@3(#doqQLy2stTq+qDu`|Dwsf+j16LuO7NS9?)w4G50fd3g=Csma_;1plBX0&D`#e`g&jgPjmki`f#|`AG5T#G9P+C4cXkMx7)%g`-gKr+hxdk02YOHbCgidq8HD-l$ zYh#~g6s%Y+9P8KuWn?s;{XM)FfXi^Yji8X|jetaGD8BbraT5}&SZWpRoa#EV!&Q9l zKCCxQLaMCqt&W|&lZ4tS{Hm}{Q;SRG4%U_}@|(7=QV1l|Y88qE?AptkA|zeg4Yp>~ z$ugQr_VBXf2Y#eVo{?=gWH&zMwS&NfpBvXg2e z+Ora>iyg*2DMJc=I+m^Wefy@+M4Zm@b*&Nlfb$(-@ z4!e?FY2Qp-(DF3me#g7UIpqhn7h#)z3s0cnn!O6}ODf^AZ@41)>yomX+ZrjXPcEZA^W(>+EDn zz<<1l;C^marDFZsYNJrgCc^i}u(67LC}MHKVU|3rKA5w)9v^AZw+)$;O9$ioZ15kg zzfSPvvx^-KCjwdWn|gUgve%3I_|tz5>%N3Pv9@@gIlhPNqr``-2V~)Mr8a7u->n*x zqS}K(aW8v%xk&Y~)d-U)bE2gfH|=0V`T&sHXi_;2-JV=`5KBIj+qASqM@#1*F10ya zyeO*TE;CIjt3G9}cnjrB8sym7ZMv|nCUG+>?<(3fgo(8qvyj-GNc_O=Wmt8ghm|$D_RzZ5R?-?s7MTsT z3S*75aw0Za+cgS|E-opt1=k)tT2snRtT4tXuq!7cr{BKr$*i^6AFng`3%GnSd2TDp z@&4Kky%o!_lb1mj0`S}dR;!I5I4t;bMb8KX@W0Nw=P>0|kkYIAv^vcV2+*eszjH4s z5#Qp<%PL;(>Oxub=8sBmcJ-qJ7wr^|tEYU>(8f-7%0XOTWVQ4JD7bBL9Mn*IH_ zmW-Q@a$ftqxx9>n`d_)MZVz$W*AS20)o}YdUQTmHwy(sd*`(r44A7KdS4Ap?e0OF(eYJQW}g!w`!Hoi5Iw== zofi2`-l#k>G=Q7w(mTgBc!B?>Lm)^oNFV1(?3gYvk;S1kwlzg@k_ufq9C0(jL4;D? zCFv{g<5%R6h}Ab^aT=ACA@rJ$8rDHAPo1%GWG5O?R51iY$y%BfLu|SExOluxc9G5yn?M$#lWA^csdE7E~MdX??UQ_`_)j~dullQC%1 zxh3BSFEv|uTC_XfJVX>*yi|RUaD7zGPEr?RLPG3!KmAVIq${&j!e%d>m@h8b;}|vI z);RCTSg$=J%n`WtJy^bmRzirD!R3Al6?kD&$$2>nORwIOS8hstZhczf%<0HGdk=rl zCvf(vHB2QI+<@%nTAm}em{b4(nI(61c6KhCvCeOgG>5}?utDNUzD=9XXdz-7wX7*u zRI)gIg#;>3B4)>~;E{;s1|obO*Vy-Zi*qJhQ!bIF3Ul|Y!kF>Zrj~0qra?sNVv(-WsS?d3Spvg7@p+on)H{M=(@x1NI`FBC=0h9 zjMqX*f8Chz<4%btmZtw*!)$4h7?XW7C+e%VYwoHjtwOd+fJRqSS8hv+&6wtCwT!$p z)aqCN_SkJ6(ml6=b)$73F0oMKNpqt~{H*(AOkC<<;M$Wi;<2^bxt2T<{|DV;3Q{>r9C2sNB^#?)ZQH5V_G8tSKC0A%r}4i^>nOOwncruHM6WGnXz zg@(f6g6t&z9!HO%!ZA#Rd&p2w@(4cVP*6}fj1Y(h1r>}4K|xSIH~b;{I@FKmFGc^; zLqq?KIDvwC4@+bGnGH`?R;7mi2%p}SM{pIJf2j6VS4qeJFbxax^|4=y{xTy=wT}7E zWnID6_0O;wwgE&zNE@UF7|OP6LShOityi2B4Aeg1K>ulFwWjUIidNGyC>*Ba<+Im8 zfPfyGYy!DCB^I;sgtdibc&}J>U8rVbrx>3dBG_4A+$LPyC8)3B{_-B{g5EKL@id7_ zR-paL&dT8sDXDXAdQdu@z_VRwd#A7+^mKA8lO}6o^SpGUb*n2am9%Daw6(0|x}}G; zs0ws-?FkFqrc5@@(RGn`(nHmzDdBqdZ2Ci*ge}+qj7#WB!H<%_)v^dL;)_Tu;9!=5 zuPS;I1tzT%N4hl`I-#ax9owygZaYc4G~}(Z447gc7(A|N+?G3DkaRxTFD_C%D?8F$SJk@7#U zl}&$CzaB{XaHX>*a+A5N)ygM6`(1;_Yzd;JaKGxS326GLOuq*5RC9^?JZe!1a;{Jq z?8?X(_3F@UaY6-=)3yNKejV*aU%-ytyX*)v;qNlu6sCVqD82i(b>FV0fZkl}Js&dX z5~yh4Qlesc=2b012yQ6?EZjvf31~6Z|3O|_-V-c+C!B{pz2ZHt zb8xR#d%|h6IP&dIq&PiZ>=yB=&Vtq_$;6rryna8J+%yjFEiy z&375p?p3AZzHG8m1b$>Rz^*OSz#A>nyjb@xLQMCv9cfk~-5jkY3>Kc!QxnbV^$U1r zDp*)J8k-$u$5prmYQqE>ZL1Gcd}Zx=u_;+N{1K{ign~KxM=Qf1c_w6)tG`V~Rpq{3 zN(@XeKt{Gj`4_H0=Z*08*g(W0_dM&Zs2&bKquDA7xCA_q(lCG?Uxk^RsKxcvtd3~z zZr>A>oOC9;U8|w(w-~Gcr%`xrr+FW6P+1sQ$B?|X#<5*Bs!ziyQ&mi!4XE@T)FSlw zVp#~htT^kApc{gan2t9njhN<~U5q*qo)9w?6TRccNL)&P?VwI=1r9m6NaBqEP49a_ zEme3oO3pZ>7y^ndvFY%V8pC-ksu*X6ODfIi@|+PL$^;OKOKdsZ%e&iy{`oLkCNb%~Z62@sHRyC{q+RL}4*Sd=?WCd1QjeJMiv zX3ohqd|h7WEm4wEuyaCXiN4Z9>YYW|r!B;yMTHPj2H5C|NXYCKH;=*5Yu5QA5$N`W zd@|%LP&Py+pSI-jGbHPjc&`T1d*Bn9fE2P~BJuV4-*|g>6|El%`Q+ChFlj(xkTy?w zaS}{vRHG18ZE$+IW?Cp2(=RJAvAnGDeB2IwpuG91)i!!~@=&=2fSk5@kl@~)kniTI zNqk3#>(MtOUqSDu^})sD=7VE^^~2g$3Zo7{)u`MU2HTRb9!mN)sIY4*2-=4=}N_`syfcEbJ%EyFUuUqpHL}5HrD3J0rK%Pea9R23zglao ze_fMEh@DrDcNo)OAdlcZ>$uW`w?F3fTI@nGLEc`aP7vz{KVM%JMPM`HoLU#<;Irx9 zO2`N}SPxd4kI1)KU(H^ksQr3&`;}AR!w5wQqc}8RKe-#D4g?`4e6SyREVYcuGrt~S zzU!uM0A??23P3KoIhBteNdlE@Ci)a_MXsja)~29+tQ+QCmCH)~^d(!3S)vAEw9R}(hh;~!3%dlZ zvLIy>)>wD{h9r*9T%;8w0dO$L$ZozDeAszLhp58?-3p^)-7CGWAUsz+C|Nu8M3;VT zM@K}>!glb2v7>}hRybGIbgnJ4)ztQN9hE#eIGcG$LNCDSm5yZJJ70n*MW#_Jg?G8k zDy#?k1?2^}()94{Gi_4()Vj345t?)m-YqmVIuwoZc8@`vB91>Ln{&zim@ipIHChtd ze%#r0dhsh;xf+7-cYK}YrwLP(vyVDW=C;yX+xofy!bg(qhf^ra-T4~YJ%|0)QP8hN zd_H&o*90fv`Qi8*k`Bn;Fiiurwh08C-TkKU7U!J_wpqk%JQ>#-LI%|mt*zNV`N%U_ zPhu_P!$jx_UiBAGL~Ve6A;?k}5Z1f9#%Qf`@$B^!6QY5LLw)bRh}oLzOI6~@7WRk* zpFJCH)nwFB!S0*Zs6Q_%!4j_cvQa*ojqG@Fgs}fOE_@!IFP6g{8!^4m%VPO))%yE) zrIzEuOR(p{ht|^hz!M%jxn+F1VJ_g7q(P;>wc!{+U^l%73NuLT4%d2oUd_z;mCm{6 zzxKq90jq>oVQ1=BG z7f3chQ;qiC5${{b%dT57<_xz8_ECo$IsU$FpcTUpVG@>2bAYRt7W)|J&)D^w(Aj(J z)u<~5kbvUlnn)Kyg}QQAyN|Smmn_+7IFvO}2+NL&08tvNaJ`L1#9R1p+btsj$bC>& zzdA4O5yfz_oHI%nsMdWIny(m~Lgk9OaKi~HmJE;bC?p^KGx21if2Z4A*BUJDxy&eT z3w@4ph?fpAhlE@i5g(gELpt3DV<|kgK1YVu^m)Dx`b?TCd0D7jO)b_ncR2k!F8jqe zgsqn@4f!nCuEeYpH9Ev4_@x=)1=E_Z9Ohq@eDb~&b+Uf=4?eNgL3M`*%_m89yKeN6 z7N%Rhiq87P1J_eD4FcfKKM6_u?Y*P=7$jUqBE=XS)=T;h|0te+r`j>Wa&NUM)D7TDfRiVyTzU))QEEji7D~~Q5tg**k%Al}NC@&3d#|-Zu zWOd%R)hB0N#l@`A7YULEW*)ZDRm@w>ykWO8Xg>Tj-je3NSnp+)%vbSaorZ$if`+`n z-@#(wh9=m)E1{qilYQ&aEvTZN%HNp{q?+qxVovyFyB>*6brU zVXTF)Y<}QDtP7iLd8lXQ?1!qnYnq<4ziO+zTbtu(pKif7Cv!oW%_7beuiHO#q#QC) z6{l>|jH-fJpX+!lAIFwOB6`mlu++-xt0VE|O>Cp&W@q0)MIzmvxV{-t<}lS`%EuR` zsrMub-F>&5$k9kq3}3pAf9%Q>e#;AR*u0g=aQD)RAz4EY_PQuvHpDn3_ckGXO*+#bB3=X+xYVo(HK82I@mNEOBVDgFgJJ=S{O92L}8(gyGJkIsg`=p84A zqw#@`DaYQchIhn+^z`PWV($bPM}Tnj$--rUXV`A47}A)*`umo{i8*BZkYcu;y%SKQ zE({c~jc!ClK@@adKE0{&PF;$&UP9--_4LhrISjRo5KJ9BI_w^7yuLTPcy9UA=yi4> zlfp8joy6z#mINgo@XcKe8s{?B+C-I-Fg}?f-+gXBYPjV zCM?(^6dUVNmwupYsoQDLGp1OY>qo!F*i)bA@|c+K`@9SzBeAJ~QjtBa@o=cL~a323Gm+hU29`Bgovsp2Osl$2B=0w<06(E%> z9Hesnju93J%^LTubML}k`ybhnC6$}CEBDjv>91AIn$9in-NLfg4Y>C zbAh>)exB{r=?mx`FVYr=aiQk}g~Hy1>C6ASQotH{ofh+9!Ux;0qSOUxrdfKr>X!og z;ccBl0Ia#N_(s^Y6acqfD75vbJ z>0x33Tql$U*q5}k;8;qE%JWr}HA1Xy?@hDK0knYY7vy^HK6y;~l|8dhD=_Rw6d7dnfzko%PCL2dE<`*Z&Jc=uclt1*oi_;7R5&-v?TOt1OG@`}AP z=2!imjDJm?^Hx@d?n>XB5n7t##(XUzC;6~_p`Ca5(fMERHBEirvswkXlXHi{ji|6Y zORS`3Z~e15)!RPm*8FojPjYWq=c${$_}tOhc?P*>{>F=MdiP*uj{W>NIXkB-S1 z7Ji?Zj_x#^9D z!2IsU3@lXd?ti;D@b0=bk1SYpW=S8qQj&K#!N0QjqW!*qNhjw_lvv#@A#CyM_oPeA zfy;mQ&el0%&>PjOrTN9a_Pp=@6V~3-JajIKOh4{yYhCO6ZfV8x0}~}&t$-Wl?|S68 z1RgWqF0#=wX5G}KtXT)17M09Bu=~C5?uO~=U##V1r!IYCdZpEJ-L-iEj|@^iEI4qm z(OZ09-ea2&oB7kOES0@KIbzA&<0^*HN*8ry4twl!t`>YX{h)&o0}ua_DH9);-I;n^ z>}KD51HBZFc*otZe%~s&rk%m0X1llWw*12V^Jm#*ADVk7!GETRNPVnf<%Y>OieD)7 zm0bO`HnDzQ=;j;G;tu~>VRZf9zUs5T?A!L+G@ss*-#+R1QnP)(Z?#-hZts_~Eb`k> zdgm%vT3B_?Kh%Y=ErcA8*b(-4%TJ+dPfz z18>(Hk;x8U_2bh%h!HQF3-Nvm^Ei6KZ5~_!Ljo{wn{%CEpf%bg}m#T*vm+VXzZr0M^6d_82pucm+b{Og=66|ac6pSW(S7I?f*`9b0> zNt0eH|A^)0m-p@6<`H9rl-SokE`DF&OWUC==AfwhK}abj|y}AuX0h$=~>;cv1HYjnm4C|gTe~DWM4fSb;4u literal 0 HcmV?d00001 From 8a8e800939508617a2597ac52a2677d6b6fb64de Mon Sep 17 00:00:00 2001 From: lukasz-glen <29129196+lukasz-glen@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:19:54 +0200 Subject: [PATCH 14/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5d0014..ce52196 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ It is convenient to create a duplicate of Mainnet network configuration. The only data that is specific is RPC URL that is access URL and points the proxy. See the example below. -![Admin Panel](./admin/docs/screenshot_admin_example.jpg) +![Metamask](./admin/docs/metamask-1.png) In case of problems check, the best place to start the investigation is to check the network traffic, for instance with a web browser's dev tools/developers tools (Ctrl+Shift+C). From 49f109fa9f4c03e6b0cd4bc02ad99fcf31f66092 Mon Sep 17 00:00:00 2001 From: lukasz-glen <29129196+lukasz-glen@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:05:10 +0200 Subject: [PATCH 15/31] Update README.md Co-authored-by: shadeofblue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ce52196..7a244c4 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Remove endpoint at runtime by providing its **name**. For example, in order to r Any client can connet RPC Reverse Proxy. A web client, scripting tools, backend servers etc. It is the same as with other Ethereum RPC providers: you need to use a user's access URL containing API key. -The proxy support CORS what enables usage within web browsers. +The proxy supports CORS, which enables usage within web browsers. Users may wish to integrate with wallets, e.g. Metamask. Below is the example of proper configuration. From 189a67151ecc8f32faf6282ab678b4ff58766f2b Mon Sep 17 00:00:00 2001 From: lukasz-glen <29129196+lukasz-glen@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:05:20 +0200 Subject: [PATCH 16/31] Update README.md Co-authored-by: shadeofblue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7a244c4..1d7615a 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ It is the same as with other Ethereum RPC providers: you need to use a user's ac The proxy supports CORS, which enables usage within web browsers. Users may wish to integrate with wallets, e.g. Metamask. -Below is the example of proper configuration. +Below is the example of a proper configuration. It is convenient to create a duplicate of Mainnet network configuration. The only data that is specific is RPC URL that is access URL and points the proxy. See the example below. From a7a36ab2ad6a6aac2292e608fb1fd4b0ec11edca Mon Sep 17 00:00:00 2001 From: lukasz-glen <29129196+lukasz-glen@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:05:33 +0200 Subject: [PATCH 17/31] Update README.md Co-authored-by: shadeofblue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d7615a..4ceb49f 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ A web client, scripting tools, backend servers etc. It is the same as with other Ethereum RPC providers: you need to use a user's access URL containing API key. The proxy supports CORS, which enables usage within web browsers. -Users may wish to integrate with wallets, e.g. Metamask. +Users may wish to integrate the proxy with wallet applications, e.g. Metamask. Below is the example of a proper configuration. It is convenient to create a duplicate of Mainnet network configuration. The only data that is specific is RPC URL that is access URL and points the proxy. From 14314d0722fc6549600840543fcc5f561b89d73c Mon Sep 17 00:00:00 2001 From: lukasz-glen <29129196+lukasz-glen@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:06:13 +0200 Subject: [PATCH 18/31] Update README.md Co-authored-by: shadeofblue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ceb49f..34841d1 100644 --- a/README.md +++ b/README.md @@ -109,5 +109,5 @@ See the example below. ![Metamask](./admin/docs/metamask-1.png) In case of problems check, the best place to start the investigation is to check the network traffic, -for instance with a web browser's dev tools/developers tools (Ctrl+Shift+C). +for instance with the web browser's dev tools/developers tools (Ctrl+Shift+C). From a49af1137da8e97d113d9d87311c83f87c3da2ec Mon Sep 17 00:00:00 2001 From: lukasz-glen <29129196+lukasz-glen@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:06:47 +0200 Subject: [PATCH 19/31] Update README.md Co-authored-by: shadeofblue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34841d1..c6cde13 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,6 @@ See the example below. ![Metamask](./admin/docs/metamask-1.png) -In case of problems check, the best place to start the investigation is to check the network traffic, +In case of any problems with the verification of the newly added network, the best place to start the investigation is to check the network traffic, for instance with the web browser's dev tools/developers tools (Ctrl+Shift+C). From 5aa432bbc9b8fce6a1401990b76070831ebda84b Mon Sep 17 00:00:00 2001 From: lukasz-glen <29129196+lukasz-glen@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:07:55 +0200 Subject: [PATCH 20/31] Update README.md Co-authored-by: shadeofblue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c6cde13..275d076 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ The proxy supports CORS, which enables usage within web browsers. Users may wish to integrate the proxy with wallet applications, e.g. Metamask. Below is the example of a proper configuration. It is convenient to create a duplicate of Mainnet network configuration. -The only data that is specific is RPC URL that is access URL and points the proxy. +The only specific piece of setup is the RPC URL, which needs to be set to the proxy's RPC endpoint. See the example below. ![Metamask](./admin/docs/metamask-1.png) From d573619f13eda220b1eebcdfe0e080553b646317 Mon Sep 17 00:00:00 2001 From: lukasz-glen <29129196+lukasz-glen@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:10:26 +0200 Subject: [PATCH 21/31] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 275d076..10dcc2a 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ The proxy supports CORS, which enables usage within web browsers. Users may wish to integrate the proxy with wallet applications, e.g. Metamask. Below is the example of a proper configuration. It is convenient to create a duplicate of Mainnet network configuration. -The only specific piece of setup is the RPC URL, which needs to be set to the proxy's RPC endpoint. +The only specific piece of setup is the RPC URL, which needs to be set to the access URL (that contains proxy's RPC endpoint and user's API key). See the example below. ![Metamask](./admin/docs/metamask-1.png) From 15ac8b4b6f7d66a4df89f7f7903db289f3f05ace Mon Sep 17 00:00:00 2001 From: "aleksander.partyka" Date: Wed, 9 Oct 2024 10:56:41 +0200 Subject: [PATCH 22/31] docs(readme.md): typo in remove_endpoint usage --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ef8c35..164e070 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Change existing endpoint's configuration at runtime by providing its **name** an Remove endpoint at runtime by providing its **name**. For example, in order to remove endpoint ***local*** : ``` -{"jsonrpc": "2.0", "method": "update_endpoint", "params": ["local"], "id": 0} +{"jsonrpc": "2.0", "method": "remove_endpoint", "params": ["local"], "id": 0} ``` **IMPORTANT:** Resulting changes are saved in local `.env` file for reuse. From df2a28e8b050a81190778d3a26c1866123a903d6 Mon Sep 17 00:00:00 2001 From: Robert Mordzon Date: Wed, 20 Nov 2024 12:54:01 +0100 Subject: [PATCH 23/31] Update README.md Web3 Pi typo fix --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 164e070..dd6f31f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Web3Pi Reverse Proxy +# Web3 Pi Reverse Proxy -A reverse proxy for Geth intended for use within Web3Pi ecosystem. +A reverse proxy for Geth intended for use within Web3 Pi ecosystem. -Web3Pi Reverse Proxy comes out-of-the-box with several features: +Web3 Pi Reverse Proxy comes out-of-the-box with several features: - Multiple geth nodes - you can hide multiple Geth nodes under single instance of reverse proxy - JSON-RPC parser - our custom parser validates JSON-RPC requests before they reach the nodes @@ -20,7 +20,7 @@ Simply install `web3pi-proxy` package using your Python package manager, using * pip install web3pi-proxy ``` -Web3Pi Reverse Proxy expects you to provide **ETH_ENDPOINTS** environment variable to your system. +Web3 Pi Reverse Proxy expects you to provide **ETH_ENDPOINTS** environment variable to your system. It should be a list of endpoint descriptors for JSON-RPC over HTTP communication with Geth. From a4da8839e757af5f606cc33343d9bffe48aea644 Mon Sep 17 00:00:00 2001 From: Marcin Gordel Date: Tue, 14 Jan 2025 09:25:15 +0100 Subject: [PATCH 24/31] Added url validation when adding and editing endpoint --- web3pi_proxy/service/endpoints/endpoint_manager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web3pi_proxy/service/endpoints/endpoint_manager.py b/web3pi_proxy/service/endpoints/endpoint_manager.py index e1b2907..e6c9581 100644 --- a/web3pi_proxy/service/endpoints/endpoint_manager.py +++ b/web3pi_proxy/service/endpoints/endpoint_manager.py @@ -59,6 +59,8 @@ def get_endpoints(self) -> dict: def add_endpoint(self, name: str, url: str) -> Union[RPCEndpoint, dict]: descriptor = EndpointConnectionDescriptor.from_url(url) + if descriptor is None: + return {"error": "Invalid URL provided"} try: endpoint = self.endpoint_pool_manager.add_pool(name, descriptor) except PoolAlreadyExistsError as error: @@ -76,6 +78,8 @@ def remove_endpoint(self, name: str) -> Union[RPCEndpoint, dict]: def update_endpoint(self, name: str, url: str) -> Union[RPCEndpoint, dict]: descriptor = EndpointConnectionDescriptor.from_url(url) + if descriptor is None: + return {"error": "Invalid URL provided"} try: self.endpoint_pool_manager.remove_pool(name) except PoolDoesNotExistError as error: From a1bd9105e81d91122bbd33337162af75a3e43326 Mon Sep 17 00:00:00 2001 From: Marcin Gordel Date: Wed, 15 Jan 2025 11:29:25 +0100 Subject: [PATCH 25/31] refactor: renamed internal function as suggested by review --- .../core/rpc/node/endpoint_pool/endpoint_connection_pool.py | 4 ++-- .../core/rpc/node/endpoint_pool/tunnel_connection_pool.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py b/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py index f12bb44..322ba3b 100644 --- a/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py +++ b/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py @@ -170,7 +170,7 @@ def __run_cleanup_thread(self) -> None: def __get_connection(self) -> EndpointConnection: return self.connections.get_nowait() - def new_connection(self) -> EndpointConnection: + def __new_connection(self) -> EndpointConnection: """Internal function, do not call directly""" def connection_factory() -> socket: # TODO is it worth to move it to object level and reuse? return BaseSocket.create_socket(self.endpoint.conn_descr.host, self.endpoint.conn_descr.port) @@ -189,7 +189,7 @@ def get(self) -> EndpointConnectionHandler: self.__lock.release() self.__logger.debug("No existing connections available, establishing new connection") try: - connection = self.new_connection() + connection = self.__new_connection() except Exception as error: self.stats.register_error_on_connection_creation() raise error diff --git a/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_connection_pool.py b/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_connection_pool.py index 03c528e..3d810be 100644 --- a/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_connection_pool.py +++ b/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_connection_pool.py @@ -34,7 +34,7 @@ def __init__( TunnelService.register(self.tunnel_api_key, self) - def new_connection(self) -> EndpointConnection: + def __new_connection(self) -> EndpointConnection: def connection_factory() -> socket: # TODO is it worth to move it to object level and reuse? self.__logger.debug("Creating socket") From 3231f9b8d3a868acb7703c18d36dfdeed71f34ae Mon Sep 17 00:00:00 2001 From: Marcin Gordel Date: Wed, 15 Jan 2025 14:49:33 +0100 Subject: [PATCH 26/31] refactor: Small corrections as suggested by review --- .../rpc/node/endpoint_pool/pool_manager.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/web3pi_proxy/core/rpc/node/endpoint_pool/pool_manager.py b/web3pi_proxy/core/rpc/node/endpoint_pool/pool_manager.py index c06b9b7..a74be8b 100644 --- a/web3pi_proxy/core/rpc/node/endpoint_pool/pool_manager.py +++ b/web3pi_proxy/core/rpc/node/endpoint_pool/pool_manager.py @@ -93,16 +93,19 @@ def check_connections(self, pools: List[EndpointConnectionPool]): self.__logger.debug(f"Endpoint `{pool.endpoint.name}`: okay.") -class HttpResponseParserListenerSyncController: +class SyncControllerResponseListener: body: bytes = b'' block_number: int = 0 block_timestamp: int = 0 __logger = get_logger("SyncController") def __init__(self, __pool_name: str): - super().__init__() self.__pool_name = __pool_name + def on_status(self, status_code: int): + self.__logger.debug(f"SyncControllerResponseListener.on_status: {status_code}") + + def on_body(self, body: bytes): self.body = self.body + body @@ -112,16 +115,13 @@ def on_message_complete(self): result = block_data.get('result') if not result: error = block_data.get('error') - if error: - self.__logger.error(f"{self.__pool_name}: Sync test failed: {error}") - else: - self.__logger.error(f"{self.__pool_name}: Sync test failed: reason unknown") + self.__logger.error(f"{self.__pool_name}: Sync test failed: {error or 'reason unknown'}") return self.block_number = int(result['number'], 16) self.block_timestamp = int(result['timestamp'], 16) except Exception as error: self.__logger.error("%s: %s", error.__class__, error) - self.__logger.error(f"{self.__pool_name}: Failed to failed to parse sync test response") + self.__logger.error(f"{self.__pool_name}: Failed to parse sync test response") # TODO: Tune parameters @@ -136,11 +136,11 @@ def __test_pool(self, pool: EndpointConnectionPool) -> bool | None: try: endpoint_connection_handler = pool.get(out_of_sync=True) req = RPCRequest() - req.headers = b'User-Agent: curl/8.0.1\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: 82\r\n' + req.headers = b'Accept: */*\r\nContent-Type: application/json\r\nContent-Length: 82\r\n' req.content = b'{"method":"eth_getBlockByNumber","params":["latest",false],"id":1,"jsonrpc":"2.0"}' endpoint_connection_handler.send(req) - response_listener = HttpResponseParserListenerSyncController(pool.endpoint.name) + response_listener = SyncControllerResponseListener(pool.endpoint.name) response_parser = HttpResponseParser(response_listener) def response_handler(res: bytes): From b33e5cd1c863d2751e9669f5aff0cdbce97f8a0a Mon Sep 17 00:00:00 2001 From: Marcin Gordel Date: Tue, 21 Jan 2025 15:05:15 +0100 Subject: [PATCH 27/31] docs: Updated readme. Added instructions for configuring and installing from source --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 63bfaa0..1df3116 100644 --- a/README.md +++ b/README.md @@ -14,27 +14,66 @@ RPC Reverse Proxy comes out-of-the-box with several features: ## Setup +You can install `web3pi-proxy` in one of two ways: + +### Install via PyPI + Simply install `web3pi-proxy` package using your Python package manager, using **pip** for example: ```bash pip install web3pi-proxy ``` -RPC Reverse Proxy expects you to provide **ETH_ENDPOINTS** environment variable to your system. +### Install from source -It should be a list of endpoint descriptors for JSON-RPC over HTTP communication with Geth. - -Refer to the following example: +To install the package from source, follow these steps: ```bash -export ETH_ENDPOINTS='[{"name": "rpi geth 1", "url": "http://eop-1.local:8545/"}, {"name": "infura", "url": "https://mainnet.infura.io/v3/"}]' +git clone https://github.com/Web3-Pi/web3-reverse-proxy.git +cd web3-reverse-proxy +python3 -m venv venv +source venv/bin/activate +pip install poetry +poetry install ``` -You can define as many endpoints as you wish and chose their names however suits you. +## Configuration + +You can define the following environment variables, and you can place them in the `.env` file: + +- **LOG_LEVEL**: Specifies the logging level. Default is `INFO`. +- **DEFAULT_RECV_BUF_SIZE**: Buffer size for socket receiving. Default is `8192`. +- **PUBLIC_SERVICE**: Whether the service is public. Default is `False`. +- **USE_UPNP**: Enables UPnP if `PUBLIC_SERVICE` is `True`. Default is `True`. +- **UPNP_DISCOVERY_TIMEOUT**: Timeout for UPnP discovery in seconds. Default is `2.5`. +- **UPNP_LEASE_TIME**: Lease time for UPnP in seconds. Default is `18000` (5 hours). +- **PROXY_LISTEN_ADDRESS**: Address for the proxy to listen on. Default is `0.0.0.0`. +- **PROXY_CONNECTION_ADDRESS**: Address clients use to connect to the proxy. Default is `None` (auto-resolved). +- **PROXY_LISTEN_PORT**: Port for the proxy to listen on. Default is `6512`. +- **NUM_PROXY_WORKERS**: Number of workers handling proxy connections. Default is `150`. +- **MAX_PENDING_CLIENT_SOCKETS**: Maximum number of pending client sockets. Default is `10000`. +- **MAX_CONCURRENT_CONNECTIONS**: Maximum number of concurrent connections. Default is `21`. +- **IDLE_CONNECTION_TIMEOUT**: Timeout for idle connections in seconds. Default is `300`. +- **SSL_ENABLED**: Whether SSL is enabled. Default is `False`. +- **SSL_CERT_FILE**: Path to SSL certificate file. Default is `cert.pem`. +- **SSL_KEY_FILE**: Path to SSL key file. Default is `key.pem`. +- **ETH_ENDPOINTS**: A JSON list of endpoint descriptors for Ethereum nodes. Example: `[{"name": "rpi4", "url": "http://localhost:8545/"}]`. If defined, this list becomes static and cannot be managed via the admin panel. Leaving it undefined enables endpoint management via a local database. +- **CACHE_ENABLED**: Whether caching is enabled. Default is `False`. +- **CACHE_EXPIRY_MS**: Cache expiry time in milliseconds. Default is `300000` (5 minutes). +- **JSON_RPC_REQUEST_PARSER_ENABLED**: Enables JSON-RPC request parsing. Default is `True`. +- **STATS_UPDATE_DELTA**: Update interval for stats in seconds. Default is `12`. +- **ADMIN_LISTEN_ADDRESS**: Address for the admin panel to listen on. Default is `0.0.0.0`. +- **ADMIN_CONNECTION_ADDRESS**: Address clients use to connect to the admin panel. Default is `None` (auto-resolved). +- **ADMIN_LISTEN_PORT**: Port for the admin panel to listen on. Default is `6561`. +- **ADMIN_AUTH_TOKEN**: Admin authentication token. Default is generated randomly at runtime. +- **DB_FILE**: Path to the database file. Default is `web3pi_proxy.sqlite3`. +- **MODE**: Proxy mode (`DEV`, `SIM`, `PROD`). Default is `PROD`. +- **LOADBALANCER**: Load balancer strategy (`RandomLoadBalancer`, `LeastBusyLoadBalancer`, `ConstantLoadBalancer`). Default is `LeastBusyLoadBalancer`. + ## Run -After configuring endpoints, you can run your reverse proxy with command +You can run your reverse proxy with command: ```bash web3pi-proxy @@ -58,7 +97,7 @@ http://0.0.0.0:6561/?token= The **admin auth token** will be output to your terminal, during the launch. -Token is not stored and will be randomly generated on each launch. +Token is not stored and will be randomly generated on each launch, unless it has been defined as an environment variable or in the .env file. Outside of admin portal, the admin service allows several operations, performed by submitting JSON-RPC requests. Use **admin auth token** in **Authorization** header of your HTTP POST request for authentication. From fc5a14db44d7a62884b307c0aa3526c33e02a87a Mon Sep 17 00:00:00 2001 From: Marcin Gordel Date: Tue, 21 Jan 2025 15:15:55 +0100 Subject: [PATCH 28/31] refactor: list replaced by table --- README.md | 62 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 1df3116..9307e2a 100644 --- a/README.md +++ b/README.md @@ -39,36 +39,38 @@ poetry install ## Configuration -You can define the following environment variables, and you can place them in the `.env` file: - -- **LOG_LEVEL**: Specifies the logging level. Default is `INFO`. -- **DEFAULT_RECV_BUF_SIZE**: Buffer size for socket receiving. Default is `8192`. -- **PUBLIC_SERVICE**: Whether the service is public. Default is `False`. -- **USE_UPNP**: Enables UPnP if `PUBLIC_SERVICE` is `True`. Default is `True`. -- **UPNP_DISCOVERY_TIMEOUT**: Timeout for UPnP discovery in seconds. Default is `2.5`. -- **UPNP_LEASE_TIME**: Lease time for UPnP in seconds. Default is `18000` (5 hours). -- **PROXY_LISTEN_ADDRESS**: Address for the proxy to listen on. Default is `0.0.0.0`. -- **PROXY_CONNECTION_ADDRESS**: Address clients use to connect to the proxy. Default is `None` (auto-resolved). -- **PROXY_LISTEN_PORT**: Port for the proxy to listen on. Default is `6512`. -- **NUM_PROXY_WORKERS**: Number of workers handling proxy connections. Default is `150`. -- **MAX_PENDING_CLIENT_SOCKETS**: Maximum number of pending client sockets. Default is `10000`. -- **MAX_CONCURRENT_CONNECTIONS**: Maximum number of concurrent connections. Default is `21`. -- **IDLE_CONNECTION_TIMEOUT**: Timeout for idle connections in seconds. Default is `300`. -- **SSL_ENABLED**: Whether SSL is enabled. Default is `False`. -- **SSL_CERT_FILE**: Path to SSL certificate file. Default is `cert.pem`. -- **SSL_KEY_FILE**: Path to SSL key file. Default is `key.pem`. -- **ETH_ENDPOINTS**: A JSON list of endpoint descriptors for Ethereum nodes. Example: `[{"name": "rpi4", "url": "http://localhost:8545/"}]`. If defined, this list becomes static and cannot be managed via the admin panel. Leaving it undefined enables endpoint management via a local database. -- **CACHE_ENABLED**: Whether caching is enabled. Default is `False`. -- **CACHE_EXPIRY_MS**: Cache expiry time in milliseconds. Default is `300000` (5 minutes). -- **JSON_RPC_REQUEST_PARSER_ENABLED**: Enables JSON-RPC request parsing. Default is `True`. -- **STATS_UPDATE_DELTA**: Update interval for stats in seconds. Default is `12`. -- **ADMIN_LISTEN_ADDRESS**: Address for the admin panel to listen on. Default is `0.0.0.0`. -- **ADMIN_CONNECTION_ADDRESS**: Address clients use to connect to the admin panel. Default is `None` (auto-resolved). -- **ADMIN_LISTEN_PORT**: Port for the admin panel to listen on. Default is `6561`. -- **ADMIN_AUTH_TOKEN**: Admin authentication token. Default is generated randomly at runtime. -- **DB_FILE**: Path to the database file. Default is `web3pi_proxy.sqlite3`. -- **MODE**: Proxy mode (`DEV`, `SIM`, `PROD`). Default is `PROD`. -- **LOADBALANCER**: Load balancer strategy (`RandomLoadBalancer`, `LeastBusyLoadBalancer`, `ConstantLoadBalancer`). Default is `LeastBusyLoadBalancer`. +You can define the following environment variables, and you can place them in the .env file (all are optional): + +| Variable | Description | Default | +|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------| +| `LOG_LEVEL` | Specifies the logging level. | `INFO` | +| `ADMIN_AUTH_TOKEN` | Admin authentication token. | Randomly generated | +| `ETH_ENDPOINTS` | A JSON list of endpoint descriptors for Ethereum nodes. Example: `[{"name": "rpi4", "url": "http://localhost:8545/"}]`. If defined, this list becomes static and cannot be managed via the admin panel. Leaving it undefined enables endpoint management via a local database. | `None` | +| `DEFAULT_RECV_BUF_SIZE` | Buffer size for socket receiving. | `8192` | +| `PUBLIC_SERVICE` | Whether the service is public. | `False` | +| `USE_UPNP` | Enables UPnP if `PUBLIC_SERVICE` is `True`. | `True` | +| `UPNP_DISCOVERY_TIMEOUT` | Timeout for UPnP discovery in seconds. | `2.5` | +| `UPNP_LEASE_TIME` | Lease time for UPnP in seconds. | `18000` (5 hours) | +| `PROXY_LISTEN_ADDRESS` | Address for the proxy to listen on. | `0.0.0.0` | +| `PROXY_CONNECTION_ADDRESS` | Address clients use to connect to the proxy. Default is `None` (auto-resolved). | `None` | +| `PROXY_LISTEN_PORT` | Port for the proxy to listen on. | `6512` | +| `NUM_PROXY_WORKERS` | Number of workers handling proxy connections. | `150` | +| `MAX_PENDING_CLIENT_SOCKETS` | Maximum number of pending client sockets. | `10000` | +| `MAX_CONCURRENT_CONNECTIONS` | Maximum number of concurrent connections. | `21` | +| `IDLE_CONNECTION_TIMEOUT` | Timeout for idle connections in seconds. | `300` | +| `SSL_ENABLED` | Whether SSL is enabled. | `False` | +| `SSL_CERT_FILE` | Path to SSL certificate file. | `cert.pem` | +| `SSL_KEY_FILE` | Path to SSL key file. | `key.pem` | +| `CACHE_ENABLED` | Whether caching is enabled. | `False` | +| `CACHE_EXPIRY_MS` | Cache expiry time in milliseconds. | `300000` (5 minutes) | +| `JSON_RPC_REQUEST_PARSER_ENABLED` | Enables JSON-RPC request parsing. | `True` | +| `STATS_UPDATE_DELTA` | Update interval for stats in seconds. | `12` | +| `ADMIN_LISTEN_ADDRESS` | Address for the admin panel to listen on. | `0.0.0.0` | +| `ADMIN_CONNECTION_ADDRESS` | Address clients use to connect to the admin panel. Default is `None` (auto-resolved). | `None` | +| `ADMIN_LISTEN_PORT` | Port for the admin panel to listen on. | `6561` | +| `DB_FILE` | Path to the database file. | `web3pi_proxy.sqlite3` | +| `MODE` | Proxy mode (`DEV`, `SIM`, `PROD`). | `PROD` | +| `LOADBALANCER` | Load balancer strategy (`RandomLoadBalancer`, `LeastBusyLoadBalancer`, `ConstantLoadBalancer`). | `LeastBusyLoadBalancer` | ## Run From 7fdf9cf405f3a7aacbcc539b63788d44cdb78882 Mon Sep 17 00:00:00 2001 From: Marcin Gordel Date: Tue, 21 Jan 2025 15:19:07 +0100 Subject: [PATCH 29/31] refactor: list replaced by table --- README.md | 61 +++++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 9307e2a..0f92b46 100644 --- a/README.md +++ b/README.md @@ -41,37 +41,36 @@ poetry install You can define the following environment variables, and you can place them in the .env file (all are optional): -| Variable | Description | Default | -|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------| -| `LOG_LEVEL` | Specifies the logging level. | `INFO` | -| `ADMIN_AUTH_TOKEN` | Admin authentication token. | Randomly generated | -| `ETH_ENDPOINTS` | A JSON list of endpoint descriptors for Ethereum nodes. Example: `[{"name": "rpi4", "url": "http://localhost:8545/"}]`. If defined, this list becomes static and cannot be managed via the admin panel. Leaving it undefined enables endpoint management via a local database. | `None` | -| `DEFAULT_RECV_BUF_SIZE` | Buffer size for socket receiving. | `8192` | -| `PUBLIC_SERVICE` | Whether the service is public. | `False` | -| `USE_UPNP` | Enables UPnP if `PUBLIC_SERVICE` is `True`. | `True` | -| `UPNP_DISCOVERY_TIMEOUT` | Timeout for UPnP discovery in seconds. | `2.5` | -| `UPNP_LEASE_TIME` | Lease time for UPnP in seconds. | `18000` (5 hours) | -| `PROXY_LISTEN_ADDRESS` | Address for the proxy to listen on. | `0.0.0.0` | -| `PROXY_CONNECTION_ADDRESS` | Address clients use to connect to the proxy. Default is `None` (auto-resolved). | `None` | -| `PROXY_LISTEN_PORT` | Port for the proxy to listen on. | `6512` | -| `NUM_PROXY_WORKERS` | Number of workers handling proxy connections. | `150` | -| `MAX_PENDING_CLIENT_SOCKETS` | Maximum number of pending client sockets. | `10000` | -| `MAX_CONCURRENT_CONNECTIONS` | Maximum number of concurrent connections. | `21` | -| `IDLE_CONNECTION_TIMEOUT` | Timeout for idle connections in seconds. | `300` | -| `SSL_ENABLED` | Whether SSL is enabled. | `False` | -| `SSL_CERT_FILE` | Path to SSL certificate file. | `cert.pem` | -| `SSL_KEY_FILE` | Path to SSL key file. | `key.pem` | -| `CACHE_ENABLED` | Whether caching is enabled. | `False` | -| `CACHE_EXPIRY_MS` | Cache expiry time in milliseconds. | `300000` (5 minutes) | -| `JSON_RPC_REQUEST_PARSER_ENABLED` | Enables JSON-RPC request parsing. | `True` | -| `STATS_UPDATE_DELTA` | Update interval for stats in seconds. | `12` | -| `ADMIN_LISTEN_ADDRESS` | Address for the admin panel to listen on. | `0.0.0.0` | -| `ADMIN_CONNECTION_ADDRESS` | Address clients use to connect to the admin panel. Default is `None` (auto-resolved). | `None` | -| `ADMIN_LISTEN_PORT` | Port for the admin panel to listen on. | `6561` | -| `DB_FILE` | Path to the database file. | `web3pi_proxy.sqlite3` | -| `MODE` | Proxy mode (`DEV`, `SIM`, `PROD`). | `PROD` | -| `LOADBALANCER` | Load balancer strategy (`RandomLoadBalancer`, `LeastBusyLoadBalancer`, `ConstantLoadBalancer`). | `LeastBusyLoadBalancer` | - +| Variable | Default | Description | +|---------------------------------|----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `LOG_LEVEL` | `INFO` | Specifies the logging level. | +| `ADMIN_AUTH_TOKEN` | Randomly generated | Admin authentication token. | +| `ETH_ENDPOINTS` | `None` | A JSON list of endpoint descriptors for Ethereum nodes. Example: `[{"name": "rpi4", "url": "http://localhost:8545/"}]`. If defined, this list becomes static and cannot be managed via the admin panel. Leaving it undefined enables endpoint management via a local database. | +| `DEFAULT_RECV_BUF_SIZE` | `8192` | Buffer size for socket receiving. | +| `PUBLIC_SERVICE` | `False` | Whether the service is public. | +| `USE_UPNP` | `True` | Enables UPnP if `PUBLIC_SERVICE` is `True`. | +| `UPNP_DISCOVERY_TIMEOUT` | `2.5` | Timeout for UPnP discovery in seconds. | +| `UPNP_LEASE_TIME` | `18000` (5 hours) | Lease time for UPnP in seconds. | +| `PROXY_LISTEN_ADDRESS` | `0.0.0.0` | Address for the proxy to listen on. | +| `PROXY_CONNECTION_ADDRESS` | `None` | Address clients use to connect to the proxy. Default is `None` (auto-resolved). | +| `PROXY_LISTEN_PORT` | `6512` | Port for the proxy to listen on. | +| `NUM_PROXY_WORKERS` | `150` | Number of workers handling proxy connections. | +| `MAX_PENDING_CLIENT_SOCKETS` | `10000` | Maximum number of pending client sockets. | +| `MAX_CONCURRENT_CONNECTIONS` | `21` | Maximum number of concurrent connections. | +| `IDLE_CONNECTION_TIMEOUT` | `300` | Timeout for idle connections in seconds. | +| `SSL_ENABLED` | `False` | Whether SSL is enabled. | +| `SSL_CERT_FILE` | `cert.pem` | Path to SSL certificate file. | +| `SSL_KEY_FILE` | `key.pem` | Path to SSL key file. | +| `CACHE_ENABLED` | `False` | Whether caching is enabled. | +| `CACHE_EXPIRY_MS` | `300000` (5 minutes) | Cache expiry time in milliseconds. | +| `JSON_RPC_REQUEST_PARSER_ENABLED` | `True` | Enables JSON-RPC request parsing. | +| `STATS_UPDATE_DELTA` | `12` | Update interval for stats in seconds. | +| `ADMIN_LISTEN_ADDRESS` | `0.0.0.0` | Address for the admin panel to listen on. | +| `ADMIN_CONNECTION_ADDRESS` | `None` | Address clients use to connect to the admin panel. Default is `None` (auto-resolved). | +| `ADMIN_LISTEN_PORT` | `6561` | Port for the admin panel to listen on. | +| `DB_FILE` | `web3pi_proxy.sqlite3` | Path to the database file. | +| `MODE` | `PROD` | Proxy mode (`DEV`, `SIM`, `PROD`). | +| `LOADBALANCER` | `LeastBusyLoadBalancer` | Load balancer strategy (`RandomLoadBalancer`, `LeastBusyLoadBalancer`, `ConstantLoadBalancer`). | ## Run From dd727abcd072932d9e0f70604775e415dc6cecc8 Mon Sep 17 00:00:00 2001 From: Marcin Gordel Date: Tue, 21 Jan 2025 15:20:18 +0100 Subject: [PATCH 30/31] refactor: list replaced by table --- README.md | 60 +++++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 0f92b46..85e83fa 100644 --- a/README.md +++ b/README.md @@ -41,36 +41,36 @@ poetry install You can define the following environment variables, and you can place them in the .env file (all are optional): -| Variable | Default | Description | -|---------------------------------|----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `LOG_LEVEL` | `INFO` | Specifies the logging level. | -| `ADMIN_AUTH_TOKEN` | Randomly generated | Admin authentication token. | -| `ETH_ENDPOINTS` | `None` | A JSON list of endpoint descriptors for Ethereum nodes. Example: `[{"name": "rpi4", "url": "http://localhost:8545/"}]`. If defined, this list becomes static and cannot be managed via the admin panel. Leaving it undefined enables endpoint management via a local database. | -| `DEFAULT_RECV_BUF_SIZE` | `8192` | Buffer size for socket receiving. | -| `PUBLIC_SERVICE` | `False` | Whether the service is public. | -| `USE_UPNP` | `True` | Enables UPnP if `PUBLIC_SERVICE` is `True`. | -| `UPNP_DISCOVERY_TIMEOUT` | `2.5` | Timeout for UPnP discovery in seconds. | -| `UPNP_LEASE_TIME` | `18000` (5 hours) | Lease time for UPnP in seconds. | -| `PROXY_LISTEN_ADDRESS` | `0.0.0.0` | Address for the proxy to listen on. | -| `PROXY_CONNECTION_ADDRESS` | `None` | Address clients use to connect to the proxy. Default is `None` (auto-resolved). | -| `PROXY_LISTEN_PORT` | `6512` | Port for the proxy to listen on. | -| `NUM_PROXY_WORKERS` | `150` | Number of workers handling proxy connections. | -| `MAX_PENDING_CLIENT_SOCKETS` | `10000` | Maximum number of pending client sockets. | -| `MAX_CONCURRENT_CONNECTIONS` | `21` | Maximum number of concurrent connections. | -| `IDLE_CONNECTION_TIMEOUT` | `300` | Timeout for idle connections in seconds. | -| `SSL_ENABLED` | `False` | Whether SSL is enabled. | -| `SSL_CERT_FILE` | `cert.pem` | Path to SSL certificate file. | -| `SSL_KEY_FILE` | `key.pem` | Path to SSL key file. | -| `CACHE_ENABLED` | `False` | Whether caching is enabled. | -| `CACHE_EXPIRY_MS` | `300000` (5 minutes) | Cache expiry time in milliseconds. | -| `JSON_RPC_REQUEST_PARSER_ENABLED` | `True` | Enables JSON-RPC request parsing. | -| `STATS_UPDATE_DELTA` | `12` | Update interval for stats in seconds. | -| `ADMIN_LISTEN_ADDRESS` | `0.0.0.0` | Address for the admin panel to listen on. | -| `ADMIN_CONNECTION_ADDRESS` | `None` | Address clients use to connect to the admin panel. Default is `None` (auto-resolved). | -| `ADMIN_LISTEN_PORT` | `6561` | Port for the admin panel to listen on. | -| `DB_FILE` | `web3pi_proxy.sqlite3` | Path to the database file. | -| `MODE` | `PROD` | Proxy mode (`DEV`, `SIM`, `PROD`). | -| `LOADBALANCER` | `LeastBusyLoadBalancer` | Load balancer strategy (`RandomLoadBalancer`, `LeastBusyLoadBalancer`, `ConstantLoadBalancer`). | +| Variable | Default | Description | +|---------------------------------|----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `LOG_LEVEL` | `INFO` | Specifies the logging level. | +| `ADMIN_AUTH_TOKEN` | Randomly generated | Admin authentication token. | +| `ETH_ENDPOINTS` | `None` | A JSON list of endpoint descriptors for Ethereum nodes. Example: `[{"name": "rpi4", "url": "http://localhost:8545/"}]`. If defined, this list becomes static and cannot be managed via the admin panel. Leaving it undefined enables endpoint management via the admin panel and local database. | +| `DEFAULT_RECV_BUF_SIZE` | `8192` | Buffer size for socket receiving. | +| `PUBLIC_SERVICE` | `False` | Whether the service is public. | +| `USE_UPNP` | `True` | Enables UPnP if `PUBLIC_SERVICE` is `True`. | +| `UPNP_DISCOVERY_TIMEOUT` | `2.5` | Timeout for UPnP discovery in seconds. | +| `UPNP_LEASE_TIME` | `18000` (5 hours) | Lease time for UPnP in seconds. | +| `PROXY_LISTEN_ADDRESS` | `0.0.0.0` | Address for the proxy to listen on. | +| `PROXY_CONNECTION_ADDRESS` | `None` | Address clients use to connect to the proxy. Default is `None` (auto-resolved). | +| `PROXY_LISTEN_PORT` | `6512` | Port for the proxy to listen on. | +| `NUM_PROXY_WORKERS` | `150` | Number of workers handling proxy connections. | +| `MAX_PENDING_CLIENT_SOCKETS` | `10000` | Maximum number of pending client sockets. | +| `MAX_CONCURRENT_CONNECTIONS` | `21` | Maximum number of concurrent connections. | +| `IDLE_CONNECTION_TIMEOUT` | `300` | Timeout for idle connections in seconds. | +| `SSL_ENABLED` | `False` | Whether SSL is enabled. | +| `SSL_CERT_FILE` | `cert.pem` | Path to SSL certificate file. | +| `SSL_KEY_FILE` | `key.pem` | Path to SSL key file. | +| `CACHE_ENABLED` | `False` | Whether caching is enabled. | +| `CACHE_EXPIRY_MS` | `300000` (5 minutes) | Cache expiry time in milliseconds. | +| `JSON_RPC_REQUEST_PARSER_ENABLED` | `True` | Enables JSON-RPC request parsing. | +| `STATS_UPDATE_DELTA` | `12` | Update interval for stats in seconds. | +| `ADMIN_LISTEN_ADDRESS` | `0.0.0.0` | Address for the admin panel to listen on. | +| `ADMIN_CONNECTION_ADDRESS` | `None` | Address clients use to connect to the admin panel. Default is `None` (auto-resolved). | +| `ADMIN_LISTEN_PORT` | `6561` | Port for the admin panel to listen on. | +| `DB_FILE` | `web3pi_proxy.sqlite3` | Path to the database file. | +| `MODE` | `PROD` | Proxy mode (`DEV`, `SIM`, `PROD`). | +| `LOADBALANCER` | `LeastBusyLoadBalancer` | Load balancer strategy (`RandomLoadBalancer`, `LeastBusyLoadBalancer`, `ConstantLoadBalancer`). | ## Run From 4cfa1ca9f744659c18d3afc31eea6584ef5e1614 Mon Sep 17 00:00:00 2001 From: Marcin Gordel Date: Tue, 28 Jan 2025 12:02:16 +0100 Subject: [PATCH 31/31] fixes related to handling of tunnel type connections --- poetry.lock | 602 +++++++++++------- .../endpoint_pool/endpoint_connection_pool.py | 4 +- .../endpoint_pool/tunnel_connection_pool.py | 6 +- .../rpc/node/endpoint_pool/tunnel_service.py | 1 + .../rpcendpoint/connection/connectiondescr.py | 7 +- web3pi_proxy/service/admin/serviceadmin.py | 10 +- .../service/endpoints/endpoint_manager.py | 32 +- web3pi_proxy/service/http/adminserver.py | 11 +- web3pi_proxy/service/http/rpcadmincalls.py | 8 +- .../service/providers/serviceprovider.py | 1 - 10 files changed, 401 insertions(+), 281 deletions(-) diff --git a/poetry.lock b/poetry.lock index 4810471..30219bf 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,15 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. [[package]] name = "astroid" -version = "3.2.4" +version = "3.3.8" description = "An abstract syntax tree for Python with inference support." optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" +groups = ["dev"] files = [ - {file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"}, - {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, + {file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"}, + {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, ] [package.dependencies] @@ -16,52 +17,54 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "attrs" -version = "24.2.0" +version = "25.1.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "black" -version = "24.8.0" +version = "24.10.0" description = "The uncompromising code formatter." optional = false -python-versions = ">=3.8" -files = [ - {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, - {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, - {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, - {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, - {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, - {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, - {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, - {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, - {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, - {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, - {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, - {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, - {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, - {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, - {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, - {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, - {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, - {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, - {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, - {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, - {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, - {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, + {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, + {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, + {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, + {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, + {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, + {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, + {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, + {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, + {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, + {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, + {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, + {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, + {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, + {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, + {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, + {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, + {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, + {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, + {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, + {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, + {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, ] [package.dependencies] @@ -75,129 +78,134 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -209,20 +217,38 @@ 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 = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} + +[[package]] +name = "colorful" +version = "0.5.6" +description = "Terminal string styling done right, in Python." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "colorful-0.5.6-py2.py3-none-any.whl", hash = "sha256:eab8c1c809f5025ad2b5238a50bd691e26850da8cac8f90d660ede6ea1af9f1e"}, + {file = "colorful-0.5.6.tar.gz", hash = "sha256:b56d5c01db1dac4898308ea889edcb113fbee3e6ec5df4bacffd61d5241b5b8d"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "dill" -version = "0.3.8" +version = "0.3.9" description = "serialize all of Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, ] [package.extras] @@ -235,6 +261,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -245,69 +273,82 @@ test = ["pytest (>=6)"] [[package]] name = "httptools" -version = "0.6.1" +version = "0.6.4" description = "A collection of framework independent HTTP protocol utils." optional = false python-versions = ">=3.8.0" -files = [ - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, - {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, - {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, - {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, - {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, - {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, - {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, - {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, - {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, - {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, - {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, - {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, - {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, - {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, - {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, - {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, - {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, - {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, - {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, - {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, - {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, - {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, +groups = ["main"] +files = [ + {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, + {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, + {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, + {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, + {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, + {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, + {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, + {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, + {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, ] [package.extras] -test = ["Cython (>=0.29.24,<0.30.0)"] +test = ["Cython (>=0.29.24)"] [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "ifaddr" version = "0.1.7" description = "Cross-platform network interface and IP address enumeration library" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "ifaddr-0.1.7-py2.py3-none-any.whl", hash = "sha256:d1f603952f0a71c9ab4e705754511e4e03b02565bc4cec7188ad6415ff534cd3"}, {file = "ifaddr-0.1.7.tar.gz", hash = "sha256:1f9e8a6ca6f16db5a37d3356f07b6e52344f6f9f7e806d618537731669eb1a94"}, @@ -319,6 +360,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -330,6 +372,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -344,6 +387,7 @@ version = "4.9.4" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +groups = ["main"] files = [ {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e214025e23db238805a600f1f37bf9f9a15413c7bf5f9d6ae194f84980c78722"}, {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec53a09aee61d45e7dbe7e91252ff0491b6b5fee3d85b2d45b173d8ab453efc1"}, @@ -452,6 +496,7 @@ 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"}, @@ -459,47 +504,60 @@ files = [ [[package]] name = "mypy" -version = "1.11.1" +version = "1.14.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" -files = [ - {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, - {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, - {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, - {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, - {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, - {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, - {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, - {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, - {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, - {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, - {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, - {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, - {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, - {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, - {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, - {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, - {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, - {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, - {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, - {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, - {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, - {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, - {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, +groups = ["dev"] +files = [ + {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, + {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, + {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, + {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, + {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, + {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, + {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, + {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, + {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, + {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, + {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, + {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, + {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, + {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, + {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, ] [package.dependencies] -mypy-extensions = ">=1.0.0" +mypy_extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.6.0" +typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] @@ -510,6 +568,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] 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"}, @@ -517,13 +576,14 @@ files = [ [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -532,6 +592,7 @@ version = "0.2.1" description = "Bring colors to your terminal." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] files = [ {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, @@ -543,6 +604,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -550,12 +612,13 @@ files = [ [[package]] name = "peewee" -version = "3.17.6" +version = "3.17.8" description = "a little orm" optional = false python-versions = "*" +groups = ["main"] files = [ - {file = "peewee-3.17.6.tar.gz", hash = "sha256:cea5592c6f4da1592b7cff8eaf655be6648a1f5857469e30037bf920c03fb8fb"}, + {file = "peewee-3.17.8.tar.gz", hash = "sha256:ce1d05db3438830b989a1b9d0d0aa4e7f6134d5f6fd57686eeaa26a3e6485a8c"}, ] [[package]] @@ -564,6 +627,7 @@ version = "1.13.0" description = "Support for migrations in Peewee ORM" optional = false python-versions = "<4.0,>=3.8" +groups = ["main"] files = [ {file = "peewee_migrate-1.13.0-py3-none-any.whl", hash = "sha256:66597f5b8549a8ff456915db60e8382daf7839eef79352027e7cf54feec56860"}, {file = "peewee_migrate-1.13.0.tar.gz", hash = "sha256:1ab67f72a0936006155e1b310c18a32f79e4dff3917cfeb10112ca92518721e5"}, @@ -575,19 +639,20 @@ peewee = ">=3,<4" [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" @@ -595,6 +660,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -610,6 +676,7 @@ version = "0.27.0" description = "A task runner that works well with poetry." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "poethepoet-0.27.0-py3-none-any.whl", hash = "sha256:0032d980a623b96e26dc7450ae200b0998be523f27d297d799b97510fe252a24"}, {file = "poethepoet-0.27.0.tar.gz", hash = "sha256:907ab4dc1bc6326be5a3b10d2aa39d1acc0ca12024317d9506fbe9c0cdc912c9"}, @@ -624,17 +691,18 @@ poetry-plugin = ["poetry (>=1.0,<2.0)"] [[package]] name = "pylint" -version = "3.2.6" +version = "3.3.3" description = "python code static checker" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" +groups = ["dev"] files = [ - {file = "pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f"}, - {file = "pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"}, + {file = "pylint-3.3.3-py3-none-any.whl", hash = "sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183"}, + {file = "pylint-3.3.3.tar.gz", hash = "sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a"}, ] [package.dependencies] -astroid = ">=3.2.4,<=3.3.0-dev0" +astroid = ">=3.3.8,<=3.4.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -654,13 +722,14 @@ testutils = ["gitpython (>3)"] [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -680,6 +749,7 @@ version = "0.12.1" description = "unittest subTest() support and subtests fixture" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-subtests-0.12.1.tar.gz", hash = "sha256:d6605dcb88647e0b7c1889d027f8ef1c17d7a2c60927ebfdc09c7b0d8120476d"}, {file = "pytest_subtests-0.12.1-py3-none-any.whl", hash = "sha256:100d9f7eb966fc98efba7026c802812ae327e8b5b37181fb260a2ea93226495c"}, @@ -695,6 +765,7 @@ 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"}, @@ -709,6 +780,7 @@ version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, @@ -723,6 +795,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -740,24 +813,56 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -766,6 +871,7 @@ version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, @@ -777,6 +883,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -788,6 +895,7 @@ version = "1.0.3" description = "Python 3 library for accessing uPnP devices." optional = false python-versions = ">=3.6,<4.0" +groups = ["main"] files = [ {file = "upnpclient-1.0.3-py3-none-any.whl", hash = "sha256:1fb1b58af8eae9bb31758152e762f13aaf8608c89110e6d1a1d7d979677ac0df"}, {file = "upnpclient-1.0.3.tar.gz", hash = "sha256:641f05fa4b8e5c5b5cc4561dab49fe5c4774d26e51378671efad4023249e69b8"}, @@ -802,13 +910,14 @@ six = ">=1.0.0,<2.0.0" [[package]] name = "urllib3" -version = "2.2.2" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] @@ -823,11 +932,12 @@ version = "2024.2.20" description = "Fetch your public IP address from external sources with Python." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "WhatIsMyIP-2024.2.20.tar.gz", hash = "sha256:e1686c05b426441b9a7fbe7bd4b46a26b3cc0f3de1de38971f2bc034932d493d"}, ] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.9" -content-hash = "c5e8ae97155539b024e504ca359b2158f089411b5e064a3c8d7f777620dd950a" +content-hash = "6f0ff570c80f7bce86002dccc576cdff0edf38244760dde72d193ea3fb0e7f2a" diff --git a/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py b/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py index 1f63a2c..53395e2 100644 --- a/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py +++ b/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py @@ -171,7 +171,7 @@ def __run_cleanup_thread(self) -> None: def __get_connection(self) -> EndpointConnection: return self.connections.get_nowait() - def __new_connection(self) -> EndpointConnection: + def _new_connection(self) -> EndpointConnection: """Internal function, do not call directly""" def connection_factory() -> socket: # TODO is it worth to move it to object level and reuse? return BaseSocket.create_socket(self.endpoint.conn_descr.host, self.endpoint.conn_descr.port) @@ -193,7 +193,7 @@ def get(self, out_of_sync: bool = False) -> EndpointConnectionHandler: self.__lock.release() self.__logger.debug("No existing connections available, establishing new connection") try: - connection = self.__new_connection() + connection = self._new_connection() except Exception as error: self.stats.register_error_on_connection_creation() raise error diff --git a/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_connection_pool.py b/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_connection_pool.py index 3d810be..526371b 100644 --- a/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_connection_pool.py +++ b/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_connection_pool.py @@ -19,8 +19,8 @@ def __init__( super().__init__(endpoint) self.__logger = get_logger(f"TunnelConnectionPool.{id(self)}") - self.tunnel_api_key = endpoint.conn_descr.extras["tunnel_service_auth_key"] - self.tunnel_proxy_establish_port: int = endpoint.conn_descr.extras["tunnel_proxy_establish_port"] + self.tunnel_api_key = endpoint.conn_descr.extras["auth_token"] + self.tunnel_proxy_establish_port: int = endpoint.conn_descr.port tunnel_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tunnel_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -34,7 +34,7 @@ def __init__( TunnelService.register(self.tunnel_api_key, self) - def __new_connection(self) -> EndpointConnection: + def _new_connection(self) -> EndpointConnection: def connection_factory() -> socket: # TODO is it worth to move it to object level and reuse? self.__logger.debug("Creating socket") diff --git a/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_service.py b/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_service.py index b71994f..cbba56c 100644 --- a/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_service.py +++ b/web3pi_proxy/core/rpc/node/endpoint_pool/tunnel_service.py @@ -22,6 +22,7 @@ def register(self, api_key: str, tunnel_connection_pool: TunnelConnectionPoolInt self.__initialize__() self.__initialized = True self.__registry[api_key] = tunnel_connection_pool + self.__logger.debug(f"Registered tunnel service for {api_key}") def unregister(self, api_key: str, tunnel_connection_pool: TunnelConnectionPoolIntf): with self.__lock: diff --git a/web3pi_proxy/core/rpc/node/rpcendpoint/connection/connectiondescr.py b/web3pi_proxy/core/rpc/node/rpcendpoint/connection/connectiondescr.py index d013906..7c6fe94 100644 --- a/web3pi_proxy/core/rpc/node/rpcendpoint/connection/connectiondescr.py +++ b/web3pi_proxy/core/rpc/node/rpcendpoint/connection/connectiondescr.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging from dataclasses import dataclass import enum import urllib3.util @@ -44,14 +45,14 @@ def from_url(cls, url) -> EndpointConnectionDescriptor | None: else: return None - return EndpointConnectionDescriptor(host, int(port), auth_key, is_ssl, url, dict(), ConnectionType.DIRECT) + return EndpointConnectionDescriptor(host, int(port), "", is_ssl, url, dict(), ConnectionType.DIRECT) @classmethod def from_dict(cls, conf: dict) -> EndpointConnectionDescriptor | None: url: str = conf["url"] conn_descr = cls.from_url(url) if not conn_descr: - return None + raise Exception(f"Invalid url provided: {url}") connection_type: str = conf.get("connection_type") if connection_type: try: @@ -61,7 +62,9 @@ def from_dict(cls, conf: dict) -> EndpointConnectionDescriptor | None: else: conn_descr.connection_type = ConnectionType.DIRECT conn_descr.extras = conf.copy() + conn_descr.auth_key = conf.get("auth_key") or "" del conn_descr.extras["name"] del conn_descr.extras["url"] conn_descr.extras.pop("connection_type", None) + print(f"Connection descriptor: {conn_descr}") return conn_descr diff --git a/web3pi_proxy/service/admin/serviceadmin.py b/web3pi_proxy/service/admin/serviceadmin.py index c91a1d1..a0fe691 100644 --- a/web3pi_proxy/service/admin/serviceadmin.py +++ b/web3pi_proxy/service/admin/serviceadmin.py @@ -206,8 +206,10 @@ def query_service_console(self) -> ReturnType: def get_endpoints(self) -> ReturnType: return self.endpoint_manager.get_endpoints() - def add_endpoint(self, name: str, url: str) -> ReturnType: - res = self.endpoint_manager.add_endpoint(name, url) + def add_endpoint(self, name: str, url: str, connection_type: str, auth_token: str | None) -> ReturnType: + res = self.endpoint_manager.add_endpoint( + {"name": name, "url": url, "connection_type": connection_type, "auth_token": auth_token} + ) if type(res) is dict: # TODO not too good error handling return res self.register_endpoint_stats(res.get_name(), res.get_connection_stats()) @@ -220,8 +222,8 @@ def remove_endpoint(self, name: str) -> ReturnType: self.remove_endpoint_stats(res.get_name()) return {"message": f"Removed endpoint '{name}'"} - def update_endpoint(self, name: str, url: str) -> ReturnType: - res = self.endpoint_manager.update_endpoint(name, url) + def update_endpoint(self, name: str, url: str, connection_type: str, auth_token: str | None) -> ReturnType: + res = self.endpoint_manager.update_endpoint({"name": name, "url": url, "connection_type": connection_type, "auth_token": auth_token}) if type(res) is dict: # TODO not too good error handling return res self.update_endpoint_stats(res.get_name(), res.get_connection_stats()) diff --git a/web3pi_proxy/service/endpoints/endpoint_manager.py b/web3pi_proxy/service/endpoints/endpoint_manager.py index cfc2e3e..74d8f8a 100644 --- a/web3pi_proxy/service/endpoints/endpoint_manager.py +++ b/web3pi_proxy/service/endpoints/endpoint_manager.py @@ -57,24 +57,26 @@ def get_endpoints(self) -> dict: endpoint_entry["auth_key"] = endpoint.conn_descr.auth_key endpoint_entry["is_ssl"] = endpoint.conn_descr.is_ssl endpoint_entry["url"] = endpoint.conn_descr.url + endpoint_entry["type"] = endpoint.conn_descr.connection_type nodes_data[endpoint.get_name()] = endpoint_entry return nodes_data - def add_endpoint(self, name: str, url: str) -> Union[RPCEndpoint, dict]: + def add_endpoint(self, conf: dict) -> Union[RPCEndpoint, dict]: if not Config.ETH_ENDPOINTS_STORE: return {"error": "the endpoint cannot be stored"} - descriptor = EndpointConnectionDescriptor.from_url(url) - if descriptor is None: - return {"error": "Invalid URL provided"} try: - endpoint = self.endpoint_pool_manager.add_pool(name, descriptor) + descriptor = EndpointConnectionDescriptor.from_dict(conf) + except Exception as error: + return {"error": str(error)} + try: + endpoint = self.endpoint_pool_manager.add_pool(conf['name'], descriptor) except PoolAlreadyExistsError as error: return {"error": error.message} - config = json.dumps({"name": name, "url": url}) + config = json.dumps(conf) try: - Endpoint.create(name=name, config=config) + Endpoint.create(name=conf['name'], config=config) except PeeweeException as error: - self.endpoint_pool_manager.remove_pool(name) # TODO use db tx instead? + self.endpoint_pool_manager.remove_pool(conf['name']) # TODO use db tx instead? return {"error": str(error)} return endpoint @@ -91,20 +93,22 @@ def remove_endpoint(self, name: str) -> Union[RPCEndpoint, dict]: return {"error": "(db inconsistent): " + str(error)} # TODO handle inconsistency return endpoint - def update_endpoint(self, name: str, url: str) -> Union[RPCEndpoint, dict]: + def update_endpoint(self, conf: dict) -> Union[RPCEndpoint, dict]: if not Config.ETH_ENDPOINTS_STORE: return {"error": "the endpoint cannot be stored"} - descriptor = EndpointConnectionDescriptor.from_url(url) - if descriptor is None: - return {"error": "Invalid URL provided"} try: - self.endpoint_pool_manager.remove_pool(name) + descriptor = EndpointConnectionDescriptor.from_dict(conf) + except Exception as error: + return {"error": str(error)} + try: + self.endpoint_pool_manager.remove_pool(conf['name']) except PoolDoesNotExistError as error: return {"error": error.message} + name = conf['name'] endpoint = self.endpoint_pool_manager.add_pool(name, descriptor) try: endpoint_db = Endpoint.get(Endpoint.name == name) - config = json.dumps({"name": name, "url": url}) + config = json.dumps(conf) endpoint_db.config = config endpoint_db.save() except PeeweeException as error: diff --git a/web3pi_proxy/service/http/adminserver.py b/web3pi_proxy/service/http/adminserver.py index 255e5e6..8c95a72 100644 --- a/web3pi_proxy/service/http/adminserver.py +++ b/web3pi_proxy/service/http/adminserver.py @@ -1,4 +1,3 @@ -import colorful import hashlib import json import random @@ -8,7 +7,9 @@ import string import threading from http.server import BaseHTTPRequestHandler -from typing import Optional, Union +from typing import Union + +import colorful from web3pi_proxy.config.conf import Config from web3pi_proxy.service.admin.serviceadmin import RPCServiceAdmin @@ -133,14 +134,14 @@ def do_POST(self): res = admin.call_by_method( json_data["method"], json_data.get("params", []) ) - except KeyError: + except KeyError as error: http_status = 400 msg = f"Unknown method: {method}" - self.__logger.error(msg) + self.__logger.error("Error occurred in method '%s': %s", method, error, exc_info=True) res = {"error": msg} except Exception as error: http_status = 500 - self.__logger.error(error.with_traceback) + self.__logger.error("Error occurred in method '%s': %s", method, error, exc_info=True) res = {"error": "Server error"} else: http_status = 400 diff --git a/web3pi_proxy/service/http/rpcadmincalls.py b/web3pi_proxy/service/http/rpcadmincalls.py index c39748f..1bea8e4 100644 --- a/web3pi_proxy/service/http/rpcadmincalls.py +++ b/web3pi_proxy/service/http/rpcadmincalls.py @@ -66,13 +66,13 @@ def get_get_endpoints(cls) -> dict: return cls.create_method_call_dict(cls.GET_ENDPOINTS) @classmethod - def get_add_endpoint(cls, name: str, url: str) -> dict: - return cls.create_method_call_dict(cls.ADD_ENDPOINT, name, url) + def get_add_endpoint(cls, config: dict) -> dict: + return cls.create_method_call_dict(cls.ADD_ENDPOINT, config) @classmethod def get_remove_endpoint(cls, name: str) -> dict: return cls.create_method_call_dict(cls.REMOVE_ENDPOINT, name) @classmethod - def get_update_endpoint(cls, name: str, url: str) -> dict: - return cls.create_method_call_dict(cls.UPDATE_ENDPOINT, name, url) + def get_update_endpoint(cls, config: dict) -> dict: + return cls.create_method_call_dict(cls.UPDATE_ENDPOINT, config) diff --git a/web3pi_proxy/service/providers/serviceprovider.py b/web3pi_proxy/service/providers/serviceprovider.py index b3dd0fe..c7f7ff1 100644 --- a/web3pi_proxy/service/providers/serviceprovider.py +++ b/web3pi_proxy/service/providers/serviceprovider.py @@ -99,7 +99,6 @@ def create_default_web3_rpc_proxy( ) -> Web3RPCProxy: if Config.ETH_ENDPOINTS_STORE: eth_endpoints = [] - uuu = Endpoint.select(Endpoint.config) for eth_endpoint_data in Endpoint.select(Endpoint.config): eth_endpoints.append(json.loads(eth_endpoint_data.config)) else: