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/README.md b/README.md index 7ef8c35..85e83fa 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 @@ -14,27 +14,67 @@ Web3Pi 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 ``` -Web3Pi 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. +### Install from source -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 (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 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 -After configuring endpoints, you can run your reverse proxy with command +You can run your reverse proxy with command: ```bash web3pi-proxy @@ -58,7 +98,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. @@ -88,7 +128,26 @@ 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. + +## 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 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 access URL (that contains proxy's RPC endpoint and user's API key). +See the example below. + +![Metamask](./admin/docs/metamask-1.png) + +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). + diff --git a/admin/admin/admin.html b/admin/admin/admin.html index 4c06f2e..7fbb4d8 100644 --- a/admin/admin/admin.html +++ b/admin/admin/admin.html @@ -156,8 +156,7 @@ +
  • User Billing Stats

  • - - + + + - -
    +
    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
    +
    + + + +
    @@ -252,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', @@ -271,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", @@ -401,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) @@ -482,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() { @@ -574,10 +644,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 `
    @@ -602,6 +673,10 @@
    Endpoint: Endpoint: Endpoint: Endpoint: Endpoint: Endpoint: Endpoint: Endpoint: 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); } diff --git a/admin/docs/metamask-1.png b/admin/docs/metamask-1.png new file mode 100644 index 0000000..4b684cd Binary files /dev/null and b/admin/docs/metamask-1.png differ diff --git a/admin/docs/metamask.png b/admin/docs/metamask.png new file mode 100644 index 0000000..cbcee3e Binary files /dev/null and b/admin/docs/metamask.png differ diff --git a/poetry.lock b/poetry.lock index 990abf3..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.2" +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.2-py3-none-any.whl", hash = "sha256:e8a0083b4bb28fcffb6207a3bfc9e5d0a68be951dd7e336d5dcf639c682388c0"}, - {file = "astroid-3.2.2.tar.gz", hash = "sha256:8ead48e31b92b2e217b6c9733a21afafe479d52d6e164dd25fb1a770c7c3cf94"}, + {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 = "23.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-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, ] [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-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.4.2" +version = "24.10.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"}, +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.6.2" +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.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {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,10 +217,12 @@ 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" @@ -220,6 +230,7 @@ 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"}, @@ -230,13 +241,14 @@ 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] @@ -245,13 +257,15 @@ 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" +groups = ["dev"] +markers = "python_version < \"3.11\"" 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] @@ -259,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"}, @@ -333,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"}, @@ -344,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"}, @@ -358,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"}, @@ -466,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"}, @@ -473,47 +504,60 @@ files = [ [[package]] name = "mypy" -version = "1.10.1" +version = "1.14.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"}, +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.1.0" +typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] @@ -524,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"}, @@ -531,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]] @@ -546,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"}, @@ -557,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"}, @@ -564,23 +612,25 @@ 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]] 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" +groups = ["main"] 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] @@ -589,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" @@ -609,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"}, @@ -624,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"}, @@ -638,17 +691,18 @@ poetry-plugin = ["poetry (>=1.0,<2.0)"] [[package]] name = "pylint" -version = "3.2.5" +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.5-py3-none-any.whl", hash = "sha256:32cd6c042b5004b8e857d727708720c54a676d1e22917cf1a2df9b4d4868abd6"}, - {file = "pylint-3.2.5.tar.gz", hash = "sha256:e9b7171e242dcc6ebd0aaa7540481d1a72860748a0a7816b8fe6cf6c80a6fe7e"}, + {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.2,<=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\""}, @@ -660,6 +714,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)"] @@ -667,13 +722,14 @@ testutils = ["gitpython (>3)"] [[package]] name = "pytest" -version = "8.2.2" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] 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.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -681,7 +737,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] @@ -693,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"}, @@ -708,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"}, @@ -722,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"}, @@ -736,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"}, @@ -753,35 +813,68 @@ 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]] 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" +groups = ["dev"] 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]] @@ -790,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"}, @@ -801,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"}, @@ -815,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] @@ -836,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" -python-versions = "^3.10" -content-hash = "ea67d9b8227c4e25c91623c82143faf7e94fbfaf3c6d8169670458e1e2df8dd4" +lock-version = "2.1" +python-versions = "^3.9" +content-hash = "6f0ff570c80f7bce86002dccc576cdff0edf38244760dde72d193ea3fb0e7f2a" diff --git a/pyproject.toml b/pyproject.toml index 41c0cc9..1a71a06 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", ] @@ -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" diff --git a/web3pi_proxy/config/conf.py b/web3pi_proxy/config/conf.py index 7b1d62a..a04350d 100644 --- a/web3pi_proxy/config/conf.py +++ b/web3pi_proxy/config/conf.py @@ -8,6 +8,7 @@ IPV6_LOOPBACK = "::1" + class ProxyMode(Enum): DEV = "DEV" SIM = "SIM" @@ -58,10 +59,10 @@ class AppConfig: # Endpoints ETH_ENDPOINTS: List[dict] = [ - {"name": "eop-1", "url": "http://eop-1.local:8545/"}, # {"name": "rpi4 geth2", "url": "http://geth-2.local:8545/"}, # {"name": "infura-1", "url": "https://mainnet.infura.io/v3/"} ] + ETH_ENDPOINTS_STORE: bool = True CACHE_ENABLED: bool = False CACHE_EXPIRY_MS: int = 300000 @@ -123,6 +124,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/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/proxy.py b/web3pi_proxy/core/proxy.py index dd18baf..9c3c23f 100644 --- a/web3pi_proxy/core/proxy.py +++ b/web3pi_proxy/core/proxy.py @@ -2,12 +2,18 @@ import select import threading import traceback +from collections import defaultdict from concurrent.futures import ThreadPoolExecutor from typing import Callable 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,25 +92,26 @@ 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()) - 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( 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? + active_client_connections.set_cs_pending(cs.fd) + if getattr(select, "EPOLLONESHOT", None): + client_poller.modify( + cs.socket, POLLIN | select.EPOLLONESHOT + ) # TODO hangup? errors? else: self.__close_client_connection(cs, client_poller, active_client_connections) @@ -117,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 @@ -130,7 +139,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 @@ -241,21 +250,23 @@ 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()) + client_poller.unregister(cs.fd) 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 + fd_lock = defaultdict(threading.Lock) + queue_cs_for_close = queue.Queue() t = threading.Thread( target=self.closing_cs, @@ -273,17 +284,26 @@ 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) - 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 - ) # 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/node/endpoint_pool/endpoint_connection_pool.py b/web3pi_proxy/core/rpc/node/endpoint_pool/endpoint_connection_pool.py index f12bb44..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 @@ -126,6 +126,7 @@ def __str__(self): class PoolStatus(str, enum.Enum): ACTIVE = "ACTIVE" DISABLED = "DISABLED" + OUT_OF_SYNC = "OUT_OF_SYNC" CLOSING = "CLOSING" CLOSED = "CLOSED" @@ -170,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) @@ -180,16 +181,19 @@ 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(): 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 @@ -226,6 +230,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 is_open(self): return self.status == self.PoolStatus.ACTIVE.value or self.status == self.PoolStatus.DISABLED.value @@ -239,6 +246,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 6cd1511..ce96da5 100644 --- a/web3pi_proxy/core/rpc/node/endpoint_pool/pool_manager.py +++ b/web3pi_proxy/core/rpc/node/endpoint_pool/pool_manager.py @@ -1,11 +1,13 @@ from __future__ import annotations +import json import select import socket 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, @@ -69,10 +71,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 @@ -97,14 +95,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 SyncControllerResponseListener: + body: bytes = b'' + block_number: int = 0 + block_timestamp: int = 0 + __logger = get_logger("SyncController") + + def __init__(self, __pool_name: str): + 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 + + 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') + 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 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'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 = SyncControllerResponseListener(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__( @@ -114,6 +212,7 @@ def __init__( ): self.load_balancer = load_balancer self.damage_controller = DamageController() + self.sync_controller = SyncController() self.pools: dict[str, EndpointConnectionPool] = {} self.__lock = RLock() @@ -128,6 +227,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: @@ -155,6 +260,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: 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..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/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/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/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/sockets/basesocket.py b/web3pi_proxy/core/sockets/basesocket.py index bbab2ff..1d7ddfe 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) 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() 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/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/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/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/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..a0fe691 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 @@ -206,23 +206,25 @@ 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) - if type(res) is dict: + 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()) 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: + 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()) return {"message": f"Updated endpoint '{name}' with address {url}"} 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 1a68bed..74d8f8a 100644 --- a/web3pi_proxy/service/endpoints/endpoint_manager.py +++ b/web3pi_proxy/service/endpoints/endpoint_manager.py @@ -1,8 +1,10 @@ import json -from pathlib import Path +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: @@ -25,7 +28,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"]) @@ -54,32 +57,60 @@ 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) -> RPCEndpoint | dict: - descriptor = EndpointConnectionDescriptor.from_url(url) # TODO from_dict + def add_endpoint(self, conf: dict) -> Union[RPCEndpoint, dict]: + if not Config.ETH_ENDPOINTS_STORE: + return {"error": "the endpoint cannot be stored"} 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} - self.__save_endpoint_conf(name, url) + config = json.dumps(conf) + try: + Endpoint.create(name=conf['name'], config=config) + except PeeweeException as error: + self.endpoint_pool_manager.remove_pool(conf['name']) # TODO use db tx instead? + return {"error": str(error)} return endpoint - def remove_endpoint(self, name: str) -> RPCEndpoint | dict: + 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) -> RPCEndpoint | dict: - descriptor = EndpointConnectionDescriptor.from_url(url) # TODO from_dict + def update_endpoint(self, conf: dict) -> Union[RPCEndpoint, dict]: + if not Config.ETH_ENDPOINTS_STORE: + return {"error": "the endpoint cannot be stored"} 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) - self.__save_endpoint_conf(name, url) + try: + endpoint_db = Endpoint.get(Endpoint.name == name) + config = json.dumps(conf) + 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/http/adminserver.py b/web3pi_proxy/service/http/adminserver.py index f9e4841..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 @@ -7,8 +6,10 @@ import socketserver import string import threading -import traceback from http.server import BaseHTTPRequestHandler +from typing import Union + +import colorful from web3pi_proxy.config.conf import Config from web3pi_proxy.service.admin.serviceadmin import RPCServiceAdmin @@ -109,7 +110,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): @@ -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 d932354..c7f7ff1 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,14 @@ 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 = [] + 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 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)